html

package
v0.18.5 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 23, 2026 License: MIT Imports: 5 Imported by: 0

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):

  1. 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.

  2. 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`).

  3. 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.

  4. 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

Constants

This section is empty.

Variables

This section is empty.

Functions

func RegisterBlockTag

func RegisterBlockTag(name, endTag string, followers ...string)

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

type Attribute struct {
	Key   string
	Value string
}

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.

func (*Attribute) Dump

func (a *Attribute) Dump(indent int) string

type AttributeEntityEncodingFromTo

type AttributeEntityEncodingFromTo struct {
	From string
	To   string
}

AttributeEntityEncodingFromTo specifies a pair of strings used during HTML attribute value encoding.

type CommentNode

type CommentNode struct {
	Text string
	Line int
}

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

type IndentConfig struct {
	SpaceIndent             bool
	IndentSize              int
	TwigBlockIndentChildren bool
}

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 Node

type Node interface {
	Dump(indent int) string
}

Node is the interface for nodes in the AST.

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 NewParser

func NewParser(input string) (NodeList, error)

func NewParserWithConfig

func NewParserWithConfig(input string, config IndentConfig) (NodeList, error)

NewParserWithConfig creates a new parser with a specific indentation configuration.

func (NodeList) Dump

func (nodeList NodeList) Dump(indent int) string

type ParentNode

type ParentNode struct {
	Trim TwigTrim
	Line int
}

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

type Pos struct {
	Offset int
	Line   int
}

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.

func (Pos) ColumnIn

func (p Pos) ColumnIn(src string) int

ColumnIn returns the 1-based byte column of this position within src.

func (Pos) String

func (p Pos) String() string

String renders the position as "line:offset". Column is omitted because it can only be derived with the source string (see ColumnIn); use that when a column is needed.

type RawNode

type RawNode struct {
	Text string
	Line int
}

RawNode holds verbatim source text — anything outside structured HTML or Twig syntax, plus the bytes of any Twig tag that has no registered handler.

func (*RawNode) Dump

func (r *RawNode) Dump(indent int) string

type RelatedLoc

type RelatedLoc struct {
	Pos Pos
	Msg string
}

RelatedLoc points to another location relevant to a ParseError (e.g. the opening tag for an unmatched closer).

type TagParseFunc

type TagParseFunc func(p *parser, openTok token) (Node, error)

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

type TemplateExpressionNode struct {
	Expression string
	Trim       TwigTrim
	Line       int
}

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

type TwigCommentNode struct {
	Body string
	Trim TwigTrim
	Line int
}

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

type TwigIfBranch struct {
	Condition string
	Body      NodeList
	Trim      TwigTrim
}

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

type TwigStandaloneTagNode struct {
	Name string
	Args string
	Trim TwigTrim
	Line int
}

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

type TwigVerbatimNode struct {
	Body      string
	OpenTrim  TwigTrim
	CloseTrim TwigTrim
	Line      int
}

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.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL