Documentation
¶
Overview ¶
AST node types for the html package. Each node has a Dump(int) string method (defined in format.go) that renders it back to its textual form.
Package html parses and formats Twig templates that contain HTML.
Architecture ¶
The package is split into four layers, each in its own file(s):
Position tracking — pos.go A Pos is a byte offset + 1-based line + 1-based byte column. A posTracker advances incrementally so line/column are O(1) per byte scanned, instead of O(n) per node.
Lexer — tokens.go, lexer.go Single-pass tokenizer that recognizes HTML element/attribute tokens, {% statement %}, {{ expression }}, and {# comment #} tokens, plus the whitespace-trim modifiers ({%- -%} etc.). The close-delimiter scan is string-literal and bracket-depth aware so {% set x = "a%}b" %} parses correctly. Twig identifiers are scanned with proper word boundaries (no more {% iff %} matching `if`).
Parser — parser.go, plus per-tag files parseDocument runs the lexer and walks the token stream. parseNodesUntil collects child nodes until a stop condition fires (EOF, the parent tag's EndTag, one of its Followers, or the matching HTML element close tag). Two RawNode chunking contexts (top-level and element-children) keep formatter output stable across passes.
AST + formatter — ast.go and format.go ast.go holds the node type definitions; format.go holds every Dump(int) string method plus IndentConfig. A package-level indentConfig is mutated via ConfiguredNodeList.Dump for back-compat; callers concurrently dumping with different configurations should serialize.
Adding a Twig tag ¶
Tag dispatch goes through a registry in tags.go. For most tags the public RegisterStandaloneTag / RegisterBlockTag helpers are enough:
func init() {
html.RegisterStandaloneTag("sw_icon") // {% sw_icon 'foo' %}
html.RegisterBlockTag("trans", "endtrans") // {% trans %}...{% endtrans %}
html.RegisterBlockTag("if", "endif", "elseif", "else")
}
Call them from your own package's init(). They panic on duplicate registration so name collisions surface at startup.
Tags with bespoke parsing logic (block, if, parent, set, verbatim, twig comment) live in dedicated tag_*.go files and call the lower-level registerTag with a custom TagSpec.Parse function.
Unregistered tags ¶
Unknown tags are folded into the surrounding RawNode (via parser.appendRawTokens) so the parser is forward-compatible with future Twig syntax — an unknown {% something %} round-trips through Dump as-is. This works perfectly for standalone tags. For block tags with a body, registration is required: without it the body's contents leak into the outer scope and the end tag becomes orphan raw text.
Public API and back-compat ¶
The public surface is small: NewParser, NewAdminParser, NewStorefrontParser, the AST node types (ElementNode, Attribute, RawNode, ...), TraverseNode, and IndentConfig. All of it is exercised by callers in internal/verifier/ and must stay stable. The fixture suite in testdata/ pins formatter output; any change must keep all fixtures byte-identical.
Smoke test ¶
internal/html/smoke_test.go walks a checked-out shopware/storefront tree (set HTML_SMOKE_CORPUS=/path/to/storefront) and parses every .twig file. It is opt-in so CI is not bound to an external clone.
Index ¶
- func RegisterBlockTag(name, endTag string, followers ...string)
- func RegisterStandaloneTag(name string)
- func SetIndentConfig(config IndentConfig)deprecated
- func TraverseNode(n NodeList, f func(*ElementNode))
- type Attribute
- type AttributeEntityEncodingFromTo
- type CommentNode
- type ConfiguredNodeList
- type ElementNode
- type IndentConfig
- type Node
- type NodeList
- type ParentNode
- type ParseError
- type Pos
- type RawNode
- type RelatedLoc
- type TagParseFunc
- type TagSpec
- type TemplateExpressionNode
- type TwigBlockNode
- type TwigCommentNode
- type TwigGenericBlockNode
- type TwigIfBranch
- type TwigIfNode
- type TwigStandaloneTagNode
- type TwigTrim
- type TwigVerbatimNode
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func RegisterBlockTag ¶
RegisterBlockTag registers a Twig tag that wraps a body, e.g. `{% trans %}...{% endtrans %}`. The endTag must match what closes the block (typically "end"+name). Optional followers are sibling tags that appear inside the body without closing it (e.g. for `{% if %}` the followers are `{"elseif", "else"}`); pass none for tags like `{% trans %}`.
Unlike standalone tags, block tags MUST be registered for the parser to know where the body ends — without registration the body's contents leak into the outer scope and the end tag becomes orphan raw text.
Panics if name is empty or already registered.
func RegisterStandaloneTag ¶
func RegisterStandaloneTag(name string)
RegisterStandaloneTag registers a Twig tag that has no body, e.g. `{% sw_icon 'foo' %}` or `{% include 'x.twig' %}`. The parser will yield a *TwigStandaloneTagNode with the tag's name and verbatim argument body.
Call from an init() in your own package to teach the parser about project-specific tags. Standalone tags that are NOT registered still round-trip through Dump as raw text — registration just gives downstream AST consumers a semantic node to reason about.
Panics if name is empty or already registered.
func SetIndentConfig
deprecated
func SetIndentConfig(config IndentConfig)
SetIndentConfig updates the global indentation configuration.
Deprecated: use ConfiguredNodeList or pass a Formatter explicitly. The global is retained for backward compatibility with existing callers but is not safe across concurrent parses with different configurations.
func TraverseNode ¶
func TraverseNode(n NodeList, f func(*ElementNode))
TraverseNode walks the AST and invokes f on every ElementNode it finds, including ones nested inside Twig structural nodes (block, if, for, embed, macro, etc.) and ones embedded in an element's Attributes slice (a Twig {% if %} can hold attribute-toggling element fragments). Downstream linters and fixers rely on this — adding a new structural node type means also teaching TraverseNode to recurse into its children.
Types ¶
type Attribute ¶
Attribute represents an HTML attribute with key and value. Like the other node types it lives in a NodeList as a pointer (*Attribute); its Dump method has a pointer receiver, so a bare Attribute value does not satisfy Node.
type AttributeEntityEncodingFromTo ¶
AttributeEntityEncodingFromTo specifies a pair of strings used during HTML attribute value encoding.
type CommentNode ¶
CommentNode represents an HTML <!-- ... --> comment.
func (*CommentNode) Dump ¶
func (c *CommentNode) Dump(indent int) string
type ConfiguredNodeList ¶
type ConfiguredNodeList struct {
Nodes NodeList
Config IndentConfig
}
ConfiguredNodeList wraps a NodeList with the IndentConfig under which it should format. NewAdminParser and NewStorefrontParser return this so the caller can Dump(0) without managing config separately.
func NewAdminParser ¶
func NewAdminParser(input string) (ConfiguredNodeList, error)
NewAdminParser creates a parser configured for admin twig files (no indentation for twig block children).
func NewStorefrontParser ¶
func NewStorefrontParser(input string) (ConfiguredNodeList, error)
NewStorefrontParser creates a parser configured for storefront twig files (indents twig block children).
func (ConfiguredNodeList) Dump ¶
func (cnl ConfiguredNodeList) Dump(indent int) string
Dump renders the nodes with the stored configuration. It temporarily swaps the package-level indentConfig so the per-node Dump methods pick it up; callers running concurrent parses with different configurations should serialize their Dump calls or migrate to an explicit Formatter (not yet exposed).
type ElementNode ¶
type ElementNode struct {
Tag string
Attributes NodeList
Children NodeList
SelfClosing bool
// Unclosed reports that the element opened with `<tag>` but its
// children parser yielded on an outer Twig terminator (e.g.
// `{% endblock %}`) before reaching `</tag>`. The closing tag is
// elsewhere in the source, typically wrapped in another control-flow
// block. The formatter therefore does NOT emit `</tag>` for unclosed
// elements — the matching `</tag>` lives as a RawNode further down.
Unclosed bool
Line int
}
ElementNode represents an HTML element.
func (*ElementNode) Dump ¶
func (e *ElementNode) Dump(indent int) string
Dump renders an HTML element with attributes, children, and end tag.
type IndentConfig ¶
IndentConfig controls how the formatter emits indentation.
func DefaultIndentConfig ¶
func DefaultIndentConfig() IndentConfig
DefaultIndentConfig creates a default indentation config with spaces.
func (IndentConfig) GetIndent ¶
func (c IndentConfig) GetIndent() string
GetIndent returns the indentation string based on configuration.
type NodeList ¶
type NodeList []Node
NodeList is a sequence of AST nodes. It has its own Dump method that arranges children with appropriate inter-node whitespace.
func NewParserWithConfig ¶
func NewParserWithConfig(input string, config IndentConfig) (NodeList, error)
NewParserWithConfig creates a new parser with a specific indentation configuration.
type ParentNode ¶
ParentNode represents `{% parent %}` or `{% parent() %}`.
func (*ParentNode) Dump ¶
func (p *ParentNode) Dump(indent int) string
type ParseError ¶
type ParseError struct {
Filename string
Pos Pos
Msg string
Related []RelatedLoc
Source string
}
ParseError is a parser-level error with source location.
func (*ParseError) Error ¶
func (e *ParseError) Error() string
func (*ParseError) PrettyError ¶
func (e *ParseError) PrettyError() string
PrettyError returns the error with a source snippet, suitable for terminal display.
type Pos ¶
Pos is a byte-based position in the source. Line is 1-based.
Column is intentionally NOT stored: it is needed only on the cold error path, so carrying it in every token (and recomputing it on every advance) is pure overhead. Compute it on demand with ColumnIn, which derives it from Offset and the source. Dropping the field shrinks Pos from 24 to 16 bytes, which in turn shrinks the token struct that embeds it — and the token stream is copied on every peek/advance/emit, so this directly cuts CPU.
type RawNode ¶
RawNode holds verbatim source text — anything outside structured HTML or Twig syntax, plus the bytes of any Twig tag that has no registered handler.
type RelatedLoc ¶
RelatedLoc points to another location relevant to a ParseError (e.g. the opening tag for an unmatched closer).
type TagParseFunc ¶
TagParseFunc is the per-tag parser hook.
type TagSpec ¶
type TagSpec struct {
// Name is the identifier after `{%` (e.g. "block", "if").
Name string
// EndTag is the closing tag, e.g. "endblock" for a block tag.
// Empty string for tags with no body ("set" with `=`, "include", "extends").
EndTag string
// Followers are sibling tags that may appear inside the body without
// closing it (e.g. "elseif", "else" inside "if").
Followers []string
// Parse turns the tag into an AST node. It is called with the cursor
// on the OPEN token of the tag.
Parse TagParseFunc
}
TagSpec describes how to parse a single Twig statement tag (e.g. "block", "if", "for", "include"). The registry is the entry point for adding new tag support — see tag_block.go, tag_if.go etc.
Lifecycle:
- Parse is called when the parser sees `{% Name ... %}`.
- openTok is the Twig statement OPEN token; the parser has not yet consumed the identifier or body. Parse implementations should advance the cursor past `%}` (and past any EndTag closer).
Followers and EndTag are advisory: they tell parseTagBody when to stop collecting children. A block tag is "open" if either EndTag is set or the body parser is invoked.
type TemplateExpressionNode ¶
TemplateExpressionNode represents a `{{ ... }}` Twig expression.
func (*TemplateExpressionNode) Dump ¶
func (t *TemplateExpressionNode) Dump(indent int) string
type TwigBlockNode ¶
type TwigBlockNode struct {
Name string
Children NodeList
OpenTrim TwigTrim
CloseTrim TwigTrim
Line int
}
TwigBlockNode represents `{% block name %}...{% endblock %}`. OpenTrim is the trim flags on the `{% block name %}` delimiters; CloseTrim is the trim flags on the `{% endblock %}` delimiters.
func (*TwigBlockNode) Dump ¶
func (t *TwigBlockNode) Dump(indent int) string
type TwigCommentNode ¶
TwigCommentNode represents a `{# ... #}` Twig comment. The body is the raw text between the delimiters, preserved verbatim (including leading and trailing whitespace).
func (*TwigCommentNode) Dump ¶
func (c *TwigCommentNode) Dump(indent int) string
type TwigGenericBlockNode ¶
type TwigGenericBlockNode struct {
Name string
Args string // raw args body, with leading/trailing space stripped
EndTag string
Body NodeList
Else NodeList
OpenTrim TwigTrim
ElseTrim TwigTrim
CloseTrim TwigTrim
Line int
}
TwigGenericBlockNode represents a Twig statement tag that opens a block, e.g. `{% for x in xs %}body{% endfor %}` or `{% embed 't' %}body{% endembed %}`. The body is parsed recursively as Twig+HTML content.
Else is populated when the block tag supports an `{% else %}` follower (currently just `{% for %}`); it holds the body between `{% else %}` and the block's EndTag. Else is nil/empty for tags without an else clause.
func (*TwigGenericBlockNode) Dump ¶
func (n *TwigGenericBlockNode) Dump(indent int) string
type TwigIfBranch ¶
TwigIfBranch is one conditional branch of a {% if %}...{% endif %} block. The first branch in TwigIfNode.Branches is the "if" itself; subsequent entries are "elseif" branches. The else (no-condition) branch is held separately on TwigIfNode.ElseChildren. Trim is the trim flags on the branch's own header delimiters.
type TwigIfNode ¶
type TwigIfNode struct {
Branches []TwigIfBranch
ElseChildren NodeList
// ElseTrim is the trim flags on the `{% else %}` delimiters when the
// else clause is present.
ElseTrim TwigTrim
// EndTrim is the trim flags on the `{% endif %}` delimiters.
EndTrim TwigTrim
Line int
}
TwigIfNode represents `{% if %}...{% elseif %}...{% else %}...{% endif %}`. Branches[0] is always the "if"; Branches[1..] are "elseif"s. ElseChildren is nil/empty when there is no {% else %} clause.
func (*TwigIfNode) Dump ¶
func (t *TwigIfNode) Dump(indent int) string
type TwigStandaloneTagNode ¶
TwigStandaloneTagNode represents a Twig tag with no body, e.g. `{% include "x.twig" %}`, `{% extends "@base" %}`, `{% set x = 1 %}`, `{% sw_include "..." with {} %}`.
func (*TwigStandaloneTagNode) Dump ¶
func (n *TwigStandaloneTagNode) Dump(indent int) string
type TwigTrim ¶
type TwigTrim struct {
Left bool // open delimiter has a leading '-' (e.g. {%-)
Right bool // close delimiter has a trailing '-' (e.g. -%})
}
TwigTrim records the whitespace-control modifiers on a single Twig delimiter pair. `{%- if -%}` is TwigTrim{Left: true, Right: true} on the if-header, for example. Twig strips surrounding whitespace at render time when a side is true, so the formatter must emit `{%-`/`-%}` (or `{{-`/`-}}`, `{#-`/`-#}`) verbatim — losing them would change rendered output.
type TwigVerbatimNode ¶
TwigVerbatimNode represents `{% verbatim %}...{% endverbatim %}`. The body is preserved as raw text — the parser does NOT recurse into the body, since the whole point of `{% verbatim %}` is to disable Twig interpretation for its contents.
func (*TwigVerbatimNode) Dump ¶
func (v *TwigVerbatimNode) Dump(indent int) string
Dump renders the verbatim block with its body byte-identical to source.