1 | // |
---|
2 | // browser.js - client-side engine |
---|
3 | // |
---|
4 | |
---|
5 | var isFileProtocol = (location.protocol === 'file:' || |
---|
6 | location.protocol === 'chrome:' || |
---|
7 | location.protocol === 'chrome-extension:' || |
---|
8 | location.protocol === 'resource:'); |
---|
9 | |
---|
10 | less.env = less.env || (location.hostname == '127.0.0.1' || |
---|
11 | location.hostname == '0.0.0.0' || |
---|
12 | location.hostname == 'localhost' || |
---|
13 | location.port.length > 0 || |
---|
14 | isFileProtocol ? 'development' |
---|
15 | : 'production'); |
---|
16 | |
---|
17 | // Load styles asynchronously (default: false) |
---|
18 | // |
---|
19 | // This is set to `false` by default, so that the body |
---|
20 | // doesn't start loading before the stylesheets are parsed. |
---|
21 | // Setting this to `true` can result in flickering. |
---|
22 | // |
---|
23 | less.async = false; |
---|
24 | |
---|
25 | // Interval between watch polls |
---|
26 | less.poll = less.poll || (isFileProtocol ? 1000 : 1500); |
---|
27 | |
---|
28 | // |
---|
29 | // Watch mode |
---|
30 | // |
---|
31 | less.watch = function () { return this.watchMode = true }; |
---|
32 | less.unwatch = function () { return this.watchMode = false }; |
---|
33 | |
---|
34 | if (less.env === 'development') { |
---|
35 | less.optimization = 0; |
---|
36 | |
---|
37 | if (/!watch/.test(location.hash)) { |
---|
38 | less.watch(); |
---|
39 | } |
---|
40 | less.watchTimer = setInterval(function () { |
---|
41 | if (less.watchMode) { |
---|
42 | loadStyleSheets(function (root, sheet, env) { |
---|
43 | if (root) { |
---|
44 | createCSS(root.toCSS(), sheet, env.lastModified); |
---|
45 | } |
---|
46 | }); |
---|
47 | } |
---|
48 | }, less.poll); |
---|
49 | } else { |
---|
50 | less.optimization = 3; |
---|
51 | } |
---|
52 | |
---|
53 | var cache; |
---|
54 | |
---|
55 | try { |
---|
56 | cache = (typeof(window.localStorage) === 'undefined') ? null : window.localStorage; |
---|
57 | } catch (_) { |
---|
58 | cache = null; |
---|
59 | } |
---|
60 | |
---|
61 | // |
---|
62 | // Get all <link> tags with the 'rel' attribute set to "stylesheet/less" |
---|
63 | // |
---|
64 | var links = document.getElementsByTagName('link'); |
---|
65 | var typePattern = /^text\/(x-)?less$/; |
---|
66 | |
---|
67 | less.sheets = []; |
---|
68 | |
---|
69 | for (var i = 0; i < links.length; i++) { |
---|
70 | if (links[i].rel === 'stylesheet/less' || (links[i].rel.match(/stylesheet/) && |
---|
71 | (links[i].type.match(typePattern)))) { |
---|
72 | less.sheets.push(links[i]); |
---|
73 | } |
---|
74 | } |
---|
75 | |
---|
76 | |
---|
77 | less.refresh = function (reload) { |
---|
78 | var startTime, endTime; |
---|
79 | startTime = endTime = new(Date); |
---|
80 | |
---|
81 | loadStyleSheets(function (root, sheet, env) { |
---|
82 | if (env.local) { |
---|
83 | log("loading " + sheet.href + " from cache."); |
---|
84 | } else { |
---|
85 | log("parsed " + sheet.href + " successfully."); |
---|
86 | createCSS(root.toCSS(), sheet, env.lastModified); |
---|
87 | } |
---|
88 | log("css for " + sheet.href + " generated in " + (new(Date) - endTime) + 'ms'); |
---|
89 | (env.remaining === 0) && log("css generated in " + (new(Date) - startTime) + 'ms'); |
---|
90 | endTime = new(Date); |
---|
91 | }, reload); |
---|
92 | |
---|
93 | loadStyles(); |
---|
94 | }; |
---|
95 | less.refreshStyles = loadStyles; |
---|
96 | |
---|
97 | less.refresh(less.env === 'development'); |
---|
98 | |
---|
99 | function loadStyles() { |
---|
100 | var styles = document.getElementsByTagName('style'); |
---|
101 | for (var i = 0; i < styles.length; i++) { |
---|
102 | if (styles[i].type.match(typePattern)) { |
---|
103 | new(less.Parser)().parse(styles[i].innerHTML || '', function (e, tree) { |
---|
104 | styles[i].type = 'text/css'; |
---|
105 | styles[i].innerHTML = tree.toCSS(); |
---|
106 | }); |
---|
107 | } |
---|
108 | } |
---|
109 | } |
---|
110 | |
---|
111 | function loadStyleSheets(callback, reload) { |
---|
112 | for (var i = 0; i < less.sheets.length; i++) { |
---|
113 | loadStyleSheet(less.sheets[i], callback, reload, less.sheets.length - (i + 1)); |
---|
114 | } |
---|
115 | } |
---|
116 | |
---|
117 | function loadStyleSheet(sheet, callback, reload, remaining) { |
---|
118 | var url = window.location.href.replace(/[#?].*$/, ''); |
---|
119 | var href = sheet.href.replace(/\?.*$/, ''); |
---|
120 | var css = cache && cache.getItem(href); |
---|
121 | var timestamp = cache && cache.getItem(href + ':timestamp'); |
---|
122 | var styles = { css: css, timestamp: timestamp }; |
---|
123 | |
---|
124 | // Stylesheets in IE don't always return the full path |
---|
125 | if (! /^(https?|file):/.test(href)) { |
---|
126 | if (href.charAt(0) == "/") { |
---|
127 | href = window.location.protocol + "//" + window.location.host + href; |
---|
128 | } else { |
---|
129 | href = url.slice(0, url.lastIndexOf('/') + 1) + href; |
---|
130 | } |
---|
131 | } |
---|
132 | |
---|
133 | xhr(sheet.href, sheet.type, function (data, lastModified) { |
---|
134 | if (!reload && styles && lastModified && |
---|
135 | (new(Date)(lastModified).valueOf() === |
---|
136 | new(Date)(styles.timestamp).valueOf())) { |
---|
137 | // Use local copy |
---|
138 | createCSS(styles.css, sheet); |
---|
139 | callback(null, sheet, { local: true, remaining: remaining }); |
---|
140 | } else { |
---|
141 | // Use remote copy (re-parse) |
---|
142 | try { |
---|
143 | new(less.Parser)({ |
---|
144 | optimization: less.optimization, |
---|
145 | paths: [href.replace(/[\w\.-]+$/, '')], |
---|
146 | mime: sheet.type |
---|
147 | }).parse(data, function (e, root) { |
---|
148 | if (e) { return error(e, href) } |
---|
149 | try { |
---|
150 | callback(root, sheet, { local: false, lastModified: lastModified, remaining: remaining }); |
---|
151 | removeNode(document.getElementById('less-error-message:' + extractId(href))); |
---|
152 | } catch (e) { |
---|
153 | error(e, href); |
---|
154 | } |
---|
155 | }); |
---|
156 | } catch (e) { |
---|
157 | error(e, href); |
---|
158 | } |
---|
159 | } |
---|
160 | }, function (status, url) { |
---|
161 | throw new(Error)("Couldn't load " + url + " (" + status + ")"); |
---|
162 | }); |
---|
163 | } |
---|
164 | |
---|
165 | function extractId(href) { |
---|
166 | return href.replace(/^[a-z]+:\/\/?[^\/]+/, '' ) // Remove protocol & domain |
---|
167 | .replace(/^\//, '' ) // Remove root / |
---|
168 | .replace(/\?.*$/, '' ) // Remove query |
---|
169 | .replace(/\.[^\.\/]+$/, '' ) // Remove file extension |
---|
170 | .replace(/[^\.\w-]+/g, '-') // Replace illegal characters |
---|
171 | .replace(/\./g, ':'); // Replace dots with colons(for valid id) |
---|
172 | } |
---|
173 | |
---|
174 | function createCSS(styles, sheet, lastModified) { |
---|
175 | var css; |
---|
176 | |
---|
177 | // Strip the query-string |
---|
178 | var href = sheet.href ? sheet.href.replace(/\?.*$/, '') : ''; |
---|
179 | |
---|
180 | // If there is no title set, use the filename, minus the extension |
---|
181 | var id = 'less:' + (sheet.title || extractId(href)); |
---|
182 | |
---|
183 | // If the stylesheet doesn't exist, create a new node |
---|
184 | if ((css = document.getElementById(id)) === null) { |
---|
185 | css = document.createElement('style'); |
---|
186 | css.type = 'text/css'; |
---|
187 | css.media = sheet.media || 'screen'; |
---|
188 | css.id = id; |
---|
189 | document.getElementsByTagName('head')[0].appendChild(css); |
---|
190 | } |
---|
191 | |
---|
192 | if (css.styleSheet) { // IE |
---|
193 | try { |
---|
194 | css.styleSheet.cssText = styles; |
---|
195 | } catch (e) { |
---|
196 | throw new(Error)("Couldn't reassign styleSheet.cssText."); |
---|
197 | } |
---|
198 | } else { |
---|
199 | (function (node) { |
---|
200 | if (css.childNodes.length > 0) { |
---|
201 | if (css.firstChild.nodeValue !== node.nodeValue) { |
---|
202 | css.replaceChild(node, css.firstChild); |
---|
203 | } |
---|
204 | } else { |
---|
205 | css.appendChild(node); |
---|
206 | } |
---|
207 | })(document.createTextNode(styles)); |
---|
208 | } |
---|
209 | |
---|
210 | // Don't update the local store if the file wasn't modified |
---|
211 | if (lastModified && cache) { |
---|
212 | log('saving ' + href + ' to cache.'); |
---|
213 | cache.setItem(href, styles); |
---|
214 | cache.setItem(href + ':timestamp', lastModified); |
---|
215 | } |
---|
216 | } |
---|
217 | |
---|
218 | function xhr(url, type, callback, errback) { |
---|
219 | var xhr = getXMLHttpRequest(); |
---|
220 | var async = isFileProtocol ? false : less.async; |
---|
221 | |
---|
222 | if (typeof(xhr.overrideMimeType) === 'function') { |
---|
223 | xhr.overrideMimeType('text/css'); |
---|
224 | } |
---|
225 | xhr.open('GET', url, async); |
---|
226 | xhr.setRequestHeader('Accept', type || 'text/x-less, text/css; q=0.9, */*; q=0.5'); |
---|
227 | xhr.send(null); |
---|
228 | |
---|
229 | if (isFileProtocol) { |
---|
230 | if (xhr.status === 0) { |
---|
231 | callback(xhr.responseText); |
---|
232 | } else { |
---|
233 | errback(xhr.status, url); |
---|
234 | } |
---|
235 | } else if (async) { |
---|
236 | xhr.onreadystatechange = function () { |
---|
237 | if (xhr.readyState == 4) { |
---|
238 | handleResponse(xhr, callback, errback); |
---|
239 | } |
---|
240 | }; |
---|
241 | } else { |
---|
242 | handleResponse(xhr, callback, errback); |
---|
243 | } |
---|
244 | |
---|
245 | function handleResponse(xhr, callback, errback) { |
---|
246 | if (xhr.status >= 200 && xhr.status < 300) { |
---|
247 | callback(xhr.responseText, |
---|
248 | xhr.getResponseHeader("Last-Modified")); |
---|
249 | } else if (typeof(errback) === 'function') { |
---|
250 | errback(xhr.status, url); |
---|
251 | } |
---|
252 | } |
---|
253 | } |
---|
254 | |
---|
255 | function getXMLHttpRequest() { |
---|
256 | if (window.XMLHttpRequest) { |
---|
257 | return new(XMLHttpRequest); |
---|
258 | } else { |
---|
259 | try { |
---|
260 | return new(ActiveXObject)("MSXML2.XMLHTTP.3.0"); |
---|
261 | } catch (e) { |
---|
262 | log("browser doesn't support AJAX."); |
---|
263 | return null; |
---|
264 | } |
---|
265 | } |
---|
266 | } |
---|
267 | |
---|
268 | function removeNode(node) { |
---|
269 | return node && node.parentNode.removeChild(node); |
---|
270 | } |
---|
271 | |
---|
272 | function log(str) { |
---|
273 | if (less.env == 'development' && typeof(console) !== "undefined") { console.log('less: ' + str) } |
---|
274 | } |
---|
275 | |
---|
276 | function error(e, href) { |
---|
277 | var id = 'less-error-message:' + extractId(href); |
---|
278 | |
---|
279 | var template = ['<ul>', |
---|
280 | '<li><label>[-1]</label><pre class="ctx">{0}</pre></li>', |
---|
281 | '<li><label>[0]</label><pre>{current}</pre></li>', |
---|
282 | '<li><label>[1]</label><pre class="ctx">{2}</pre></li>', |
---|
283 | '</ul>'].join('\n'); |
---|
284 | |
---|
285 | var elem = document.createElement('div'), timer, content; |
---|
286 | |
---|
287 | elem.id = id; |
---|
288 | elem.className = "less-error-message"; |
---|
289 | |
---|
290 | content = '<h3>' + (e.message || 'There is an error in your .less file') + |
---|
291 | '</h3>' + '<p><a href="' + href + '">' + href + "</a> "; |
---|
292 | |
---|
293 | if (e.extract) { |
---|
294 | content += 'on line ' + e.line + ', column ' + (e.column + 1) + ':</p>' + |
---|
295 | template.replace(/\[(-?\d)\]/g, function (_, i) { |
---|
296 | return (parseInt(e.line) + parseInt(i)) || ''; |
---|
297 | }).replace(/\{(\d)\}/g, function (_, i) { |
---|
298 | return e.extract[parseInt(i)] || ''; |
---|
299 | }).replace(/\{current\}/, e.extract[1].slice(0, e.column) + '<span class="error">' + |
---|
300 | e.extract[1].slice(e.column) + '</span>'); |
---|
301 | } |
---|
302 | elem.innerHTML = content; |
---|
303 | |
---|
304 | // CSS for error messages |
---|
305 | createCSS([ |
---|
306 | '.less-error-message ul, .less-error-message li {', |
---|
307 | 'list-style-type: none;', |
---|
308 | 'margin-right: 15px;', |
---|
309 | 'padding: 4px 0;', |
---|
310 | 'margin: 0;', |
---|
311 | '}', |
---|
312 | '.less-error-message label {', |
---|
313 | 'font-size: 12px;', |
---|
314 | 'margin-right: 15px;', |
---|
315 | 'padding: 4px 0;', |
---|
316 | 'color: #cc7777;', |
---|
317 | '}', |
---|
318 | '.less-error-message pre {', |
---|
319 | 'color: #ee4444;', |
---|
320 | 'padding: 4px 0;', |
---|
321 | 'margin: 0;', |
---|
322 | 'display: inline-block;', |
---|
323 | '}', |
---|
324 | '.less-error-message pre.ctx {', |
---|
325 | 'color: #dd4444;', |
---|
326 | '}', |
---|
327 | '.less-error-message h3 {', |
---|
328 | 'font-size: 20px;', |
---|
329 | 'font-weight: bold;', |
---|
330 | 'padding: 15px 0 5px 0;', |
---|
331 | 'margin: 0;', |
---|
332 | '}', |
---|
333 | '.less-error-message a {', |
---|
334 | 'color: #10a', |
---|
335 | '}', |
---|
336 | '.less-error-message .error {', |
---|
337 | 'color: red;', |
---|
338 | 'font-weight: bold;', |
---|
339 | 'padding-bottom: 2px;', |
---|
340 | 'border-bottom: 1px dashed red;', |
---|
341 | '}' |
---|
342 | ].join('\n'), { title: 'error-message' }); |
---|
343 | |
---|
344 | elem.style.cssText = [ |
---|
345 | "font-family: Arial, sans-serif", |
---|
346 | "border: 1px solid #e00", |
---|
347 | "background-color: #eee", |
---|
348 | "border-radius: 5px", |
---|
349 | "-webkit-border-radius: 5px", |
---|
350 | "-moz-border-radius: 5px", |
---|
351 | "color: #e00", |
---|
352 | "padding: 15px", |
---|
353 | "margin-bottom: 15px" |
---|
354 | ].join(';'); |
---|
355 | |
---|
356 | if (less.env == 'development') { |
---|
357 | timer = setInterval(function () { |
---|
358 | if (document.body) { |
---|
359 | if (document.getElementById(id)) { |
---|
360 | document.body.replaceChild(elem, document.getElementById(id)); |
---|
361 | } else { |
---|
362 | document.body.insertBefore(elem, document.body.firstChild); |
---|
363 | } |
---|
364 | clearInterval(timer); |
---|
365 | } |
---|
366 | }, 10); |
---|
367 | } |
---|
368 | } |
---|
369 | |
---|