CustomTagNode & Spec Adapter
This page documents the AST node used to represent a custom tag and the adapter that translates a tag class into a runtime CustomTagSpec consumed by the parsers/renderers. These live in Docara core.
CustomTagNode (AST)
Location: Simai\Docara\CustomTags\CustomTagNode
Represents a parsed custom tag block in the CommonMark AST.
Constructor
public function __construct(
private string $type,
private array $attrs = [], // merged + filtered attributes
private array $meta = [], // parser metadata (e.g., openMatch, attrStr)
) {}
Core methods
getType(): string- tag type (e.g.,example).isContainer(): bool- alwaystrue; the node can contain any child blocks/inline nodes.getAttrs(): array- current attribute map (merged defaults + inline, optionally filtered).setAttrs(array $attrs): void- replace the attribute map (used afterattrsFilter).addClass(string $class): void- append classes; whitespace-split, deduplicate, preserve order.getMeta(): array- parser metadata (e.g.,['openMatch' => ..., 'attrStr' => 'class:"mb-4"']).
Use
getMeta()['openMatch']in a per-tagattrsFilter($attrs, $meta)or renderer to derive attributes from named regex captures.
Typical lifecycle
- Created by
UniversalBlockParserwhen the open line matches a tag'sopenRegex(). - Attributes are parsed via
Attrsand merged withbaseAttrs(). - Optional per-tag
attrsFilter($attrs, $meta)may updateattrs(viasetAttrs). - Rendered by
CustomTagRenderer(default wrapper or per-tag renderer closure).
CustomTagAdapter (Tag -> Spec)
Location: Simai\Docara\CustomTags\CustomTagAdapter
Converts a tag class (implementing CustomTagInterface / extending BaseTag) into an immutable CustomTagSpec consumed by the parsing/rendering layer.
API
public static function toSpec(CustomTagInterface $tag): CustomTagSpec
Responsibilities
- Extract identity & regexes
type: fromtype()openRegex: fromopenRegex(); must be non-empty or an exception is thrown.closeRegex: fromcloseRegex(); coerced tonullfor single-line tags.
- Presentation defaults
htmlTag: fromhtmlTag()(default wrapper element).baseAttrs: frombaseAttrs().
- Behavioral flags
allowNestingSame: fromallowNestingSame().
- Customization hooks
attrsFilter: fromattrsFilter(); signaturefn(array $attrs, array $meta): array.renderer: fromrenderer(); signaturefn(CustomTagNode $node, ChildNodeRendererInterface $children): mixed.
Error handling
- If
openRegex()returns an empty value, the adapter throwsInvalidArgumentExceptionidentifying the tag.
How they work together
- During environment setup, each tag class is converted by
CustomTagAdapterinto aCustomTagSpecand stored inCustomTagRegistry. UniversalBlockParserconsumes specs to:- detect opens/closes;
- create
CustomTagNodewith merged attributes and meta; - apply early
attrsFilter($attrs, $meta).
CustomTagRendereruses the spec to either:- call a per-tag
renderer($node, $children); or - render a default
HtmlElement($spec->htmlTag, $node->getAttrs(), ...).
- call a per-tag
Usage patterns
-
Add derived classes at render time:
$node->addClass('variant-' . ($meta['openMatch']['variant'] ?? 'default'));Prefer doing this in
attrsFilter()for clearer separation of concerns. -
Single-line tags: return an empty/falsey
closeRegex()from the tag; the adapter will passnullto the spec, and the block will close immediately after the opening line.
Pitfalls & recommendations
- Keep
type()globally unique; specs are keyed by type. - Ensure
openRegex()exposes a namedattrsgroup if you expect inline attributes. - Don't mutate attributes inside renderers unless necessary; use
attrsFilter()oraddClass()earlier in the flow. - If you override
openRegex()/closeRegex(), maintain^anchors and the Unicodeumodifier.
Testing checklist
- Spec creation fails when
openRegex()is empty. - Node attributes merge correctly and
addClass()deduplicates. attrsFilter($attrs, $meta)sees bothopenMatchandattrStr.- Renderer path respects per-tag renderer vs default wrapper.