learnlytics-go/templ/parser/v2/elementparser.go
2025-03-20 12:35:13 +01:00

510 lines
14 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package parser
import (
"fmt"
"html"
"strings"
"github.com/a-h/parse"
"github.com/a-h/templ/parser/v2/goexpression"
)
// Element.
// Element open tag.
type elementOpenTag struct {
Name string
Attributes []Attribute
IndentAttrs bool
NameRange Range
Void bool
}
var elementOpenTagParser = parse.Func(func(pi *parse.Input) (e elementOpenTag, ok bool, err error) {
start := pi.Position()
// <
if _, ok, err = lt.Parse(pi); err != nil || !ok {
return
}
// Element name.
l := pi.Position().Line
if e.Name, ok, err = elementNameParser.Parse(pi); err != nil || !ok {
pi.Seek(start.Index)
return
}
e.NameRange = NewRange(pi.PositionAt(pi.Index()-len(e.Name)), pi.Position())
if e.Attributes, ok, err = (attributesParser{}).Parse(pi); err != nil || !ok {
pi.Seek(start.Index)
return
}
// If any attribute is not on the same line as the element name, indent them.
if pi.Position().Line != l {
e.IndentAttrs = true
}
// Optional whitespace.
if _, _, err = parse.OptionalWhitespace.Parse(pi); err != nil {
pi.Seek(start.Index)
return
}
// />
if _, ok, err = parse.String("/>").Parse(pi); err != nil {
return
}
if ok {
e.Void = true
return
}
// >
if _, ok, err = gt.Parse(pi); err != nil {
return
}
// If it's not a self-closing or complete open element, we have an error.
if !ok {
err = parse.Error(fmt.Sprintf("<%s>: malformed open element", e.Name), pi.Position())
return
}
return e, true, nil
})
// Attribute name.
var (
attributeNameFirst = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ:_@"
attributeNameSubsequent = attributeNameFirst + "-.0123456789*"
attributeNameParser = parse.Func(func(in *parse.Input) (name string, ok bool, err error) {
start := in.Index()
var prefix, suffix string
if prefix, ok, err = parse.RuneIn(attributeNameFirst).Parse(in); err != nil || !ok {
return
}
if suffix, ok, err = parse.StringUntil(parse.RuneNotIn(attributeNameSubsequent)).Parse(in); err != nil {
in.Seek(start)
return
}
if len(suffix)+1 > 128 {
ok = false
err = parse.Error("attribute names must be < 128 characters long", in.Position())
return
}
return prefix + suffix, true, nil
})
)
type attributeValueParser struct {
EqualsAndQuote parse.Parser[string]
Suffix parse.Parser[string]
UseSingleQuote bool
}
func (avp attributeValueParser) Parse(pi *parse.Input) (value string, ok bool, err error) {
start := pi.Index()
if _, ok, err = avp.EqualsAndQuote.Parse(pi); err != nil || !ok {
return
}
if value, ok, err = parse.StringUntil(avp.Suffix).Parse(pi); err != nil || !ok {
pi.Seek(start)
return
}
if _, ok, err = avp.Suffix.Parse(pi); err != nil || !ok {
pi.Seek(start)
return
}
return value, true, nil
}
// Constant attribute.
var (
attributeValueParsers = []attributeValueParser{
// Double quoted.
{EqualsAndQuote: parse.String(`="`), Suffix: parse.String(`"`), UseSingleQuote: false},
// Single quoted.
{EqualsAndQuote: parse.String(`='`), Suffix: parse.String(`'`), UseSingleQuote: true},
// Unquoted.
// A valid unquoted attribute value in HTML is any string of text that is not an empty string,
// and that doesnt contain spaces, tabs, line feeds, form feeds, carriage returns, ", ', `, =, <, or >.
{EqualsAndQuote: parse.String("="), Suffix: parse.Any(parse.RuneIn(" \t\n\r\"'`=<>/"), parse.EOF[string]()), UseSingleQuote: false},
}
constantAttributeParser = parse.Func(func(pi *parse.Input) (attr ConstantAttribute, ok bool, err error) {
start := pi.Index()
// Optional whitespace leader.
if _, ok, err = parse.OptionalWhitespace.Parse(pi); err != nil || !ok {
return
}
// Attribute name.
if attr.Name, ok, err = attributeNameParser.Parse(pi); err != nil || !ok {
pi.Seek(start)
return
}
attr.NameRange = NewRange(pi.PositionAt(pi.Index()-len(attr.Name)), pi.Position())
for _, p := range attributeValueParsers {
attr.Value, ok, err = p.Parse(pi)
if err != nil {
pos := pi.Position()
if pErr, isParseError := err.(parse.ParseError); isParseError {
pos = pErr.Pos
}
return attr, false, parse.Error(fmt.Sprintf("%s: %v", attr.Name, err), pos)
}
if ok {
attr.SingleQuote = p.UseSingleQuote
break
}
}
if !ok {
pi.Seek(start)
return attr, false, nil
}
attr.Value = html.UnescapeString(attr.Value)
// Only use single quotes if actually required, due to double quote in the value (prefer double quotes).
attr.SingleQuote = attr.SingleQuote && strings.Contains(attr.Value, "\"")
return attr, true, nil
})
)
// BoolConstantAttribute.
var boolConstantAttributeParser = parse.Func(func(pi *parse.Input) (attr BoolConstantAttribute, ok bool, err error) {
start := pi.Index()
// Optional whitespace leader.
if _, ok, err = parse.OptionalWhitespace.Parse(pi); err != nil || !ok {
return
}
// Attribute name.
if attr.Name, ok, err = attributeNameParser.Parse(pi); err != nil || !ok {
pi.Seek(start)
return
}
attr.NameRange = NewRange(pi.PositionAt(pi.Index()-len(attr.Name)), pi.Position())
// We have a name, but if we have an equals sign, it's not a constant boolean attribute.
next, ok := pi.Peek(1)
if !ok {
err = parse.Error("boolConstantAttributeParser: unexpected EOF after attribute name", pi.Position())
return
}
if next == "=" || next == "?" {
// It's one of the other attribute types.
pi.Seek(start)
return attr, false, nil
}
if !(next == " " || next == "\t" || next == "\r" || next == "\n" || next == "/" || next == ">") {
err = parse.Error(fmt.Sprintf("boolConstantAttributeParser: expected attribute name to end with space, newline, '/>' or '>', but got %q", next), pi.Position())
return attr, false, err
}
return attr, true, nil
})
// BoolExpressionAttribute.
var boolExpressionStart = parse.Or(parse.String("?={ "), parse.String("?={"))
var boolExpressionAttributeParser = parse.Func(func(pi *parse.Input) (r BoolExpressionAttribute, ok bool, err error) {
start := pi.Index()
// Optional whitespace leader.
if _, ok, err = parse.OptionalWhitespace.Parse(pi); err != nil || !ok {
pi.Seek(start)
return
}
// Attribute name.
if r.Name, ok, err = attributeNameParser.Parse(pi); err != nil || !ok {
pi.Seek(start)
return
}
r.NameRange = NewRange(pi.PositionAt(pi.Index()-len(r.Name)), pi.Position())
// Check whether this is a boolean expression attribute.
if _, ok, err = boolExpressionStart.Parse(pi); err != nil || !ok {
pi.Seek(start)
return
}
// Once we have a prefix, we must have an expression that returns a boolean.
if r.Expression, err = parseGo("boolean attribute", pi, goexpression.Expression); err != nil {
return r, false, err
}
// Eat the Final brace.
if _, ok, err = closeBraceWithOptionalPadding.Parse(pi); err != nil || !ok {
err = parse.Error("boolean expression: missing closing brace", pi.Position())
pi.Seek(start)
return
}
return r, true, nil
})
var expressionAttributeParser = parse.Func(func(pi *parse.Input) (attr ExpressionAttribute, ok bool, err error) {
start := pi.Index()
// Optional whitespace leader.
if _, ok, err = parse.OptionalWhitespace.Parse(pi); err != nil || !ok {
return
}
// Attribute name.
if attr.Name, ok, err = attributeNameParser.Parse(pi); err != nil || !ok {
pi.Seek(start)
return
}
attr.NameRange = NewRange(pi.PositionAt(pi.Index()-len(attr.Name)), pi.Position())
// ={
if _, ok, err = parse.Or(parse.String("={ "), parse.String("={")).Parse(pi); err != nil || !ok {
pi.Seek(start)
return
}
// Expression.
if attr.Expression, err = parseGoSliceArgs(pi); err != nil {
return attr, false, err
}
// Eat whitespace, plus the final brace.
if _, _, err = parse.OptionalWhitespace.Parse(pi); err != nil {
return attr, false, err
}
if _, ok, err = closeBrace.Parse(pi); err != nil || !ok {
err = parse.Error("string expression attribute: missing closing brace", pi.Position())
return
}
return attr, true, nil
})
var spreadAttributesParser = parse.Func(func(pi *parse.Input) (attr SpreadAttributes, ok bool, err error) {
start := pi.Index()
// Optional whitespace leader.
if _, ok, err = parse.OptionalWhitespace.Parse(pi); err != nil || !ok {
return
}
// Eat the first brace.
if _, ok, err = openBraceWithOptionalPadding.Parse(pi); err != nil ||
!ok {
pi.Seek(start)
return
}
// Expression.
if attr.Expression, err = parseGo("spread attributes", pi, goexpression.Expression); err != nil {
return
}
// Check if end of expression has "..." for spread.
if !strings.HasSuffix(attr.Expression.Value, "...") {
pi.Seek(start)
ok = false
return
}
// Remove extra spread characters from expression.
attr.Expression.Value = strings.TrimSuffix(attr.Expression.Value, "...")
attr.Expression.Range.To.Col -= 3
attr.Expression.Range.To.Index -= 3
// Eat the final brace.
if _, ok, err = closeBraceWithOptionalPadding.Parse(pi); err != nil || !ok {
err = parse.Error("attribute spread expression: missing closing brace", pi.Position())
return
}
return attr, true, nil
})
// Attributes.
type attributeParser struct{}
func (attributeParser) Parse(in *parse.Input) (out Attribute, ok bool, err error) {
if out, ok, err = boolExpressionAttributeParser.Parse(in); err != nil || ok {
return
}
if out, ok, err = expressionAttributeParser.Parse(in); err != nil || ok {
return
}
if out, ok, err = conditionalAttribute.Parse(in); err != nil || ok {
return
}
if out, ok, err = boolConstantAttributeParser.Parse(in); err != nil || ok {
return
}
if out, ok, err = spreadAttributesParser.Parse(in); err != nil || ok {
return
}
if out, ok, err = constantAttributeParser.Parse(in); err != nil || ok {
return
}
return
}
var attribute attributeParser
type attributesParser struct{}
func (attributesParser) Parse(in *parse.Input) (attributes []Attribute, ok bool, err error) {
for {
var attr Attribute
attr, ok, err = attribute.Parse(in)
if err != nil {
return
}
if !ok {
break
}
attributes = append(attributes, attr)
}
return attributes, true, nil
}
// Element name.
var (
elementNameFirst = "abcdefghijklmnopqrstuvwxyz"
elementNameSubsequent = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-:"
elementNameParser = parse.Func(func(in *parse.Input) (name string, ok bool, err error) {
start := in.Index()
var prefix, suffix string
if prefix, ok, err = parse.RuneIn(elementNameFirst).Parse(in); err != nil || !ok {
return
}
if suffix, ok, err = parse.StringUntil(parse.RuneNotIn(elementNameSubsequent)).Parse(in); err != nil || !ok {
in.Seek(start)
return
}
if len(suffix)+1 > 128 {
ok = false
err = parse.Error("element names must be < 128 characters long", in.Position())
return
}
return prefix + suffix, true, nil
})
)
// Void element closer.
var voidElementCloser voidElementCloserParser
type voidElementCloserParser struct{}
var voidElementCloseTags = []string{"</area>", "</base>", "</br>", "</col>", "</command>", "</embed>", "</hr>", "</img>", "</input>", "</keygen>", "</link>", "</meta>", "</param>", "</source>", "</track>", "</wbr>"}
func (voidElementCloserParser) Parse(pi *parse.Input) (n Node, ok bool, err error) {
var ve string
for _, ve = range voidElementCloseTags {
s, canPeekLen := pi.Peek(len(ve))
if !canPeekLen {
continue
}
if !strings.EqualFold(s, ve) {
continue
}
// Found a match.
ok = true
break
}
if !ok {
return nil, false, nil
}
pi.Take(len(ve))
return nil, true, nil
}
// Element.
var element elementParser
type elementParser struct{}
func (elementParser) Parse(pi *parse.Input) (n Node, ok bool, err error) {
var r Element
start := pi.Position()
// Check the open tag.
var ot elementOpenTag
if ot, ok, err = elementOpenTagParser.Parse(pi); err != nil || !ok {
return
}
r.Name = ot.Name
r.Attributes = ot.Attributes
r.IndentAttrs = ot.IndentAttrs
r.NameRange = ot.NameRange
// Once we've got an open tag, the rest must be present.
l := pi.Position().Line
// If the element is self-closing, even if it's not really a void element (br, hr etc.), we can return early.
if ot.Void || r.IsVoidElement() {
// Escape early, no need to try to parse children for self-closing elements.
return addTrailingSpaceAndValidate(start, r, pi)
}
// Parse children.
closer := StripType(parse.All(parse.String("</"), parse.String(ot.Name), parse.Rune('>')))
tnp := newTemplateNodeParser(closer, fmt.Sprintf("<%s>: close tag", ot.Name))
nodes, _, err := tnp.Parse(pi)
if err != nil {
notFoundErr, isNotFoundError := err.(UntilNotFoundError)
if isNotFoundError {
err = notFoundErr.ParseError
}
return r, false, err
}
r.Children = nodes.Nodes
// If the children are not all on the same line, indent them.
if l != pi.Position().Line {
r.IndentChildren = true
}
// Close tag.
_, ok, err = closer.Parse(pi)
if err != nil {
return r, false, err
}
if !ok {
err = parse.Error(fmt.Sprintf("<%s>: expected end tag not present or invalid tag contents", r.Name), pi.Position())
return r, false, err
}
return addTrailingSpaceAndValidate(start, r, pi)
}
func addTrailingSpaceAndValidate(start parse.Position, e Element, pi *parse.Input) (n Node, ok bool, err error) {
// Elide any void close tags.
if _, _, err = voidElementCloser.Parse(pi); err != nil {
return e, false, err
}
// Add trailing space.
ws, _, err := parse.Whitespace.Parse(pi)
if err != nil {
return e, false, err
}
e.TrailingSpace, err = NewTrailingSpace(ws)
if err != nil {
return e, false, err
}
// Validate.
var msgs []string
if msgs, ok = e.Validate(); !ok {
err = parse.Error(fmt.Sprintf("<%s>: %s", e.Name, strings.Join(msgs, ", ")), start)
return e, false, err
}
return e, true, nil
}