|
| 1 | +package expansion |
| 2 | + |
| 3 | +import ( |
| 4 | + "bytes" |
| 5 | +) |
| 6 | + |
| 7 | +const ( |
| 8 | + operator = '$' |
| 9 | + referenceOpener = '(' |
| 10 | + referenceCloser = ')' |
| 11 | +) |
| 12 | + |
| 13 | +// syntaxWrap returns the input string wrapped by the expansion syntax. |
| 14 | +func syntaxWrap(input string) string { |
| 15 | + return string(operator) + string(referenceOpener) + input + string(referenceCloser) |
| 16 | +} |
| 17 | + |
| 18 | +// MappingFuncFor returns a mapping function for use with Expand that |
| 19 | +// implements the expansion semantics defined in the expansion spec; it |
| 20 | +// returns the input string wrapped in the expansion syntax if no mapping |
| 21 | +// for the input is found. |
| 22 | +func MappingFuncFor(context ...map[string]string) func(string) string { |
| 23 | + return func(input string) string { |
| 24 | + for _, vars := range context { |
| 25 | + val, ok := vars[input] |
| 26 | + if ok { |
| 27 | + return val |
| 28 | + } |
| 29 | + } |
| 30 | + |
| 31 | + return syntaxWrap(input) |
| 32 | + } |
| 33 | +} |
| 34 | + |
| 35 | +// Expand replaces variable references in the input string according to |
| 36 | +// the expansion spec using the given mapping function to resolve the |
| 37 | +// values of variables. |
| 38 | +func Expand(input string, mapping func(string) string) string { |
| 39 | + var buf bytes.Buffer |
| 40 | + checkpoint := 0 |
| 41 | + for cursor := 0; cursor < len(input); cursor++ { |
| 42 | + if input[cursor] == operator && cursor+1 < len(input) { |
| 43 | + // Copy the portion of the input string since the last |
| 44 | + // checkpoint into the buffer |
| 45 | + buf.WriteString(input[checkpoint:cursor]) |
| 46 | + |
| 47 | + // Attempt to read the variable name as defined by the |
| 48 | + // syntax from the input string |
| 49 | + read, isVar, advance := tryReadVariableName(input[cursor+1:]) |
| 50 | + |
| 51 | + if isVar { |
| 52 | + // We were able to read a variable name correctly; |
| 53 | + // apply the mapping to the variable name and copy the |
| 54 | + // bytes into the buffer |
| 55 | + buf.WriteString(mapping(read)) |
| 56 | + } else { |
| 57 | + // Not a variable name; copy the read bytes into the buffer |
| 58 | + buf.WriteString(read) |
| 59 | + } |
| 60 | + |
| 61 | + // Advance the cursor in the input string to account for |
| 62 | + // bytes consumed to read the variable name expression |
| 63 | + cursor += advance |
| 64 | + |
| 65 | + // Advance the checkpoint in the input string |
| 66 | + checkpoint = cursor + 1 |
| 67 | + } |
| 68 | + } |
| 69 | + |
| 70 | + // Return the buffer and any remaining unwritten bytes in the |
| 71 | + // input string. |
| 72 | + return buf.String() + input[checkpoint:] |
| 73 | +} |
| 74 | + |
| 75 | +// tryReadVariableName attempts to read a variable name from the input |
| 76 | +// string and returns the content read from the input, whether that content |
| 77 | +// represents a variable name to perform mapping on, and the number of bytes |
| 78 | +// consumed in the input string. |
| 79 | +// |
| 80 | +// The input string is assumed not to contain the initial operator. |
| 81 | +func tryReadVariableName(input string) (string, bool, int) { |
| 82 | + switch input[0] { |
| 83 | + case operator: |
| 84 | + // Escaped operator; return it. |
| 85 | + return input[0:1], false, 1 |
| 86 | + case referenceOpener: |
| 87 | + // Scan to expression closer |
| 88 | + for i := 1; i < len(input); i++ { |
| 89 | + if input[i] == referenceCloser { |
| 90 | + return input[1:i], true, i + 1 |
| 91 | + } |
| 92 | + } |
| 93 | + |
| 94 | + // Incomplete reference; return it. |
| 95 | + return string(operator) + string(referenceOpener), false, 1 |
| 96 | + default: |
| 97 | + // Not the beginning of an expression, ie, an operator |
| 98 | + // that doesn't begin an expression. Return the operator |
| 99 | + // and the first rune in the string. |
| 100 | + return (string(operator) + string(input[0])), false, 1 |
| 101 | + } |
| 102 | +} |
0 commit comments