Tags are JSX elements backed by a plugin. TemplateDX ships five built-in tags (<If>, <ElseIf>, <Else>, <ForEach>, <Raw>) and exposes two ways to register your own: a global static API and a per-instance API on TemplateDX.
Creating custom tags (TypeScript)
Extend TagPlugin
Create a class that extends TagPlugin and implements transform:
import { Node } from 'mdast';
import { TagPlugin, PluginContext } from '@agentmark-ai/templatedx';
interface MyTagProps {
prefix?: string;
}
class MyTagPlugin extends TagPlugin<MyTagProps> {
async transform(
props: MyTagProps,
children: Node[],
context: PluginContext
): Promise<Node[] | Node> {
const { nodeHelpers } = context;
const content = nodeHelpers.toMarkdown({
type: 'root',
children: children,
});
const prefix = props.prefix ?? '> ';
const prefixedContent = content
.split('\n')
.map(line => prefix + line)
.join('\n');
return [{
type: 'text',
value: prefixedContent,
} as Node];
}
}
The transform method
transform(props, children, context) must return a Promise<Node | Node[]>.
context: PluginContext exposes:
| Field | Type | Description |
|---|
nodeHelpers | NodeHelpers | AST utilities (see below) |
scope | Scope | Current variable scope; read via scope.get(key), create a child with scope.createChild({ key: value }) |
createNodeTransformer | (scope: Scope) => any | Factory for recursively transforming child nodes under a new scope |
tagName | string | Name of the tag being processed — set by the transformer from node.name |
tagName is typed as string, but the conditional plugin (tag-plugins/conditional.ts) defensively guards if (!tagName) throw .... Built-in tags can rely on tagName being set; advanced consumers that invoke a plugin’s transform directly (outside the transformer) should pass a valid name.
nodeHelpers surface
| Member | Purpose |
|---|
isMdxJsxElement(node) | true for flow or text JSX elements |
isMdxJsxFlowElement(node) | true for block-level JSX |
isMdxJsxTextElement(node) | true for inline JSX |
isParentNode(node) | true if the node has a children array |
toMarkdown(node) | Serialize an AST node back to Markdown/text |
hasFunctionBody(node) | true for expression nodes containing an arrow function (e.g. <ForEach> callback) |
getFunctionBody(node) | Returns { body, argumentNames } for arrow-function expression nodes |
NODE_TYPES | Constants for AST node-type strings (TEXT, PARAGRAPH, MDX_JSX_FLOW_ELEMENT, …) |
Register the plugin (static or instance API)
The registry exposes both a static API (process-wide) and an instance API (scoped to a TemplateDX engine).
Static (global) registration — the simplest path; everything using the default transform/stringify exports will see it:
import { TagPluginRegistry } from '@agentmark-ai/templatedx';
TagPluginRegistry.register(new MyTagPlugin(), ['MyTag', 'Prefix']);
Instance (scoped) registration — use when you want plugins isolated per engine (e.g. a server handling multiple tenants with different tag sets):
import { TemplateDX } from '@agentmark-ai/templatedx';
const engine = new TemplateDX({ includeBuiltins: true });
engine.registerTagPlugin(new MyTagPlugin(), ['MyTag']);
const rendered = await engine.transform(ast, { /* props */ });
new TemplateDX({ includeBuiltins: true }) copies the static built-ins (If, ElseIf, Else, ForEach, Raw) into the instance; pass false to start empty.
Use the tag in a template
<MyTag prefix="// ">
This is some content
that will be prefixed
</MyTag>
Example: Quote tag
import { Node, Root } from 'mdast';
import { TagPlugin, PluginContext, TagPluginRegistry } from '@agentmark-ai/templatedx';
interface QuoteProps {
author?: string;
}
class QuotePlugin extends TagPlugin<QuoteProps> {
async transform(
props: QuoteProps,
children: Node[],
context: PluginContext
): Promise<Node[]> {
const { nodeHelpers } = context;
const content = nodeHelpers.toMarkdown({
type: 'root',
children: children,
} as Root);
let result = content
.split('\n')
.map(line => '> ' + line)
.join('\n');
if (props.author) {
result += `\n> \n> -- ${props.author}`;
}
return [{
type: 'text',
value: result,
} as Node];
}
}
TagPluginRegistry.register(new QuotePlugin(), ['Quote']);
Usage:
<Quote author="Albert Einstein">
Imagination is more important than knowledge.
</Quote>
Output:
> Imagination is more important than knowledge.
>
> -- Albert Einstein
Creating custom tags (Python)
agentmark-templatedx (Python) mirrors the TS surface. Subclass TagPlugin, implement async def transform, and register via the static (register_global) or instance API.
from templatedx import TagPlugin, TagPluginRegistry
from templatedx.tag_plugin import PluginContext
class QuotePlugin(TagPlugin):
async def transform(self, props, children, context: PluginContext):
content = context.node_helpers.to_markdown(children)
author = props.get("author")
body = "\n".join(f"> {line}" for line in content.split("\n"))
if author:
body += f"\n> \n> -- {author}"
return [{"type": "text", "value": body}]
# Global (static) registration
TagPluginRegistry.register_global(QuotePlugin(), ["Quote"])
The Python PluginContext dataclass has node_helpers (snake_case mirror of the TS NodeHelpers surface), create_node_transformer, scope, and tag_name.
For instance-scoped registration, construct the engine and use register_tag_plugin:
from templatedx import TemplateDX
engine = TemplateDX() # always copies global built-ins on init
engine.register_tag_plugin(QuotePlugin(), ["Quote"])
result = await engine.transform(ast, {"name": "Alice"})
Built-in tags
ForEach
The ForEach tag loops over an array.
Syntax
<ForEach arr={props.arr}>
{(item, index) => ...}
</ForEach>
Parameters
arr: Array<T> — an array of items you want to iterate on
children: (item: T, index: number) => Node — a callback function for each item
Example
<ForEach arr={[1, 2]}>
{(item, index) => (
<>
* item: {item}, index: {index}
</>
)}
</ForEach>
Output
* item: 1, index: 0
* item: 2, index: 1
Conditionals
The If, ElseIf, and Else tags let you conditionally output content.
Syntax
<If condition={props.boolA}>
...
</If>
<ElseIf condition={props.boolB}>
...
</ElseIf>
<Else>
...
</Else>
Parameters
If / ElseIf:
condition: boolean — the condition to check
children: Node — the node to render if the condition is true
Else:
children: Node — the content to render if no previous condition was met
Example
<If condition={1 + 1 == 3}>
1 + 1 is not 3
</If>
<ElseIf condition={1 + 1 == 2}>
1 + 1 is 2
</ElseIf>
<Else>
Fallback
</Else>
Output
Raw
The Raw tag outputs its children verbatim, without expression interpolation.
Syntax
Parameters
children: Node — the raw text
Example
<Raw>
{props.name}
</Raw>
Output