/* * Copyright (c) 2018 DeineAgentur UG https://www.deineagentur.com. All rights reserved. * Licensed under the MIT License. See LICENSE file in the project root for full license information. */ /* Package plurals is the pluralform compiler to get the correct translation id of the plural string */ package plurals import ( "errors" "fmt" "regexp" "strconv" "strings" ) type match struct { openPos int closePos int } var pat = regexp.MustCompile(`(\?|:|\|\||&&|==|!=|>=|>|<=|<|%|\d+|n)`) type testToken interface { compile(tokens []string) (test test, err error) } type cmpTestBuilder func(val uint32, flipped bool) test type logicTestBuild func(left test, right test) test var ternaryToken ternaryStruct type ternaryStruct struct{} func (ternaryStruct) compile(tokens []string) (expr Expression, err error) { main, err := splitTokens(tokens, "?") if err != nil { return expr, err } test, err := compileTest(strings.Join(main.Left, "")) if err != nil { return expr, err } actions, err := splitTokens(main.Right, ":") if err != nil { return expr, err } trueAction, err := compileExpression(strings.Join(actions.Left, "")) if err != nil { return expr, err } falseAction, err := compileExpression(strings.Join(actions.Right, "")) if err != nil { return expr, nil } return ternary{ test: test, trueExpr: trueAction, falseExpr: falseAction, }, nil } var constToken constValStruct type constValStruct struct{} func (constValStruct) compile(tokens []string) (expr Expression, err error) { if len(tokens) == 0 { return expr, errors.New("got nothing instead of constant") } if len(tokens) != 1 { return expr, fmt.Errorf("invalid constant: %s", strings.Join(tokens, "")) } i, err := strconv.Atoi(tokens[0]) if err != nil { return expr, err } return constValue{value: i}, nil } func compileLogicTest(tokens []string, sep string, builder logicTestBuild) (test test, err error) { split, err := splitTokens(tokens, sep) if err != nil { return test, err } left, err := compileTest(strings.Join(split.Left, "")) if err != nil { return test, err } right, err := compileTest(strings.Join(split.Right, "")) if err != nil { return test, err } return builder(left, right), nil } var orToken orStruct type orStruct struct{} func (orStruct) compile(tokens []string) (test test, err error) { return compileLogicTest(tokens, "||", buildOr) } func buildOr(left test, right test) test { return or{left: left, right: right} } var andToken andStruct type andStruct struct{} func (andStruct) compile(tokens []string) (test test, err error) { return compileLogicTest(tokens, "&&", buildAnd) } func buildAnd(left test, right test) test { return and{left: left, right: right} } func compileMod(tokens []string) (math math, err error) { split, err := splitTokens(tokens, "%") if err != nil { return math, err } if len(split.Left) != 1 || split.Left[0] != "n" { return math, errors.New("Modulus operation requires 'n' as left operand") } if len(split.Right) != 1 { return math, errors.New("Modulus operation requires simple integer as right operand") } i, err := parseUint32(split.Right[0]) if err != nil { return math, err } return mod{value: uint32(i)}, nil } func subPipe(modTokens []string, actionTokens []string, builder cmpTestBuilder, flipped bool) (test test, err error) { modifier, err := compileMod(modTokens) if err != nil { return test, err } if len(actionTokens) != 1 { return test, errors.New("can only get modulus of integer") } i, err := parseUint32(actionTokens[0]) if err != nil { return test, err } action := builder(uint32(i), flipped) return pipe{ modifier: modifier, action: action, }, nil } func compileEquality(tokens []string, sep string, builder cmpTestBuilder) (test test, err error) { split, err := splitTokens(tokens, sep) if err != nil { return test, err } if len(split.Left) == 1 && split.Left[0] == "n" { if len(split.Right) != 1 { return test, errors.New("test can only compare n to integers") } i, err := parseUint32(split.Right[0]) if err != nil { return test, err } return builder(i, false), nil } else if len(split.Right) == 1 && split.Right[0] == "n" { if len(split.Left) != 1 { return test, errors.New("test can only compare n to integers") } i, err := parseUint32(split.Left[0]) if err != nil { return test, err } return builder(i, true), nil } else if contains(split.Left, "n") && contains(split.Left, "%") { return subPipe(split.Left, split.Right, builder, false) } return test, errors.New("equality test must have 'n' as one of the two tests") } var eqToken eqStruct type eqStruct struct{} func (eqStruct) compile(tokens []string) (test test, err error) { return compileEquality(tokens, "==", buildEq) } func buildEq(val uint32, flipped bool) test { return equal{value: val} } var neqToken neqStruct type neqStruct struct{} func (neqStruct) compile(tokens []string) (test test, err error) { return compileEquality(tokens, "!=", buildNeq) } func buildNeq(val uint32, flipped bool) test { return notequal{value: val} } var gtToken gtStruct type gtStruct struct{} func (gtStruct) compile(tokens []string) (test test, err error) { return compileEquality(tokens, ">", buildGt) } func buildGt(val uint32, flipped bool) test { return gt{value: val, flipped: flipped} } var gteToken gteStruct type gteStruct struct{} func (gteStruct) compile(tokens []string) (test test, err error) { return compileEquality(tokens, ">=", buildGte) } func buildGte(val uint32, flipped bool) test { return gte{value: val, flipped: flipped} } var ltToken ltStruct type ltStruct struct{} func (ltStruct) compile(tokens []string) (test test, err error) { return compileEquality(tokens, "<", buildLt) } func buildLt(val uint32, flipped bool) test { return lt{value: val, flipped: flipped} } var lteToken lteStruct type lteStruct struct{} func (lteStruct) compile(tokens []string) (test test, err error) { return compileEquality(tokens, "<=", buildLte) } func buildLte(val uint32, flipped bool) test { return lte{value: val, flipped: flipped} } type testTokenDef struct { op string token testToken } var precedence = []testTokenDef{ {op: "||", token: orToken}, {op: "&&", token: andToken}, {op: "==", token: eqToken}, {op: "!=", token: neqToken}, {op: ">=", token: gteToken}, {op: ">", token: gtToken}, {op: "<=", token: lteToken}, {op: "<", token: ltToken}, } type splitted struct { Left []string Right []string } // Find index of token in list of tokens func index(tokens []string, sep string) int { for index, token := range tokens { if token == sep { return index } } return -1 } // Split a list of tokens by a token into a splitted struct holding the tokens // before and after the token to be split by. func splitTokens(tokens []string, sep string) (s splitted, err error) { index := index(tokens, sep) if index == -1 { return s, fmt.Errorf("'%s' not found in ['%s']", sep, strings.Join(tokens, "','")) } return splitted{ Left: tokens[:index], Right: tokens[index+1:], }, nil } // Scan a string for parenthesis func scan(s string) <-chan match { ch := make(chan match) go func() { depth := 0 opener := 0 for index, char := range s { switch char { case '(': if depth == 0 { opener = index } depth++ case ')': depth-- if depth == 0 { ch <- match{ openPos: opener, closePos: index + 1, } } } } close(ch) }() return ch } // Split the string into tokens func split(s string) <-chan string { ch := make(chan string) go func() { s = strings.Replace(s, " ", "", -1) if !strings.Contains(s, "(") { ch <- s } else { last := 0 end := len(s) for info := range scan(s) { if last != info.openPos { ch <- s[last:info.openPos] } ch <- s[info.openPos:info.closePos] last = info.closePos } if last != end { ch <- s[last:] } } close(ch) }() return ch } // Tokenizes a string into a list of strings, tokens grouped by parenthesis are // not split! If the string starts with ( and ends in ), those are stripped. func tokenize(s string) []string { /* TODO: Properly detect if the string starts with a ( and ends with a ) and that those two form a matching pair. Eg: (foo) -> true; (foo)(bar) -> false; */ if s[0] == '(' && s[len(s)-1] == ')' { s = s[1 : len(s)-1] } ret := []string{} for chunk := range split(s) { if len(chunk) != 0 { if chunk[0] == '(' && chunk[len(chunk)-1] == ')' { ret = append(ret, chunk) } else { for _, token := range pat.FindAllStringSubmatch(chunk, -1) { ret = append(ret, token[0]) } } } else { fmt.Printf("Empty chunk in string '%s'\n", s) } } return ret } // Compile a string containing a plural form expression to a Expression object. func Compile(s string) (expr Expression, err error) { if s == "0" { return constValue{value: 0}, nil } if !strings.Contains(s, "?") { s += "?1:0" } return compileExpression(s) } // Check if a token is in a slice of strings func contains(haystack []string, needle string) bool { for _, s := range haystack { if s == needle { return true } } return false } // Compiles an expression (ternary or constant) func compileExpression(s string) (expr Expression, err error) { tokens := tokenize(s) if contains(tokens, "?") { return ternaryToken.compile(tokens) } return constToken.compile(tokens) } // Compiles a test (comparison) func compileTest(s string) (test test, err error) { tokens := tokenize(s) for _, tokenDef := range precedence { if contains(tokens, tokenDef.op) { return tokenDef.token.compile(tokens) } } return test, errors.New("cannot compile") } func parseUint32(s string) (ui uint32, err error) { i, err := strconv.ParseUint(s, 10, 32) if err != nil { return ui, err } return uint32(i), nil }