Skip to content

Instantly share code, notes, and snippets.

@renskiy
Created July 9, 2018 08:01
Show Gist options
  • Select an option

  • Save renskiy/d2725afa620be9cd4bdf0f15e9339655 to your computer and use it in GitHub Desktop.

Select an option

Save renskiy/d2725afa620be9cd4bdf0f15e9339655 to your computer and use it in GitHub Desktop.
Syntax analyser of strings like "(foo (bar baz :arg 42))"
package include_parser
import (
"regexp"
"strings"
"errors"
"fmt"
)
var whitespaces = regexp.MustCompile("\\s+")
const maxNestingLevel = 3
type Node struct {
name string
nodes map[string]Node
args map[string]string
}
func New(value string) (*Node, error) {
if tokens, err := tokenize(value); err != nil {
return nil, err
} else {
if nodes, _, err := parse(tokens); err != nil {
return nil, err
} else {
return &Node{"", nodes, nil}, nil
}
}
}
func tokenize(params string) ([]string, error) {
params = whitespaces.ReplaceAllString(params, " ")
params = strings.Trim(params, "'\" ")
var (
tokens []string
start, nestingLevel int
)
capture := func(index int) error {
if start < index {
var token = params[start:index]
if strings.HasPrefix(token, ":") && len(token) == 1 {
return errors.New(fmt.Sprintf("parameter without name (position: %v)", index - 1))
}
tokens = append(tokens, token)
}
start = index + 1
return nil
}
for index, char := range params {
switch char {
case '(':
start = index + 1
tokens = append(tokens, string(char))
nestingLevel++
case ')':
if err := capture(index); err != nil {
return nil, err
}
tokens = append(tokens, string(char))
nestingLevel--
case ' ':
if err := capture(index); err != nil {
return nil, err
}
}
if nestingLevel < 0 {
return nil, errors.New(fmt.Sprintf("unexpected closing parentheses (position: %v)", index))
}
if nestingLevel > maxNestingLevel {
return nil, errors.New(fmt.Sprintf("parentheses nesting level exceeded (position: %v)", index))
}
}
if nestingLevel > 0 {
return nil, errors.New("unclosed parentheses")
}
if start < len(params) {
tokens = append(tokens, params[start:])
}
return tokens, nil
}
func parse(tokens []string, startRef ...*int) (map[string]Node, map[string]string, error) {
var (
nodes = make(map[string]Node)
args = make(map[string]string)
start *int
)
if len(startRef) > 0 {
start = startRef[0]
} else {
startInitial := 0
start = &startInitial
}
for *start < len(tokens) {
token := tokens[*start]
if token == "(" {
*start++
token = tokens[*start]
if strings.HasPrefix(token, ":") {
return nil, nil, errors.New(fmt.Sprintf("parameter %v for object without name", token))
}
if token != ")" {
*start++
if childNodes, nodeArgs, err := parse(tokens, start); err != nil {
return nil, nil, errors.New(fmt.Sprintf("parameter %v for object without name", token))
} else {
nodes[token] = Node{token, childNodes, nodeArgs}
}
}
} else if token == ")" {
break
} else if strings.HasPrefix(token, ":") {
*start++
if *start >= len(tokens) {
return nil, nil, errors.New(fmt.Sprintf("not provided value for %v", token))
}
if argValue := tokens[*start]; argValue == ")" {
return nil, nil, errors.New(fmt.Sprintf("not provided value for %v", token))
} else {
args[token[1:]] = argValue
}
} else {
nodes[token] = Node{token, nil, nil}
}
*start++
}
return nodes, args, nil
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment