# Extensions URL: /docs/extending/extensions/ Section: extending Tags: extending, extensions, plugins -------------------------------------------------------------------------------- Extensions Extensions let you add custom template tags to Kida without modifying core code. An extension registers new block keywords, parses them into AST nodes, and compiles those nodes into Python statements. Extension Base Class Subclass kida.extensions.Extension and declare your tag keywords, node types, and optional end keywords: from kida.extensions import Extension class MyExtension(Extension): tags = {"mytag"} # Block keywords this extension handles node_types = {"MyTagNode"} # Node class names this extension compiles end_keywords = {"endmytag"} # End keywords for tags with bodies (optional) Every extension receives the Environment instance on initialization and can provide filters, tests, and globals alongside custom tags: class MyExtension(Extension): tags = {"mytag"} node_types = {"MyTagNode"} def get_filters(self): return {"myfilter": lambda v: v.upper()} def get_tests(self): return {"even": lambda v: v % 2 == 0} def get_globals(self): return {"MY_CONST": 42} Registering Extensions Pass extension classes (not instances) to the Environment: from kida import Environment env = Environment(extensions=[MyExtension]) On initialization, the environment: Instantiates each extension class with itself (ext = ExtClass(self)) Registers get_filters(), get_tests(), and get_globals() results Maps each tag in tags to the extension instance (parser dispatch) Maps each name in node_types to the extension instance (compiler dispatch) Merges end_keywords into the parser's end-keyword set Custom Node Types Define your AST node as a frozen dataclass inheriting from Node: from dataclasses import dataclass from kida.nodes.base import Node @dataclass(frozen=True, slots=True) class MyTagNode(Node): name: str body: tuple # child nodes The Node base class provides lineno and col_offset fields for source mapping. Use frozen=True and slots=True for immutable, memory-efficient nodes. Compiler Dispatch The compiler resolves nodes by type name. When it encounters a node whose class name matches a node_types entry, it calls that extension's compile() method: Compiler._compile_node(node) -> look up type(node).__name__ in _extension_compilers -> ext.compile(compiler, node) This is an O(1) dictionary lookup. The extension's compile() method must return a list[ast.stmt] of Python AST statements. End Keywords If your custom tag wraps a body (like {% mytag %}...{% endmytag %}), declare the closing keyword in end_keywords so the parser recognizes it: class MyExtension(Extension): tags = {"mytag"} node_types = {"MyTagNode"} end_keywords = {"endmytag"} End keywords are merged into the parser's keyword set at initialization. Without this, the parser will not recognize {% endmytag %} as a valid block terminator. You can also use the universal {% end %} syntax to close extension tags, but declaring end_keywords enables the explicit form. Example Build a complete {% debug expr %} tag that prints a value during rendering: import ast from dataclasses import dataclass from kida import Environment from kida.extensions import Extension from kida.nodes.base import Node @dataclass(frozen=True, slots=True) class DebugNode(Node): expr: object # Expr node from parser class DebugExtension(Extension): tags = {"debug"} node_types = {"DebugNode"} def parse(self, parser, tag_name): # Current token is "debug" — advance past it token = parser._advance() # Parse the expression after the tag name expr = parser._parse_expression() # Expect the block-end delimiter (%}) parser._expect(parser.TokenType.BLOCK_END) return DebugNode( lineno=token.lineno, col_offset=token.col_offset, expr=expr, ) def compile(self, compiler, node): # Compile the Kida expression into a Python AST expression expr = compiler._compile_expr(node.expr) # Return a print() call statement return [ ast.Expr( value=ast.Call( func=ast.Name(id="print", ctx=ast.Load()), args=[expr], keywords=[], ) ) ] # Register and use env = Environment(extensions=[DebugExtension]) Template usage: {% debug user.name %} {# Prints the value of user.name to stdout during rendering #} Extension with a Body For tags that wrap content, declare end_keywords and use _parse_body(): @dataclass(frozen=True, slots=True) class TimedNode(Node): label: object body: tuple class TimedExtension(Extension): tags = {"timed"} node_types = {"TimedNode"} end_keywords = {"endtimed"} def parse(self, parser, tag_name): token = parser._advance() label = parser._parse_expression() parser._expect(parser.TokenType.BLOCK_END) body = parser._parse_body() # Consume {% end %} or {% endtimed %} parser._advance() # {% parser._advance() # end/endtimed parser._expect(parser.TokenType.BLOCK_END) return TimedNode( lineno=token.lineno, col_offset=token.col_offset, label=label, body=tuple(body), ) def compile(self, compiler, node): # Compile body nodes body_stmts = [] for child in node.body: body_stmts.extend(compiler._compile_node(child)) label_expr = compiler._compile_expr(node.label) # Wrap body in timing instrumentation return [ ast.Expr(value=ast.Call( func=ast.Name(id="print", ctx=ast.Load()), args=[ast.JoinedStr(values=[ ast.Constant(value="[timed] start: "), ast.FormattedValue(value=label_expr, conversion=-1), ])], keywords=[], )), *body_stmts, ast.Expr(value=ast.Call( func=ast.Name(id="print", ctx=ast.Load()), args=[ast.JoinedStr(values=[ ast.Constant(value="[timed] end: "), ast.FormattedValue(value=label_expr, conversion=-1), ])], keywords=[], )), ] Template usage: {% timed "sidebar" %} {% include "partials/sidebar.html" %} {% end %} See Also Custom Filters -- Add custom filters Custom Tests -- Add custom tests Custom Globals -- Add global variables Custom Loaders -- Add template loaders -------------------------------------------------------------------------------- Metadata: - Word Count: 702 - Reading Time: 4 minutes