You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@maven.apache.org by sl...@apache.org on 2026/08/09 21:30:48 UTC

(maven-doxia-converter) 01/01: Document migrating a project's site from APT to Markdown

This is an automated email from the ASF dual-hosted git repository.

slachiewicz pushed a commit to branch agent/apt-to-markdown-migration-guide
in repository https://gitbox.apache.org/repos/asf/maven-doxia-converter.git

commit 57b269c63c614c85b94aea36dfd9dbc6f7be13c1
Author: Sylwester Lachiewicz <sl...@apache.org>
AuthorDate: Sun Aug 9 23:30:37 2026 +0200

    Document migrating a project's site from APT to Markdown
    
    doxia-converter does the mechanical part of turning src/site/apt into
    src/site/markdown, but several differences between the two formats are
    not mechanical and damage a page silently: a Velocity line comment eats
    an ATX heading below level one in a .md.vm file, an APT-escaped
    ${...} reference becomes live and interpolates, and an autodetected
    ISO-8859-1 input encoding turns an escaped space into a stray 0xa0 byte.
    
    Add a page collecting those cases, the workarounds, the APT constructs
    Markdown cannot express, and the ones that come out better after
    conversion. Add tools/normalize-site-page.py, which reduces a generated
    page to its visible text plus link targets so that a before/after diff
    of the built site shows only what a reader would notice.
    
    Generated-by: Claude Opus 5 (1M context)
---
 src/site/markdown/migrating-apt-to-markdown.md | 147 +++++++++++++++++++++++++
 src/site/site.xml                              |   1 +
 tools/normalize-site-page.py                   |  59 ++++++++++
 3 files changed, 207 insertions(+)

diff --git a/src/site/markdown/migrating-apt-to-markdown.md b/src/site/markdown/migrating-apt-to-markdown.md
new file mode 100644
index 0000000..84c1d4a
--- /dev/null
+++ b/src/site/markdown/migrating-apt-to-markdown.md
@@ -0,0 +1,147 @@
+<!--
+Licensed to the Apache Software Foundation (ASF) under one
+or more contributor license agreements.  See the NOTICE file
+distributed with this work for additional information
+regarding copyright ownership.  The ASF licenses this file
+to you under the Apache License, Version 2.0 (the
+"License"); you may not use this file except in compliance
+with the License.  You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing,
+software distributed under the License is distributed on an
+"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+KIND, either express or implied.  See the License for the
+specific language governing permissions and limitations
+under the License.
+-->
+
+# Migrating a project's site from APT to Markdown
+
+`doxia-converter` does most of the mechanical work of turning `src/site/apt` into
+`src/site/markdown`, but a handful of differences between the two formats are not mechanical
+and will silently damage a page if they go unnoticed. This page collects them, together with
+the procedure that catches them.
+
+Nothing here needs a POM change: `maven-site-plugin` already ships `doxia-module-markdown`.
+
+## The one rule that matters
+
+**Build the site before the migration, build it again afterwards, and compare the generated
+HTML.** Every problem described below was found that way and none of them is visible by
+reading the converted Markdown. A page that looks right can be missing a whole heading.
+
+## Converting
+
+```
+doxia-converter -in src/site/apt/index.apt.vm -from apt \
+                -out src/site/markdown -to markdown -outEncoding UTF-8
+```
+
+The converter renames `x.apt` to `x.md` and `x.apt.vm` to `x.md.vm` by itself.
+
+Pass `-outEncoding UTF-8` explicitly. Without it the output follows the autodetected input
+encoding, and an ASCII source is detected as ISO-8859-1; an APT escaped space (`\ `, which
+becomes U+00A0) is then written as a lone `0xa0` byte.
+
+## Velocity and Markdown disagree about `#`
+
+A Markdown ATX heading below level one starts with `##`, which is also how a Velocity line
+comment starts. In a `*.md.vm` file Velocity removes the heading before Doxia ever sees the
+document: the line simply disappears from the rendered page, with no error and no warning.
+Level one is safe, because a single `#` followed by a space is literal to Velocity.
+
+There are three ways out, in order of preference.
+
+**Drop the `.vm`.** Most `index.apt.vm` pages only use Velocity to interpolate
+`${project.name}` into the title. Spell the title out, name the file `index.md`, and the
+problem disappears along with the template.
+
+**Use a setext underline for level two.** A title line followed by a line of `-` is a level
+two heading and starts with no `#` at all:
+
+```
+Configuration
+-------------
+```
+
+The title line needs a **blank line before it**. Without one it is read as a lazy
+continuation of the preceding paragraph or list item and the heading is lost — a different
+failure from the one being fixed, and just as quiet.
+
+**Shield deeper headings.** There is no setext form below level two, so wrap the heading in
+Velocity's unparsed block:
+
+```
+#[[### Advanced options]]#
+```
+
+Doxia 2.1.1 and later shield these automatically in the site renderer, but a page that must
+build with an older Doxia has to do it itself.
+
+## References the source meant to show, not resolve
+
+APT writes `$\{project.version\}` when a page wants to display `${project.version}` rather
+than interpolate it. The parser unescapes that, so the converted `*.vm` holds a live
+reference and Velocity resolves it: a page documenting a default value ends up showing
+something like `[org.apache.maven.model.ReportPlugin@c754401]`.
+
+Write these as `${esc.d}{project.version}`. `${esc.d}` comes from Velocity's `EscapeTool`,
+which the site renderer always provides, and always yields a literal `$`.
+
+Do **not** use a backslash. `\${project.version}` only renders as `${project.version}` when
+`project.version` actually resolves in the document's Velocity context; when it does not,
+Velocity emits the backslash too and the page shows `\${project.version}`.
+
+The converter warns about every reference that was literal in the source and is live in the
+output, so watch its output rather than hunting for these by hand.
+
+## Things Markdown cannot express
+
+Some APT constructs have no Markdown equivalent. None of them is a converter defect, but each
+changes the rendered page, so decide deliberately:
+
+* **Table captions.** APT puts a caption line after a table and Doxia renders `<caption>`.
+  Markdown has no caption syntax; the text becomes a paragraph after the table.
+* **Intraword emphasis.** The sink writes `_x_`, which CommonMark does not treat as emphasis
+  when it follows a word character, so `Set<i>String</i>` renders as literal `Set_String_`.
+* **Typographic substitution.** The Markdown module turns straight quotes into curly quotes
+  and `...` into an ellipsis in prose. Code spans and code blocks are left alone.
+
+## Differences that are improvements
+
+Several APT constructs render incorrectly today and come out *better* after conversion. Do
+not "fix" these back:
+
+* `{{{#My Anchor}My Anchor}}` renders as `href="#My Anchor"` pointing at `id="My_Anchor"`, so
+  the link never resolves. The converted Markdown links to `#My_Anchor`.
+* `<<<${basedir}/src>>>` renders as `$<a id="basedir">basedir</a>/src` — APT reads the braces
+  as an anchor definition and invents an anchor.
+* `<<<\<overlay\>>>>` can end up with an anchor spliced into the middle of the code span.
+
+## Checking the result
+
+Comparing raw HTML is too noisy to be useful: `<b>` becomes `<strong>`, `<i>` becomes `<em>`,
+attribute order changes, and `<code>` and `<a>` nest the other way round. Reduce each page to
+its visible text plus its link targets and compare that instead. A script that does so ships
+with this project as `tools/normalize-site-page.py`:
+
+```
+diff <(tools/normalize-site-page.py before/index.html) \
+     <(tools/normalize-site-page.py after/index.html)
+```
+
+Two mistakes are easy to make when scripting the comparison and both produce a reassuring
+but meaningless result:
+
+* **Check the exit status of `mvn site`.** A failed build leaves the previous output in
+  `target/site`, so a comparison against it reports no difference at all.
+* **Derive the page list from the built site, not from an inventory of the APT files.** Once
+  the sources are converted that inventory is empty, and an empty list compares nothing.
+
+## What not to convert
+
+* Anything under `src/test` or `src/it`. Those APT files are fixtures for Doxia's own tests.
+* Anything under `src/main/resources/archetype-resources`. That is template content generated
+  into a user's new project, not the project's own documentation.
diff --git a/src/site/site.xml b/src/site/site.xml
index f84caf5..096510e 100644
--- a/src/site/site.xml
+++ b/src/site/site.xml
@@ -24,6 +24,7 @@ under the License.
     <menu name="Overview">
       <item name="Introduction" href="index.html"/>
       <item name="Usage" href="usage.html"/>
+      <item name="Migrating APT to Markdown" href="migrating-apt-to-markdown.html"/>
       <!-- According to https://issues.apache.org/jira/browse/MNGSITE-152 -->
       <item name="License" href="http://www.apache.org/licenses/"/>
       <item name="Download" href="download.html"/>
diff --git a/tools/normalize-site-page.py b/tools/normalize-site-page.py
new file mode 100755
index 0000000..ff7fb97
--- /dev/null
+++ b/tools/normalize-site-page.py
@@ -0,0 +1,59 @@
+#!/usr/bin/env python3
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+"""Reduce a generated site page to the parts a migration must not change.
+
+An APT page and the Markdown page it was converted from render the same document with
+differences that carry no meaning: <b> against <strong>, attribute order, whether <code>
+sits inside <a> or the other way round. Comparing raw HTML drowns a real regression in
+those. This prints the visible text plus the link targets, one token per line, so that
+
+    diff <(normalize-site-page.py before/index.html) <(normalize-site-page.py after/index.html)
+
+shows only what a reader would notice.
+
+usage: normalize-site-page.py <generated-html-file>
+"""
+import io
+import re
+import sys
+
+html = io.open(sys.argv[1], encoding="utf-8", errors="replace").read()
+
+# the skin wraps the document; only the rendered document itself is of interest
+match = re.search(r"<main.*?</main>", html, re.DOTALL)
+body = match.group(0) if match else html
+
+body = re.sub(r"<!--.*?-->", "", body, flags=re.DOTALL)
+
+# keep the structural facts a reader depends on, as visible tokens
+body = re.sub(r'<a[^>]*href="([^"]*)"[^>]*>', r" [LINK:\1] ", body)
+body = re.sub(r"<(h[1-6])[^>]*>", r" [\1] ", body)
+body = re.sub(r"<pre[^>]*>", " [PRE] ", body)
+body = re.sub(r"<t[hd](?=[ />])[^>]*>", " [CELL] ", body)  # not <thead>
+body = re.sub(r"<li[^>]*>", " [ITEM] ", body)
+body = re.sub(r"<[^>]+>", " ", body)
+
+body = body.replace("&quot;", '"').replace("&apos;", "'").replace("&#x27;", "'")
+# the Markdown module applies typographic substitution in prose; it is not a content change
+body = body.replace("&#x2018;", "'").replace("&#x2019;", "'")
+body = body.replace("&#x201c;", '"').replace("&#x201d;", '"')
+body = body.replace("&#x2026;", "...").replace("…", "...")
+body = body.replace("&lt;", "<").replace("&gt;", ">").replace("&amp;", "&")
+body = re.sub(r"\s+", " ", body).strip()
+
+sys.stdout.write("\n".join(body.split(" ")))