zfb
GitHub repository

Type to search...

to open search from anywhere

Directives registry

The Core primitive that maps :::name / ::name / :name directive syntax to JSX components.

DirectiveRegistry is the Core primitive that maps CommonMark Directives syntax — container (:::name), leaf (::name[label]), and text (:name[label]) — to JSX component calls in the compiled output.

The primitive exists in Core, but the registry visitor is not added to the normal pipeline by default. It runs only when markdown.features.directives is supplied, or when Rust code inserts a DirectiveRegistry manually. You interact with it in two ways:

  • From config — use the opt-in directives feature to register directive names without writing Rust. Supplying directives: {} still wires an empty registry.

  • From Rust — populate a DirectiveRegistry directly and insert it into the pipeline. See Custom Directives.

Directive shapes

The registry handles three directive shapes:

  • Container:::name[label]::: wraps multi-paragraph body content into a JSX component.

  • Leaf::name[label]{attrs} produces a self-closing component with no children.

  • Text:name[label]{attrs} is an inline component.

Zero defaults

The registry ships with no directive names pre-registered. Every :::name you use must be explicitly registered — either via the directives feature in zfb.config.ts or by populating the registry in Rust. When the registry is active, an unrecognised directive name emits a warning diagnostic and leaves the source paragraph unchanged.

Typed attribute schemas (from #584)

The registry accepts typed attribute schemas for each registered directive. Unknown attributes emit a build-time warning (the attr still passes through unchanged). Type-coercion failures (e.g. a non-boolean value for a Boolean attr) emit hard errors. Each attribute is declared as an AttrSchema:

FieldDescription
nameAttribute name as written in the MDX source (e.g. "tone", "data-foo").
tyExpected type — see AttrType below.
defaultValue applied when the attr is absent and required is false. None means no default.
requiredWhen true and the attr is absent with no default, a diagnostic is emitted.

AttrType has four variants:

VariantAcceptsNotes
StringAny string value, passed through verbatim.No validation.
Enum(Vec<String>)One of the declared allowed values (case-sensitive).A value outside the list emits a diagnostic.
Boolean"true" / "false" (case-insensitive).A bare attribute with no =value also counts as true. Emits as the string "true"/"false" in JSX (v1 only supports string-literal attributes).
NumberAny value parseable via f64::from_str.The original source string is stored (not the parsed number) so the JSX emitter passes it through unchanged; a non-parseable value emits a diagnostic.

The schema is declared alongside the register call:

registry.register(
    DirectiveDef::text("badge", "Badge")
        .with_attrs(vec![
            AttrSchema { name: "tone".into(), ty: AttrType::String, default: None, required: false },
            AttrSchema {
                name: "size".into(),
                ty: AttrType::Enum(vec!["sm".into(), "md".into(), "lg".into()]),
                default: Some("md".into()),
                required: false,
            },
        ]),
);

Custom directives

To register additional directives or override built-ins, see Custom Directives — the author-facing path that does not require writing Rust.

See also

Revision History

CreatedUpdated