UNPKG

10.9 kBJavaScriptView Raw
1function createLogger() {
2 var readline = require("readline"),
3 logStream = process.stdout;
4 return {
5 print(data = "", end = "\n") {
6 process.stdout.write(data + end);
7 },
8 log(data = "", end = "\n") {
9 logStream.write(data + end);
10 },
11 clear() {
12 readline.clearLine(process.stdout, -1);
13 readline.cursorTo(process.stdout, 0, null);
14 },
15 startDebug() {
16 logStream = process.stderr;
17 }
18 };
19}
20
21function createTransformer() {
22 var map = new Map, self, haye = require("haye");
23 return self = {
24 add({name, transform, pre}) {
25 if (pre) {
26 pre = haye.fromPipe(pre).toArray();
27 }
28 map.set(name, {transform, pre});
29 },
30 transform({resource, transforms = [], content}) {
31 for (var {name, args} of transforms) {
32 var {transform, pre} = map.get(name);
33 if (pre) content = self.transform({resource, transforms: pre, content});
34 if (!Array.isArray(args)) {
35 if (args == null) {
36 args = [];
37 } else {
38 args = [args];
39 }
40 }
41 content = transform(resource.args, content, ...args);
42 }
43
44 return content;
45 },
46 load: function({transforms = []}) {
47 transforms.forEach(t => self.add(t));
48 }
49 };
50}
51
52function getLineRange(text, pos) {
53 // FIXME: this might fail if \r\n messed up
54 var i, result = {};
55 i = text.lastIndexOf("\n", pos);
56 result.start = i + 1;
57 if (text[i - 1] == "\r") {
58 result.beforeStart = i - 1;
59 } else {
60 result.beforeStart = i;
61 }
62 i = text.indexOf("\n", pos);
63 if (text[i - 1] == "\r") {
64 result.end = i - 1;
65 } else {
66 result.end = i;
67 }
68 result.afterEnd = i + 1;
69 return result;
70}
71
72function parseArguments(text) {
73 var haye = require("haye"),
74 result = haye.fromPipe(text).toArray(),
75 resource = result[0],
76 transforms = result.slice(1);
77
78 if (resource.args == null) {
79 resource.args = resource.name;
80 resource.name = "file";
81 }
82
83 return {resource, transforms};
84}
85
86function parseRegex(text) {
87 var flags = text.match(/[a-z]*$/)[0];
88 return new RegExp(text.slice(1, -(flags.length + 1)), flags);
89}
90
91function parseString(text) {
92 if (text[0] == "'") {
93 text = '"' + text.slice(1, -1).replace(/([^\\]|$)"/g, '$1\\"') + '"';
94 }
95 return JSON.parse(text);
96}
97
98function parseInline(text, pos = 0, flags = {}) {
99 var {default: jsTokens, matchToToken} = require("js-tokens"),
100 match, token, info = {type: "$inline"};
101
102 jsTokens.lastIndex = pos;
103 jsTokens.exec(text); // skip $inline
104 match = jsTokens.exec(text);
105 if (match[0] == ".") {
106 token = matchToToken(jsTokens.exec(text));
107 if (token.type != "name") {
108 throw new Error;
109 } else {
110 info.type += "." + token.value;
111 }
112 match = jsTokens.exec(text);
113 if (!match) {
114 info.end = text.length;
115 return info;
116 }
117 }
118 if (match[0] != "(") {
119 info.end = jsTokens.lastIndex;
120 return info;
121 }
122 info.params = [];
123 flags.needValue = true;
124 while ((match = jsTokens.exec(text))) {
125 token = matchToToken(match);
126 if (token.type == "whitespace" || token.type == "comment") {
127 continue;
128 }
129 if (token.value == ")") {
130 info.end = jsTokens.lastIndex;
131 break;
132 }
133 if (flags.needValue == (token.type == "punctuator")) {
134 throw new Error(`Failed to parse $inline statement at ${match.index}`);
135 } else {
136 flags.needValue = !flags.needValue;
137 if (token.type == "punctuator") continue;
138 }
139 if (token.type == "regex") {
140 token.value = parseRegex(token.value);
141 } else if (token.type == "number") {
142 token.value = +token.value;
143 } else if (token.type == "string") {
144 if (!token.closed) token.value += token.value[0];
145 token.value = parseString(token.value);
146 }
147 info.params.push(token.value);
148 }
149 if (!info.end) {
150 throw new Error("Missing right parenthesis");
151 }
152 return info;
153}
154
155function* inlines(content) {
156 var re = /\$inline[.(]/gi,
157 match, type, params,
158 flags = {};
159
160 while ((match = re.exec(content))) {
161 ({type, params, end: re.lastIndex} = parseInline(content, match.index, flags));
162
163 if (flags.skip) {
164 if (type == "$inline.skipEnd") {
165 flags.skip = false;
166 }
167 continue;
168 }
169
170 if (flags.start) {
171 if (type != "$inline.end") {
172 continue;
173 }
174 flags.start.end = getLineRange(content, match.index).beforeStart;
175 if (flags.start.start > flags.start.end) {
176 throw new Error(`$inline.start and $inline.end must not present at the same line`);
177 }
178 yield flags.start;
179 flags.start = null;
180 continue;
181 }
182
183 if (flags.open) {
184 if (type != "$inline.close") {
185 continue;
186 }
187 var offset = params && params[0] || 0;
188 flags.open.end = match.index - offset;
189 yield flags.open;
190 flags.open = null;
191 continue;
192 }
193
194 if (type == "$inline.skipStart") {
195 flags.skip = true;
196 continue;
197 }
198
199 if (type == "$inline.start") {
200 flags.start = {
201 type, params,
202 start: getLineRange(content, match.index).afterEnd
203 };
204 continue;
205 }
206
207 if (type == "$inline.open") {
208 flags.open = {
209 type, params,
210 start: re.lastIndex + (params[1] || 0)
211 };
212 continue;
213 }
214
215 if (type == "$inline") {
216 yield {
217 type, params,
218 start: match.index,
219 end: re.lastIndex
220 };
221 continue;
222 }
223
224 if (type == "$inline.line") {
225 var {start, end} = getLineRange(content, match.index);
226 yield {
227 type, params,
228 start, end
229 };
230 continue;
231 }
232
233 if (type == "$inline.shortcut") {
234 yield {
235 type, params
236 };
237 continue;
238 }
239
240 throw new Error(`${type} is not a valid $inline statement`);
241 }
242
243 if (flags.start) {
244 throw new Error(`Failed to match $inline.start at ${flags.start.start}, missing $inline.end`);
245 }
246
247 if (flags.open) {
248 throw new Error(`Failed to match $inline.open at ${flags.open.start}, missing $inline.close`);
249 }
250}
251
252function createResourceCenter() {
253 var map = new Map, self;
254 return self = {
255 read({from, resource}) {
256 return map.get(resource.name)({from, resource});
257 },
258 add({name, read}) {
259 map.set(name, read);
260 },
261 load({resources = []}) {
262 resources.forEach(r => self.add(r));
263 }
264 };
265}
266
267function inline({
268 resource, from, transformer = createTransformer(), transforms,
269 resourceCenter, depth = 0, maxDepth = 10, dependency = {}, shortcuts = createShortcuts()
270}) {
271 if (depth > maxDepth) {
272 throw new Error(`Max recursion depth ${maxDepth} exceeded, if you are not making an infinite loop please increase --max-depth limit`);
273 }
274
275 var content = resourceCenter.read({from, resource}),
276 text = [],
277 i = 0;
278
279 dependency = dependency[resource.args] = {};
280
281 for (var result of inlines(content)) {
282 if (result.type == "$inline.shortcut") {
283 shortcuts.add(resource.args, ...result.params);
284 continue;
285 }
286 var args = shortcuts.expand(resource.args, result.params[0]);
287 Object.assign(
288 result,
289 parseArguments(args),
290 {
291 from: resource,
292 transformer,
293 resourceCenter,
294 shortcuts,
295 depth: depth + 1,
296 maxDepth,
297 dependency
298 }
299 );
300 text.push(content.slice(i, result.start), inline(result));
301 i = result.end;
302 }
303
304 shortcuts.remove(resource.args);
305
306 text.push(content.slice(i));
307
308 content = text.join("");
309
310 content = transformer.transform({resource, transforms, content});
311
312 return content;
313}
314
315function moduleRoot() {
316 var path = require("pathlib"),
317 fs = require("fs"),
318 pkg = path("./folder/package.json").resolve();
319
320 do {
321 pkg = pkg.move("..");
322 try {
323 fs.accessSync(pkg.path);
324 } catch (err) {
325 continue;
326 }
327 return pkg.dir();
328 } while (!pkg.dir().isRoot());
329}
330
331function loadConfig({transformer, resourceCenter, logger, shortcuts}) {
332 var config = require("./.inline.js");
333
334 transformer.load(config);
335 resourceCenter.load(config);
336 shortcuts.load(config);
337
338 var mr = moduleRoot();
339
340 if (!mr) return;
341
342 var configPath = mr.extend(".inline.js").path;
343
344 try {
345 config = require(configPath);
346 } catch (err) {
347 config = null;
348 }
349
350 if (config) {
351 logger.log(`Load ${configPath}\n`);
352 transformer.load(config);
353 resourceCenter.load(config);
354 shortcuts.load(config);
355 }
356}
357
358function createShortcuts() {
359 var global = new Map,
360 local = new Map,
361 self;
362 function getExpandor(file, name) {
363 if (local.has(file) && local.get(file).has(name)) {
364 return local.get(file).get(name);
365 }
366 if (global.has(name)) {
367 return global.get(name);
368 }
369 return null;
370 }
371 return self = {
372 add(file, name, expand) {
373 if (!local.has(file)) {
374 local.set(file, new Map);
375 }
376 local.get(file).set(name, expand);
377 },
378 addGlobal({name, expand}) {
379 global.set(name, expand);
380 },
381 expand(file, args) {
382 var haye = require("haye"),
383 [shortcut, ...pipes] = haye.fromPipe(args).toArray(),
384 expandor = getExpandor(file, shortcut.name);
385 if (!expandor) {
386 return args;
387 }
388 if (!Array.isArray(shortcut.args)) {
389 shortcut.args = [shortcut.args];
390 }
391 var expanded;
392 if (typeof expandor == "function") {
393 expanded = expandor(file, ...shortcut.args);
394 } else if (typeof expandor == "string") {
395 expanded = expandor.replace(/\$(\d+|&)/g, (match, n) => {
396 if (n == "&") {
397 return shortcut.args.join(",");
398 }
399 return shortcut.args[n - 1];
400 });
401 } else {
402 throw new Error(`expandor must be a string or function: ${expandor}`);
403 }
404 pipes = pipes.map(({name, args}) => {
405 if (!args) {
406 return name;
407 }
408 name += ":";
409 if (!Array.isArray(args)) {
410 args = [args];
411 }
412 name += args.join(",");
413 return name;
414 });
415
416 return [expanded].concat(pipes).join("|");
417 },
418 remove(file) {
419 local.delete(file);
420 },
421 load({shortcuts = []}) {
422 shortcuts.forEach(self.addGlobal);
423 }
424 };
425}
426
427function init({
428 args: {
429 "--out": out,
430 "--dry-run": dry,
431 "--max-depth": maxDepth,
432 "<entry_file>": file,
433 },
434 logger = createLogger(),
435 transformer = createTransformer(),
436 resourceCenter = createResourceCenter(),
437 shortcuts = createShortcuts()
438}) {
439 if (!dry && !out) {
440 logger.startDebug();
441 }
442
443 logger.log("inline-js started\n");
444
445 loadConfig({transformer, resourceCenter, logger, shortcuts});
446
447 var path = require("pathlib"),
448 fs = require("fs-extra"),
449 treeify = require("treeify"),
450 resource = {
451 name: "file",
452 args: path.resolve(file)
453 },
454 dependency = {},
455 content = inline({
456 resource, resourceCenter, transformer, maxDepth, dependency, shortcuts
457 });
458
459 var [root] = Object.keys(dependency);
460 logger.log(`Result inline tree:`);
461 logger.log(root);
462 logger.log(treeify.asTree(dependency[root]));
463
464 if (dry) {
465 logger.log(`[dry] Output to ${out ? path.resolve(out) : "stdout"}`);
466 } else if (out) {
467 fs.outputFileSync(out, content);
468 } else {
469 logger.print(content, "");
470 }
471}
472
473module.exports = {
474 init, inlines, inline, parseInline, createShortcuts,
475};