Skip to content

Experimental - Wikitext serialize tests - for discussion#9922

Draft
pmario wants to merge 25 commits into
TiddlyWiki:masterfrom
pmario:wikitext-serialize-tests
Draft

Experimental - Wikitext serialize tests - for discussion#9922
pmario wants to merge 25 commits into
TiddlyWiki:masterfrom
pmario:wikitext-serialize-tests

Conversation

@pmario

@pmario pmario commented Jul 12, 2026

Copy link
Copy Markdown
Member

Changed files at a glance. This experimental PR contains some core changes, that will be separated. See #9908

Group Files What to look for
plugins/tiddlywiki/wikitext-serialize/rules/ 26 One serializer module per wiki rule. Each rule first tries to reuse the original source text. If that is not possible, it rebuilds the wikitext from the parse tree. Rules no longer emit the separators between blocks.
plugins/tiddlywiki/wikitext-serialize/utils/parsetree.js 1 The central walker. It dispatches to the rule serializers and adds the separators between blocks. In fidelity mode it splices the original whitespace back in. It also detects attribute quoting styles. All shared helpers live here.
core/modules/parsers/wikiparser/ 11 The parser side. wikiparser.js trims block spans and stamps the block position. The ten rule files add small annotations, fix the terminator bugs and wrap hard linebreak regions. Each file belongs to one sub-issue below.
editions/test/tiddlers/tests/*.js 8 The test drivers. test-wikitext-serialize.js runs four groups of tests. Six new spec files cover the core changes, one file per change. Each of them fails without its fix.
editions/test/tiddlers/tests/data/serialize/ 90 The test data. Identity fixtures round-trip their own text byte for byte. Normalize pairs compare an Input against an ExpectedWikitext. Every file has a one line description. The pairs also contain an info tiddler that explains the test.

A side effect of these experiments is a mechanism, that will allow us to create a "pretty printer" for TW wikitext.

pmario added 24 commits July 8, 2026 17:10
Extend the wikitext-serialize fixtures with documented but uncommon
syntax, mainly containment forms like triple quoted attributes.

* Source the cases from the tw5.com docs; undocumented parser
  behaviour may still be missing.
* Cover quote containment in attributes, macro parameters and
  parameter defaults, plus substituted values with embedded backticks.
* Cover nested definitions with named \end, nested conditionals,
  nested block quotes and single line procedures.
* Cover table pseudo rows and cell merging, description list nesting,
  quote lists, heading classes and ext link schemes.
* Add a fixture for the 5.4.0 ((var)) display syntax.
* Expect 16 of 51 fixtures to fail: they encode behaviour the
  serializer cannot reproduce yet.
Separate upstream fixtures from the new coverage so a failing spec
points at a small single purpose file.

* Restore the original X.tid fixtures to their upstream content; they
  form a control group that must stay green.
* Move the docs-based cases from the last commit into X-docs.tid files.
* Move the parser-parity cases into X-new.tid files.
* Expect 19 of 77 specs to fail; all failures are in the new files.
Use parse tree source positions to reproduce the original formatting of
attributes and macro parameters, and add normalization tests.

* Thread the serializer options through rule serializers and
  serializeAttribute; the test driver now passes the tiddler source.
* Detect the original quote style (single, double, triple, bracket,
  backtick, unquoted, valueless) from the source via node positions and
  reuse it when it still fits the value; fall back to content-driven
  minimal quoting instead of always double quotes.
* Serialize ((var)) attribute references and keep the space before >>
  when a parameter ends with >.
* Add a normalization test harness: compound tiddlers tagged
  wikitext-serialize-normalize-spec hold Input and ExpectedOutput
  subtiddlers, with an optional canonical mode without source access.
* Add five X-normalized fixtures; failures drop from 19 to 13, the
  rest are structural rule gaps.
Make the \whitespace pragma round-trippable and reproduce definition
pragmas verbatim from their source slice.

* Return a void parse tree node from the core whitespace rule, exactly
  like \rules does; it was the only pragma leaving no trace in the
  tree, so serializers could never see it. Rendering is unchanged and
  the cost is within benchmark noise (0.1% on a PageTemplate render).
* Add $tw.utils.serializeFromSource returning a node's exact source
  slice, guarded by fragments that must still appear in it, so edited
  trees fall back to canonical serialization.
* Use the slice in the macrodef and fnprocdef serializers to preserve
  named \end markers, default quoting and single line definitions.
* Add a whitespace serializer rule that recovers the pragma separator
  from the source gap; the canonical fallback emits a blank line.
* Rename the normalize fixture subtiddler ExpectedOutput to
  ExpectedWikitext to avoid confusion with the rendered ExpectedResult
  of the wiki test specs.
* Add fixtures: whitespace round-trip and spacing normalization, \end
  trailing spaces for \procedure, whitespace pragmas and indented
  nested definitions inside procedure bodies; the definition fixtures
  now round-trip verbatim.
Serialize the three inline rules that still lost formatting or
content: html, image and the multi valued variable display.

* Slice the opening tag from the source so multi line attribute
  layouts survive; a fragment check falls back to canonical
  serialization for mutated attributes.
* Restore whitespace only gaps between children from the source; they
  cover line breaks eaten by \whitespace trim and the blank line that
  switches an element to block mode, and replace the fixed blank line
  joiner of block rules with the true gap before a close tag.
* Omit the close tag when the parser recorded an implicit one.
* Serialize all image attributes in order instead of only width and
  height, so class attributes are no longer dropped.
* Add the mvvdisplayinline serializer: decompile the join filter the
  parser generates back into the ((var)) and (((filter))) forms.
* Turn the attribute whitespace fixture canonical: the opening tag
  slice preserves the spacing in fidelity mode, and canonical html
  attributes quote their values.
Fix the conditional serializer losing else content and reproduce the
original clause layout from the source.

* Stamp isConditional on the list widgets synthesized by parseIfClause
  and record each elseif clause span, so consumers can tell synthesized
  clauses from real $list widgets without sniffing attributes.
* Distinguish elseif clauses by that flag; a nested <%if%> or a $list
  widget at the start of an else body was misread as an elseif clause
  and mangled or dropped.
* Stitch the serialized clause bodies together with the source text
  between them: the clause markers only exist in the source, and the
  stitch preserves marker spacing and clause indentation. Mutated
  trees fall back to a simplified canonical emitter.
Serialize inline code runs with the delimiter they were written with,
or the minimal safe delimiter without source access.

* Read the original delimiter from the source at the node position;
  the parse tree does not record whether ` or `` matched.
* Fall back to double backticks when the text contains a backtick or
  is empty, eg ``a`b`` and ````; two bare backticks would reparse as
  an opening double delimiter.
* Extend the fixtures with embedded, leading and empty backtick runs
  and pin the canonical minimal delimiter choice.
Stop inventing a blank line between a pragma area comment and the
following pragma.

* Recover the separator from the source gap between the comment and
  its first chained child; a comment directly above a \define kept a
  single newline in the source but serialized with a blank line.
* Keep the blank line as the canonical fallback for body comments and
  trees without source access.
* Extend the fixture to pin both separator styles: a comment directly
  above the pragma and one separated by a blank line.
ATTENTION: this commit contains a core parse tree change that needs its
own upstream issue and PR, separate from the serializer plugin PR. Move
core/modules/parsers/wikiparser/rules/hardlinebreaks.js and
editions/test/tiddlers/tests/test-wikitext-parser.js into that PR.

plugins/tiddlywiki/wikitext-serialize/rules/hardlinebreaks.js belongs
to the serializer PR but requires the core change to be merged first.

Give the hard linebreak region a container node so inner content keeps
its own rule names.

* Return a single void wrapper from the core rule instead of a flat
  node array; the rule stamping overwrote inner rule names, e.g. bold
  inside a region became rule hardlinebreaks, which lost the producer
  for serialization and source mapping.
* Stamp the br nodes explicitly and retire the isRuleStart and
  isRuleEnd flags; the wrapper carries the region span.
* Serialize the region as fences plus children; formatted content now
  dispatches to its own rules, and the opening fence form ("""text or
  a line break) is recovered from the source.
Needs UPSTREAM PR: $:/core/modules/parsers/wikiparser/rules/quoteblock.js

Room for improvements:
Extract the shared stitch helper (TiddlyWiki5-99v); fix the terminator
newline swallow (TiddlyWiki5-40b); drop the serializeCanonical legacy
fallbacks, core and plugin ship together.

Record what the quote parser knows and reproduce quotes from the
source instead of dropping citations, classes and nesting.

* Record marker, userClasses and isQuoteCite in the core rule: the
  class attribute fuses user classes with the synthesized tc-quote,
  and without the marker depth a serializer cannot emit parseable
  nested quotes at all.
* Serialize citations on the opening and closing lines, classes in
  dot form and nested quotes; cite children were dropped and every
  quote was labelled with the class text tc-quote.
* Stitch the quote from the source in fidelity mode, preserving
  marker depth, indentation and citation spacing; strip exactly one
  blank line joiner so a real paragraph-final newline survives.
* Extend the fixture with a five deep marker nesting case.
Needs UPSTREAM PR: $:/core/modules/parsers/wikiparser/rules/list.js

Room for improvements:
The row joiner gap logic is another cousin of TiddlyWiki5-99v.

Record what the list parser knows and fix multi class items and blank
line separators.

* Record rowMarker and blankLineBefore on list item nodes: the parser
  walks the marker string and consumes the blank line between merged
  lists, then dropped both, forcing serializers to recompute markers
  from the tag nesting and read separators from the source.
* Give every item class its own dot, eg *.first.second; multiple
  classes were emitted with a single dot and inner spaces.
* Collect one row per item line; the blankLineBefore flag reproduces
  the separator without source access and the source gap refines it
  to the exact bytes.
Room for improvements:
The leaf stitch is another cousin of TiddlyWiki5-99v; a mode canonical
fixture for the grid reconstruction path is still missing.

Serialize every documented table feature instead of plain grids only,
and stop crashing on captions.

* Stitch the table from the source between the inline leaf nodes of
  the cells and the caption: pipes, merge markers, alignment spaces
  and class rows only exist in the source. Containers are re-sorted
  by position because the parser moves the caption to the front.
* Rebuild the grid canonically without source access: rowspan and
  colspan attributes are re-emitted as ~ and < marker cells, cell
  alignment as the space convention, valign as ^ and , prefixes, and
  captions, headers, footers and class rows as their pseudo rows.
* Stop reading the caption container as a row list; its inline
  content crashed the old code on any |c row.
Avoid the collision with the established _canonical_uri field by
calling the tree-only fallback path "ast-only" in the test harness and
the plugin.

* Switch the mode field to ast-only in the eight normalize fixtures
  and reword their descriptions
* Rename Attribute-normalized-canonical.tid to Attribute-normalized-quotes.tid
* Update the mode check and its comment in test-wikitext-serialize.js
* Rename serializeCanonical to serializeFromTree
* Reword the remaining canonical comments in parsetree.js and
  mvvdisplayinline.js
Deduplicate the source stitching so each rule shrinks to its tree-only
emitter plus one helper call.

TODO:
Fold the list.js row joiner refinement onto recoverSourceGap; fix the
quoteblock terminator newline swallow (TiddlyWiki5-40b).

* Add $tw.utils.serializeStitched with options.children and
  options.isBoundary, stripping exactly one block joiner per gap
* Add $tw.utils.recoverSourceGap for separators the tree does not record
* Collapse the stitch copies in conditional.js, quoteblock.js and table.js
* Stitch the html inner region instead of the per-gap restoreGap loop
* Recover the pragma gaps in whitespace.js and commentblock.js with the
  helper
* Drop the stray trailing newline after fnprocdef children
Prove the tree-only serializer guarantee across the whole fixture
corpus and fix every defect the sweep surfaced.

Needs UPSTREAM PR: $:/core/modules/parsers/wikiparser/rules/html.js
Needs UPSTREAM PR: $:/core/modules/parsers/wikiparser/rules/quoteblock.js
(amends the quoteblock annotation already flagged in aa2f0ea)

* Add a third describe block that serializes every identity fixture
  without source, re-parses the output and deep-compares the trees;
  the comparison helper drops position fields and undefined keys and
  sorts orderedAttributes by name, so it does not depend on the
  assertion framework
* Drop the final block joiner at the root, keeping the line end a
  trailing <<macro>> call or block widget needs
* Quote macrodef and fnprocdef parameter defaults with the shared
  $tw.utils.quoteParameterDefault and emit ((var)) MVV defaults
* Capture the quoteblock marker before a nested quote overwrites
  this.match, so an outer <<< no longer serializes as <<<<
* Record blockPosition and blockContent on html nodes; isBlock fuses
  the parse position with the content mode and cannot be split again
* Emit the html trailing joiner from blockPosition and the blank line
  after the open tag from blockContent
* Update the parse tags expectations for the new html annotations
* Document each describe block with a browser console recipe
ATTENTION: this commit is a standalone core bug fix that needs its own
upstream issue and PR, separate from the serializer plugin PR. It
contains only core parser rules and one new test file, so it can be
cherry-picked as a unit. There is no dependency in either direction
with the serializer plugin.

A single newline before a closing marker was swallowed into the
preceding paragraph text node, while the blank line form was not.

* Anchor the quoteblock and styleblock terminators from the newline
* Prefix the html and conditional block terminators with an optional
  newline; a mandatory anchor would break legal mid-line close tags
* Leave the inline paths unchanged: a newline in an inline run is real
  content
* Correct the (:?^| typo to (?:^| in the fnprocdef end marker regexp
* Add test-wikitext-block-terminators.js, red without the rule fixes
Keep block conditional bodies block when serializing from the tree
alone: the blank line after a clause marker was not recorded anywhere,
so tree-only output re-parsed to inline clauses.

Needs UPSTREAM PR: $:/core/modules/parsers/wikiparser/rules/conditional.js
(extends the conditional annotations from d905319)

* Stamp blockContent on the clause body containers in the core rule,
  mirroring the html annotation
* Emit the blank line after each clause marker from blockContent in
  serializeFromTree
* Add Conditional-new.tid covering block bodies closed by a single
  newline; its AST-only sweep spec was red before the annotation
* Extend Html-new.tid with block content ending at the close tag
Give every core annotation unit its own red test, so each planned
upstream PR ships with a spec that fails without its rule change.

TODO:
Move each spec file into its upstream PR per the extraction recipes in
beads-planning plans/upstream-*-issue-pr.md.

* Add test-wikitext-whitespace-pragma.js pinning the void marker node
* Add test-wikitext-conditional-annotations.js pinning isConditional,
  the elseif clause span and blockContent
* Add test-wikitext-quoteblock-annotations.js pinning marker,
  userClasses and isQuoteCite, plus the nested re-entrancy case
* Add test-wikitext-list-annotations.js pinning rowMarker and
  blankLineBefore
Feed all 237 Output subtiddlers of the wiki-based rendering tests
through the fidelity round-trip, then fix every gap the corpus
surfaced.

Needs UPSTREAM PR: $:/core/modules/parsers/wikiparser/rules/html.js
(records openTagStart/openTagEnd for self-closing tags too; belongs
to the html annotations unit)

* Add a describe block round-tripping every Output subtiddler
* Stitch the root blocks and paragraph children with the true source
  gaps, advancing over pragma chains by their deepest end
* Emit source slices for text nodes whose span trim-equals their
  text, restoring the bytes eaten by \whitespace trim
* Recover the pragma separators in macrodef, fnprocdef and import
  and stitch chained children with the new serializeChildren helper
* Keep block emissions span exact: slices for transcludeblock,
  filteredtranscludeblock, codeblock and styleblock, options
  pass-through for their inline twins, tail repairs for horizrule
  and macrocallblock, no tail strip on stitched tables
* Emit the suppression prefix of suppressed syslinks; the old check
  looked for children the text node never has
* Slice macro calls and macro-valued attributes to preserve the
  author's parameter layout; MacroCallBlock-normalized becomes a
  mode: ast-only pair
ATTENTION: this commit is a standalone core change that belongs to the
span discipline cluster (own upstream issue and PR, after the block
terminator PR whose story it extends). It contains only core parser
files and one new test file, so it can be cherry-picked as a unit.

Block rules must consume their terminating line end to parse, but the
stamped node spans swallowed it, so the separator byte belonged to a
different owner per rule: transcludeblock, filteredtranscludeblock,
horizrule, styleblock, typedblock and table included it, the other
block rules and all pragmas did not. typedblock's span also started at
its content instead of the opening $$$ marker.

* Trim one trailing line end run from the last returned node at the
  block dispatch, never trimming into a child's span, e.g. an
  unterminated quote whose last text node keeps its newline
* Trim the row and container ends in the table rule; the dispatch
  cannot reach them below the child span floor
* Start the typedblock span at the opening $$$ marker
* Add test-wikitext-block-spans.js, one spec per block and pragma
  rule asserting both span ends; red for seven spans without the fixes
* Update the horizrule and table expectations for the shifted ends
ATTENTION: this commit is a standalone core change that belongs to the
span discipline cluster and travels in the same upstream PR as the
span commit before it.

The parse position is not recoverable from the tree: macrocallblock
produces the same node shape as its inline twin, but block position
decides the separators around a node. The html rule already recorded
it for itself; every block rule needs it before a serializer can own
block separation centrally.

Convention: blockPosition true on block dispatched nodes and on block
positioned html elements, false on inline html elements (their isBlock
fuses in the content mode, so absence must not mean inline there),
absent on other inline nodes; paragraphs signal block position through
their parseblock rule name.

* Stamp blockPosition next to the rule name at the block dispatch
* Assert the stamp per block rule in test-wikitext-block-spans.js
* Add the field to the affected literal expectations: macrocall and
  filtered transclusion roots, both hr nodes, the table root and the
  block dispatched h1 inside a div
Make the serializer mirror the parser's span discipline: rule
serializers emit exactly their own syntax, and the walker alone
separates block siblings, with the true source gap in fidelity mode
and one blank line from the tree alone.

* Add isBlockNode reading blockPosition, with a legacy fallback to
  isBlock and the block rule name suffix for unannotated trees
* Emit the separator between block siblings in the walker's array
  branch; nothing follows the last block
* Delete the per rule blank line tails from heading, horizrule,
  codeblock, typedblock, table, quoteblock, list, styleblock,
  transcludeblock, filteredtranscludeblock, macrocallblock, html and
  conditional, and the tail repairs that inv 1 made dead code
* Drop the joiner strip from serializeStitched and the root tail
  strip; no artificial joiners are left to undo
* Serialize block children as one array in styleblock and quoteblock
  so the walker separates them
* Give conditional markers and the html close tag their line end
  after block content, or a line terminated child like a heading
  would swallow them
* Reduce the inline twins to plain delegation; their trims are no-ops
  now
Give each of the 89 serializer fixtures a description field, and
each of the ten normalize pairs an info subtiddler that renders as
documentation when pasted into a wiki.

* Add a one line bottom line up front description to every fixture
* Add an info subtiddler to each normalize pair: the transformation
  first, a why paragraph, then Covers and Mode bullets naming the
  serializer modules with their full titles
* Label FunctionDefinition-normalized-whitespace an identity pin
* Keep the trailing whitespace test bytes untouched; the drivers
  ignore both additions, so the spec count stays unchanged
Cover the tree-only grid emitter with a normalize pair: leading merge
markers rebuild in trailing form and the pseudo rows re-emit in class,
caption, body, footer order.

* Add Table-normalized.tid with a class row, header cells, both merge
  forms, a rowspan, a trailing caption row and a footer row
* Pin the emission verified by re-parse equivalence: |>|Cell9 |
  becomes |Cell9 |<| and the caption row moves below the class row
@netlify

netlify Bot commented Jul 12, 2026

Copy link
Copy Markdown

Deploy Preview for tiddlywiki-previews ready!

Name Link
🔨 Latest commit 9ba6487
🔍 Latest deploy log https://app.netlify.com/projects/tiddlywiki-previews/deploys/6a5420667014b4000928789e
😎 Deploy Preview https://deploy-preview-9922--tiddlywiki-previews.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.
🤖 Make changes Run an agent on this branch

To edit notification comments on pull requests, go to your Netlify project configuration.

@github-actions

Copy link
Copy Markdown

Confirmed: pmario has already signed the Contributor License Agreement (see contributing.md)

@github-actions

github-actions Bot commented Jul 12, 2026

Copy link
Copy Markdown

📊 Build Size Comparison: empty.html

Branch Size
Base (master) 2492.1 KB
PR 2497.3 KB

Diff: ⬆️ Increase: +5.2 KB


⚠️ Change Note Status

This PR appears to contain code changes but doesn't include a change note.

Please add a change note by creating a .tid file in editions/tw5.com/tiddlers/releasenotes/<version>/

📚 Documentation: Release Notes and Changes

💡 Note: If this is a documentation-only change, you can ignore this message.

@pmario

pmario commented Jul 12, 2026

Copy link
Copy Markdown
Member Author

@linonetwo ... Have a closer look here. It should pretty much do round trips with every valid wikitext, that is available in data/ driven tests. I did create several new tests and could use all existing other tests too.

Give quoteblock, styleblock, html and conditional their own it()
blocks so a failure names the culprit rule; the five expectations
are unchanged.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant