Skip to content

Fix inline nodes stored outside getChildren() not being transformed - #1441

Merged
jaapio merged 1 commit into
mainfrom
bugfix/compile-sub-nodes
Oct 28, 2026
Merged

jaapio merged 1 commit into
mainfrom
bugfix/compile-sub-nodes

Conversation

@jaapio

@jaapio jaapio commented Oct 27, 2026

Copy link
Copy Markdown
Member

Nodes such as definition-list terms and classifiers, code-block captions, admonition titles, figure images, confval options and Bootstrap card parts keep other nodes in properties instead of returning them from getChildren(). The shadow tree never saw those nodes, so transformers (variable substitution, roles, references) were never applied to them. A |substitution| in a caption or a definition-list term was therefore rendered without its replacement.

Introduce a compiler-owned registry of "node fields" so these nodes become part of the shadow tree without changing the node classes:

  • NodeField describes one edge type: items(), replaceAt() and removeAt(). Every modification returns the new owner, because nodes are immutable.
  • ChildrenField is the default field for the ordinary children of a CompoundNode, using the existing replaceNode()/removeNode().
  • InlineField (plus InlineListField and InlineMapField) is registered with a getter and a setter per property. Several fields can be registered for one node class, and nodes whose property may be removed are marked nullable.
  • AttachedNodeRegistry collects InlineFields from tagged AttachedNodeProviders. Core, reStructuredText and Bootstrap each provide their own.

TreeNode now remembers the field and key a child came from, and replaces or removes it through that field. The new owner is then written back to its own parent, so immutable owners nested at any depth are handled by one code path. TreeNode::getTraversalChildren() returns registered-field nodes followed by ordinary children, and DocumentNodeTraverser iterates that.

The registry is injected into DocumentNodeTraverser by Compiler, which now builds (and releases) the shadow tree itself.

To write fields back, add immutable with*() methods to DefinitionListItemNode, AdmonitionNode, FigureNode, GeneralDirectiveNode, SidebarNode, ConfvalNode, CardNode and AccordionItemNode, and make the affected properties non-readonly.

Add integration fixtures for variables in captions and tables, unit tests for the registry and for replacing/removing nodes through fields.

refs #1426
Resolves #1425

@jaapio
jaapio requested a review from linawolf October 27, 2026 23:23
@jaapio

jaapio commented Oct 27, 2026

Copy link
Copy Markdown
Member Author

@linawolf this is my attempt to fix #1425. The amount of code I needed is much more, but I think this gives a cleaner way of allowing extensions to extend the fields of nodes they want to replace.

Your extension can find a subnode to be added by creating a new provider. This will instruct the compiler to also access that method.

@jaapio
jaapio force-pushed the bugfix/compile-sub-nodes branch from 5c8fd8d to 3a8fc22 Compare October 28, 2026 19:20
Nodes such as definition-list terms and classifiers, code-block
captions, admonition titles, figure images, confval options and
Bootstrap card parts keep other nodes in properties instead of
returning them from getChildren(). The shadow tree never saw those
nodes, so transformers (variable substitution, roles, references)
were never applied to them. A `|substitution|` in a caption or a
definition-list term was therefore rendered without its replacement.

Introduce a compiler-owned registry of "node fields" so these nodes
become part of the shadow tree without changing the node classes:

- NodeField describes one edge type: items(), replaceAt() and
  removeAt(). Every modification returns the new owner, because nodes
  are immutable.
- ChildrenField is the default field for the ordinary children of a
  CompoundNode, using the existing replaceNode()/removeNode().
- InlineField (plus InlineListField and InlineMapField) is registered
  with a getter and a setter per property. Several fields can be
  registered for one node class, and nodes whose property may be
  removed are marked nullable.
- AttachedNodeRegistry collects InlineFields from tagged
  AttachedNodeProviders. Core, reStructuredText and Bootstrap each
  provide their own.

TreeNode now remembers the field and key a child came from, and
replaces or removes it through that field. The new owner is then
written back to its own parent, so immutable owners nested at any
depth are handled by one code path. TreeNode::getTraversalChildren()
returns registered-field nodes followed by ordinary children, and
DocumentNodeTraverser iterates that.

The registry is injected into DocumentNodeTraverser by Compiler, which
now builds (and releases) the shadow tree itself.

To write fields back, add immutable with*() methods to
DefinitionListItemNode, AdmonitionNode, FigureNode, GeneralDirectiveNode,
SidebarNode, ConfvalNode, CardNode and AccordionItemNode, and make the
affected properties non-readonly.

Add integration fixtures for variables in captions and tables, unit
tests for the registry and for replacing/removing nodes through fields.
@jaapio
jaapio force-pushed the bugfix/compile-sub-nodes branch from 3a8fc22 to 6aff0ba Compare October 28, 2026 20:12
@jaapio
jaapio merged commit 270685a into main Oct 28, 2026
73 checks passed
@jaapio
jaapio deleted the bugfix/compile-sub-nodes branch October 28, 2026 20:46
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.

Node transformers do not visit inline nodes in definition-list terms

1 participant