waveterm/pkg/vdom/cssparser/cssparser.go
2024-10-17 14:50:36 -07:00

160 lines
3.4 KiB
Go

// Copyright 2024, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
package cssparser
import (
"fmt"
"strings"
"unicode"
)
type Parser struct {
Input string
Pos int
Length int
InQuote bool
QuoteChar rune
OpenParens int
Debug bool
}
func MakeParser(input string) *Parser {
return &Parser{
Input: input,
Length: len(input),
}
}
func (p *Parser) Parse() (map[string]string, error) {
result := make(map[string]string)
lastProp := ""
for {
p.skipWhitespace()
if p.eof() {
break
}
propName, err := p.parseIdentifierColon(lastProp)
if err != nil {
return nil, err
}
lastProp = propName
p.skipWhitespace()
value, err := p.parseValue(propName)
if err != nil {
return nil, err
}
result[propName] = value
p.skipWhitespace()
if p.eof() {
break
}
if !p.expectChar(';') {
break
}
}
p.skipWhitespace()
if !p.eof() {
return nil, fmt.Errorf("bad style attribute, unexpected character %q at pos %d", string(p.Input[p.Pos]), p.Pos+1)
}
return result, nil
}
func (p *Parser) parseIdentifierColon(lastProp string) (string, error) {
start := p.Pos
for !p.eof() {
c := p.peekChar()
if isIdentChar(c) || c == '-' {
p.advance()
} else {
break
}
}
attrName := p.Input[start:p.Pos]
p.skipWhitespace()
if p.eof() {
return "", fmt.Errorf("bad style attribute, expected colon after property %q, got EOF, at pos %d", attrName, p.Pos+1)
}
if attrName == "" {
return "", fmt.Errorf("bad style attribute, invalid property name after property %q, at pos %d", lastProp, p.Pos+1)
}
if !p.expectChar(':') {
return "", fmt.Errorf("bad style attribute, bad property name starting with %q, expected colon, got %q, at pos %d", attrName, string(p.Input[p.Pos]), p.Pos+1)
}
return attrName, nil
}
func (p *Parser) parseValue(propName string) (string, error) {
start := p.Pos
quotePos := 0
parenPosStack := make([]int, 0)
for !p.eof() {
c := p.peekChar()
if p.InQuote {
if c == p.QuoteChar {
p.InQuote = false
} else if c == '\\' {
p.advance()
}
} else {
if c == '"' || c == '\'' {
p.InQuote = true
p.QuoteChar = c
quotePos = p.Pos
} else if c == '(' {
p.OpenParens++
parenPosStack = append(parenPosStack, p.Pos)
} else if c == ')' {
if p.OpenParens == 0 {
return "", fmt.Errorf("unmatched ')' at pos %d", p.Pos+1)
}
p.OpenParens--
parenPosStack = parenPosStack[:len(parenPosStack)-1]
} else if c == ';' && p.OpenParens == 0 {
break
}
}
p.advance()
}
if p.eof() && p.InQuote {
return "", fmt.Errorf("bad style attribute, while parsing attribute %q, unmatched quote at pos %d", propName, quotePos+1)
}
if p.eof() && p.OpenParens > 0 {
return "", fmt.Errorf("bad style attribute, while parsing property %q, unmatched '(' at pos %d", propName, parenPosStack[len(parenPosStack)-1]+1)
}
return strings.TrimSpace(p.Input[start:p.Pos]), nil
}
func isIdentChar(r rune) bool {
return unicode.IsLetter(r) || unicode.IsDigit(r)
}
func (p *Parser) skipWhitespace() {
for !p.eof() && unicode.IsSpace(p.peekChar()) {
p.advance()
}
}
func (p *Parser) expectChar(expected rune) bool {
if !p.eof() && p.peekChar() == expected {
p.advance()
return true
}
return false
}
func (p *Parser) peekChar() rune {
if p.Pos >= p.Length {
return 0
}
return rune(p.Input[p.Pos])
}
func (p *Parser) advance() {
p.Pos++
}
func (p *Parser) eof() bool {
return p.Pos >= p.Length
}