/app/scripts/vendor/hm.js
JavaScript | 459 lines | 439 code | 7 blank | 13 comment | 7 complexity | 1e89fc681249dae1c7c138c9b1931aa3 MD5 | raw file
1/**
2 * @license hm 0.2.1 Copyright (c) 2010-2012, The Dojo Foundation All Rights Reserved.
3 * Available via the MIT or new BSD license.
4 * see: http://github.com/jrburke/require-hm for details
5 */
6
7/*jslint plusplus: true, regexp: true */
8/*global require, XMLHttpRequest, ActiveXObject, define, process, window,
9console */
10
11define(['esprima', 'module'], function (esprima, module) {
12 'use strict';
13
14 var fs, getXhr,
15 progIds = ['Msxml2.XMLHTTP', 'Microsoft.XMLHTTP', 'Msxml2.XMLHTTP.4.0'],
16
17 exportRegExp = /export\s+([A-Za-z\d\_]+)(\s+([A-Za-z\d\_]+))?/g,
18 commentRegExp = /(\/\*([\s\S]*?)\*\/|[^\:]\/\/(.*)$)/mg,
19 importModuleRegExp = /module|import/g,
20 commaRegExp = /\,\s*$/,
21 spaceRegExp = /\s+/,
22 quoteRegExp = /['"]/,
23 endingPuncRegExp = /[\,\;]\s*$/,
24 moduleNameRegExp = /['"]([^'"]+)['"]/,
25 startQuoteRegExp = /^['"]/,
26 braceRegExp = /[\{\}]/g,
27 buildMap = {},
28
29 fetchText = function () {
30 throw new Error('Environment unsupported.');
31 };
32
33 if (typeof window !== "undefined" && window.navigator && window.document) {
34 // Browser action
35 getXhr = function () {
36 //Would love to dump the ActiveX crap in here. Need IE 6 to die first.
37 var xhr, i, progId;
38 if (typeof XMLHttpRequest !== "undefined") {
39 return new XMLHttpRequest();
40 } else {
41 for (i = 0; i < 3; i++) {
42 progId = progIds[i];
43 try {
44 xhr = new ActiveXObject(progId);
45 } catch (e) {}
46
47 if (xhr) {
48 progIds = [progId]; // so faster next time
49 break;
50 }
51 }
52 }
53
54 if (!xhr) {
55 throw new Error("getXhr(): XMLHttpRequest not available");
56 }
57
58 return xhr;
59 };
60
61 fetchText = function (url, callback) {
62 var xhr = getXhr();
63 xhr.open('GET', url, true);
64 xhr.onreadystatechange = function (evt) {
65 //Do not explicitly handle errors, those should be
66 //visible via console output in the browser.
67 if (xhr.readyState === 4) {
68 callback(xhr.responseText);
69 }
70 };
71 xhr.send(null);
72 };
73 } else if (typeof process !== "undefined" &&
74 process.versions &&
75 !!process.versions.node) {
76 //Using special require.nodeRequire, something added by r.js.
77 fs = require.nodeRequire('fs');
78 fetchText = function (path, callback) {
79 callback(fs.readFileSync(path, 'utf8'));
80 };
81 }
82
83
84 /**
85 * Helper function for iterating over an array. If the func returns
86 * a true value, it will break out of the loop.
87 */
88 function each(ary, func) {
89 if (ary) {
90 var i;
91 for (i = 0; i < ary.length; i += 1) {
92 if (ary[i] && func(ary[i], i, ary)) {
93 break;
94 }
95 }
96 }
97 }
98
99 /**
100 * Cycles over properties in an object and calls a function for each
101 * property value. If the function returns a truthy value, then the
102 * iteration is stopped.
103 */
104 function eachProp(obj, func) {
105 var prop;
106 for (prop in obj) {
107 if (obj.hasOwnProperty(prop)) {
108 if (func(obj[prop], prop)) {
109 break;
110 }
111 }
112 }
113 }
114
115 /**
116 * Inserts the hm! loader plugin prefix if necessary. If
117 * there is already a ! in the string, then leave it be, and if it
118 * starts with a ! it means "use normal AMD loading for this dependency".
119 *
120 * @param {String} id
121 * @returns id
122 */
123 function cleanModuleId(id) {
124 id = moduleNameRegExp.exec(id)[1];
125 var index = id.indexOf('!');
126
127 if (index === -1) {
128 // Needs the hm prefix.
129 id = 'hm!' + id;
130 } else if (index === 0) {
131 //Normal AMD loading, strip off the ! sign.
132 id = id.substring(1);
133 }
134
135 return id;
136 }
137
138 function convertImportSyntax(tokens, start, end, moduleTarget) {
139 var token = tokens[start],
140 cursor = start,
141 replacement = '',
142 localVars = {},
143 moduleRef,
144 moduleId,
145 star,
146 currentVar;
147
148 //Convert module target to an AMD usable name. If a string,
149 //then needs to be accessed via require()
150 if (startQuoteRegExp.test(moduleTarget)) {
151 moduleId = cleanModuleId(moduleTarget);
152 moduleRef = 'require("' + moduleId + '")';
153 } else {
154 moduleRef = moduleTarget;
155 }
156
157 if (token.type === 'Punctuator' && token.value === '*') {
158 //import * from z
159 //If not using a module ID that is a require call, then
160 //discard it.
161 if (moduleId) {
162 star = moduleId;
163 replacement = '/*IMPORTSTAR:' + star + '*/\n';
164 } else {
165 throw new Error('import * on local reference ' + moduleTarget +
166 ' no supported.');
167 }
168 } else if (token.type === 'Identifier') {
169 //import y from z
170 replacement += 'var ' + token.value + ' = ' +
171 moduleRef + '.' + token.value + ';';
172 } else if (token.type === 'Punctuator' && token.value === '{') {
173 //import {y} from z
174 //import {x, y} from z
175 //import {x: localX, y: localY} from z
176 cursor += 1;
177 token = tokens[cursor];
178 while (cursor !== end && token.value !== '}') {
179 if (token.type === 'Identifier') {
180 if (currentVar) {
181 localVars[currentVar] = token.value;
182 currentVar = null;
183 } else {
184 currentVar = token.value;
185 }
186 } else if (token.type === 'Punctuator') {
187 if (token.value === ',') {
188 if (currentVar) {
189 localVars[currentVar] = currentVar;
190 currentVar = null;
191 }
192 }
193 }
194 cursor += 1;
195 token = tokens[cursor];
196 }
197 if (currentVar) {
198 localVars[currentVar] = currentVar;
199 }
200
201 //Now serialize the localVars
202 eachProp(localVars, function (localName, importProp) {
203 replacement += 'var ' + localName + ' = ' +
204 moduleRef + '.' + importProp + ';\n';
205 });
206 } else {
207 throw new Error('Invalid import: import ' +
208 token.value + ' ' + tokens[start + 1].value +
209 ' ' + tokens[start + 2].value);
210 }
211
212 return {
213 star: star,
214 replacement: replacement
215 };
216 }
217
218 function convertModuleSyntax(tokens, i) {
219 //Converts `foo = 'bar'` to `foo = require('bar')`
220 var varName = tokens[i],
221 eq = tokens[i + 1],
222 id = tokens[i + 2];
223
224 if (varName.type === 'Identifier' &&
225 eq.type === 'Punctuator' && eq.value === '=' &&
226 id.type === 'String') {
227 return varName.value + ' = require("' + cleanModuleId(id.value) + '")';
228 } else {
229 throw new Error('Invalid module reference: module ' +
230 varName.value + ' ' + eq.value + ' ' + id.value);
231 }
232 }
233
234 function compile(path, text) {
235 var stars = [],
236 moduleMap = {},
237 transforms = {},
238 targets = [],
239 currentIndex = 0,
240 //Remove comments from the text to be scanned
241 scanText = text.replace(commentRegExp, ""),
242 transformedText = text,
243 transformInputText,
244 startIndex,
245 segmentIndex,
246 match,
247 tempText,
248 transformed,
249 tokens;
250
251 try {
252 tokens = esprima.parse(text, {
253 tokens: true,
254 range: true
255 }).tokens;
256 } catch (e) {
257 throw new Error('Esprima cannot parse: ' + path + ': ' + e);
258 }
259
260 each(tokens, function (token, i) {
261 if (token.type !== 'Keyword' && token.type !== 'Identifier') {
262 //Not relevant, skip
263 return;
264 }
265
266 var next = tokens[i + 1],
267 next2 = tokens[i + 2],
268 next3 = tokens[i + 3],
269 cursor = i,
270 replacement,
271 moduleTarget,
272 target,
273 convertedImport;
274
275 if (token.value === 'export') {
276 // EXPORTS
277 if (next.type === 'Keyword') {
278 if (next.value === 'var' || next.value === 'let') {
279 targets.push({
280 start: token.range[0],
281 end: next2.range[0],
282 replacement: 'exports.'
283 });
284 } else if (next.value === 'function' && next2.type === 'Identifier') {
285 targets.push({
286 start: token.range[0],
287 end: next2.range[1],
288 replacement: 'exports.' + next2.value +
289 ' = function '
290 });
291 } else {
292 throw new Error('Invalid export: ' + token.value +
293 ' ' + next.value + ' ' + tokens[i + 2]);
294 }
295 } else if (next.type === 'Identifier') {
296 targets.push({
297 start: token.range[0],
298 end: next.range[1],
299 replacement: 'exports.' + next.value +
300 ' = ' + next.value
301 });
302 } else {
303 throw new Error('Invalid export: ' + token.value +
304 ' ' + next.value + ' ' + tokens[i + 2]);
305 }
306 } else if (token.value === 'module') {
307 // MODULE
308 // module Bar = "bar.js";
309 replacement = 'var ';
310 target = {
311 start: token.range[0]
312 };
313
314 while (token.value === 'module' || (token.type === 'Punctuator'
315 && token.value === ',')) {
316 cursor = cursor + 1;
317 replacement += convertModuleSyntax(tokens, cursor);
318 token = tokens[cursor + 3];
319 //Current module spec does not allow for
320 //module a = 'a', b = 'b';
321 //must end in semicolon. But keep this in case for later,
322 //as comma separators would be nice.
323 //esprima will throw if comma is not allowed.
324 if ((token.type === 'Punctuator' && token.value === ',')) {
325 replacement += ',\n';
326 }
327 }
328
329 target.end = token.range[0];
330 target.replacement = replacement;
331 targets.push(target);
332 } else if (token.value === 'import') {
333 // IMPORT
334 //import * from z;
335 //import y from z;
336 //import {y} from z;
337 //import {x, y} from z;
338 //import {x: localX, y: localY} from z;
339 cursor = i;
340 //Find the "from" in the stream
341 while (tokens[cursor] &&
342 (tokens[cursor].type !== 'Identifier' ||
343 tokens[cursor].value !== 'from')) {
344 cursor += 1;
345 }
346
347 //Increase cursor one more value to find the module target
348 moduleTarget = tokens[cursor + 1].value;
349 convertedImport = convertImportSyntax(tokens, i + 1, cursor - 1, moduleTarget);
350 replacement = convertedImport.replacement;
351 if (convertedImport.star) {
352 stars.push(convertedImport.star);
353 }
354
355 targets.push({
356 start: token.range[0],
357 end: tokens[cursor + 3].range[0],
358 replacement: replacement
359 });
360 }
361 });
362
363 //Now sort all the targets, but by start position, with the
364 //furthest start position first, since we need to transpile
365 //in reverse order.
366 targets.sort(function (a, b) {
367 return a.start > b.start ? -1 : 1;
368 });
369
370 //Now walk backwards through targets and do source modifications
371 //to AMD. Going backwards is important since the modifications will
372 //modify the length of the string.
373 each(targets, function (target, i) {
374 transformedText = transformedText.substring(0, target.start) +
375 target.replacement +
376 transformedText.substring(target.end, transformedText.length);
377 });
378
379 return {
380 text: "define(function (require, exports, module) {\n" +
381 transformedText +
382 '\n});',
383 stars: stars
384 };
385 }
386
387 function finishLoad(require, load, name, transformedText, text, isBuild) {
388 //Hold on to the transformed text if a build.
389 if (isBuild) {
390 buildMap[name] = transformedText;
391 }
392
393 load.fromText(name, transformedText);
394
395 if (module.config().logTransform) {
396 console.log("INPUT:\n" + text + "\n\nTRANSFORMED:\n" + transformedText);
397 }
398
399 //Give result to load. Need to wait until the module
400 //is fully parsed, which will happen after this
401 //execution.
402 require([name], function (value) {
403 load(value);
404 });
405 }
406
407 return {
408 version: '0.2.1',
409
410 write: function (pluginName, name, write) {
411 if (buildMap.hasOwnProperty(name)) {
412 var text = buildMap[name];
413 write.asModule(pluginName + "!" + name, text);
414 }
415 },
416
417 load: function (name, require, load, config) {
418 var path = require.toUrl(name + '.hm');
419 fetchText(path, function (text) {
420 var result = compile(path, text),
421 transformedText = result.text;
422
423 //IE with conditional comments on cannot handle the
424 //sourceURL trick, so skip it if enabled.
425 /*@if (@_jscript) @else @*/
426 if (!config.isBuild) {
427 transformedText += "\r\n//@ sourceURL=" + path;
428 }
429 /*@end@*/
430
431 if (result.stars && result.stars.length) {
432 //First load any imports that require recursive analysis
433 //TODO: this will break if there is a circular
434 //dependency with each file doing an import * on each other.
435 require(result.stars, function () {
436 var i, star, mod, starText, prop;
437
438 //Now fix up the import * items for each module.
439 for (i = 0; i < result.stars.length; i++) {
440 star = result.stars[i];
441 starText = '';
442 mod = arguments[i];
443 for (prop in mod) {
444 if (mod.hasOwnProperty(prop)) {
445 starText += 'var ' + prop + ' = require("' + star + '").' + prop + '; ';
446 }
447 }
448 transformedText = transformedText.replace('/*IMPORTSTAR:' + star + '*/', starText);
449 }
450
451 finishLoad(require, load, name, transformedText, text, config.isBuild);
452 });
453 } else {
454 finishLoad(require, load, name, transformedText, text, config.isBuild);
455 }
456 });
457 }
458 };
459});