CustomTagsExtension & Registries
How custom tags are wired into League CommonMark in Docara core, and how registries provide specs to the parsing/rendering pipeline.
Components overview
- CustomTagsExtension - CommonMark extension that installs our parsers and renderer.
- CustomTagRegistry - Runtime registry of
CustomTagSpecobjects (one per tag type), used by parsers/renderers. - TagRegistry - Factory that accepts tag class instances and produces a
CustomTagRegistry(via the adapter). - CustomTagSpec - Immutable data object describing a tag: regexes, wrapper, defaults, hooks.
CustomTagsExtension
Location: Simai\Docara\CustomTags\CustomTagsExtension
Registers the block parser and renderer with the CommonMark environment.
final class CustomTagsExtension implements ExtensionInterface
{
public function __construct(private CustomTagRegistry $registry) {}
public function register(EnvironmentBuilderInterface $env): void
{
$env->addBlockStartParser(new UniversalBlockParser($this->registry), 0);
$env->addRenderer(CustomTagNode::class, new CustomTagRenderer($this->registry));
// Inline parser hook is reserved:
// $env->addInlineParser(new UniversalInlineParser($this->registry));
}
}
Notes
- Priority is
0here; adjust if you introduce other block parsers that might conflict. - Installed by
Simai\Docara\Parserduring environment setup.
CustomTagRegistry
Location: Simai\Docara\CustomTags\CustomTagRegistry
Provides fast lookup of specs by type and enumerates specs for scanning.
final class CustomTagRegistry
{
/** @var array<string,CustomTagSpec> */
private array $byType = [];
/** @return CustomTagSpec[] */
public function getSpecs(): array { return array_values($this->byType); }
public function get(string $type): ?CustomTagSpec { return $this->byType[$type] ?? null; }
public function register(CustomTagSpec $s): void { $this->byType[$s->type] = $s; }
}
TagRegistry (factory)
Location: Simai\Docara\CustomTags\TagRegistry
Converts tag classes (extending BaseTag) into a runtime registry of specs, validating types and preventing duplicates.
final class TagRegistry
{
/**
* @param CustomTagInterface[] $tags
*/
public static function register(array $tags): CustomTagRegistry
{
$registry = new CustomTagRegistry;
$seen = [];
foreach ($tags as $tag) {
if (! $tag instanceof CustomTagInterface) {
throw new \InvalidArgumentException('All items must implement CustomTagInterface');
}
$type = $tag->type();
if (isset($seen[$type])) {
throw new \RuntimeException(\"Duplicate custom tag type '{$type}'\");
}
$seen[$type] = true;
$registry->register(CustomTagAdapter::toSpec($tag));
}
return $registry;
}
}
CustomTagSpec (data contract)
Location: Simai\Docara\CustomTags\CustomTagSpec
Immutable description of a tag used by the parser and renderer.
string $type- Tag identity used in!type/!endtype.string $openRegex- Anchored regex for the opening line; should expose a named capture(?<attrs>...)if inline attributes are supported.?string $closeRegex- Anchored regex for the closing line;nullmeans single-line tag.string $htmlTag- Default wrapper element (e.g.,div,section).array $baseAttrs- Default attributes merged with inline ones; class values concatenate/deduplicate.bool $allowNestingSame- Whether the same tag type can be nested.?callable $attrsFilter- Signaturefn(array $attrs, array $meta): array; runs early to normalize/whitelist.?callable $renderer- Signaturefn(CustomTagNode $node, ChildNodeRendererInterface $children): mixed.
Created by CustomTagAdapter::toSpec($tag).
End-to-end wiring
- Config:
config('tags')lists tag class short names. - Provider:
CustomTagServiceProviderinstantiates those tags and callsTagRegistry::register(...), bindingCustomTagRegistry. - Parser:
Simai\Docara\Parserbuilds the CommonMark environment and installsCustomTagsExtensionwith the bound registry. - Parsing:
UniversalBlockParserusesgetSpecs()to try opens/close per line; on match it creates aCustomTagNodeand applies earlyattrsFilter. - Rendering:
CustomTagRendererrenders nodes with either the per-tagrendereror the default wrapper.
Troubleshooting
- Extension not applied: ensure
ParserinstallsCustomTagsExtensionand DI providesCustomTagRegistry. - Tags not recognized: confirm
TagRegistry::register()receives instances of your tag classes and thatopenRegex()is not empty (adapter will throw otherwise). - Per-tag renderer not called: ensure the tag's
renderer()returns a closure and that the registry used by the renderer is the same one used by the block parser.
Testing checklist
- Environment contains our block start parser and node renderer.
CustomTagRegistry::getSpecs()returns the expected set of types.- Spec lookups by type work during rendering (
CustomTagRendererpath). - Single-line tags behave correctly when
closeRegexisnull. - Same-type nesting rule enforced by the block parser using
allowNestingSame.