|
25 | 25 | /** |
26 | 26 | * Constructs a new MetaScript instance. |
27 | 27 | * @exports MetaScript |
28 | | - * @param {string=} source Source to compile |
| 28 | + * @param {string} source Source to compile |
| 29 | + * @param {string=} filename Source file name if known, defaults to `"main"`. |
29 | 30 | * @constructor |
30 | 31 | */ |
31 | | - var MetaScript = function(source) { |
| 32 | + var MetaScript = function(source, filename) { |
32 | 33 |
|
33 | 34 | /** |
34 | | - * Meta program source. |
35 | | - * @type {?string} |
| 35 | + * Original source. |
| 36 | + * @type {string} |
36 | 37 | */ |
37 | | - this.program = typeof source !== 'undefined' ? MetaScript.compile(source) : null; |
| 38 | + this.source = source; |
| 39 | + |
| 40 | + /** |
| 41 | + * Original source file name. |
| 42 | + * @type {string} |
| 43 | + */ |
| 44 | + this.filename = filename || "main"; |
| 45 | + |
| 46 | + /** |
| 47 | + * The compiled meta program's source. |
| 48 | + * @type {string} |
| 49 | + */ |
| 50 | + this.program = MetaScript.compile(source); |
38 | 51 | }; |
39 | 52 |
|
40 | 53 | /** |
|
56 | 69 | expr = /(\/\/\?|\/\*\?)(=?)/g, // Line/block expression |
57 | 70 | exprLine = /\n|$/g, // Line terminator |
58 | 71 | exprBlock = /\*\//g, // Block terminator |
| 72 | + exprEmpty = /(^|\n)([ \t]*)$/, // Empty line expression |
59 | 73 | match, matchEnd, // Matches |
60 | 74 | s, // Temporary string |
61 | 75 | indent = '', // Indentation |
62 | 76 | lastIndent = '', // Last indentation |
63 | | - out = []; // Output stack |
| 77 | + out = [], // Output stack |
| 78 | + empty; // Line empty? |
64 | 79 |
|
65 | 80 | // Escapes a string to be used in a JavaScript string enclosed in single quotes |
66 | 81 | function escapestr(s) { |
|
99 | 114 |
|
100 | 115 | // Get leading contents |
101 | 116 | s = source.substring(index, match.index); |
| 117 | + |
| 118 | + empty = exprEmpty.test(s); |
102 | 119 |
|
103 | 120 | // Look if it is a line or a block of meta |
104 | | - if (match[1].indexOf('*') < 0) { // Line |
105 | | - |
| 121 | + if (match[1].indexOf('*') < 0) { // Line //? asd |
| 122 | + |
106 | 123 | // Trim whitespaces in front of the line and remember the indentation |
107 | 124 | if (match[2] !== '=') |
108 | | - s = s.replace(/(^|\n)([ \t]*)$/, function($0, $1, $2) { indent = $2; return $1; }); |
| 125 | + s = s.replace(exprEmpty, function($0, $1, $2) { indent = $2; return $1; }); |
109 | 126 |
|
110 | 127 | // Append leading contents |
111 | 128 | append(s); |
|
119 | 136 | out.push('__=\''+escapestr(lastIndent = indent)+'\';\n'); |
120 | 137 | } |
121 | 138 | out.push(evaluate(source.substring(match.index+3, matchEnd.index).trim())); |
122 | | - if (match[2] === '=') |
| 139 | + if (!empty || match[2] === '=') |
123 | 140 | out.push('writeln();\n'); |
124 | 141 |
|
125 | 142 | // Move on |
|
169 | 186 | * Compiles the source to a meta program and transforms it using the specified scope. On node.js, this will wrap the |
170 | 187 | * entire process in a new VM context. |
171 | 188 | * @param {string} source Source |
172 | | - * @param {Object} scope Scope |
| 189 | + * @param {string=} filename Source file name |
| 190 | + * @param {!Object} scope Scope |
173 | 191 | * @param {string=} basedir Base directory for includes, defaults to `.` on node and `/` in the browser |
174 | 192 | * @returns {string} Transformed source |
175 | 193 | */ |
176 | | - MetaScript.transform = function(source, scope, basedir) { |
| 194 | + MetaScript.transform = function(source, filename, scope, basedir) { |
| 195 | + if (typeof filename === 'object') { |
| 196 | + basedir = scope; |
| 197 | + scope = filename; |
| 198 | + filename = undefined; |
| 199 | + } |
177 | 200 | if (MetaScript.IS_NODE) { |
178 | 201 | var vm = require("vm"), |
179 | 202 | sandbox; |
180 | | - vm.runInNewContext('__result = new MetaScript(__source).transform(__scope, __basedir);', sandbox = { |
| 203 | + vm.runInNewContext('__result = new MetaScript(__source, __filename).transform(__scope, __basedir);', sandbox = { |
181 | 204 | __source : source, |
| 205 | + __filename : filename, |
182 | 206 | __scope : scope, |
183 | 207 | __basedir : basedir, |
184 | 208 | MetaScript : MetaScript |
185 | 209 | }); |
186 | 210 | return sandbox.__result; |
187 | 211 | } else { |
188 | | - return new MetaScript(source).transform(scope, basedir); // Will probably pollute the global namespace |
| 212 | + return new MetaScript(source, filename).transform(scope, basedir); // Will probably pollute the global namespace |
189 | 213 | } |
190 | 214 | }; |
191 | 215 |
|
|
215 | 239 | * @param {*} s Contents to write |
216 | 240 | */ |
217 | 241 | function write(s) { |
| 242 | + // Strip trailing white spaces on lines |
218 | 243 | __out.push(s+""); |
219 | 244 | } |
220 | 245 |
|
|
264 | 289 | /** |
265 | 290 | * Includes another source file. |
266 | 291 | * @function include |
267 | | - * @param {string} __filename File to include |
268 | | - * @param {boolean} __absolute Whether the path is absolute, defaults to `false` for a relative path |
| 292 | + * @param {string} filename File to include. May be a glob expression on node.js. |
| 293 | + * @param {boolean} absolute Whether the path is absolute, defaults to `false` for a relative path |
269 | 294 | */ |
270 | | - function include(__filename, __absolute) { |
271 | | - __filename = __absolute ? __filename : (basedir === '/' ? basedir : basedir + '/') + __filename; |
272 | | - var __source; |
| 295 | + function include(filename, absolute) { |
| 296 | + filename = absolute ? filename : (basedir === '/' ? basedir : basedir + '/') + filename; |
| 297 | + var ____ = __; |
273 | 298 | if (MetaScript.IS_NODE) { |
274 | | - var files = require("glob").sync(__filename); |
275 | | - __source = ""; |
276 | | - files.forEach(function(file, i) { |
277 | | - if (__source !== '') // Add line break between includes |
278 | | - __source += __source.indexOf('\r\n') >= 0 ? '\r\n' : '\n'; |
279 | | - __source += require("fs").readFileSync(file)+""; |
| 299 | + var files = require("glob").sync(filename); |
| 300 | + files.sort(naturalCompare); // Sort these naturally (e.g. int8 < int16) |
| 301 | + files.forEach(function(file) { |
| 302 | + __eval(MetaScript.compile(indent(require("fs").readFileSync(file)+"", __)), file, filename); |
| 303 | + __ = ____; |
280 | 304 | }); |
281 | 305 | } else { // Pull it synchronously, FIXME: Is this working? |
282 | 306 | var request = XHR(); |
283 | 307 | request.open('GET', filename, false); |
284 | 308 | request.send(null); |
285 | 309 | if (typeof request.responseText === 'string') { // status is 0 on local filesystem |
286 | | - __source = request.responseText; |
| 310 | + __eval(MetaScript.compile(indent(request.responseText, __)), request.responseText, filename); |
| 311 | + __ = ____; |
287 | 312 | } else throw(new Error("Failed to fetch '"+filename+"': "+request.status)); |
288 | 313 | } |
289 | | - var ____ = __; |
290 | | - try { |
291 | | - var __program = MetaScript.compile(indent(__source, __)); |
292 | | - eval(__program); // see: (*) |
293 | | - } catch (err) { |
294 | | - if (err.rethrow) throw(err); |
295 | | - err = new Error(err.message+" in included meta program of '"+__filename+"':\n"+indent(__source, 4)); |
296 | | - err.rethrow = true; |
297 | | - throw(err); |
298 | | - } |
299 | | - __ = ____; |
300 | 314 | } |
301 | 315 |
|
302 | 316 | /** |
|
315 | 329 |
|
316 | 330 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////// |
317 | 331 |
|
318 | | - try { |
319 | | - (function() { |
320 | | - // Using a wrapper function we enforce a unified behaviour of 'var's between the main and included |
321 | | - // sources, making them always local. Of course it would be possible to make just the main source's vars |
322 | | - // globally visible, but that'd be kinda hard to explain and maintain in a reliable way. |
323 | | - eval(__program); // see: (*) |
324 | | - })(); |
325 | | - return __out.join(''); |
326 | | - } catch (err) { |
327 | | - if (err.rethrow) throw(err); |
328 | | - err = new Error(err.message+" in main meta program:\n"+indent(vars.join('')+this.program, 4)); |
329 | | - err.rethrow = true; |
330 | | - throw(err); |
| 332 | + /** |
| 333 | + * Evaluates a meta program. |
| 334 | + * @param {string} __program Meta program source |
| 335 | + * @param {string} __source Original source |
| 336 | + * @param {string} __filename Source file name |
| 337 | + * @inner |
| 338 | + * @private |
| 339 | + */ |
| 340 | + function __eval(__program, __source, __filename) { |
| 341 | + try { |
| 342 | + eval(__program); |
| 343 | + } catch (err) { |
| 344 | + if (err.rethrow) throw(err); |
| 345 | + err = new Error(err.message+" in meta program of '"+__filename+"':\n"+__err2code(__program, err)); |
| 346 | + err.rethrow = true; |
| 347 | + throw(err); |
| 348 | + } |
| 349 | + } |
| 350 | + |
| 351 | + /** |
| 352 | + * Generates a code view of eval'ed code from an Error. |
| 353 | + * @param {string} program Failed program |
| 354 | + * @param {!Error} err Error caught |
| 355 | + * @returns {string} Code view |
| 356 | + * @inner |
| 357 | + * @private |
| 358 | + */ |
| 359 | + function __err2code(program, err) { |
| 360 | + if (typeof err.stack !== 'string') |
| 361 | + return indent(program, 4); |
| 362 | + var match = /<anonymous>:(\d+):(\d+)\)/.exec(err.stack); |
| 363 | + if (!match) { |
| 364 | + return indent(program, 4); |
| 365 | + } |
| 366 | + var line = parseInt(match[1], 10)-1, |
| 367 | + start = line - 3, |
| 368 | + end = line + 4, |
| 369 | + lines = program.split("\n"); |
| 370 | + if (start < 0) start = 0; |
| 371 | + if (end > lines.length) end = lines.length; |
| 372 | + var code = []; |
| 373 | + // start = 0; end = lines.length; |
| 374 | + while (start < end) { |
| 375 | + code.push(start === line ? "--> "+lines[start] : " "+lines[start]); |
| 376 | + start++; |
| 377 | + } |
| 378 | + return indent(code.join('\n'), 4); |
331 | 379 | } |
| 380 | + |
| 381 | + __eval(__program, this.source, this.filename); |
| 382 | + return __out.join('').replace(/[ \t]+(\r?\n)/g, function($0, $1) { return $1; }); |
332 | 383 | }; |
333 | 384 |
|
334 | 385 | // (*) The use of eval() is - of course - potentially evil, but there is no way around it without making the library |
335 | 386 | // harder to use. To limit the impact we always use a fresh VM context under node.js in MetaScript.transform. |
| 387 | + |
| 388 | + /** |
| 389 | + * Compares two strings naturally, like in `"file9" < "file10"`. |
| 390 | + * @param {string} a |
| 391 | + * @param {string} b |
| 392 | + * @returns {number} |
| 393 | + * @version 0.4.4 |
| 394 | + * @author Lauri Rooden - https://github.com/litejs/natural-compare-lite |
| 395 | + * @license MIT License - http://lauri.rooden.ee/mit-license.txt |
| 396 | + */ |
| 397 | + function naturalCompare(a, b) { |
| 398 | + if (a != b) for (var i, ca, cb = 1, ia = 0, ib = 0; cb;) { |
| 399 | + ca = a.charCodeAt(ia++) || 0; |
| 400 | + cb = b.charCodeAt(ib++) || 0; |
| 401 | + if (ca < 58 && ca > 47 && cb < 58 && cb > 47) { |
| 402 | + for (i = ia; ca = a.charCodeAt(ia), ca < 58 && ca > 47; ia++); |
| 403 | + ca = (a.slice(i - 1, ia) | 0) + 1; |
| 404 | + for (i = ib; cb = b.charCodeAt(ib), cb < 58 && cb > 47; ib++); |
| 405 | + cb = (b.slice(i - 1, ib) | 0) + 1; |
| 406 | + } |
| 407 | + if (ca != cb) return (ca < cb) ? -1 : 1; |
| 408 | + } |
| 409 | + return 0; |
| 410 | + } |
336 | 411 |
|
337 | 412 | /** |
338 | 413 | * Constructs a XMLHttpRequest object. |
|
0 commit comments