mirror of
https://github.com/omniscale/magnacarto.git
synced 2025-02-22 23:24:09 +01:00
682 lines
15 KiB
Go
682 lines
15 KiB
Go
package mss
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"io/ioutil"
|
|
"os"
|
|
"regexp"
|
|
|
|
"strconv"
|
|
|
|
"github.com/omniscale/magnacarto/color"
|
|
)
|
|
|
|
// Decoder decodes one or more MSS files. Parse/ParseFile can be called
|
|
// multiple times to decode dependent .mss files. MSS() returns the current
|
|
// decoded style.
|
|
type Decoder struct {
|
|
mss *MSS
|
|
vars *Properties
|
|
scanner *scanner
|
|
nextTok *token
|
|
lastTok *token
|
|
expr *expression
|
|
lastValue Value
|
|
warnings []warning
|
|
filename string // for warnings/errors only
|
|
filesParsed int
|
|
propertyIndex int
|
|
}
|
|
|
|
type warning struct {
|
|
line, col int
|
|
file string
|
|
msg string
|
|
}
|
|
|
|
type position struct {
|
|
line int
|
|
column int
|
|
filename string
|
|
filenum int
|
|
index int
|
|
}
|
|
|
|
func (w *warning) String() string {
|
|
file := w.file
|
|
if file == "" {
|
|
file = "?"
|
|
}
|
|
return fmt.Sprintf("%s in %s line: %d col: %d", w.msg, file, w.line, w.col)
|
|
}
|
|
|
|
// New will allocate a new MSS Decoder
|
|
func New() *Decoder {
|
|
mss := newMSS()
|
|
return &Decoder{mss: mss, vars: &Properties{}, expr: &expression{}}
|
|
}
|
|
|
|
// MSS returns the current decoded style.
|
|
func (d *Decoder) MSS() *MSS {
|
|
return d.mss
|
|
}
|
|
|
|
// Vars returns the current set of all variables.
|
|
// Call Evaluate first to resolve expressions, functions and variables.
|
|
func (d *Decoder) Vars() *Properties {
|
|
return d.vars
|
|
}
|
|
|
|
func (d *Decoder) next() *token {
|
|
if d.nextTok != nil {
|
|
tok := d.nextTok
|
|
d.nextTok = nil
|
|
d.lastTok = tok
|
|
return tok
|
|
}
|
|
for {
|
|
tok := d.scanner.Next()
|
|
if tok.t == tokenError {
|
|
d.error(d.pos(tok), tok.value)
|
|
}
|
|
if tok.t != tokenS && tok.t != tokenComment {
|
|
d.lastTok = tok
|
|
return tok
|
|
}
|
|
}
|
|
}
|
|
|
|
func (d *Decoder) backup() {
|
|
if d.nextTok != nil || d.lastTok == nil {
|
|
d.error(d.pos(d.nextTok), "internal parser bug: double backup (%v, %v)", d.nextTok, d.lastTok)
|
|
}
|
|
d.nextTok = d.lastTok
|
|
}
|
|
|
|
// ParseFile parses the given .mss file.
|
|
// Can be called multiple times to parse a style split into multiple files.
|
|
func (d *Decoder) ParseFile(filename string) error {
|
|
d.filename = filename
|
|
defer func() { d.filename = "" }()
|
|
|
|
r, err := os.Open(filename)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer r.Close()
|
|
content, err := ioutil.ReadAll(r)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return d.ParseString(string(content))
|
|
}
|
|
|
|
func (d *Decoder) ParseString(content string) (err error) {
|
|
d.filesParsed += 1
|
|
d.scanner = newScanner(content)
|
|
|
|
defer func() {
|
|
if r := recover(); r != nil {
|
|
switch x := r.(type) {
|
|
case string:
|
|
err = errors.New(x)
|
|
case error:
|
|
err = x
|
|
default:
|
|
err = fmt.Errorf("unexpected error: %v, %T", r, r)
|
|
}
|
|
}
|
|
}()
|
|
for {
|
|
tok := d.next()
|
|
if tok.t == tokenEOF {
|
|
break
|
|
}
|
|
if tok.t == tokenError {
|
|
return fmt.Errorf(tok.String())
|
|
}
|
|
|
|
d.topLevel(tok)
|
|
}
|
|
return err
|
|
}
|
|
|
|
// Evaluate evaluates all expressions and resolves all references to variables.
|
|
// Must be called after last ParseFile/ParseString call.
|
|
func (d *Decoder) Evaluate() (err error) {
|
|
defer func() {
|
|
if r := recover(); r != nil {
|
|
switch x := r.(type) {
|
|
case string:
|
|
err = errors.New(x)
|
|
case error:
|
|
err = x
|
|
default:
|
|
err = fmt.Errorf("unexpected error: %v, %T", r, r)
|
|
}
|
|
}
|
|
}()
|
|
|
|
d.evaluateProperties(d.vars, false)
|
|
d.evaluateProperties(d.mss.Map(), true)
|
|
for _, b := range d.mss.root.blocks {
|
|
d.evaluateBlock(b)
|
|
}
|
|
return err
|
|
}
|
|
|
|
func (d *Decoder) evaluateBlock(b *block) {
|
|
d.evaluateProperties(b.properties, true)
|
|
for _, b := range b.blocks {
|
|
d.evaluateBlock(b)
|
|
}
|
|
}
|
|
|
|
func (d *Decoder) evaluateExpression(expr *expression) Value {
|
|
// resolve all vars in the expression before evaluating it
|
|
for i := range expr.code {
|
|
if expr.code[i].T == typeVar {
|
|
varname := expr.code[i].Value.(string)
|
|
v, _ := d.vars.get(varname)
|
|
if v == nil {
|
|
d.error(expr.pos, "missing var %s in expression", varname)
|
|
}
|
|
if expr, ok := v.(*expression); ok {
|
|
// evaluate recursive
|
|
v = d.evaluateExpression(expr)
|
|
d.vars.set(varname, v)
|
|
}
|
|
t := d.valueType(v)
|
|
if t == typeUnknown {
|
|
d.error(expr.pos, "unable to determine type of var %s (%v)", varname, v)
|
|
}
|
|
expr.code[i] = code{Value: v, T: t}
|
|
}
|
|
}
|
|
v, err := expr.evaluate()
|
|
if err != nil {
|
|
d.error(expr.pos, "expression error: %v", err)
|
|
}
|
|
return v
|
|
}
|
|
|
|
func (d *Decoder) valueType(v interface{}) codeType {
|
|
switch v.(type) {
|
|
case string:
|
|
return typeString
|
|
case float64:
|
|
return typeNum
|
|
case color.Color:
|
|
return typeColor
|
|
case bool:
|
|
return typeBool
|
|
case []Value:
|
|
return typeList // TODO convert v to typeList?
|
|
default:
|
|
return typeUnknown
|
|
}
|
|
}
|
|
|
|
func (d *Decoder) evaluateProperties(properties *Properties, validate bool) {
|
|
if properties == nil {
|
|
return
|
|
}
|
|
for _, k := range properties.keys() {
|
|
if expr, ok := properties.getKey(k).(*expression); ok {
|
|
v := d.evaluateExpression(expr)
|
|
if validate {
|
|
if validProp, validVal := validProperty(k.name, v); !validProp {
|
|
d.warn(properties.pos(k), "invalid property %v %v", k.name, v)
|
|
} else if !validVal {
|
|
d.warn(properties.pos(k), "invalid property value for %v %v", k.name, v)
|
|
}
|
|
}
|
|
attr := properties.values[k]
|
|
properties.setPos(k, v, attr.pos)
|
|
}
|
|
}
|
|
}
|
|
|
|
func (d *Decoder) topLevel(tok *token) {
|
|
switch tok.t {
|
|
case tokenAtKeyword:
|
|
keyword := tok.value[1:]
|
|
d.expect(tokenColon)
|
|
d.expressionList()
|
|
d.expect(tokenSemicolon)
|
|
d.vars.set(keyword, d.lastValue)
|
|
case tokenHash, tokenAttachment, tokenClass, tokenLBracket:
|
|
d.rule(tok)
|
|
case tokenIdent:
|
|
if tok.value != "Map" {
|
|
d.error(d.pos(tok), "only 'Map' identifier expected at top level, got %v", tok)
|
|
}
|
|
d.mss.pushMapBlock()
|
|
d.expect(tokenLBrace)
|
|
d.block()
|
|
d.mss.popBlock()
|
|
// todo mark block as Map
|
|
default:
|
|
d.error(d.pos(tok), "unexpected token at top level, got %v", tok)
|
|
}
|
|
}
|
|
|
|
func (d *Decoder) rule(tok *token) {
|
|
d.mss.pushBlock()
|
|
d.selectors(tok)
|
|
d.expect(tokenLBrace)
|
|
d.block()
|
|
d.mss.popBlock()
|
|
}
|
|
|
|
func (d *Decoder) block() {
|
|
for {
|
|
tok := d.next()
|
|
switch tok.t {
|
|
case tokenHash, tokenAttachment, tokenClass, tokenLBracket:
|
|
d.rule(tok)
|
|
case tokenIdent, tokenInstance:
|
|
keyword := tok.value
|
|
if tok.t == tokenInstance {
|
|
d.mss.setInstance(tok.value[:len(tok.value)-1]) // strip /
|
|
tok = d.next()
|
|
if tok.t != tokenIdent {
|
|
d.error(d.pos(tok), "expected property name for instance, found %v", tok)
|
|
}
|
|
keyword = tok.value
|
|
}
|
|
d.expect(tokenColon)
|
|
d.expressionList()
|
|
d.mss.setProperty(keyword, d.lastValue,
|
|
position{line: tok.line, column: tok.column, filename: d.filename, filenum: d.filesParsed, index: d.propertyIndex},
|
|
)
|
|
d.propertyIndex += 1
|
|
d.expectEndOfStatement()
|
|
case tokenRBrace:
|
|
return
|
|
default:
|
|
d.error(d.pos(tok), "unexpected token %v", tok)
|
|
}
|
|
}
|
|
}
|
|
|
|
// decode multiple selectors, eg:
|
|
// #foo, #bar[zoom=3]
|
|
func (d *Decoder) selectors(tok *token) {
|
|
for {
|
|
if tok.t == tokenHash || tok.t == tokenAttachment || tok.t == tokenClass || tok.t == tokenLBracket {
|
|
d.selector(tok)
|
|
tok = d.next()
|
|
if tok.t == tokenComma {
|
|
tok = d.next() // TODO non-selector after comma?
|
|
if tok.t == tokenLBrace {
|
|
// dangling comma
|
|
d.backup()
|
|
break
|
|
}
|
|
|
|
continue
|
|
}
|
|
d.backup()
|
|
} else {
|
|
d.error(d.pos(tok), "expected layer, attachment, class or filter, got %v", tok)
|
|
}
|
|
break
|
|
}
|
|
}
|
|
|
|
// decode single selector, eg:
|
|
// #foo::attachment[filter=foo][zoom>=12]
|
|
func (d *Decoder) selector(tok *token) {
|
|
d.mss.pushSelector()
|
|
for {
|
|
switch tok.t {
|
|
case tokenHash:
|
|
d.mss.addLayer(tok.value[1:]) // strip #
|
|
case tokenAttachment:
|
|
d.mss.addAttachment(tok.value[2:]) // strip ::
|
|
case tokenClass:
|
|
d.mss.addClass(tok.value[1:]) // strip .
|
|
case tokenLBracket:
|
|
d.filters(tok)
|
|
}
|
|
|
|
tok = d.next()
|
|
if tok.t == tokenHash || tok.t == tokenAttachment || tok.t == tokenClass || tok.t == tokenLBracket {
|
|
continue
|
|
} else {
|
|
d.backup()
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
// decode multiple filters. eg:
|
|
// [filter=foo][zoom>=12]
|
|
func (d *Decoder) filters(tok *token) {
|
|
for {
|
|
d.filter()
|
|
tok = d.next()
|
|
if tok.t == tokenLBracket {
|
|
continue
|
|
} else {
|
|
d.backup()
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
// decode single filters. eg:
|
|
// [filter=foo]
|
|
func (d *Decoder) filter() {
|
|
tok := d.next()
|
|
if tok.t == tokenIdent && tok.value == "zoom" {
|
|
compOp := d.comp()
|
|
tok = d.next()
|
|
if tok.t != tokenNumber {
|
|
d.error(d.pos(tok), "zoom requires num, got %v", tok)
|
|
}
|
|
level, err := strconv.ParseInt(tok.value, 10, 64)
|
|
if err != nil {
|
|
d.error(d.pos(tok), "invalid zoom level %v: %v", tok, err)
|
|
}
|
|
if compOp == REGEX {
|
|
d.error(d.pos(tok), "regular expressions are not allowed for zoom levels")
|
|
}
|
|
d.mss.addZoom(compOp, level)
|
|
d.expect(tokenRBracket)
|
|
return
|
|
}
|
|
|
|
var field string
|
|
switch tok.t {
|
|
case tokenString:
|
|
field = tok.value[1 : len(tok.value)-1]
|
|
case tokenIdent:
|
|
field = tok.value
|
|
default:
|
|
d.error(d.pos(tok), "expected zoom or field name in filter, got '%s'", tok.value)
|
|
}
|
|
|
|
compOp := d.comp()
|
|
var value interface{}
|
|
if compOp == MODULO {
|
|
// Modulo comparsions expect the divider, a comparsion and a value, eg: x % 2 = 1
|
|
// These extra values are stored in the filter value inside a ModuloComparsion struct.
|
|
tok = d.next()
|
|
if tok.t != tokenNumber {
|
|
d.error(d.pos(tok), "expected %v found %v", tokenNumber, tok)
|
|
}
|
|
div, err := strconv.ParseInt(tok.value, 10, 64)
|
|
if err != nil {
|
|
d.error(d.pos(tok), "expected integer for modulo, found %v", tok)
|
|
}
|
|
|
|
modCompOp := d.comp()
|
|
if modCompOp > NEQ {
|
|
d.error(d.pos(tok), "expected simple comparsion, found %v", modCompOp)
|
|
}
|
|
tok = d.next()
|
|
if tok.t != tokenNumber {
|
|
d.error(d.pos(tok), "expected %v found %v", tokenNumber, tok)
|
|
}
|
|
compValue, err := strconv.ParseInt(tok.value, 10, 64)
|
|
if err != nil {
|
|
d.error(d.pos(tok), "expected integer for modulo comparsion, found %v", tok)
|
|
}
|
|
value = ModuloComparsion{Div: int(div), CompOp: modCompOp, Value: int(compValue)}
|
|
} else {
|
|
// All other comparsions expect a single value.
|
|
tok = d.next()
|
|
switch tok.t {
|
|
case tokenString:
|
|
value = tok.value[1 : len(tok.value)-1]
|
|
case tokenNumber:
|
|
value, _ = strconv.ParseFloat(tok.value, 64)
|
|
case tokenIdent:
|
|
if tok.value == "null" {
|
|
value = nil
|
|
} else {
|
|
d.error(d.pos(tok), "unexpected value in filter '%s'", tok.value)
|
|
}
|
|
default:
|
|
d.error(d.pos(tok), "unexpected value in filter '%s'", tok.value)
|
|
}
|
|
}
|
|
d.expect(tokenRBracket)
|
|
d.mss.addFilter(field, compOp, value)
|
|
}
|
|
|
|
// decode comparision. eg:
|
|
// = or >=
|
|
func (d *Decoder) comp() CompOp {
|
|
tok := d.next()
|
|
if tok.t != tokenComp && tok.t != tokenModulo {
|
|
d.error(d.pos(tok), "expected comparsion, got '%s'", tok.value)
|
|
}
|
|
compOp, err := parseCompOp(tok.value)
|
|
if err != nil {
|
|
d.error(d.pos(tok), "invalid comparsion operator '%s': %v", tok.value, err)
|
|
}
|
|
return compOp
|
|
}
|
|
|
|
// expect consumes the next token checks that it is of type t
|
|
func (d *Decoder) expect(t tokenType) {
|
|
if tok := d.next(); tok.t != t {
|
|
d.error(d.pos(tok), "expected %v found %v", t, tok)
|
|
}
|
|
}
|
|
|
|
// expectEndOfStatement checks for semicolon or closing block `}`
|
|
func (d *Decoder) expectEndOfStatement() {
|
|
tok := d.next()
|
|
|
|
if tok.t == tokenRBrace {
|
|
d.backup()
|
|
return
|
|
}
|
|
d.backup()
|
|
d.expect(tokenSemicolon)
|
|
}
|
|
|
|
func (d *Decoder) expressionList() {
|
|
startTok := d.next()
|
|
d.backup()
|
|
|
|
d.expression()
|
|
for {
|
|
tok := d.next()
|
|
if tok.t == tokenComma {
|
|
d.expression()
|
|
} else if tok.t == tokenFunction && d.expr.code[len(d.expr.code)-1].T == typeFunctionEnd {
|
|
// non-comma separated list, only between functions, e.g. raster-colorizer-stops: stop(0, #47443e) stop(50, #77654a);
|
|
d.backup()
|
|
d.expression()
|
|
} else {
|
|
d.backup()
|
|
break
|
|
}
|
|
}
|
|
|
|
d.expr.pos = position{line: startTok.line, column: startTok.column, filename: d.filename, filenum: d.filesParsed, index: d.propertyIndex}
|
|
d.propertyIndex += 1
|
|
d.lastValue = d.expr
|
|
d.expr = &expression{}
|
|
}
|
|
|
|
func (d *Decoder) expression() {
|
|
d.exprPart()
|
|
}
|
|
|
|
func (d *Decoder) exprPart() {
|
|
d.mulExpr()
|
|
|
|
for {
|
|
tok := d.next()
|
|
if tok.t == tokenPlus {
|
|
d.mulExpr()
|
|
d.expr.addOperator(typeAdd)
|
|
} else if tok.t == tokenMinus {
|
|
d.mulExpr()
|
|
d.expr.addOperator(typeSubtract)
|
|
} else {
|
|
d.backup()
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
func (d *Decoder) mulExpr() {
|
|
d.negOrValue()
|
|
|
|
for {
|
|
tok := d.next()
|
|
if tok.t == tokenMultiply {
|
|
d.negOrValue()
|
|
d.expr.addOperator(typeMultiply)
|
|
} else if tok.t == tokenDivide {
|
|
d.negOrValue()
|
|
d.expr.addOperator(typeDivide)
|
|
} else {
|
|
d.backup()
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
func (d *Decoder) negOrValue() {
|
|
tok := d.next()
|
|
if tok.t == tokenMinus {
|
|
tok := d.next()
|
|
d.value(tok)
|
|
d.expr.addOperator(typeNegation)
|
|
} else {
|
|
d.value(tok)
|
|
}
|
|
}
|
|
|
|
var urlPath = regexp.MustCompile(`url\(['"]?(.*?)['"]?\)`) // TODO quote handling is borked, eg url('foo") or url('foo) is matched
|
|
|
|
func (d *Decoder) value(tok *token) {
|
|
switch tok.t {
|
|
case tokenString:
|
|
d.expr.addValue(tok.value[1:len(tok.value)-1], typeString)
|
|
case tokenNumber:
|
|
v, err := strconv.ParseFloat(tok.value, 64)
|
|
if err != nil {
|
|
d.error(d.pos(tok), "invalid float %v: %s", v, err)
|
|
}
|
|
d.expr.addValue(v, typeNum)
|
|
case tokenPercentage:
|
|
v, err := strconv.ParseFloat(tok.value[:len(tok.value)-1], 64)
|
|
if err != nil {
|
|
d.error(d.pos(tok), "invalid float %v: %s", v, err)
|
|
}
|
|
d.expr.addValue(v, typePercent)
|
|
case tokenIdent:
|
|
switch tok.value {
|
|
case "true":
|
|
d.expr.addValue(true, typeBool)
|
|
case "false":
|
|
d.expr.addValue(false, typeBool)
|
|
case "null":
|
|
d.expr.addValue(nil, typeKeyword)
|
|
default:
|
|
c, err := color.Parse(tok.value)
|
|
if err == nil {
|
|
d.expr.addValue(c, typeColor)
|
|
} else {
|
|
// TODO check for valid keywords
|
|
d.expr.addValue(tok.value, typeKeyword)
|
|
}
|
|
}
|
|
case tokenHash:
|
|
c, err := color.Parse(tok.value)
|
|
if err != nil {
|
|
d.error(d.pos(tok), "%v, got %v", err, tok)
|
|
}
|
|
d.expr.addValue(c, typeColor)
|
|
case tokenAtKeyword:
|
|
d.expr.addValue(tok.value[1:], typeVar)
|
|
case tokenURI:
|
|
match := urlPath.FindStringSubmatch(tok.value)
|
|
d.expr.addValue(match[1], typeURL)
|
|
case tokenLBracket:
|
|
// [field]
|
|
tok = d.next()
|
|
if tok.t != tokenIdent {
|
|
d.error(d.pos(tok), "expected identifier in field name, got %v", tok)
|
|
}
|
|
d.expr.addValue("["+tok.value+"]", typeField)
|
|
d.expect(tokenRBracket)
|
|
case tokenFunction:
|
|
d.expr.addValue(tok.value[:len(tok.value)-1], typeFunction) // strip lparen
|
|
d.functionParams()
|
|
case tokenLParen:
|
|
d.exprPart()
|
|
d.expect(tokenRParen)
|
|
default:
|
|
d.error(d.pos(tok), "unexpected value %v", tok)
|
|
}
|
|
}
|
|
|
|
func (d *Decoder) functionParams() {
|
|
for {
|
|
d.exprPart()
|
|
tok := d.next()
|
|
if tok.t == tokenRParen {
|
|
d.expr.addValue(nil, typeFunctionEnd)
|
|
break
|
|
}
|
|
if tok.t == tokenComma {
|
|
continue
|
|
}
|
|
d.error(d.pos(tok), "expected end of function or comma, got %v", tok)
|
|
}
|
|
}
|
|
|
|
type ParseError struct {
|
|
Filename string
|
|
Line int
|
|
Column int
|
|
Err string
|
|
}
|
|
|
|
func (p *ParseError) Error() string {
|
|
file := p.Filename
|
|
if file == "" {
|
|
file = "?"
|
|
}
|
|
return fmt.Sprintf("%s in %s line: %d col: %d", p.Err, file, p.Line, p.Column)
|
|
}
|
|
|
|
func (d *Decoder) pos(tok *token) position {
|
|
return position{
|
|
filename: d.filename,
|
|
line: tok.line,
|
|
column: tok.column,
|
|
filenum: d.filesParsed,
|
|
}
|
|
}
|
|
|
|
func (d *Decoder) error(pos position, format string, args ...interface{}) {
|
|
panic(&ParseError{
|
|
Filename: pos.filename,
|
|
Line: pos.line,
|
|
Column: pos.column,
|
|
Err: fmt.Sprintf(format, args...),
|
|
})
|
|
}
|
|
|
|
func (d *Decoder) warn(pos position, format string, args ...interface{}) {
|
|
d.warnings = append(d.warnings,
|
|
warning{
|
|
file: pos.filename,
|
|
line: pos.line,
|
|
col: pos.column,
|
|
msg: fmt.Sprintf(format, args...),
|
|
},
|
|
)
|
|
}
|