1 | const {default: jsTokens, matchToToken} = require("js-tokens");
|
2 |
|
3 | function getLineRange(text, pos) {
|
4 |
|
5 | let start, end;
|
6 | start = text.lastIndexOf("\n", pos - 1) + 1;
|
7 | if (start < 0) {
|
8 | start = 0;
|
9 | }
|
10 | end = text.indexOf("\n", pos);
|
11 | if (end < 0) {
|
12 | end = text.length;
|
13 | }
|
14 | return {start, end};
|
15 | }
|
16 |
|
17 | function parseRegex(text) {
|
18 | var flags = text.match(/[a-z]*$/)[0];
|
19 | return new RegExp(text.slice(1, -(flags.length + 1)), flags);
|
20 | }
|
21 |
|
22 | function parseString(text) {
|
23 | if (text[0] == "'" || text[0] == "`") {
|
24 | text = '"' + text.slice(1, -1).replace(/([^\\]|$)"/g, '$1\\"') + '"';
|
25 | }
|
26 | return JSON.parse(text);
|
27 | }
|
28 |
|
29 | function parseDirective(text, pos = 0, flags = {}) {
|
30 | var match, token, info = {
|
31 | type: "$inline",
|
32 | start: null,
|
33 | end: null,
|
34 | params: []
|
35 | };
|
36 |
|
37 | jsTokens.lastIndex = pos;
|
38 | jsTokens.exec(text);
|
39 | match = jsTokens.exec(text);
|
40 | if (match[0] == ".") {
|
41 | token = matchToToken(jsTokens.exec(text));
|
42 | if (token.type != "name") {
|
43 | throw new Error(`Expecting $inline method but got "${token.value}"`);
|
44 | } else {
|
45 | info.type += "." + token.value;
|
46 | }
|
47 | match = jsTokens.exec(text);
|
48 | if (!match) {
|
49 | info.end = text.length;
|
50 | return info;
|
51 | }
|
52 | }
|
53 | if (match[0] != "(") {
|
54 | info.end = jsTokens.lastIndex;
|
55 | return info;
|
56 | }
|
57 | flags.needValue = true;
|
58 | while ((match = jsTokens.exec(text))) {
|
59 | token = matchToToken(match);
|
60 | if (token.type == "whitespace" || token.type == "comment") {
|
61 | continue;
|
62 | }
|
63 | if (token.value == ")") {
|
64 | info.end = jsTokens.lastIndex;
|
65 | break;
|
66 | }
|
67 | if (flags.needValue == (token.type == "punctuator")) {
|
68 | throw new Error(`Failed to parse $inline statement at ${match.index}`);
|
69 | } else {
|
70 | flags.needValue = !flags.needValue;
|
71 | if (token.type == "punctuator") continue;
|
72 | }
|
73 | if (token.type == "regex") {
|
74 | token.value = parseRegex(token.value);
|
75 | } else if (token.type == "number") {
|
76 | token.value = +token.value;
|
77 | } else if (token.type == "string") {
|
78 | if (!token.closed) token.value += token.value[0];
|
79 | token.value = parseString(token.value);
|
80 | }
|
81 | info.params.push(token.value);
|
82 | }
|
83 | if (!info.end) {
|
84 | throw new Error("Missing right parenthesis");
|
85 | }
|
86 | return info;
|
87 | }
|
88 |
|
89 | function parseText(content) {
|
90 | const output = [];
|
91 | let lastIndex = 0;
|
92 | var re = /\$inline[.(]/gi,
|
93 | match, type, params,
|
94 | flags = {};
|
95 |
|
96 | function addDirective(directive) {
|
97 | if (lastIndex !== directive.start) {
|
98 | output.push({
|
99 | type: "text",
|
100 | value: content.slice(lastIndex, directive.start)
|
101 | });
|
102 | }
|
103 | output.push(directive);
|
104 | lastIndex = directive.end;
|
105 | }
|
106 |
|
107 | while ((match = re.exec(content))) {
|
108 | ({type, params, end: re.lastIndex} = parseDirective(content, match.index, flags));
|
109 |
|
110 | if (flags.skip) {
|
111 | if (type == "$inline.skipEnd" && (flags.skip === params[0] || flags.skip === true)) {
|
112 | flags.skip = false;
|
113 | }
|
114 | continue;
|
115 | }
|
116 |
|
117 | if (flags.start) {
|
118 | if (type != "$inline.end") {
|
119 | continue;
|
120 | }
|
121 | flags.start.end = getLineRange(content, match.index).start - 1;
|
122 | if (flags.start.start > flags.start.end) {
|
123 | throw new Error(`There must be at leat one line between $inline.start and $inline.end`);
|
124 | }
|
125 | addDirective(flags.start);
|
126 | flags.start = null;
|
127 | continue;
|
128 | }
|
129 |
|
130 | if (flags.open) {
|
131 | if (type != "$inline.close") {
|
132 | continue;
|
133 | }
|
134 | var offset = params && params[0] || 0;
|
135 | flags.open.end = match.index - offset;
|
136 | addDirective(flags.open);
|
137 | flags.open = null;
|
138 | continue;
|
139 | }
|
140 |
|
141 | if (type == "$inline.skipStart") {
|
142 | flags.skip = params[0] || true;
|
143 | continue;
|
144 | }
|
145 |
|
146 | if (type == "$inline.start") {
|
147 | flags.start = {
|
148 | type, params,
|
149 | start: getLineRange(content, match.index).end + 1
|
150 | };
|
151 | continue;
|
152 | }
|
153 |
|
154 | if (type == "$inline.open") {
|
155 | flags.open = {
|
156 | type, params,
|
157 | start: re.lastIndex + (params[1] || 0)
|
158 | };
|
159 | continue;
|
160 | }
|
161 |
|
162 | if (type == "$inline") {
|
163 | addDirective({
|
164 | type, params,
|
165 | start: match.index,
|
166 | end: re.lastIndex
|
167 | });
|
168 | continue;
|
169 | }
|
170 |
|
171 | if (type == "$inline.line") {
|
172 | var {start, end} = getLineRange(content, match.index);
|
173 | addDirective({
|
174 | type, params,
|
175 | start, end
|
176 | });
|
177 | continue;
|
178 | }
|
179 |
|
180 | if (type == "$inline.shortcut") {
|
181 | output.push({
|
182 | type, params
|
183 | });
|
184 | continue;
|
185 | }
|
186 |
|
187 | throw new Error(`${type} is not a valid $inline statement (position ${match.index})`);
|
188 | }
|
189 |
|
190 | if (lastIndex !== content.length) {
|
191 | output.push({
|
192 | type: "text",
|
193 | value: content.slice(lastIndex)
|
194 | });
|
195 | }
|
196 |
|
197 | if (flags.start) {
|
198 | throw new Error(`Failed to match $inline.start at ${flags.start.start}, missing $inline.end`);
|
199 | }
|
200 |
|
201 | if (flags.open) {
|
202 | throw new Error(`Failed to match $inline.open at ${flags.open.start}, missing $inline.close`);
|
203 | }
|
204 |
|
205 | return output;
|
206 | }
|
207 |
|
208 | function parsePipes(text) {
|
209 | const nameRe = /\s*((?:\\:|\\\||[^:|])+?)\s*([:|]|$)/y;
|
210 | const valueRe = /\s*((?:\\\||\\,|[^|,])+?)\s*([,|]|$)/y;
|
211 | const output = [];
|
212 | while (nameRe.lastIndex < text.length) {
|
213 | const match = text.match(nameRe);
|
214 | const pipe = {
|
215 | name: unescapePipeName(match[1]),
|
216 | args: []
|
217 | };
|
218 | output.push(pipe);
|
219 | if (match[2] === "|") {
|
220 | continue;
|
221 | }
|
222 | valueRe.lastIndex = nameRe.lastIndex;
|
223 | while (valueRe.lastIndex < text.length) {
|
224 | const match = text.match(valueRe);
|
225 | pipe.args.push(unescapePipeValue(match[1]));
|
226 | if (match[2] === "|") {
|
227 | break;
|
228 | }
|
229 | }
|
230 | nameRe.lastIndex = valueRe.lastIndex;
|
231 | }
|
232 | return output;
|
233 | }
|
234 |
|
235 | function unescapePipeName(text) {
|
236 | return text.replace(/\\([:|])/g, "$1");
|
237 | }
|
238 |
|
239 | function unescapePipeValue(text) {
|
240 | return text.replace(/\\([,|])/g, "$1");
|
241 | }
|
242 |
|
243 | function escapePipeName(text) {
|
244 | return text.replace(/[:|]/g, "\\$1");
|
245 | }
|
246 |
|
247 | function escapePipeValue(text) {
|
248 | return text.replace(/[,|]/g, "\\$1");
|
249 | }
|
250 |
|
251 | function pipesToString(pipes) {
|
252 | return pipes.map(pipe => {
|
253 | const name = escapePipeName(pipe.name);
|
254 | if (!pipe.args.length) {
|
255 | return name;
|
256 | }
|
257 | const args = pipe.args.map(escapePipeValue);
|
258 | return `${name}:${args.join(",")}`;
|
259 | }).join("|");
|
260 | }
|
261 |
|
262 | module.exports = {
|
263 | parseText, parsePipes, parseDirective,
|
264 | escapePipeName, escapePipeValue, pipesToString
|
265 | };
|