save caret position when ending a list
[fnpeditor.git] / libs / text.js
1 /**
2  * @license RequireJS text 2.0.6 Copyright (c) 2010-2012, The Dojo Foundation All Rights Reserved.
3  * Available via the MIT or new BSD license.
4  * see: http://github.com/requirejs/text for details
5  */
6 /*jslint regexp: true */
7 /*global require, XMLHttpRequest, ActiveXObject,
8   define, window, process, Packages,
9   java, location, Components, FileUtils */
10
11 define(['module'], function (module) {
12     'use strict';
13
14     var text, fs, Cc, Ci,
15         progIds = ['Msxml2.XMLHTTP', 'Microsoft.XMLHTTP', 'Msxml2.XMLHTTP.4.0'],
16         xmlRegExp = /^\s*<\?xml(\s)+version=[\'\"](\d)*.(\d)*[\'\"](\s)*\?>/im,
17         bodyRegExp = /<body[^>]*>\s*([\s\S]+)\s*<\/body>/im,
18         hasLocation = typeof location !== 'undefined' && location.href,
19         defaultProtocol = hasLocation && location.protocol && location.protocol.replace(/\:/, ''),
20         defaultHostName = hasLocation && location.hostname,
21         defaultPort = hasLocation && (location.port || undefined),
22         buildMap = [],
23         masterConfig = (module.config && module.config()) || {};
24
25     text = {
26         version: '2.0.6',
27
28         strip: function (content) {
29             //Strips <?xml ...?> declarations so that external SVG and XML
30             //documents can be added to a document without worry. Also, if the string
31             //is an HTML document, only the part inside the body tag is returned.
32             if (content) {
33                 content = content.replace(xmlRegExp, "");
34                 var matches = content.match(bodyRegExp);
35                 if (matches) {
36                     content = matches[1];
37                 }
38             } else {
39                 content = "";
40             }
41             return content;
42         },
43
44         jsEscape: function (content) {
45             return content.replace(/(['\\])/g, '\\$1')
46                 .replace(/[\f]/g, "\\f")
47                 .replace(/[\b]/g, "\\b")
48                 .replace(/[\n]/g, "\\n")
49                 .replace(/[\t]/g, "\\t")
50                 .replace(/[\r]/g, "\\r")
51                 .replace(/[\u2028]/g, "\\u2028")
52                 .replace(/[\u2029]/g, "\\u2029");
53         },
54
55         createXhr: masterConfig.createXhr || function () {
56             //Would love to dump the ActiveX crap in here. Need IE 6 to die first.
57             var xhr, i, progId;
58             if (typeof XMLHttpRequest !== "undefined") {
59                 return new XMLHttpRequest();
60             } else if (typeof ActiveXObject !== "undefined") {
61                 for (i = 0; i < 3; i += 1) {
62                     progId = progIds[i];
63                     try {
64                         xhr = new ActiveXObject(progId);
65                     } catch (e) {}
66
67                     if (xhr) {
68                         progIds = [progId];  // so faster next time
69                         break;
70                     }
71                 }
72             }
73
74             return xhr;
75         },
76
77         /**
78          * Parses a resource name into its component parts. Resource names
79          * look like: module/name.ext!strip, where the !strip part is
80          * optional.
81          * @param {String} name the resource name
82          * @returns {Object} with properties "moduleName", "ext" and "strip"
83          * where strip is a boolean.
84          */
85         parseName: function (name) {
86             var modName, ext, temp,
87                 strip = false,
88                 index = name.indexOf("."),
89                 isRelative = name.indexOf('./') === 0 ||
90                              name.indexOf('../') === 0;
91
92             if (index !== -1 && (!isRelative || index > 1)) {
93                 modName = name.substring(0, index);
94                 ext = name.substring(index + 1, name.length);
95             } else {
96                 modName = name;
97             }
98
99             temp = ext || modName;
100             index = temp.indexOf("!");
101             if (index !== -1) {
102                 //Pull off the strip arg.
103                 strip = temp.substring(index + 1) === "strip";
104                 temp = temp.substring(0, index);
105                 if (ext) {
106                     ext = temp;
107                 } else {
108                     modName = temp;
109                 }
110             }
111
112             return {
113                 moduleName: modName,
114                 ext: ext,
115                 strip: strip
116             };
117         },
118
119         xdRegExp: /^((\w+)\:)?\/\/([^\/\\]+)/,
120
121         /**
122          * Is an URL on another domain. Only works for browser use, returns
123          * false in non-browser environments. Only used to know if an
124          * optimized .js version of a text resource should be loaded
125          * instead.
126          * @param {String} url
127          * @returns Boolean
128          */
129         useXhr: function (url, protocol, hostname, port) {
130             var uProtocol, uHostName, uPort,
131                 match = text.xdRegExp.exec(url);
132             if (!match) {
133                 return true;
134             }
135             uProtocol = match[2];
136             uHostName = match[3];
137
138             uHostName = uHostName.split(':');
139             uPort = uHostName[1];
140             uHostName = uHostName[0];
141
142             return (!uProtocol || uProtocol === protocol) &&
143                    (!uHostName || uHostName.toLowerCase() === hostname.toLowerCase()) &&
144                    ((!uPort && !uHostName) || uPort === port);
145         },
146
147         finishLoad: function (name, strip, content, onLoad) {
148             content = strip ? text.strip(content) : content;
149             if (masterConfig.isBuild) {
150                 buildMap[name] = content;
151             }
152             onLoad(content);
153         },
154
155         load: function (name, req, onLoad, config) {
156             //Name has format: some.module.filext!strip
157             //The strip part is optional.
158             //if strip is present, then that means only get the string contents
159             //inside a body tag in an HTML string. For XML/SVG content it means
160             //removing the <?xml ...?> declarations so the content can be inserted
161             //into the current doc without problems.
162
163             // Do not bother with the work if a build and text will
164             // not be inlined.
165             if (config.isBuild && !config.inlineText) {
166                 onLoad();
167                 return;
168             }
169
170             masterConfig.isBuild = config.isBuild;
171
172             var parsed = text.parseName(name),
173                 nonStripName = parsed.moduleName +
174                     (parsed.ext ? '.' + parsed.ext : ''),
175                 url = req.toUrl(nonStripName),
176                 useXhr = (masterConfig.useXhr) ||
177                          text.useXhr;
178
179             //Load the text. Use XHR if possible and in a browser.
180             if (!hasLocation || useXhr(url, defaultProtocol, defaultHostName, defaultPort)) {
181                 text.get(url, function (content) {
182                     text.finishLoad(name, parsed.strip, content, onLoad);
183                 }, function (err) {
184                     if (onLoad.error) {
185                         onLoad.error(err);
186                     }
187                 });
188             } else {
189                 //Need to fetch the resource across domains. Assume
190                 //the resource has been optimized into a JS module. Fetch
191                 //by the module name + extension, but do not include the
192                 //!strip part to avoid file system issues.
193                 req([nonStripName], function (content) {
194                     text.finishLoad(parsed.moduleName + '.' + parsed.ext,
195                                     parsed.strip, content, onLoad);
196                 });
197             }
198         },
199
200         write: function (pluginName, moduleName, write, config) {
201             if (buildMap.hasOwnProperty(moduleName)) {
202                 var content = text.jsEscape(buildMap[moduleName]);
203                 write.asModule(pluginName + "!" + moduleName,
204                                "define(function () { return '" +
205                                    content +
206                                "';});\n");
207             }
208         },
209
210         writeFile: function (pluginName, moduleName, req, write, config) {
211             var parsed = text.parseName(moduleName),
212                 extPart = parsed.ext ? '.' + parsed.ext : '',
213                 nonStripName = parsed.moduleName + extPart,
214                 //Use a '.js' file name so that it indicates it is a
215                 //script that can be loaded across domains.
216                 fileName = req.toUrl(parsed.moduleName + extPart) + '.js';
217
218             //Leverage own load() method to load plugin value, but only
219             //write out values that do not have the strip argument,
220             //to avoid any potential issues with ! in file names.
221             text.load(nonStripName, req, function (value) {
222                 //Use own write() method to construct full module value.
223                 //But need to create shell that translates writeFile's
224                 //write() to the right interface.
225                 var textWrite = function (contents) {
226                     return write(fileName, contents);
227                 };
228                 textWrite.asModule = function (moduleName, contents) {
229                     return write.asModule(moduleName, fileName, contents);
230                 };
231
232                 text.write(pluginName, nonStripName, textWrite, config);
233             }, config);
234         }
235     };
236
237     if (masterConfig.env === 'node' || (!masterConfig.env &&
238             typeof process !== "undefined" &&
239             process.versions &&
240             !!process.versions.node)) {
241         //Using special require.nodeRequire, something added by r.js.
242         fs = require.nodeRequire('fs');
243
244         text.get = function (url, callback) {
245             var file = fs.readFileSync(url, 'utf8');
246             //Remove BOM (Byte Mark Order) from utf8 files if it is there.
247             if (file.indexOf('\uFEFF') === 0) {
248                 file = file.substring(1);
249             }
250             callback(file);
251         };
252     } else if (masterConfig.env === 'xhr' || (!masterConfig.env &&
253             text.createXhr())) {
254         text.get = function (url, callback, errback, headers) {
255             var xhr = text.createXhr(), header;
256             xhr.open('GET', url, true);
257
258             //Allow plugins direct access to xhr headers
259             if (headers) {
260                 for (header in headers) {
261                     if (headers.hasOwnProperty(header)) {
262                         xhr.setRequestHeader(header.toLowerCase(), headers[header]);
263                     }
264                 }
265             }
266
267             //Allow overrides specified in config
268             if (masterConfig.onXhr) {
269                 masterConfig.onXhr(xhr, url);
270             }
271
272             xhr.onreadystatechange = function (evt) {
273                 var status, err;
274                 //Do not explicitly handle errors, those should be
275                 //visible via console output in the browser.
276                 if (xhr.readyState === 4) {
277                     status = xhr.status;
278                     if (status > 399 && status < 600) {
279                         //An http 4xx or 5xx error. Signal an error.
280                         err = new Error(url + ' HTTP status: ' + status);
281                         err.xhr = xhr;
282                         errback(err);
283                     } else {
284                         callback(xhr.responseText);
285                     }
286
287                     if (masterConfig.onXhrComplete) {
288                         masterConfig.onXhrComplete(xhr, url);
289                     }
290                 }
291             };
292             xhr.send(null);
293         };
294     } else if (masterConfig.env === 'rhino' || (!masterConfig.env &&
295             typeof Packages !== 'undefined' && typeof java !== 'undefined')) {
296         //Why Java, why is this so awkward?
297         text.get = function (url, callback) {
298             var stringBuffer, line,
299                 encoding = "utf-8",
300                 file = new java.io.File(url),
301                 lineSeparator = java.lang.System.getProperty("line.separator"),
302                 input = new java.io.BufferedReader(new java.io.InputStreamReader(new java.io.FileInputStream(file), encoding)),
303                 content = '';
304             try {
305                 stringBuffer = new java.lang.StringBuffer();
306                 line = input.readLine();
307
308                 // Byte Order Mark (BOM) - The Unicode Standard, version 3.0, page 324
309                 // http://www.unicode.org/faq/utf_bom.html
310
311                 // Note that when we use utf-8, the BOM should appear as "EF BB BF", but it doesn't due to this bug in the JDK:
312                 // http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=4508058
313                 if (line && line.length() && line.charAt(0) === 0xfeff) {
314                     // Eat the BOM, since we've already found the encoding on this file,
315                     // and we plan to concatenating this buffer with others; the BOM should
316                     // only appear at the top of a file.
317                     line = line.substring(1);
318                 }
319
320                 stringBuffer.append(line);
321
322                 while ((line = input.readLine()) !== null) {
323                     stringBuffer.append(lineSeparator);
324                     stringBuffer.append(line);
325                 }
326                 //Make sure we return a JavaScript string and not a Java string.
327                 content = String(stringBuffer.toString()); //String
328             } finally {
329                 input.close();
330             }
331             callback(content);
332         };
333     } else if (masterConfig.env === 'xpconnect' || (!masterConfig.env &&
334             typeof Components !== 'undefined' && Components.classes &&
335             Components.interfaces)) {
336         //Avert your gaze!
337         Cc = Components.classes,
338         Ci = Components.interfaces;
339         Components.utils['import']('resource://gre/modules/FileUtils.jsm');
340
341         text.get = function (url, callback) {
342             var inStream, convertStream,
343                 readData = {},
344                 fileObj = new FileUtils.File(url);
345
346             //XPCOM, you so crazy
347             try {
348                 inStream = Cc['@mozilla.org/network/file-input-stream;1']
349                            .createInstance(Ci.nsIFileInputStream);
350                 inStream.init(fileObj, 1, 0, false);
351
352                 convertStream = Cc['@mozilla.org/intl/converter-input-stream;1']
353                                 .createInstance(Ci.nsIConverterInputStream);
354                 convertStream.init(inStream, "utf-8", inStream.available(),
355                 Ci.nsIConverterInputStream.DEFAULT_REPLACEMENT_CHARACTER);
356
357                 convertStream.readString(inStream.available(), readData);
358                 convertStream.close();
359                 inStream.close();
360                 callback(readData.value);
361             } catch (e) {
362                 throw new Error((fileObj && fileObj.path || '') + ': ' + e);
363             }
364         };
365     }
366     return text;
367 });