1 | function 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 |
|
21 | function 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 |
|
52 | function getLineRange(text, pos) {
|
53 |
|
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 |
|
72 | function 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 |
|
86 | function parseRegex(text) {
|
87 | var flags = text.match(/[a-z]*$/)[0];
|
88 | return new RegExp(text.slice(1, -(flags.length + 1)), flags);
|
89 | }
|
90 |
|
91 | function parseString(text) {
|
92 | if (text[0] == "'") {
|
93 | text = '"' + text.slice(1, -1).replace(/([^\\]|$)"/g, '$1\\"') + '"';
|
94 | }
|
95 | return JSON.parse(text);
|
96 | }
|
97 |
|
98 | function 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);
|
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 |
|
155 | function* 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 |
|
252 | function 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 |
|
267 | function 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 |
|
315 | function 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 |
|
331 | function 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 |
|
358 | function 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 |
|
427 | function 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 |
|
473 | module.exports = {
|
474 | init, inlines, inline, parseInline, createShortcuts,
|
475 | };
|