| f9d3b7bd | 05-May-2026 |
Andreas Gohr <andi@splitbrain.org> |
Externallink: add per-scheme angle-bracket autolinks for MD syntax
Adds CommonMark §6.5 <URL> autolinks to Externallink, gated to md/md+dw/dw+md syntax via ModeRegistry::isMdPreferred(). Per-scheme
Externallink: add per-scheme angle-bracket autolinks for MD syntax
Adds CommonMark §6.5 <URL> autolinks to Externallink, gated to md/md+dw/dw+md syntax via ModeRegistry::isMdPreferred(). Per-scheme patterns share the existing conf/scheme.conf allow-list so unknown schemes fall through to literal cdata instead of being silently dropped by the renderer. Internal whitespace inside the brackets disqualifies the autolink and the whole envelope is emitted as cdata to keep the bare-URL detector off the URL.
LinksTest gains 5 cases covering success, internal-whitespace and leading-whitespace disqualification, unregistered scheme fallthrough, and the dw-only no-op path. SpecCompatRenderer URL encoder is updated to match cmark-gfm's HREF_SAFE table (square brackets and a few other characters move from safe to encoded). skip.php loses the obsolete #356 entry and gains #605/#606/#607/#609 explaining the unregistered- scheme cases that the per-scheme regex naturally rejects.
show more ...
|
| c2248fda | 05-May-2026 |
Andreas Gohr <andi@splitbrain.org> |
Block: collapse [ \t]*\n[ \t]* to \n inside paragraphs
Strip [ \t]+ on either side of the soft-break joiner emitted for a single eol, and ltrim the first cdata of each paragraph. Without this, DokuW
Block: collapse [ \t]*\n[ \t]* to \n inside paragraphs
Strip [ \t]+ on either side of the soft-break joiner emitted for a single eol, and ltrim the first cdata of each paragraph. Without this, DokuWiki preserved leading/trailing whitespace on continuation lines verbatim, which is invisible in HTML but may visible in plain-text and other renderers. It is also a requirement in the Markdown spec.
Re-baseline the parser-mode tests that pinned the old preserve behavior (cdata adjacent to <code>/<file>/<rss>/header/footnote).
show more ...
|
| d379b737 | 05-May-2026 |
Andreas Gohr <andi@splitbrain.org> |
GfmSpecTest: neutralize DW typography for spec roundtrip
Force $conf[typography] = 0 in renderMarkdown() so the Quotes and MultiplyEntity modes are not loaded, override entity() in SpecCompatRendere
GfmSpecTest: neutralize DW typography for spec roundtrip
Force $conf[typography] = 0 in renderMarkdown() so the Quotes and MultiplyEntity modes are not loaded, override entity() in SpecCompatRenderer to emit the original match instead of the typographic glyph, and switch _xmlEntities() from ENT_QUOTES to ENT_COMPAT so `'` stays literal in body text while `"` is still escaped to ". Drops three skip entries (#308, #310, #353) that existed only to paper over the same divergence and unblocks #16, #25 and #670.
show more ...
|
| f57da51c | 05-May-2026 |
Andreas Gohr <gohr@cosmocode.de> |
Preformatted: leave boundary \n in stream when next line has content
Adds a zero-width lookahead exit (?=\n[^ \t\n]) ahead of the existing consuming \n exit. When an indented code block is followed
Preformatted: leave boundary \n in stream when next line has content
Adds a zero-width lookahead exit (?=\n[^ \t\n]) ahead of the existing consuming \n exit. When an indented code block is followed by a non- blank line, the boundary newline now stays available for downstream block-level matchers (GfmHr, GfmHeader, etc.) instead of being eaten on the way out of preformatted mode.
Concretely fixes a thematic-break-after-indented-code case (GFM spec case 85's trailing ----): without this change, GfmHr's \n anchor failed because preformatted had already consumed the newline, and the bare ---- fell through to Entity which converted --- to an em-dash.
The consuming branch is kept as a fall-through for the blank-line and end-of-input cases, where a pure lookahead would trip the lexer's no-advance safety check.
Six PreformattedTest expectations updated: trailing cdata after a preformatted block now carries the leading \n (rendered output is unchanged — paragraph whitespace is trimmed).
show more ...
|
| ab6ac090 | 04-May-2026 |
Andreas Gohr <andi@splitbrain.org> |
add tests pinning blank-line tolerance in GFM listblock |
| b37c6ef7 | 04-May-2026 |
Andreas Gohr <andi@splitbrain.org> |
more test skips |
| 6359e7fd | 04-May-2026 |
Andreas Gohr <andi@splitbrain.org> |
percent-encode URLs in SpecCompatRenderer to match spec output
CommonMark's reference renderer percent-encodes URL bytes outside the RFC 3986 unreserved/reserved set (and existing %XX sequences pass
percent-encode URLs in SpecCompatRenderer to match spec output
CommonMark's reference renderer percent-encodes URL bytes outside the RFC 3986 unreserved/reserved set (and existing %XX sequences pass through unchanged). DokuWiki's XHTML renderer leaves UTF-8 and backslashes literal in href, which is fine for live wiki output but diverges byte-for-byte from spec.
Adds specEncodeUrl() to the spec-compat renderer and applies it in specLink(). Same shape as the earlier `→`->`\t` substitution: a test-harness alignment with spec convention, no production behavior change.
Unskips #510 (backslash in URL) and #511 (entity / percent-encoding in URL); both now match spec output with the parser-side decoding from the previous commit and the renderer-side encoding here.
show more ...
|
| eb15e634 | 04-May-2026 |
Andreas Gohr <andi@splitbrain.org> |
extract Helpers\HtmlEntity, wire into GfmCode and GfmLink URL slot
Numeric and named HTML entity decoding moves out of GfmHtmlEntity into a pure helper, so capture-by-regex modes can apply the same
extract Helpers\HtmlEntity, wire into GfmCode and GfmLink URL slot
Numeric and named HTML entity decoding moves out of GfmHtmlEntity into a pure helper, so capture-by-regex modes can apply the same decode post-extraction (the inline lexer never reaches their bodies). Mirrors the Helpers\Escape pattern.
Wired up in two slots:
- GfmCode info string: föö now decodes to föö in the language class. Clears spec example #330.
- GfmLink URL: GfmLink::extractUrl() decodes entities. URL pattern extends from `[^)\n]+` to `(?:\\.|[^)\n])+` so an escaped \) no longer terminates the URL early; the existing post-classify Escape::unescapeBackslashes call strips the backslashes after Link::classify has done its work. Clears #504, #506, #508.
Skip #328 with a self-contained title-slot reason: the URL side now decodes correctly, but the title attribute is still discarded (DokuWiki link instructions have no title slot).
show more ...
|
| d2085866 | 04-May-2026 |
Andreas Gohr <andi@splitbrain.org> |
extend GfmNumericEntity to HTML5 named entities, rename to GfmHtmlEntity
Numeric refs are still decoded explicitly: PHP's html_entity_decode returns the input unchanged for U+0000, surrogates, U+10F
extend GfmNumericEntity to HTML5 named entities, rename to GfmHtmlEntity
Numeric refs are still decoded explicitly: PHP's html_entity_decode returns the input unchanged for U+0000, surrogates, U+10FFFF, and BMP noncharacters where CommonMark requires U+FFFD or the literal codepoint. Named refs delegate to html_entity_decode with ENT_HTML5, which carries the full HTML5 named-entity table (including multi- codepoint decodes like ≧̸ -> U+2267 + U+0338).
Unknown names stay literal: the original &xxx; passes through as cdata and the renderer's &-escaping turns it into &xxx;.
show more ...
|
| 09f34c31 | 04-May-2026 |
Andreas Gohr <andi@splitbrain.org> |
apply spec convention: → represents a tab in GfmSpecTest
CommonMark spec.txt uses U+2192 RIGHTWARDS ARROW to visually mark literal tab characters in examples (see spec.txt, "About this document"). S
apply spec convention: → represents a tab in GfmSpecTest
CommonMark spec.txt uses U+2192 RIGHTWARDS ARROW to visually mark literal tab characters in examples (see spec.txt, "About this document"). Substitute → for \t in both markdown input and expected HTML so the corpus exercises real tab handling.
Surfaced by GfmNumericEntity: example #336 (	foo) now decodes the entity to a tab and produces correct output, but the harness was comparing against literal → in the expected HTML.
show more ...
|
| 150dc5f2 | 04-May-2026 |
Andreas Gohr <andi@splitbrain.org> |
add GfmNumericEntity for CommonMark numeric character references
Decodes &#nnn; (decimal, 1-7 digits) and &#xhhh; / &#Xhhh; (hex, 1-6 digits) to the corresponding Unicode codepoint, emitted as plain
add GfmNumericEntity for CommonMark numeric character references
Decodes &#nnn; (decimal, 1-7 digits) and &#xhhh; / &#Xhhh; (hex, 1-6 digits) to the corresponding Unicode codepoint, emitted as plain cdata. Codepoint 0, codepoints above U+10FFFF, and the surrogate range U+D800..U+DFFF map to U+FFFD per the spec.
Distinct from the typography Entity mode, which is renderer-side configurable via entities.conf. Numeric refs are not configurable so decoding happens at parse time and the renderer needs no changes.
Lexer leftmost-match consumes the run before any structural pattern, so *foo* renders as literal *foo* and * foo does not start a list - matching the spec rule that numeric refs cannot stand in for structural markers.
show more ...
|
| b414dba2 | 04-May-2026 |
Andreas Gohr <gohr@cosmocode.de> |
skip a few more spec tests
Those are all deliberately not supported cases |
| 13a62f81 | 04-May-2026 |
Andreas Gohr <andi@splitbrain.org> |
rename syntax flavors 'dokuwiki' / 'markdown' to 'dw' / 'md'
Symmetry with the existing 'dw+md' / 'md+dw' setting values. |
| c4bcbc2e | 04-May-2026 |
Andreas Gohr <andi@splitbrain.org> |
add GfmLinebreak for GFM hard line breaks
Two-or-more trailing spaces, or a single backslash, immediately before a non-final newline render as a `<br/>`. Both delimiter forms share a single SUBSTITI
add GfmLinebreak for GFM hard line breaks
Two-or-more trailing spaces, or a single backslash, immediately before a non-final newline render as a `<br/>`. Both delimiter forms share a single SUBSTITION mode at sort 140, loaded under any MD-active syntax (markdown, dw+md, md+dw); pure dokuwiki is unaffected.
Reuses the existing `linebreak` handler call and renderer; no new instructions or renderer changes. SpecCompatRenderer overrides linebreak() to emit the spec's `<br />` shape. Examples 662, 663 (line break inside a raw HTML tag) are skipped — raw HTML is not passed through by default.
show more ...
|
| 3e6baeff | 30-Apr-2026 |
Andreas Gohr <andi@splitbrain.org> |
replace DW Hr with unified GfmHr
Single mode covers both DokuWiki (4+ dashes) and GFM (3+ of -/*/_) horizontal rules; pattern self-narrows on $conf['syntax']. Always loaded across all four syntax se
replace DW Hr with unified GfmHr
Single mode covers both DokuWiki (4+ dashes) and GFM (3+ of -/*/_) horizontal rules; pattern self-narrows on $conf['syntax']. Always loaded across all four syntax settings, mirroring the GfmQuote replacement pattern. Same `hr` handler call so renderers and the call API are unchanged.
Drops DW's old [ \t]* leading-whitespace tolerance — inert in practice past 0-1 spaces (Preformatted at sort 20 intercepts everything ≥ 2 spaces or any tab).
Spec examples 13, 20, 26-28, 224 turn green; 17, 21-24, 29, 30, 31 go to skip.php as deliberate non-implementations (whitespace tolerance and list-precedence cases).
show more ...
|
| 309a0852 | 30-Apr-2026 |
Andreas Gohr <andi@splitbrain.org> |
replace DW Quote with unified GfmQuote
GfmQuote covers blockquote parsing for both DokuWiki and GFM dialects in a single mode. Same quote_open/quote_close handler instructions; a DW-preferred post-p
replace DW Quote with unified GfmQuote
GfmQuote covers blockquote parsing for both DokuWiki and GFM dialects in a single mode. Same quote_open/quote_close handler instructions; a DW-preferred post-pass flattens sub-parsed paragraph wrapping into linebreak calls so existing pages keep their <br/>-between-lines rendering. MD-preferred keeps the <p>-wrapped spec shape.
Block content (lists, fenced code, tables) inside `>` quotes now renders, since the body is sub-parsed. Headers stay excluded (BASEONLY) — TOC and section-edit anchors don't compose with <blockquote>, same rationale as GfmListblock.
Convert ModeRegistry's sub-parser cache into an acquire/release pool to support same-key re-entrancy: a list inside a quote re-enters gfm_quote during the list-item sub-parse, and the inner call needs its own parser instance even though the exclusion key matches. GfmListblock is updated to use the new acquire/release primitives.
show more ...
|
| f7c6e4ac | 30-Apr-2026 |
Andreas Gohr <gohr@cosmocode.de> |
add listo_open_start sibling method for GFM start numbers
Reverts the listo_open signature widening from 5a2118acc and instead adds a sibling method `listo_open_start($start = 1)` on the renderer hi
add listo_open_start sibling method for GFM start numbers
Reverts the listo_open signature widening from 5a2118acc and instead adds a sibling method `listo_open_start($start = 1)` on the renderer hierarchy. The base default delegates to listo_open() so renderers that don't override it still produce a valid (but unnumbered) list; xhtml's override emits <ol start="N">.
The handler now emits 'listo_open_start' only for ordered lists with a non-default first number; plain ordered lists keep emitting the unchanged 'listo_open' instruction. This preserves the historical listo_open / listu_open signatures (zero-arg base, $classes-only xhtml form from 2016) so the 17 plugin renderers found via codesearch keep working without modification, while still implementing GFM's "5. foo" -> <ol start="5"> rule.
show more ...
|
| 74031e46 | 28-Apr-2026 |
Andreas Gohr <andi@splitbrain.org> |
add GfmEscape for GFM backslash escapes
Implements GFM §6.1 backslash-escape handling. GfmEscape is a sort-5 inline mode in CATEGORY_SUBSTITION that claims `\X` for any escapable ASCII punctuation c
add GfmEscape for GFM backslash escapes
Implements GFM §6.1 backslash-escape handling. GfmEscape is a sort-5 inline mode in CATEGORY_SUBSTITION that claims `\X` for any escapable ASCII punctuation char before competing delimiters can match. The shared character class lives on Helpers\Escape so the lexer pattern and the post-hoc unescape stay in lockstep.
Whole-span captures (GfmCode info string, GfmLink label/URL) bypass the lexer; those modes call Escape::unescapeBackslashes() on the relevant slot. GfmLink skips the unescape when the URL classifies as a windowssharelink so the leading \\host survives intact.
GfmTable cells get a separate per-cell `\|` to `|` pass in the rewriter to honour the tables-extension rule that pipes always unescape, even inside code spans where standard §6.1 escapes don't fire.
show more ...
|
| 3dabe4e0 | 28-Apr-2026 |
Andreas Gohr <andi@splitbrain.org> |
add GfmTable for GFM tables
Implements the GFM pipe-table extension as a CONTAINER mode at sort 55, one below DW Table at 60. A lookahead-validated entry pattern asserts a header line plus a `:?-+:?
add GfmTable for GFM tables
Implements the GFM pipe-table extension as a CONTAINER mode at sort 55, one below DW Table at 60. A lookahead-validated entry pattern asserts a header line plus a `:?-+:?` delimiter row before consuming any input, so non-table paragraphs containing pipes flow through unchanged. Cells are inline-only per spec.
Handler\GfmTable rewrites the flat token stream into the canonical table_open / tablethead_* / tabletbody_* / table_close sequence, deriving per-column alignment from the delimiter row, padding short body rows (spec 202), truncating long ones (spec 204), and falling back to a single cdata when the column count mismatches (spec 203).
`tabletbody_open` / `tabletbody_close` are emitted for the first time; they are part of the base renderer API but DW Table never used them. Added to Block's blockOpen / blockClose lists alongside `tabletfoot_*` for symmetry. SpecCompatRenderer gains minimal table-element overrides so spec roundtrip output matches GFM's `<table><thead><tr><th>` shape without DW's wrapper div, row/col counter classes, or align-as-class.
show more ...
|
| 685560eb | 28-Apr-2026 |
Andreas Gohr <andi@splitbrain.org> |
add GfmListblock for GFM lists
GfmListblock captures an entire list block atomically with one addSpecialPattern match, then walks the captured text in handle() grouping lines into items. Each item's
add GfmListblock for GFM lists
GfmListblock captures an entire list block atomically with one addSpecialPattern match, then walks the captured text in handle() grouping lines into items. Each item's body is dedented to its content column and parsed by ModeRegistry::getSubParser() so block content (paragraphs, fenced code, blockquotes, plugin blocks) works inside items uniformly. Sub-parsed calls are wrapped in a Nest call before they reach the outer handler, matching the Footnote pattern: the main handler's Block rewriter treats nest as opaque and the renderer base class unwraps it transparently, so multi-paragraph items don't get double-wrapped in <p>.
Marker syntax: -, *, + (unordered) or 1-9 digits followed by . or ) (ordered). Indentation is a 2-space-multiple step starting at 0; depth = (indent / 2) + 1, odd indents round down, tabs become two spaces. The first ordered item's number drives the start attribute on <ol> via the listo_open $start parameter.
GfmLists subclasses AbstractListsRewriter with the GFM marker parser; the state machine on the base class is shared with DW Lists.
GfmListblock loads only when $conf['syntax'] is markdown or md+dw. Under those settings the DW Listblock is suppressed because the two list models conflict — DW's mandatory 2-space indent rule vs GFM's zero-indent top-level rule, and -/*/+ markers shared. Plugins that relied on Listblock loading under md+dw will see it absent there.
Sub-parser exclusion set: CATEGORY_BASEONLY (no Header inside list items) and gfm_listblock itself (defensive guard against re-entry on pathological inputs; nested lists are handled by the outer pattern, not by re-entry).
Tests cover marker variants, ordered start numbers, nested lists at two and three levels, inline formatting inside items, marker- character switches keeping one list, type switches splitting the list, fenced code inside items, multi-paragraph (loose) items, and two regressions on blank-line tolerance inside the captured block. SpecCompatRenderer learns to render the list call sequence, and spec.txt tests for digit/marker-width/lazy-continuation behavior that GfmListblock deliberately doesn't implement are documented in gfm-spec/skip.php with the per-bucket reasons (A-F).
Drops two now-obsolete entries from skip.php (image escapes that land via earlier GfmLink/GfmMedia work) and inlines the Setext explanation that previously pointed at SPEC.md. Replaces the SPEC.md reference in GfmEmphasisTest with the inline reason.
show more ...
|
| 9172eccf | 28-Apr-2026 |
Andreas Gohr <andi@splitbrain.org> |
add sub-parser support to Handler / Parser / ModeRegistry
A block mode that wants to parse the body of one of its captured matches needs a second Parser instance configured with the active modes min
add sub-parser support to Handler / Parser / ModeRegistry
A block mode that wants to parse the body of one of its captured matches needs a second Parser instance configured with the active modes minus whatever would re-enter the outer mode. Doing this by hand is verbose and easy to get wrong — modes hold a $Lexer slot that addMode() overwrites, so the same mode object can't be shared between the main parser and a sub-parser.
Three small additions:
Handler::reset() — clears calls, status, currentModeName, and installs a fresh CallWriter. Lets one Handler instance be parsed against repeatedly without state bleed.
Parser::getHandler() — accessor; sub-parser callers need it to reach the handler for reset() and for harvesting the produced call list.
ModeRegistry::getSubParser($excludeCategories, $excludeModes) — returns a cached Parser preconfigured with every active mode except those excluded. Mode objects are cloned before being attached so connectTo()'s assignment to $Lexer does not clobber the main parser's references. Cache key is the exclusion-set; default exclusion is CATEGORY_BASEONLY (no Header inside the sub-parsed content).
Tests cover Handler::reset's full clear, sub-parser caching, default and custom exclusions, registry-reset propagation, and the clone-not-share invariant for $Lexer.
show more ...
|
| 96d096f1 | 27-Apr-2026 |
Andreas Gohr <andi@splitbrain.org> |
remove getLineStartMarkers registry — sort order already wins
Preformatted's entry pattern carried a `(?![\*\-])` negative lookahead to defer to list modes on indented bullet lines. 0cecf9d50 (2005,
remove getLineStartMarkers registry — sort order already wins
Preformatted's entry pattern carried a `(?![\*\-])` negative lookahead to defer to list modes on indented bullet lines. 0cecf9d50 (2005, "new parser added") introduced it hardcoded; 7958e6980 (2026, "decouple hardcoded mode names in Eol and Preformatted") refactored that hardcoded knowledge into register/getLineStartMarkers on ModeRegistry so each list mode owned its marker chars. Both preserved the behavior verbatim; neither documented why it was needed.
Tracing the lexer, it isn't. ParallelRegex merges all entry patterns into one PCRE expression; PCRE returns the leftmost match and breaks ties on expression order. Modes are added in sort order via ModeRegistry::getModes(), so Listblock (sort 10) always precedes Preformatted (sort 20) and wins the tie on " - foo" without any lookahead. The only test that caught a difference was testPreformattedList, which happened to register modes in non-canonical order - that was a test bug.
This patch drops the lookahead in Preformatted::connectTo, the registerLineStartMarkers call in Listblock::preConnect, the register/getLineStartMarkers methods on ModeRegistry, and the three registry-API unit tests. testPreformattedList now registers Listblock before Preformatted.
show more ...
|
| 1e28e406 | 23-Apr-2026 |
Andreas Gohr <andi@splitbrain.org> |
split Parsing\Helpers into per-domain Link / Media / Code classes |
| 781f5c71 | 23-Apr-2026 |
Andreas Gohr <andi@splitbrain.org> |
gate monospace, unformatted, file on DokuWiki syntax
These DokuWiki specific modes should only be loaded when DokuWiki syntax is still wanted, not in Markdown-only mode. Expands the ModeRegistryTest
gate monospace, unformatted, file on DokuWiki syntax
These DokuWiki specific modes should only be loaded when DokuWiki syntax is still wanted, not in Markdown-only mode. Expands the ModeRegistryTest data provider to cover the full always-loaded and DW-always sets.
show more ...
|
| b1c59bed | 23-Apr-2026 |
Andreas Gohr <andi@splitbrain.org> |
add GfmCode / GfmFile for fenced code blocks
GfmCode (backticks) emits the `code` handler instruction; GfmFile (tildes) emits `file`. Column-0 fences only, no length pairing between opener and close
add GfmCode / GfmFile for fenced code blocks
GfmCode (backticks) emits the `code` handler instruction; GfmFile (tildes) emits `file`. Column-0 fences only, no length pairing between opener and closer, and unclosed fences stay literal — matching DokuWiki's `<code>` tag convention. The info string accepts DW's full attribute vocabulary (language, filename, [options]) through a new shared `Helpers::parseCodeAttributes` that `Code` also uses, with `html` aliased to `html4strict` and `-` meaning "no language".
Preformatted's indent threshold is now preference-gated: 2 spaces in DW-preferred settings, 4 spaces in MD-preferred, matching GFM's indented code block rule. A single tab is a trigger in both.
show more ...
|