Repository navigation
docs: Accept backslash-escaped less-than sign in mkdocs - #8076
Open
wenzeslaus wants to merge 7 commits into
Open
wenzeslaus wants to merge 7 commits into
wenzeslaus wants to merge 7 commits into
Conversation
Markdown docs written for CommonMark (pandoc conversion, GitHub) use \< to escape the less-than sign, e.g., in author emails as \<user at example.com\> or in comparisons like x \< 0. Python-Markdown, used by MkDocs, follows the original Markdown rules where < is not escapable, so the backslash was rendered in the HTML manual. Add a MkDocs hook which registers a small Python-Markdown extension adding < to ESCAPED_CHARS, the same mechanism the tables extension uses for |. This fixes the rendering for core and addons without changing the sources. Written with the help of Claude Code.
The docstring shows CommonMark escapes like \<name\>. A raw string lets the source show them as they are instead of doubled backslashes. Written with the help of Claude Code.
The module docstring now describes the general purpose of the hook, the CommonMark versus Python-Markdown difference in backslash escapes, and the class docstring explains the less-than sign specifically. The note about the tables extension is removed as it is not needed to understand the code. Written with the help of Claude Code.
The hook module is about backslash escapes in general, so name it hook_escape.py rather than after the less-than sign only. Written with the help of Claude Code.
petrasovaa
requested changes
Oct 18, 2026
petrasovaa
left a comment
Contributor
There was a problem hiding this comment.
This seems incompatible with moving to Zensical, see #7938.
Zensical, which the manual build is being prepared for, has no Python hooks, but it uses Python-Markdown and reads markdown_extensions from mkdocs.yml like MkDocs does. Zensical has the same issue with \< as MkDocs, so the fix is still needed after the switch. Register the escape as a Python-Markdown extension in markdown_extensions instead of with a hook. The generator imports it by name, so the MkDocs build commands in the Makefile, CMake, and the documentation workflow put the scripts directory on PYTHONPATH. Written with the help of Claude Code.
Member
Author
|
Zensical also uses Python-Markdown and shows the same stray backslashes, so I replaced the hook with a Python-Markdown extension, which both MkDocs and Zensical load unlike the hooks. |
The module docstring described how the extension is loaded by the site generators. That belongs to the commit and PR descriptions, so keep the docstring to what the extension does. Written with the help of Claude Code.
petrasovaa
reviewed
Oct 20, 2026
petrasovaa
left a comment
Contributor
There was a problem hiding this comment.
This should be merged before #7938, and that one needs to be adjusted ($(MDDIR)/scripts rule and the CMake make_directory for scripts must stay).
Member
Author
There was a problem hiding this comment.
Now a full three-row SPDX header is present.
Written with the help of Claude Code.
wenzeslaus
enabled auto-merge (squash)
October 27, 2026 12:47
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The MkDocs manual shows a stray backslash wherever the Markdown source tries to escape a less-than sign (
<), for example in the r.watershed author line\<markus.metz.giswork at gmail.com\>(see screenshot below).The escape is valid CommonMark and renders fine on GitHub, but Python-Markdown used by MkDocs does not treat
<as escapable. While this is simply a choice one way or the other (based on the selected standard), I suggest aligning with GitHub here, although my general preference would be a simpler text (no slash). This adds a Python-Markdown extension which adds<to the escapable characters, which fixes core and addon pages without editing the sources. The extension works with both MkDocs and Zensical.This is the first custom Markdown extension (a new hook for this would not be the first hook, but hooks are supported only in MkDocs).
The issue in rendered documentation (screenshot)
Markdown as rendered by GitHub (screenshot)
Details (AI generated)
Cause
The
\<comes from the pandoc HTML-to-Markdown conversion (utils/grass_html2md.sh). Pandoc writes CommonMark/GFM, where any ASCII punctuation can be escaped with a backslash. MkDocs uses Python-Markdown, which follows the original Markdown rules. Those rules list\,`,*,_,{},[],(),#,+,-,.,!(Python-Markdown adds>), but not<. The maintainer confirms that only this subset is escapable (Python-Markdown/markdown#1464). So\<x\>renders as\<x>.GitHub renders these files correctly, and so does
g.md2man, which already treats\<as an escape (utils/g.md2man/gmd.py). Only the HTML manual is affected.Change
The new module
man/mkdocs/scripts/grass_markdown.pyis a small Python-Markdown extension (EscapeLessThanExtension) that adds<tomd.ESCAPED_CHARS. Python-Markdown provides this mechanism for extensions, and its built-intablesextension uses it to add|, the same waysmartyadds the quote characters. The extension is listed undermarkdown_extensionsinmkdocs.ymland installed by both the Makefile and the CMake build. The site generator imports it by name, so the build commands (Makefile, CMake, documentation workflow) put thescriptsdirectory onPYTHONPATH. Without it, the build stops with an error rather than rendering the backslashes.The extension is not a hook, so it does not conflict with the move to Zensical (#7938), which has no Python hooks. Zensical 0.0.71 uses Python-Markdown, reads
markdown_extensionsfrommkdocs.yml, and renders\<with the same stray backslash as MkDocs, so the fix is still needed after the switch. Zensical loads the extension as it is.Addon docs are built with the core
man/mkdocsconfig (.github/workflows/documentation.yml), so addon pages are fixed as well.Tested with full local builds of the manual, with MkDocs (
make build-mkdocswith the pinned requirements) and with Zensical 0.0.71 (on the same tree, without theactions.htmloverride which depends on the other hook). In both, no page contains a backslash before an escaped less-than sign. Results from a minimal project with mkdocs 1.6.1 and Python-Markdown 3.11:\<markus.metz.giswork at gmail.com\>\<markus.metz.giswork at gmail.com><markus.metz.giswork at gmail.com>x \< 5x \< 5x < 5\<x\>in a table cell\<x><x>`code \<x\>`code \<x\>Affected lines
These are lines that render a stray backslash with stock Python-Markdown. There are no cases inside code spans or fenced code.
Examples:
\<markus.metz.giswork at gmail.com\>Per Henrik Johansen \<*phj (at) norgit.no*\>Lars Ahlzen \<*lars (at) ahlzen.com*\>[\<darrell@mccauley-usa.com\>](mailto:darrell@mccauley-usa.com)length is shorter than *thresh* or *thresh* \< 0data points for 0 \< *p* \< 1"GIS_FLAG\_\<NAME\>" for flags and "GIS_OPT\_\<NAME\>"\[\[\[**\<GISDBASE\>/**\]**\<PROJECT\>/**\]| GRASS module \<-\> | \<--\> | ODBC Interface |"\<raster name\> in mapset \<mapset name\>"if the object is \<= n pixels thickChristoph Simon \<ciccio at kiosknet com.br\>*ERROR: Vector layer \<layer\> exists. ...*The names of the output maps are \<output\>bio01, \<output\>bio02,0; 0 \< θ ≤ 22.5; 22.5 \< θ ≤ 45; ...(15.00 \> lat \<= 45.00), Tropical (-15.00 \> lat \<= 15.00)Alternatives considered
Fix the source lines (46 in grass, 31 in addons). I prepared and render-tested a patch for this. Each line can be changed in one of these ways:
<entity:\<markus...\>becomes<markus...>. This works in every renderer, but HTML entities make the Markdown harder to read for a character this common.<where it cannot start an HTML tag, as the original Markdown rules suggest. This works when<is followed by a space, digit,=or-(x < 5,<= 0.5,<->). It does not work for<name>,<output>bio01or<user at host>, because Python-Markdown treats those as raw HTML and they disappear from the page.`<layer>`or`opt_<name>=<value>`. These are often better semantically, but it is a larger editorial change.Each of these only fixes the existing lines. New docs written for GitHub/CommonMark, or converted with pandoc, would bring
\<back unless a lint check is added.pymdownx.escapeall(suggested in A line ending that is preceded by a backslash is not parsed as a hard line break Python-Markdown/markdown#1464). This makes every backslash an escape, which goes further than CommonMark and breaks existing docs:%APPDATA%\postgresql\pgpass.confin grass-pg.md would render as%APPDATA%postgresqlpgpass.conf.C:\\Program Files\\Java\\...in addons r.maxent.setup and m.printws would lose its backslashes.Post-process pandoc output in
utils/grass_html2md.sh. The conversion is already done, so this would only affect future conversions and would not fix the existing files.MkDocs hook (the first version of this PR). The hook registered the same extension and needed no
PYTHONPATHsetting, because MkDocs loads hooks by file path. Zensical has no Python hooks, so it would not survive the switch (docs: Make edit links and sitemap independent of the site generator #7938).Replace
\<with<when the pages are generated, inutils/mkmarkdown.py. This works with any Markdown renderer and leaves the sources unchanged, but it needs its own detection of code spans and fenced code, and it covers only pages generated by that script.Wait for the switch to Zensical. Zensical 0.0.71 uses Python-Markdown and renders
\<with the same stray backslash, so the switch alone does not fix it.Suggested squash-merge commit message
The extension and this description were written with Claude Code (Claude Opus 5.5) and reviewed by the author of the PR.