UNPKG

6.31 kBJavaScriptView Raw
1const {default: jsTokens, matchToToken} = require("js-tokens");
2
3function getLineRange(text, pos) {
4 // FIXME: should we handle \r?
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
17function parseRegex(text) {
18 var flags = text.match(/[a-z]*$/)[0];
19 return new RegExp(text.slice(1, -(flags.length + 1)), flags);
20}
21
22function 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
29function 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); // skip $inline
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
89function 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
208function 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
235function unescapePipeName(text) {
236 return text.replace(/\\([:|])/g, "$1");
237}
238
239function unescapePipeValue(text) {
240 return text.replace(/\\([,|])/g, "$1");
241}
242
243function escapePipeName(text) {
244 return text.replace(/[:|]/g, "\\$1");
245}
246
247function escapePipeValue(text) {
248 return text.replace(/[,|]/g, "\\$1");
249}
250
251function 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
262module.exports = {
263 parseText, parsePipes, parseDirective,
264 escapePipeName, escapePipeValue, pipesToString
265};