From 7ed0ea9acebb11d70136326863743bd96b515430 Mon Sep 17 00:00:00 2001 From: Rose Hogenson Date: Sat, 25 Oct 2025 17:23:18 -0700 Subject: Some small improvements --- asspb.go | 98 ++++++++++++++++++++++++++++++++++++++--------------------- asspb_test.go | 9 ++++++ 2 files changed, 73 insertions(+), 34 deletions(-) diff --git a/asspb.go b/asspb.go index 0bb3730..a1e8593 100644 --- a/asspb.go +++ b/asspb.go @@ -63,6 +63,21 @@ // \unnnn unicode code point U+nnnn // \Unnnnnnnn unicode code point U+nnnnnnnn // +// As an extension to the C11 escapes, a backslash immediately before a newline +// character (0x0a) will remove the newline character from the resulting string +// +// 'backslash also can \ +// remove newlines' +// # equivalent to +// 'backslash also can remove newlines' +// +// If multiple string literals are written next to each other with only +// whitespace or comments in between, the result is to concatenate the strings +// +// 'multiple strings' " concatenated" +// # equivalent to +// 'multiple strings concatenated' +// // # Bool // // Bool values can be true or false (classic), and are written using one of the @@ -98,7 +113,16 @@ // {key1: "value1" key2: "value2"} // // Keys can be alphanumeric or use underscore; no other characters are -// permitted. Values can be any of the value types here described. +// permitted. Values can be any of the value types here described. Key-value +// pairs must be written with a : between the key and value, except when the +// value is syntactically a list or a message (in that case the colon +// is optional) +// +// { +// key1: "value1" +// key2 {} +// key3 [] +// } // // As a special case, when a key is written more than once in a message, it's // treated the same as if the values had been written in a list. If some of the @@ -122,6 +146,7 @@ import ( "fmt" "regexp" "strconv" + "strings" "unicode/utf8" ) @@ -194,13 +219,13 @@ func (p *parser) parseNum() (any, bool) { return n, true } -var escapesRE = regexp.MustCompile(`\\(.|[0-7]{3}|x[0-9a-fA-F]{2}|u[0-9a-fA-F]{4}|U[0-9a-fA-F]{8})`) +var escapesRE = regexp.MustCompile(`(?s)\\(.|[0-7]{3}|x[0-9a-fA-F]{2}|u[0-9a-fA-F]{4}|U[0-9a-fA-F]{8})`) func init() { escapesRE.Longest() } -func unescape(idx int, rawStr []byte) (string, error) { +func unescape(idx int, rawStr []byte) ([]byte, error) { var err error escaped := escapesRE.ReplaceAllFunc(rawStr, func(escape []byte) []byte { switch string(escape) { @@ -226,6 +251,8 @@ func unescape(idx int, rawStr []byte) (string, error) { return []byte("\t") case `\v`: return []byte("\v") + case "\\\n": + return nil } if bytes.HasPrefix(escape, []byte(`\x`)) || bytes.HasPrefix(escape, []byte(`\u`)) || bytes.HasPrefix(escape, []byte(`\U`)) { var n int64 @@ -247,41 +274,44 @@ func unescape(idx int, rawStr []byte) (string, error) { return utf8.AppendRune(nil, rune(n)) }) if err != nil { - return "", err - } - return string(escaped), nil -} - -var stringRE = regexp.MustCompile(`^(([^'\\]|\\.)*)'`) - -func (p *parser) parseString() (string, error) { - p.skipSpace() - rawStr := stringRE.FindSubmatch(p.data[p.i:]) - if rawStr == nil { - return "", fmt.Errorf("%d: syntax error: invalid string", p.i) - } - s, err := unescape(p.i, rawStr[1]) - if err != nil { - return "", err + return nil, err } - p.i += len(rawStr[0]) - return s, nil + return escaped, nil } -var doubleStringRE = regexp.MustCompile(`^(([^"\\]|\\.)*)"`) +var ( + stringRE = regexp.MustCompile(`(?s)^(([^'\\]|\\.)*)'`) + doubleStringRE = regexp.MustCompile(`(?s)^(([^"\\]|\\.)*)"`) +) -func (p *parser) parseDoubleString() (string, error) { - p.skipSpace() - rawStr := doubleStringRE.FindSubmatch(p.data[p.i:]) - if rawStr == nil { - return "", fmt.Errorf("%d: syntax error: invalid string", p.i) +func (p *parser) parseString(double bool) (string, error) { + re := stringRE + if double { + re = doubleStringRE } - s, err := unescape(p.i, rawStr[1]) - if err != nil { - return "", err + s := new(strings.Builder) + for { + p.skipSpace() + rawStr := re.FindSubmatch(p.data[p.i:]) + if rawStr == nil { + return "", fmt.Errorf("%d: syntax error: invalid string", p.i) + } + ss, err := unescape(p.i, rawStr[1]) + if err != nil { + return "", err + } + s.Write(ss) + p.i += len(rawStr[0]) + switch { + case p.parseLit("'"): + re = stringRE + continue + case p.parseLit(`"`): + re = doubleStringRE + continue + } + return s.String(), nil } - p.i += len(rawStr[0]) - return s, nil } var fieldRE = regexp.MustCompile(`^[a-zA-Z_][a-zA-Z_0-9]*`) @@ -319,9 +349,9 @@ func (p *parser) parseVal() (any, error) { case p.parseLit("["): val, err = p.parseList() case p.parseLit("'"): - val, err = p.parseString() + val, err = p.parseString(false) case p.parseLit(`"`): - val, err = p.parseDoubleString() + val, err = p.parseString(true) case p.parseLit("true"), p.parseLit("yes"), p.parseLit("on"): val = true case p.parseLit("false"), p.parseLit("no"), p.parseLit("off"): diff --git a/asspb_test.go b/asspb_test.go index 6803733..b92a95b 100644 --- a/asspb_test.go +++ b/asspb_test.go @@ -170,6 +170,15 @@ field: 2`, desc: "CStyleLineComment", msg: `field: {} // line comment`, want: map[string]any{"field": map[string]any{}}, + }, { + desc: "ConcatStrings", + msg: `field: 'that'"'"'s cool'`, + want: map[string]any{"field": "that's cool"}, + }, { + desc: "RemoveNewline", + msg: `field: 'remove newline \ +from string'`, + want: map[string]any{"field": "remove newline from string"}, }} { t.Run(tc.desc, func(t *testing.T) { t.Parallel() -- cgit v1.3.1