893 lines
19 KiB
Go
893 lines
19 KiB
Go
package interpres
|
|
|
|
import (
|
|
"fmt"
|
|
"strconv"
|
|
"strings"
|
|
)
|
|
|
|
// parser is a recursive-descent TOML parser producing a map[string]any tree.
|
|
type parser struct {
|
|
src []rune
|
|
pos int
|
|
line int
|
|
|
|
root map[string]any
|
|
current map[string]any
|
|
headers map[string]bool
|
|
frozen map[string]bool
|
|
dotted map[string]bool
|
|
arrays map[string]bool
|
|
|
|
currentPath []string
|
|
}
|
|
|
|
func (p *parser) parse() (map[string]any, error) {
|
|
p.root = map[string]any{}
|
|
p.current = p.root
|
|
p.headers = map[string]bool{}
|
|
p.frozen = map[string]bool{}
|
|
p.dotted = map[string]bool{}
|
|
p.arrays = map[string]bool{}
|
|
p.currentPath = nil
|
|
|
|
for {
|
|
if err := p.skipBlank(); err != nil {
|
|
return nil, err
|
|
}
|
|
if p.eof() {
|
|
break
|
|
}
|
|
c := p.peek()
|
|
switch {
|
|
case c == '[':
|
|
if err := p.parseTableHeader(); err != nil {
|
|
return nil, err
|
|
}
|
|
default:
|
|
if err := p.parseKeyValue(); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
if err := p.expectLineEnd(); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
return p.root, nil
|
|
}
|
|
|
|
// --- table headers ---------------------------------------------------------
|
|
|
|
func (p *parser) parseTableHeader() error {
|
|
array := false
|
|
p.next() // consume '['
|
|
if !p.eof() && p.peek() == '[' {
|
|
array = true
|
|
p.next()
|
|
}
|
|
|
|
key, err := p.parseKeyPath()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
p.skipInline()
|
|
if p.eof() || p.peek() != ']' {
|
|
return p.errf("expected ']' to close table header")
|
|
}
|
|
p.next()
|
|
if array {
|
|
if p.eof() || p.peek() != ']' {
|
|
return p.errf("expected ']]' to close array-of-tables header")
|
|
}
|
|
p.next()
|
|
}
|
|
|
|
if array {
|
|
tbl, err := p.appendArrayTable(key)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
// A new array-of-tables element starts a fresh scope: sub-table headers
|
|
// and inline-table freezes from the previous element no longer apply.
|
|
p.resetScopeUnder(key)
|
|
p.arrays[pathKey(key)] = true
|
|
p.current = tbl
|
|
p.currentPath = key
|
|
return nil
|
|
}
|
|
|
|
pk := pathKey(key)
|
|
if p.headers[pk] || p.dotted[pk] || p.arrays[pk] {
|
|
return p.errf("table %q is defined more than once", strings.Join(key, "."))
|
|
}
|
|
p.headers[pk] = true
|
|
|
|
tbl, err := p.tableAt(key)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
p.current = tbl
|
|
p.currentPath = key
|
|
return nil
|
|
}
|
|
|
|
// tableAt walks (creating intermediate tables) to the table named by key,
|
|
// relative to the document root, rejecting any step into a frozen inline table.
|
|
func (p *parser) tableAt(key []string) (map[string]any, error) {
|
|
cur := p.root
|
|
path := make([]string, 0, len(key))
|
|
for _, k := range key {
|
|
path = append(path, k)
|
|
if p.frozen[pathKey(path)] {
|
|
return nil, p.errf("cannot extend inline table %q", strings.Join(path, "."))
|
|
}
|
|
existing, ok := cur[k]
|
|
if !ok {
|
|
next := map[string]any{}
|
|
cur[k] = next
|
|
cur = next
|
|
continue
|
|
}
|
|
switch v := existing.(type) {
|
|
case map[string]any:
|
|
cur = v
|
|
case []map[string]any:
|
|
if len(v) == 0 {
|
|
return nil, p.errf("key %q is an empty array of tables", k)
|
|
}
|
|
cur = v[len(v)-1]
|
|
default:
|
|
return nil, p.errf("key %q is not a table", k)
|
|
}
|
|
}
|
|
return cur, nil
|
|
}
|
|
|
|
func (p *parser) appendArrayTable(key []string) (map[string]any, error) {
|
|
parent := p.root
|
|
for _, k := range key[:len(key)-1] {
|
|
existing, ok := parent[k]
|
|
if !ok {
|
|
next := map[string]any{}
|
|
parent[k] = next
|
|
parent = next
|
|
continue
|
|
}
|
|
switch v := existing.(type) {
|
|
case map[string]any:
|
|
parent = v
|
|
case []map[string]any:
|
|
parent = v[len(v)-1]
|
|
default:
|
|
return nil, p.errf("key %q is not a table", k)
|
|
}
|
|
}
|
|
|
|
leaf := key[len(key)-1]
|
|
tbl := map[string]any{}
|
|
switch existing := parent[leaf].(type) {
|
|
case nil:
|
|
parent[leaf] = []map[string]any{tbl}
|
|
case []map[string]any:
|
|
parent[leaf] = append(existing, tbl)
|
|
default:
|
|
return nil, p.errf("key %q is not an array of tables", leaf)
|
|
}
|
|
return tbl, nil
|
|
}
|
|
|
|
// --- key/value -------------------------------------------------------------
|
|
|
|
func (p *parser) parseKeyValue() error {
|
|
key, err := p.parseKeyPath()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
p.skipInline()
|
|
if p.eof() || p.peek() != '=' {
|
|
return p.errf("expected '=' after key")
|
|
}
|
|
p.next()
|
|
p.skipInline()
|
|
|
|
val, err := p.parseValue()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
dest := p.current
|
|
abs := append([]string{}, p.currentPath...)
|
|
for _, k := range key[:len(key)-1] {
|
|
abs = append(abs, k)
|
|
if p.frozen[pathKey(abs)] {
|
|
return p.errf("cannot extend inline table %q", strings.Join(abs, "."))
|
|
}
|
|
if p.headers[pathKey(abs)] {
|
|
return p.errf("cannot extend table %q with a dotted key", strings.Join(abs, "."))
|
|
}
|
|
p.dotted[pathKey(abs)] = true
|
|
existing, ok := dest[k]
|
|
if !ok {
|
|
next := map[string]any{}
|
|
dest[k] = next
|
|
dest = next
|
|
continue
|
|
}
|
|
m, ok := existing.(map[string]any)
|
|
if !ok {
|
|
return p.errf("key %q is not a table", k)
|
|
}
|
|
dest = m
|
|
}
|
|
leaf := key[len(key)-1]
|
|
abs = append(abs, leaf)
|
|
if _, exists := dest[leaf]; exists {
|
|
return p.errf("duplicate key %q", leaf)
|
|
}
|
|
dest[leaf] = val
|
|
p.freezeInline(abs, val)
|
|
return nil
|
|
}
|
|
|
|
// freezeInline marks the path of an inline table (and any nested inline tables)
|
|
// as immutable, so a later header or dotted key cannot extend it.
|
|
func (p *parser) freezeInline(path []string, val any) {
|
|
m, ok := val.(map[string]any)
|
|
if !ok {
|
|
return
|
|
}
|
|
p.frozen[pathKey(path)] = true
|
|
for k, v := range m {
|
|
child := append(append([]string{}, path...), k)
|
|
p.freezeInline(child, v)
|
|
}
|
|
}
|
|
|
|
// resetScopeUnder forgets the header and freeze records nested under key, which
|
|
// belong to the previous element of an array of tables.
|
|
func (p *parser) resetScopeUnder(key []string) {
|
|
prefix := pathKey(key) + "\x00"
|
|
for k := range p.headers {
|
|
if strings.HasPrefix(k, prefix) {
|
|
delete(p.headers, k)
|
|
}
|
|
}
|
|
for k := range p.frozen {
|
|
if strings.HasPrefix(k, prefix) {
|
|
delete(p.frozen, k)
|
|
}
|
|
}
|
|
}
|
|
|
|
// parseKeyPath parses a dotted key into its components.
|
|
func (p *parser) parseKeyPath() ([]string, error) {
|
|
var parts []string
|
|
for {
|
|
p.skipInline()
|
|
part, err := p.parseKeyComponent()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
parts = append(parts, part)
|
|
p.skipInline()
|
|
if !p.eof() && p.peek() == '.' {
|
|
p.next()
|
|
continue
|
|
}
|
|
break
|
|
}
|
|
return parts, nil
|
|
}
|
|
|
|
func (p *parser) parseKeyComponent() (string, error) {
|
|
if p.eof() {
|
|
return "", p.errf("expected a key")
|
|
}
|
|
switch c := p.peek(); c {
|
|
case '"':
|
|
if p.lookahead(`"""`) {
|
|
return "", p.errf("multiline strings are not allowed in keys")
|
|
}
|
|
return p.parseBasicString()
|
|
case '\'':
|
|
if p.lookahead(`'''`) {
|
|
return "", p.errf("multiline strings are not allowed in keys")
|
|
}
|
|
return p.parseLiteralString()
|
|
default:
|
|
start := p.pos
|
|
for !p.eof() {
|
|
c := p.peek()
|
|
if (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') ||
|
|
(c >= '0' && c <= '9') || c == '_' || c == '-' {
|
|
p.next()
|
|
continue
|
|
}
|
|
break
|
|
}
|
|
if p.pos == start {
|
|
return "", p.errf("invalid key character %q", string(p.peek()))
|
|
}
|
|
return string(p.src[start:p.pos]), nil
|
|
}
|
|
}
|
|
|
|
// --- values ----------------------------------------------------------------
|
|
|
|
func (p *parser) parseValue() (any, error) {
|
|
if p.eof() {
|
|
return nil, p.errf("expected a value")
|
|
}
|
|
switch c := p.peek(); {
|
|
case c == '"':
|
|
return p.parseBasicString()
|
|
case c == '\'':
|
|
return p.parseLiteralString()
|
|
case c == '[':
|
|
return p.parseArray()
|
|
case c == '{':
|
|
return p.parseInlineTable()
|
|
case c == 't' || c == 'f':
|
|
return p.parseBool()
|
|
default:
|
|
return p.parseAtom()
|
|
}
|
|
}
|
|
|
|
func (p *parser) parseBool() (any, error) {
|
|
if p.match("true") {
|
|
return true, nil
|
|
}
|
|
if p.match("false") {
|
|
return false, nil
|
|
}
|
|
return nil, p.errf("invalid value")
|
|
}
|
|
|
|
// parseAtom handles numbers, inf/nan, and date-times.
|
|
func (p *parser) parseAtom() (any, error) {
|
|
start := p.pos
|
|
p.scanBareToken()
|
|
tok := string(p.src[start:p.pos])
|
|
if tok == "" {
|
|
return nil, p.errf("expected a value")
|
|
}
|
|
// A date may be followed by a space and a time, forming one date-time.
|
|
if isDateToken(tok) && !p.eof() && p.peek() == ' ' &&
|
|
p.pos+1 < len(p.src) && p.src[p.pos+1] >= '0' && p.src[p.pos+1] <= '9' {
|
|
p.next() // consume the separating space
|
|
timeStart := p.pos
|
|
p.scanBareToken()
|
|
tok = tok + " " + string(p.src[timeStart:p.pos])
|
|
}
|
|
if v, ok := parseDateTime(tok); ok {
|
|
return v, nil
|
|
}
|
|
v, err := decodeNumber(tok)
|
|
if err != nil {
|
|
return nil, p.errf("%s", err)
|
|
}
|
|
return v, nil
|
|
}
|
|
|
|
// scanBareToken advances past a bare value token (number, bool, or date-time),
|
|
// stopping at whitespace, a separator, or a comment.
|
|
func (p *parser) scanBareToken() {
|
|
for !p.eof() {
|
|
c := p.peek()
|
|
if c == ' ' || c == '\t' || c == '\n' || c == '\r' ||
|
|
c == ',' || c == ']' || c == '}' || c == '#' {
|
|
return
|
|
}
|
|
p.next()
|
|
}
|
|
}
|
|
|
|
// --- strings ---------------------------------------------------------------
|
|
|
|
func (p *parser) parseBasicString() (string, error) {
|
|
if p.lookahead(`"""`) {
|
|
return p.parseMultilineString('"', true)
|
|
}
|
|
p.next() // opening quote
|
|
var b strings.Builder
|
|
for {
|
|
if p.eof() {
|
|
return "", p.errf("unterminated string")
|
|
}
|
|
c := p.next()
|
|
switch c {
|
|
case '"':
|
|
return b.String(), nil
|
|
case '\n':
|
|
return "", p.errf("unterminated string")
|
|
case '\r':
|
|
return "", p.errf("bare carriage return is not allowed in a string")
|
|
case '\\':
|
|
r, err := p.readEscape()
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
b.WriteRune(r)
|
|
default:
|
|
if isControlRune(c) {
|
|
return "", p.errf("control character U+%04X is not allowed in a string", c)
|
|
}
|
|
b.WriteRune(c)
|
|
}
|
|
}
|
|
}
|
|
|
|
func (p *parser) parseLiteralString() (string, error) {
|
|
if p.lookahead(`'''`) {
|
|
return p.parseMultilineString('\'', false)
|
|
}
|
|
p.next() // opening quote
|
|
var b strings.Builder
|
|
for {
|
|
if p.eof() {
|
|
return "", p.errf("unterminated literal string")
|
|
}
|
|
c := p.next()
|
|
if c == '\'' {
|
|
return b.String(), nil
|
|
}
|
|
if c == '\n' {
|
|
return "", p.errf("unterminated literal string")
|
|
}
|
|
if c == '\r' {
|
|
return "", p.errf("bare carriage return is not allowed in a string")
|
|
}
|
|
if isControlRune(c) {
|
|
return "", p.errf("control character U+%04X is not allowed in a string", c)
|
|
}
|
|
b.WriteRune(c)
|
|
}
|
|
}
|
|
|
|
func (p *parser) parseMultilineString(quote rune, escapes bool) (string, error) {
|
|
p.skipN(3) // opening delimiter
|
|
// A newline immediately after the opening delimiter is trimmed.
|
|
if !p.eof() && p.peek() == '\r' {
|
|
p.next()
|
|
}
|
|
if !p.eof() && p.peek() == '\n' {
|
|
p.line++
|
|
p.next()
|
|
}
|
|
|
|
var b strings.Builder
|
|
for {
|
|
if p.eof() {
|
|
return "", p.errf("unterminated multiline string")
|
|
}
|
|
if p.peek() == quote {
|
|
// Count the run of delimiter characters. The last three close the
|
|
// string; up to two extra ones belong to the content.
|
|
n := 0
|
|
for p.pos+n < len(p.src) && p.src[p.pos+n] == quote {
|
|
n++
|
|
}
|
|
if n >= 3 {
|
|
if n > 5 {
|
|
return "", p.errf("too many '%c' before the closing delimiter", quote)
|
|
}
|
|
for i := 0; i < n-3; i++ {
|
|
b.WriteRune(quote)
|
|
}
|
|
p.skipN(n)
|
|
return b.String(), nil
|
|
}
|
|
for i := 0; i < n; i++ {
|
|
b.WriteRune(quote)
|
|
p.next()
|
|
}
|
|
continue
|
|
}
|
|
c := p.next()
|
|
if c == '\n' {
|
|
p.line++
|
|
b.WriteRune(c)
|
|
continue
|
|
}
|
|
if c == '\r' {
|
|
if !p.eof() && p.peek() == '\n' {
|
|
b.WriteRune(c)
|
|
continue
|
|
}
|
|
return "", p.errf("bare carriage return is not allowed in a string")
|
|
}
|
|
if escapes && c == '\\' {
|
|
// Line-ending backslash trims the following whitespace/newlines.
|
|
if p.trimLineEndingBackslash() {
|
|
continue
|
|
}
|
|
r, err := p.readEscape()
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
b.WriteRune(r)
|
|
continue
|
|
}
|
|
if isControlRune(c) {
|
|
return "", p.errf("control character U+%04X is not allowed in a string", c)
|
|
}
|
|
b.WriteRune(c)
|
|
}
|
|
}
|
|
|
|
// trimLineEndingBackslash consumes whitespace through the next newline (and the
|
|
// blank lines that follow) when a backslash is the last token on a line.
|
|
// It reports whether it did so.
|
|
func (p *parser) trimLineEndingBackslash() bool {
|
|
save, saveLine := p.pos, p.line
|
|
for !p.eof() {
|
|
c := p.peek()
|
|
if c == ' ' || c == '\t' || c == '\r' {
|
|
p.next()
|
|
continue
|
|
}
|
|
if c == '\n' {
|
|
break
|
|
}
|
|
// Not a line-ending backslash; restore.
|
|
p.pos, p.line = save, saveLine
|
|
return false
|
|
}
|
|
if p.eof() {
|
|
p.pos, p.line = save, saveLine
|
|
return false
|
|
}
|
|
// Consume the newline and all following whitespace.
|
|
for !p.eof() {
|
|
c := p.peek()
|
|
if c == '\n' {
|
|
p.line++
|
|
p.next()
|
|
continue
|
|
}
|
|
if c == ' ' || c == '\t' || c == '\r' {
|
|
p.next()
|
|
continue
|
|
}
|
|
break
|
|
}
|
|
return true
|
|
}
|
|
|
|
func (p *parser) readEscape() (rune, error) {
|
|
if p.eof() {
|
|
return 0, p.errf("unterminated escape sequence")
|
|
}
|
|
c := p.next()
|
|
switch c {
|
|
case 'b':
|
|
return '\b', nil
|
|
case 't':
|
|
return '\t', nil
|
|
case 'n':
|
|
return '\n', nil
|
|
case 'f':
|
|
return '\f', nil
|
|
case 'r':
|
|
return '\r', nil
|
|
case '"':
|
|
return '"', nil
|
|
case '\\':
|
|
return '\\', nil
|
|
case 'u':
|
|
return p.readUnicode(4)
|
|
case 'U':
|
|
return p.readUnicode(8)
|
|
default:
|
|
return 0, p.errf("invalid escape sequence \\%c", c)
|
|
}
|
|
}
|
|
|
|
func (p *parser) readUnicode(n int) (rune, error) {
|
|
if p.pos+n > len(p.src) {
|
|
return 0, p.errf("invalid unicode escape")
|
|
}
|
|
hex := string(p.src[p.pos : p.pos+n])
|
|
p.pos += n
|
|
v, err := strconv.ParseInt(hex, 16, 64)
|
|
if err != nil {
|
|
return 0, p.errf("invalid unicode escape \\%s", hex)
|
|
}
|
|
if v > 0x10FFFF || (v >= 0xD800 && v <= 0xDFFF) {
|
|
return 0, p.errf("escape \\%s is not a valid Unicode scalar value", hex)
|
|
}
|
|
return rune(v), nil
|
|
}
|
|
|
|
// --- arrays and inline tables ---------------------------------------------
|
|
|
|
func (p *parser) parseArray() (any, error) {
|
|
p.next() // '['
|
|
arr := []any{}
|
|
for {
|
|
if err := p.skipArraySpace(); err != nil {
|
|
return nil, err
|
|
}
|
|
if p.eof() {
|
|
return nil, p.errf("unterminated array")
|
|
}
|
|
if p.peek() == ']' {
|
|
p.next()
|
|
return arr, nil
|
|
}
|
|
v, err := p.parseValue()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
arr = append(arr, v)
|
|
if err := p.skipArraySpace(); err != nil {
|
|
return nil, err
|
|
}
|
|
if p.eof() {
|
|
return nil, p.errf("unterminated array")
|
|
}
|
|
switch p.peek() {
|
|
case ',':
|
|
p.next()
|
|
case ']':
|
|
p.next()
|
|
return arr, nil
|
|
default:
|
|
return nil, p.errf("expected ',' or ']' in array")
|
|
}
|
|
}
|
|
}
|
|
|
|
func (p *parser) parseInlineTable() (any, error) {
|
|
p.next() // '{'
|
|
tbl := map[string]any{}
|
|
assigned := map[string]bool{}
|
|
p.skipInline()
|
|
if !p.eof() && p.peek() == '}' {
|
|
p.next()
|
|
return tbl, nil
|
|
}
|
|
for {
|
|
p.skipInline()
|
|
key, err := p.parseKeyPath()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
p.skipInline()
|
|
if p.eof() || p.peek() != '=' {
|
|
return nil, p.errf("expected '=' in inline table")
|
|
}
|
|
p.next()
|
|
p.skipInline()
|
|
val, err := p.parseValue()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
dest := tbl
|
|
path := make([]string, 0, len(key))
|
|
for _, k := range key[:len(key)-1] {
|
|
path = append(path, k)
|
|
if assigned[pathKey(path)] {
|
|
return nil, p.errf("key %q is already defined", strings.Join(path, "."))
|
|
}
|
|
existing, ok := dest[k]
|
|
if !ok {
|
|
m := map[string]any{}
|
|
dest[k] = m
|
|
dest = m
|
|
continue
|
|
}
|
|
m, isMap := existing.(map[string]any)
|
|
if !isMap {
|
|
return nil, p.errf("key %q is already defined", k)
|
|
}
|
|
dest = m
|
|
}
|
|
leaf := key[len(key)-1]
|
|
path = append(path, leaf)
|
|
if _, exists := dest[leaf]; exists {
|
|
return nil, p.errf("duplicate key %q in inline table", leaf)
|
|
}
|
|
dest[leaf] = val
|
|
assigned[pathKey(path)] = true
|
|
|
|
p.skipInline()
|
|
if p.eof() {
|
|
return nil, p.errf("unterminated inline table")
|
|
}
|
|
switch p.peek() {
|
|
case ',':
|
|
p.next()
|
|
case '}':
|
|
p.next()
|
|
return tbl, nil
|
|
default:
|
|
return nil, p.errf("expected ',' or '}' in inline table")
|
|
}
|
|
}
|
|
}
|
|
|
|
// --- scanning helpers ------------------------------------------------------
|
|
|
|
func (p *parser) eof() bool { return p.pos >= len(p.src) }
|
|
func (p *parser) peek() rune { return p.src[p.pos] }
|
|
|
|
func (p *parser) next() rune {
|
|
c := p.src[p.pos]
|
|
p.pos++
|
|
return c
|
|
}
|
|
|
|
func (p *parser) skipN(n int) {
|
|
for i := 0; i < n && !p.eof(); i++ {
|
|
p.next()
|
|
}
|
|
}
|
|
|
|
func (p *parser) match(word string) bool {
|
|
if p.lookahead(word) {
|
|
p.skipN(len([]rune(word)))
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
func (p *parser) lookahead(s string) bool {
|
|
r := []rune(s)
|
|
if p.pos+len(r) > len(p.src) {
|
|
return false
|
|
}
|
|
for i, c := range r {
|
|
if p.src[p.pos+i] != c {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
// skipInline consumes spaces and tabs only.
|
|
func (p *parser) skipInline() {
|
|
for !p.eof() {
|
|
if c := p.peek(); c == ' ' || c == '\t' {
|
|
p.next()
|
|
continue
|
|
}
|
|
break
|
|
}
|
|
}
|
|
|
|
// skipArraySpace consumes whitespace, newlines, and comments inside arrays.
|
|
func (p *parser) skipArraySpace() error {
|
|
for !p.eof() {
|
|
switch p.peek() {
|
|
case ' ', '\t':
|
|
p.next()
|
|
case '\r':
|
|
if err := p.expectCRLF(); err != nil {
|
|
return err
|
|
}
|
|
case '\n':
|
|
p.line++
|
|
p.next()
|
|
case '#':
|
|
if err := p.skipComment(); err != nil {
|
|
return err
|
|
}
|
|
default:
|
|
return nil
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// skipBlank consumes whitespace, blank lines, and comments between statements.
|
|
func (p *parser) skipBlank() error {
|
|
for !p.eof() {
|
|
switch p.peek() {
|
|
case ' ', '\t':
|
|
p.next()
|
|
case '\r':
|
|
if err := p.expectCRLF(); err != nil {
|
|
return err
|
|
}
|
|
case '\n':
|
|
p.line++
|
|
p.next()
|
|
case '#':
|
|
if err := p.skipComment(); err != nil {
|
|
return err
|
|
}
|
|
default:
|
|
return nil
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (p *parser) skipComment() error {
|
|
p.next() // consume '#'
|
|
for !p.eof() {
|
|
c := p.peek()
|
|
switch {
|
|
case c == '\n':
|
|
return nil
|
|
case c == '\r':
|
|
if p.pos+1 < len(p.src) && p.src[p.pos+1] == '\n' {
|
|
return nil
|
|
}
|
|
return p.errf("bare carriage return is not allowed")
|
|
case c == '\t':
|
|
p.next()
|
|
case c < 0x20 || c == 0x7f:
|
|
return p.errf("control character U+%04X is not allowed in a comment", c)
|
|
default:
|
|
p.next()
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// expectCRLF consumes a carriage return that must be immediately followed by a
|
|
// line feed; a bare CR is invalid.
|
|
func (p *parser) expectCRLF() error {
|
|
if p.pos+1 < len(p.src) && p.src[p.pos+1] == '\n' {
|
|
p.next() // consume CR; the LF is handled by the caller
|
|
return nil
|
|
}
|
|
return p.errf("bare carriage return is not allowed")
|
|
}
|
|
|
|
// expectLineEnd consumes trailing inline whitespace and an optional comment,
|
|
// then requires a newline or end of input.
|
|
func (p *parser) expectLineEnd() error {
|
|
p.skipInline()
|
|
if p.eof() {
|
|
return nil
|
|
}
|
|
if p.peek() == '#' {
|
|
if err := p.skipComment(); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
if p.eof() {
|
|
return nil
|
|
}
|
|
if p.peek() == '\r' {
|
|
if err := p.expectCRLF(); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
if p.eof() {
|
|
return nil
|
|
}
|
|
if p.peek() == '\n' {
|
|
p.line++
|
|
p.next()
|
|
return nil
|
|
}
|
|
return p.errf("unexpected %q after value", string(p.peek()))
|
|
}
|
|
|
|
func (p *parser) errf(format string, args ...any) error {
|
|
return &SyntaxError{Line: p.line, Msg: fmt.Sprintf(format, args...)}
|
|
}
|
|
|
|
// pathKey joins key components with a NUL separator so a dotted path can be
|
|
// used as a map key for tracking defined tables.
|
|
func pathKey(parts []string) string {
|
|
return strings.Join(parts, "\x00")
|
|
}
|
|
|
|
// isControlRune reports whether r is a control character disallowed in a string
|
|
// literal. Tab, line feed, and carriage return are permitted (handled
|
|
// elsewhere); everything else below U+0020, plus U+007F, is rejected.
|
|
func isControlRune(r rune) bool {
|
|
if r == '\t' || r == '\n' || r == '\r' {
|
|
return false
|
|
}
|
|
return r < 0x20 || r == 0x7f
|
|
}
|