|
1 | | -{ |
2 | | - // Joins all consecutive strings in a collection without clobbering any |
3 | | - // non-string members. |
4 | | - function coalesce (parts) { |
5 | | - const result = []; |
6 | | - for (let i = 0; i < parts.length; i++) { |
7 | | - const part = parts[i]; |
8 | | - const ri = result.length - 1; |
9 | | - if (typeof part === 'string' && typeof result[ri] === 'string') { |
10 | | - result[ri] = result[ri] + part; |
11 | | - } else { |
12 | | - result.push(part); |
13 | | - } |
14 | | - } |
15 | | - return result; |
16 | | - } |
17 | | - |
18 | | - function flatten (parts) { |
19 | | - return parts.reduce(function (flat, rest) { |
20 | | - return flat.concat(Array.isArray(rest) ? flatten(rest) : rest); |
21 | | - }, []); |
22 | | - } |
23 | | -} |
24 | | -bodyContent = content:(tabStop / bodyContentText)* { return content; } |
25 | | -bodyContentText = text:bodyContentChar+ { return text.join(''); } |
26 | | -bodyContentChar = escaped / !tabStop char:. { return char; } |
27 | | - |
28 | | -escaped = '\\' char:. { return char; } |
29 | | -tabStop = tabStopWithTransformation / tabStopWithPlaceholder / tabStopWithoutPlaceholder / simpleTabStop |
30 | | - |
31 | | -simpleTabStop = '$' index:[0-9]+ { |
32 | | - return { index: parseInt(index.join("")), content: [] }; |
33 | | -} |
34 | | -tabStopWithoutPlaceholder = '${' index:[0-9]+ '}' { |
35 | | - return { index: parseInt(index.join("")), content: [] }; |
36 | | -} |
37 | | -tabStopWithPlaceholder = '${' index:[0-9]+ ':' content:placeholderContent '}' { |
38 | | - return { index: parseInt(index.join("")), content: content }; |
39 | | -} |
40 | | -tabStopWithTransformation = '${' index:[0-9]+ substitution:transformationSubstitution '}' { |
41 | | - return { |
42 | | - index: parseInt(index.join(""), 10), |
43 | | - content: [], |
44 | | - substitution: substitution |
45 | | - }; |
46 | | -} |
47 | | - |
48 | | -placeholderContent = content:(tabStop / placeholderContentText / variable )* { return flatten(content); } |
49 | | -placeholderContentText = text:placeholderContentChar+ { return coalesce(text); } |
50 | | -placeholderContentChar = escaped / placeholderVariableReference / !tabStop !variable char:[^}] { return char; } |
51 | | - |
52 | | -placeholderVariableReference = '$' digit:[0-9]+ { |
53 | | - return { index: parseInt(digit.join(""), 10), content: [] }; |
54 | | -} |
55 | | - |
56 | | -variable = '${' variableContent '}' { |
57 | | - return ''; // we eat variables and do nothing with them for now |
58 | | -} |
59 | | -variableContent = content:(variable / variableContentText)* { return content; } |
60 | | -variableContentText = text:variableContentChar+ { return text.join(''); } |
61 | | -variableContentChar = !variable char:('\\}' / [^}]) { return char; } |
62 | | - |
63 | | -escapedForwardSlash = pair:'\\/' { return pair; } |
64 | | - |
65 | | -// A pattern and replacement for a transformed tab stop. |
66 | | -transformationSubstitution = '/' find:(escapedForwardSlash / [^/])* '/' replace:formatString* '/' flags:[imy]* { |
67 | | - let reFind = new RegExp(find.join(''), flags.join('') + 'g'); |
68 | | - return { find: reFind, replace: replace[0] }; |
69 | | -} |
70 | | - |
71 | | -formatString = content:(formatStringEscape / formatStringReference / escapedForwardSlash / [^/])+ { |
72 | | - return content; |
73 | | -} |
74 | | -// Backreferencing a substitution. Different from a tab stop. |
75 | | -formatStringReference = '$' digits:[0-9]+ { |
76 | | - return { backreference: parseInt(digits.join(''), 10) }; |
77 | | -}; |
78 | | -// One of the special control flags in a format string for case folding and |
79 | | -// other tasks. |
80 | | -formatStringEscape = '\\' flag:[ULulErn$] { |
81 | | - return { escape: flag }; |
82 | | -} |
| 1 | +/* |
| 2 | +
|
| 3 | +Target grammar: |
| 4 | +
|
| 5 | +(Based on VS Code and TextMate, with particular emphasis on supporting LSP snippets) |
| 6 | +See https://microsoft.github.io/language-server-protocol/specification#snippet_syntax |
| 7 | +
|
| 8 | +any ::= (text | tabstop | choice | variable)* |
| 9 | +
|
| 10 | +text ::= anything that's not something else |
| 11 | +
|
| 12 | +tabstop ::= '$' int | '${' int '}' | '${' int transform '}' | '${' int ':' any '}' |
| 13 | +
|
| 14 | +choice ::= '${' int '|' text (',' text)* '|}' |
| 15 | +
|
| 16 | +variable ::= '$' var | '${' var '}' | '${' var ':' any '}' | '${' var transform '}' |
| 17 | +
|
| 18 | +transform ::= '/' regex '/' replace '/' options |
| 19 | +
|
| 20 | +replace ::= (format | text)* |
| 21 | +
|
| 22 | +format ::= '$' int | '${' int '}' | '${' int ':' modifier '}' | '${' int ':+' if:replace '}' | '${' int ':?' if:replace ':' else:replace '}' | '${' int ':-' else:replace '}' | '${' int ':' else:replace '}' |
| 23 | +
|
| 24 | +regex ::= JS regex value |
| 25 | +
|
| 26 | +options ::= JS regex options // NOTE: Unrecognised options should be ignored for the best fault tolerance (can log a warning though) |
| 27 | +
|
| 28 | +var ::= [a-zA-Z_][a-zA-Z_0-9]* |
| 29 | +
|
| 30 | +int ::= [0-9]+ |
| 31 | +
|
| 32 | +*/ |
| 33 | + |
| 34 | +// Grab anything that isn't \ or $, then try to build a special node out of it, and (at the top level) if that fails then just accept it as text |
| 35 | +topLevelContent = content:(text / escapedTopLevel / tabStop / choice / variable / any)* { return content; } |
| 36 | + |
| 37 | +tabStopContent = content:(text / escapedTabStop / tabStop / choice / variable)* { return content; } |
| 38 | + |
| 39 | +tabStop = tabStopSimple / tabStopWithoutPlaceholder / tabStopWithPlaceholder / tabStopWithTransform |
| 40 | + |
| 41 | +tabStopSimple = '$' n:integer { return { index: n }; } |
| 42 | + |
| 43 | +tabStopWithoutPlaceholder = '${' n:integer '}' { return { index: n }; } |
| 44 | + |
| 45 | +tabStopWithPlaceholder = '${' n:integer ':' content:tabStopContent '}' { return { index: n, content }; } |
| 46 | + |
| 47 | +tabStopWithTransform = '${' n:integer t:transformation '}' { return { index: n, transformation: t }; } |
| 48 | + |
| 49 | +transformation = '/' capture:regexString '/' replace:replace '/' flags:flags { return { capture, flags, replace }; } |
| 50 | + |
| 51 | +regexString = r:[^/]* { return r.join(""); } |
| 52 | + |
| 53 | +replace = (format / replaceText)* |
| 54 | + |
| 55 | +format = formatSimple / formatPlain / formatWithModifier / formatWithIf / formatWithIfElse / formatWithElse |
| 56 | + |
| 57 | +formatSimple = '$' n:integer { return { backreference: n }; } |
| 58 | + |
| 59 | +formatPlain = '${' n:integer '}' { return { backreference: n }; } |
| 60 | + |
| 61 | +formatWithModifier = '${' n:integer ':' modifier:modifier '}' { return { backreference: n, modifier }; } |
| 62 | + |
| 63 | +formatWithIf = '${' n:integer ':+' ifContent:replace '}' { return { backreference: n, ifContent }; } |
| 64 | + |
| 65 | +formatWithIfElse = '${' n:integer ':?' ifContent:replace ':' elseContent:replace '}' { return { backreference: n, ifContent, elseContent }; } |
| 66 | + |
| 67 | +formatWithElse = '${' n:integer ':' '-'? elseContent:replace { return { backreference: n, elseContent }; } |
| 68 | + |
| 69 | +modifier = '/' modifier:var { return modifier; } |
| 70 | + |
| 71 | +flags = f:[a-z]* { return f; } |
| 72 | + |
| 73 | +choice = '${' n:integer '|' choiceText (',' choiceText)* '|}' |
| 74 | + |
| 75 | +variable = variableSimple / variablePlain / variableWithPlaceholder / variableWithTransform |
| 76 | + |
| 77 | +variableSimple = '$' v:var { return { variable: v }; } |
| 78 | + |
| 79 | +variablePlain = '${' v:var '}' { return { variable: v }; } |
| 80 | + |
| 81 | +variableWithPlaceholder = '${' v:var ':' content:tabStopContent '}' { return { variable: v, content }; } |
| 82 | + |
| 83 | +variableWithTransform = '${' v:var t:transformation '}' { return { variable: v, transformation: t }; } |
| 84 | + |
| 85 | +text = t:[^$\\}]+ { return t.join("") } |
| 86 | + |
| 87 | +choiceText = t:[^,|]+ { return t.join(""); } |
| 88 | + |
| 89 | +replaceText = t:[^$\\}/]+ { return t.join(""); } |
| 90 | + |
| 91 | +// Match an escaped character. The set of characters that can be escaped is based on context, generally restricted to the minimum set that enables expressing any text content |
| 92 | +escapedTopLevel = '\\' c:[$\\}] { return c; } |
| 93 | + |
| 94 | +escapedTabStop = escapedTopLevel |
| 95 | + |
| 96 | +escapedChoice = '\\' c:[$\\,|] { return c; } |
| 97 | + |
| 98 | +// Match nonnegative integers like those used for tab stop ordering |
| 99 | +integer = digits:[0-9]+ { return parseInt(digits.join(""), 10); } |
| 100 | + |
| 101 | +// Match variable names like TM_SELECTED_TEXT |
| 102 | +var = a:[a-zA-Z_] b:[a-zA-Z_0-9]* { return a + b.join(""); } |
| 103 | + |
| 104 | +// Match any single character. Useful to resolve any parse errors where something that looked like it would be special had malformed syntax. |
| 105 | +any = a:. { return a; } |
0 commit comments