PageRenderTime 77ms CodeModel.GetById 18ms RepoModel.GetById 1ms app.codeStats 0ms

/node_modules/less-middleware/node_modules/less/lib/less/parser.js

https://gitlab.com/mba811/webAudio
JavaScript | 2109 lines | 1538 code | 204 blank | 367 comment | 374 complexity | 99f7597ba6c9fb5a2e1a0080cb867ba1 MD5 | raw file
Possible License(s): Apache-2.0, MIT, BSD-3-Clause, MPL-2.0-no-copyleft-exception, 0BSD

Large files files are truncated, but you can click here to view the full file

  1. var less, tree;
  2. // Node.js does not have a header file added which defines less
  3. if (less === undefined) {
  4. less = exports;
  5. tree = require('./tree');
  6. less.mode = 'node';
  7. }
  8. //
  9. // less.js - parser
  10. //
  11. // A relatively straight-forward predictive parser.
  12. // There is no tokenization/lexing stage, the input is parsed
  13. // in one sweep.
  14. //
  15. // To make the parser fast enough to run in the browser, several
  16. // optimization had to be made:
  17. //
  18. // - Matching and slicing on a huge input is often cause of slowdowns.
  19. // The solution is to chunkify the input into smaller strings.
  20. // The chunks are stored in the `chunks` var,
  21. // `j` holds the current chunk index, and `currentPos` holds
  22. // the index of the current chunk in relation to `input`.
  23. // This gives us an almost 4x speed-up.
  24. //
  25. // - In many cases, we don't need to match individual tokens;
  26. // for example, if a value doesn't hold any variables, operations
  27. // or dynamic references, the parser can effectively 'skip' it,
  28. // treating it as a literal.
  29. // An example would be '1px solid #000' - which evaluates to itself,
  30. // we don't need to know what the individual components are.
  31. // The drawback, of course is that you don't get the benefits of
  32. // syntax-checking on the CSS. This gives us a 50% speed-up in the parser,
  33. // and a smaller speed-up in the code-gen.
  34. //
  35. //
  36. // Token matching is done with the `$` function, which either takes
  37. // a terminal string or regexp, or a non-terminal function to call.
  38. // It also takes care of moving all the indices forwards.
  39. //
  40. //
  41. less.Parser = function Parser(env) {
  42. var input, // LeSS input string
  43. i, // current index in `input`
  44. j, // current chunk
  45. saveStack = [], // holds state for backtracking
  46. furthest, // furthest index the parser has gone to
  47. chunks, // chunkified input
  48. current, // current chunk
  49. currentPos, // index of current chunk, in `input`
  50. parser,
  51. parsers,
  52. rootFilename = env && env.filename;
  53. // Top parser on an import tree must be sure there is one "env"
  54. // which will then be passed around by reference.
  55. if (!(env instanceof tree.parseEnv)) {
  56. env = new tree.parseEnv(env);
  57. }
  58. var imports = this.imports = {
  59. paths: env.paths || [], // Search paths, when importing
  60. queue: [], // Files which haven't been imported yet
  61. files: env.files, // Holds the imported parse trees
  62. contents: env.contents, // Holds the imported file contents
  63. contentsIgnoredChars: env.contentsIgnoredChars, // lines inserted, not in the original less
  64. mime: env.mime, // MIME type of .less files
  65. error: null, // Error in parsing/evaluating an import
  66. push: function (path, currentFileInfo, importOptions, callback) {
  67. var parserImports = this;
  68. this.queue.push(path);
  69. var fileParsedFunc = function (e, root, fullPath) {
  70. parserImports.queue.splice(parserImports.queue.indexOf(path), 1); // Remove the path from the queue
  71. var importedPreviously = fullPath === rootFilename;
  72. parserImports.files[fullPath] = root; // Store the root
  73. if (e && !parserImports.error) { parserImports.error = e; }
  74. callback(e, root, importedPreviously, fullPath);
  75. };
  76. if (less.Parser.importer) {
  77. less.Parser.importer(path, currentFileInfo, fileParsedFunc, env);
  78. } else {
  79. less.Parser.fileLoader(path, currentFileInfo, function(e, contents, fullPath, newFileInfo) {
  80. if (e) {fileParsedFunc(e); return;}
  81. var newEnv = new tree.parseEnv(env);
  82. newEnv.currentFileInfo = newFileInfo;
  83. newEnv.processImports = false;
  84. newEnv.contents[fullPath] = contents;
  85. if (currentFileInfo.reference || importOptions.reference) {
  86. newFileInfo.reference = true;
  87. }
  88. if (importOptions.inline) {
  89. fileParsedFunc(null, contents, fullPath);
  90. } else {
  91. new(less.Parser)(newEnv).parse(contents, function (e, root) {
  92. fileParsedFunc(e, root, fullPath);
  93. });
  94. }
  95. }, env);
  96. }
  97. }
  98. };
  99. function save() { currentPos = i; saveStack.push( { current: current, i: i, j: j }); }
  100. function restore() { var state = saveStack.pop(); current = state.current; currentPos = i = state.i; j = state.j; }
  101. function forget() { saveStack.pop(); }
  102. function sync() {
  103. if (i > currentPos) {
  104. current = current.slice(i - currentPos);
  105. currentPos = i;
  106. }
  107. }
  108. function isWhitespace(str, pos) {
  109. var code = str.charCodeAt(pos | 0);
  110. return (code <= 32) && (code === 32 || code === 10 || code === 9);
  111. }
  112. //
  113. // Parse from a token, regexp or string, and move forward if match
  114. //
  115. function $(tok) {
  116. var tokType = typeof tok,
  117. match, length;
  118. // Either match a single character in the input,
  119. // or match a regexp in the current chunk (`current`).
  120. //
  121. if (tokType === "string") {
  122. if (input.charAt(i) !== tok) {
  123. return null;
  124. }
  125. skipWhitespace(1);
  126. return tok;
  127. }
  128. // regexp
  129. sync ();
  130. if (! (match = tok.exec(current))) {
  131. return null;
  132. }
  133. length = match[0].length;
  134. // The match is confirmed, add the match length to `i`,
  135. // and consume any extra white-space characters (' ' || '\n')
  136. // which come after that. The reason for this is that LeSS's
  137. // grammar is mostly white-space insensitive.
  138. //
  139. skipWhitespace(length);
  140. if(typeof(match) === 'string') {
  141. return match;
  142. } else {
  143. return match.length === 1 ? match[0] : match;
  144. }
  145. }
  146. // Specialization of $(tok)
  147. function $re(tok) {
  148. if (i > currentPos) {
  149. current = current.slice(i - currentPos);
  150. currentPos = i;
  151. }
  152. var m = tok.exec(current);
  153. if (!m) {
  154. return null;
  155. }
  156. skipWhitespace(m[0].length);
  157. if(typeof m === "string") {
  158. return m;
  159. }
  160. return m.length === 1 ? m[0] : m;
  161. }
  162. var _$re = $re;
  163. // Specialization of $(tok)
  164. function $char(tok) {
  165. if (input.charAt(i) !== tok) {
  166. return null;
  167. }
  168. skipWhitespace(1);
  169. return tok;
  170. }
  171. function skipWhitespace(length) {
  172. var oldi = i, oldj = j,
  173. curr = i - currentPos,
  174. endIndex = i + current.length - curr,
  175. mem = (i += length),
  176. inp = input,
  177. c;
  178. for (; i < endIndex; i++) {
  179. c = inp.charCodeAt(i);
  180. if (c > 32) {
  181. break;
  182. }
  183. if ((c !== 32) && (c !== 10) && (c !== 9) && (c !== 13)) {
  184. break;
  185. }
  186. }
  187. current = current.slice(length + i - mem + curr);
  188. currentPos = i;
  189. if (!current.length && (j < chunks.length - 1)) {
  190. current = chunks[++j];
  191. skipWhitespace(0); // skip space at the beginning of a chunk
  192. return true; // things changed
  193. }
  194. return oldi !== i || oldj !== j;
  195. }
  196. function expect(arg, msg, index) {
  197. // some older browsers return typeof 'function' for RegExp
  198. var result = (Object.prototype.toString.call(arg) === '[object Function]') ? arg.call(parsers) : $(arg);
  199. if (result) {
  200. return result;
  201. }
  202. error(msg || (typeof(arg) === 'string' ? "expected '" + arg + "' got '" + input.charAt(i) + "'"
  203. : "unexpected token"));
  204. }
  205. // Specialization of expect()
  206. function expectChar(arg, msg) {
  207. if (input.charAt(i) === arg) {
  208. skipWhitespace(1);
  209. return arg;
  210. }
  211. error(msg || "expected '" + arg + "' got '" + input.charAt(i) + "'");
  212. }
  213. function error(msg, type) {
  214. var e = new Error(msg);
  215. e.index = i;
  216. e.type = type || 'Syntax';
  217. throw e;
  218. }
  219. // Same as $(), but don't change the state of the parser,
  220. // just return the match.
  221. function peek(tok) {
  222. if (typeof(tok) === 'string') {
  223. return input.charAt(i) === tok;
  224. } else {
  225. return tok.test(current);
  226. }
  227. }
  228. // Specialization of peek()
  229. function peekChar(tok) {
  230. return input.charAt(i) === tok;
  231. }
  232. function getInput(e, env) {
  233. if (e.filename && env.currentFileInfo.filename && (e.filename !== env.currentFileInfo.filename)) {
  234. return parser.imports.contents[e.filename];
  235. } else {
  236. return input;
  237. }
  238. }
  239. function getLocation(index, inputStream) {
  240. var n = index + 1,
  241. line = null,
  242. column = -1;
  243. while (--n >= 0 && inputStream.charAt(n) !== '\n') {
  244. column++;
  245. }
  246. if (typeof index === 'number') {
  247. line = (inputStream.slice(0, index).match(/\n/g) || "").length;
  248. }
  249. return {
  250. line: line,
  251. column: column
  252. };
  253. }
  254. function getDebugInfo(index, inputStream, env) {
  255. var filename = env.currentFileInfo.filename;
  256. if(less.mode !== 'browser' && less.mode !== 'rhino') {
  257. filename = require('path').resolve(filename);
  258. }
  259. return {
  260. lineNumber: getLocation(index, inputStream).line + 1,
  261. fileName: filename
  262. };
  263. }
  264. function LessError(e, env) {
  265. var input = getInput(e, env),
  266. loc = getLocation(e.index, input),
  267. line = loc.line,
  268. col = loc.column,
  269. callLine = e.call && getLocation(e.call, input).line,
  270. lines = input.split('\n');
  271. this.type = e.type || 'Syntax';
  272. this.message = e.message;
  273. this.filename = e.filename || env.currentFileInfo.filename;
  274. this.index = e.index;
  275. this.line = typeof(line) === 'number' ? line + 1 : null;
  276. this.callLine = callLine + 1;
  277. this.callExtract = lines[callLine];
  278. this.stack = e.stack;
  279. this.column = col;
  280. this.extract = [
  281. lines[line - 1],
  282. lines[line],
  283. lines[line + 1]
  284. ];
  285. }
  286. LessError.prototype = new Error();
  287. LessError.prototype.constructor = LessError;
  288. this.env = env = env || {};
  289. // The optimization level dictates the thoroughness of the parser,
  290. // the lower the number, the less nodes it will create in the tree.
  291. // This could matter for debugging, or if you want to access
  292. // the individual nodes in the tree.
  293. this.optimization = ('optimization' in this.env) ? this.env.optimization : 1;
  294. //
  295. // The Parser
  296. //
  297. parser = {
  298. imports: imports,
  299. //
  300. // Parse an input string into an abstract syntax tree,
  301. // @param str A string containing 'less' markup
  302. // @param callback call `callback` when done.
  303. // @param [additionalData] An optional map which can contains vars - a map (key, value) of variables to apply
  304. //
  305. parse: function (str, callback, additionalData) {
  306. var root, line, lines, error = null, globalVars, modifyVars, preText = "";
  307. i = j = currentPos = furthest = 0;
  308. globalVars = (additionalData && additionalData.globalVars) ? less.Parser.serializeVars(additionalData.globalVars) + '\n' : '';
  309. modifyVars = (additionalData && additionalData.modifyVars) ? '\n' + less.Parser.serializeVars(additionalData.modifyVars) : '';
  310. if (globalVars || (additionalData && additionalData.banner)) {
  311. preText = ((additionalData && additionalData.banner) ? additionalData.banner : "") + globalVars;
  312. parser.imports.contentsIgnoredChars[env.currentFileInfo.filename] = preText.length;
  313. }
  314. str = str.replace(/\r\n/g, '\n');
  315. // Remove potential UTF Byte Order Mark
  316. input = str = preText + str.replace(/^\uFEFF/, '') + modifyVars;
  317. parser.imports.contents[env.currentFileInfo.filename] = str;
  318. // Split the input into chunks.
  319. chunks = (function (input) {
  320. var len = input.length, level = 0, parenLevel = 0,
  321. lastOpening, lastOpeningParen, lastMultiComment, lastMultiCommentEndBrace,
  322. chunks = [], emitFrom = 0,
  323. parserCurrentIndex, currentChunkStartIndex, cc, cc2, matched;
  324. function fail(msg, index) {
  325. error = new(LessError)({
  326. index: index || parserCurrentIndex,
  327. type: 'Parse',
  328. message: msg,
  329. filename: env.currentFileInfo.filename
  330. }, env);
  331. }
  332. function emitChunk(force) {
  333. var len = parserCurrentIndex - emitFrom;
  334. if (((len < 512) && !force) || !len) {
  335. return;
  336. }
  337. chunks.push(input.slice(emitFrom, parserCurrentIndex + 1));
  338. emitFrom = parserCurrentIndex + 1;
  339. }
  340. for (parserCurrentIndex = 0; parserCurrentIndex < len; parserCurrentIndex++) {
  341. cc = input.charCodeAt(parserCurrentIndex);
  342. if (((cc >= 97) && (cc <= 122)) || (cc < 34)) {
  343. // a-z or whitespace
  344. continue;
  345. }
  346. switch (cc) {
  347. case 40: // (
  348. parenLevel++;
  349. lastOpeningParen = parserCurrentIndex;
  350. continue;
  351. case 41: // )
  352. if (--parenLevel < 0) {
  353. return fail("missing opening `(`");
  354. }
  355. continue;
  356. case 59: // ;
  357. if (!parenLevel) { emitChunk(); }
  358. continue;
  359. case 123: // {
  360. level++;
  361. lastOpening = parserCurrentIndex;
  362. continue;
  363. case 125: // }
  364. if (--level < 0) {
  365. return fail("missing opening `{`");
  366. }
  367. if (!level && !parenLevel) { emitChunk(); }
  368. continue;
  369. case 92: // \
  370. if (parserCurrentIndex < len - 1) { parserCurrentIndex++; continue; }
  371. return fail("unescaped `\\`");
  372. case 34:
  373. case 39:
  374. case 96: // ", ' and `
  375. matched = 0;
  376. currentChunkStartIndex = parserCurrentIndex;
  377. for (parserCurrentIndex = parserCurrentIndex + 1; parserCurrentIndex < len; parserCurrentIndex++) {
  378. cc2 = input.charCodeAt(parserCurrentIndex);
  379. if (cc2 > 96) { continue; }
  380. if (cc2 == cc) { matched = 1; break; }
  381. if (cc2 == 92) { // \
  382. if (parserCurrentIndex == len - 1) {
  383. return fail("unescaped `\\`");
  384. }
  385. parserCurrentIndex++;
  386. }
  387. }
  388. if (matched) { continue; }
  389. return fail("unmatched `" + String.fromCharCode(cc) + "`", currentChunkStartIndex);
  390. case 47: // /, check for comment
  391. if (parenLevel || (parserCurrentIndex == len - 1)) { continue; }
  392. cc2 = input.charCodeAt(parserCurrentIndex + 1);
  393. if (cc2 == 47) {
  394. // //, find lnfeed
  395. for (parserCurrentIndex = parserCurrentIndex + 2; parserCurrentIndex < len; parserCurrentIndex++) {
  396. cc2 = input.charCodeAt(parserCurrentIndex);
  397. if ((cc2 <= 13) && ((cc2 == 10) || (cc2 == 13))) { break; }
  398. }
  399. } else if (cc2 == 42) {
  400. // /*, find */
  401. lastMultiComment = currentChunkStartIndex = parserCurrentIndex;
  402. for (parserCurrentIndex = parserCurrentIndex + 2; parserCurrentIndex < len - 1; parserCurrentIndex++) {
  403. cc2 = input.charCodeAt(parserCurrentIndex);
  404. if (cc2 == 125) { lastMultiCommentEndBrace = parserCurrentIndex; }
  405. if (cc2 != 42) { continue; }
  406. if (input.charCodeAt(parserCurrentIndex + 1) == 47) { break; }
  407. }
  408. if (parserCurrentIndex == len - 1) {
  409. return fail("missing closing `*/`", currentChunkStartIndex);
  410. }
  411. parserCurrentIndex++;
  412. }
  413. continue;
  414. case 42: // *, check for unmatched */
  415. if ((parserCurrentIndex < len - 1) && (input.charCodeAt(parserCurrentIndex + 1) == 47)) {
  416. return fail("unmatched `/*`");
  417. }
  418. continue;
  419. }
  420. }
  421. if (level !== 0) {
  422. if ((lastMultiComment > lastOpening) && (lastMultiCommentEndBrace > lastMultiComment)) {
  423. return fail("missing closing `}` or `*/`", lastOpening);
  424. } else {
  425. return fail("missing closing `}`", lastOpening);
  426. }
  427. } else if (parenLevel !== 0) {
  428. return fail("missing closing `)`", lastOpeningParen);
  429. }
  430. emitChunk(true);
  431. return chunks;
  432. })(str);
  433. if (error) {
  434. return callback(new(LessError)(error, env));
  435. }
  436. current = chunks[0];
  437. // Start with the primary rule.
  438. // The whole syntax tree is held under a Ruleset node,
  439. // with the `root` property set to true, so no `{}` are
  440. // output. The callback is called when the input is parsed.
  441. try {
  442. root = new(tree.Ruleset)(null, this.parsers.primary());
  443. root.root = true;
  444. root.firstRoot = true;
  445. } catch (e) {
  446. return callback(new(LessError)(e, env));
  447. }
  448. root.toCSS = (function (evaluate) {
  449. return function (options, variables) {
  450. options = options || {};
  451. var evaldRoot,
  452. css,
  453. evalEnv = new tree.evalEnv(options);
  454. //
  455. // Allows setting variables with a hash, so:
  456. //
  457. // `{ color: new(tree.Color)('#f01') }` will become:
  458. //
  459. // new(tree.Rule)('@color',
  460. // new(tree.Value)([
  461. // new(tree.Expression)([
  462. // new(tree.Color)('#f01')
  463. // ])
  464. // ])
  465. // )
  466. //
  467. if (typeof(variables) === 'object' && !Array.isArray(variables)) {
  468. variables = Object.keys(variables).map(function (k) {
  469. var value = variables[k];
  470. if (! (value instanceof tree.Value)) {
  471. if (! (value instanceof tree.Expression)) {
  472. value = new(tree.Expression)([value]);
  473. }
  474. value = new(tree.Value)([value]);
  475. }
  476. return new(tree.Rule)('@' + k, value, false, null, 0);
  477. });
  478. evalEnv.frames = [new(tree.Ruleset)(null, variables)];
  479. }
  480. try {
  481. var preEvalVisitors = [],
  482. visitors = [
  483. new(tree.joinSelectorVisitor)(),
  484. new(tree.processExtendsVisitor)(),
  485. new(tree.toCSSVisitor)({compress: Boolean(options.compress)})
  486. ], i, root = this;
  487. if (options.plugins) {
  488. for(i =0; i < options.plugins.length; i++) {
  489. if (options.plugins[i].isPreEvalVisitor) {
  490. preEvalVisitors.push(options.plugins[i]);
  491. } else {
  492. if (options.plugins[i].isPreVisitor) {
  493. visitors.splice(0, 0, options.plugins[i]);
  494. } else {
  495. visitors.push(options.plugins[i]);
  496. }
  497. }
  498. }
  499. }
  500. for(i = 0; i < preEvalVisitors.length; i++) {
  501. preEvalVisitors[i].run(root);
  502. }
  503. evaldRoot = evaluate.call(root, evalEnv);
  504. for(i = 0; i < visitors.length; i++) {
  505. visitors[i].run(evaldRoot);
  506. }
  507. if (options.sourceMap) {
  508. evaldRoot = new tree.sourceMapOutput(
  509. {
  510. contentsIgnoredCharsMap: parser.imports.contentsIgnoredChars,
  511. writeSourceMap: options.writeSourceMap,
  512. rootNode: evaldRoot,
  513. contentsMap: parser.imports.contents,
  514. sourceMapFilename: options.sourceMapFilename,
  515. sourceMapURL: options.sourceMapURL,
  516. outputFilename: options.sourceMapOutputFilename,
  517. sourceMapBasepath: options.sourceMapBasepath,
  518. sourceMapRootpath: options.sourceMapRootpath,
  519. outputSourceFiles: options.outputSourceFiles,
  520. sourceMapGenerator: options.sourceMapGenerator
  521. });
  522. }
  523. css = evaldRoot.toCSS({
  524. compress: Boolean(options.compress),
  525. dumpLineNumbers: env.dumpLineNumbers,
  526. strictUnits: Boolean(options.strictUnits),
  527. numPrecision: 8});
  528. } catch (e) {
  529. throw new(LessError)(e, env);
  530. }
  531. if (options.cleancss && less.mode === 'node') {
  532. var CleanCSS = require('clean-css'),
  533. cleancssOptions = options.cleancssOptions || {};
  534. if (cleancssOptions.keepSpecialComments === undefined) {
  535. cleancssOptions.keepSpecialComments = "*";
  536. }
  537. cleancssOptions.processImport = false;
  538. cleancssOptions.noRebase = true;
  539. if (cleancssOptions.noAdvanced === undefined) {
  540. cleancssOptions.noAdvanced = true;
  541. }
  542. return new CleanCSS(cleancssOptions).minify(css);
  543. } else if (options.compress) {
  544. return css.replace(/(^(\s)+)|((\s)+$)/g, "");
  545. } else {
  546. return css;
  547. }
  548. };
  549. })(root.eval);
  550. // If `i` is smaller than the `input.length - 1`,
  551. // it means the parser wasn't able to parse the whole
  552. // string, so we've got a parsing error.
  553. //
  554. // We try to extract a \n delimited string,
  555. // showing the line where the parse error occured.
  556. // We split it up into two parts (the part which parsed,
  557. // and the part which didn't), so we can color them differently.
  558. if (i < input.length - 1) {
  559. i = furthest;
  560. var loc = getLocation(i, input);
  561. lines = input.split('\n');
  562. line = loc.line + 1;
  563. error = {
  564. type: "Parse",
  565. message: "Unrecognised input",
  566. index: i,
  567. filename: env.currentFileInfo.filename,
  568. line: line,
  569. column: loc.column,
  570. extract: [
  571. lines[line - 2],
  572. lines[line - 1],
  573. lines[line]
  574. ]
  575. };
  576. }
  577. var finish = function (e) {
  578. e = error || e || parser.imports.error;
  579. if (e) {
  580. if (!(e instanceof LessError)) {
  581. e = new(LessError)(e, env);
  582. }
  583. return callback(e);
  584. }
  585. else {
  586. return callback(null, root);
  587. }
  588. };
  589. if (env.processImports !== false) {
  590. new tree.importVisitor(this.imports, finish)
  591. .run(root);
  592. } else {
  593. return finish();
  594. }
  595. },
  596. //
  597. // Here in, the parsing rules/functions
  598. //
  599. // The basic structure of the syntax tree generated is as follows:
  600. //
  601. // Ruleset -> Rule -> Value -> Expression -> Entity
  602. //
  603. // Here's some Less code:
  604. //
  605. // .class {
  606. // color: #fff;
  607. // border: 1px solid #000;
  608. // width: @w + 4px;
  609. // > .child {...}
  610. // }
  611. //
  612. // And here's what the parse tree might look like:
  613. //
  614. // Ruleset (Selector '.class', [
  615. // Rule ("color", Value ([Expression [Color #fff]]))
  616. // Rule ("border", Value ([Expression [Dimension 1px][Keyword "solid"][Color #000]]))
  617. // Rule ("width", Value ([Expression [Operation "+" [Variable "@w"][Dimension 4px]]]))
  618. // Ruleset (Selector [Element '>', '.child'], [...])
  619. // ])
  620. //
  621. // In general, most rules will try to parse a token with the `$()` function, and if the return
  622. // value is truly, will return a new node, of the relevant type. Sometimes, we need to check
  623. // first, before parsing, that's when we use `peek()`.
  624. //
  625. parsers: parsers = {
  626. //
  627. // The `primary` rule is the *entry* and *exit* point of the parser.
  628. // The rules here can appear at any level of the parse tree.
  629. //
  630. // The recursive nature of the grammar is an interplay between the `block`
  631. // rule, which represents `{ ... }`, the `ruleset` rule, and this `primary` rule,
  632. // as represented by this simplified grammar:
  633. //
  634. // primary → (ruleset | rule)+
  635. // ruleset → selector+ block
  636. // block → '{' primary '}'
  637. //
  638. // Only at one point is the primary rule not called from the
  639. // block rule: at the root level.
  640. //
  641. primary: function () {
  642. var mixin = this.mixin, $re = _$re, root = [], node;
  643. while (current)
  644. {
  645. node = this.extendRule() || mixin.definition() || this.rule() || this.ruleset() ||
  646. mixin.call() || this.comment() || this.rulesetCall() || this.directive();
  647. if (node) {
  648. root.push(node);
  649. } else {
  650. if (!($re(/^[\s\n]+/) || $re(/^;+/))) {
  651. break;
  652. }
  653. }
  654. if (peekChar('}')) {
  655. break;
  656. }
  657. }
  658. return root;
  659. },
  660. // We create a Comment node for CSS comments `/* */`,
  661. // but keep the LeSS comments `//` silent, by just skipping
  662. // over them.
  663. comment: function () {
  664. var comment;
  665. if (input.charAt(i) !== '/') { return; }
  666. if (input.charAt(i + 1) === '/') {
  667. return new(tree.Comment)($re(/^\/\/.*/), true, i, env.currentFileInfo);
  668. }
  669. comment = $re(/^\/\*(?:[^*]|\*+[^\/*])*\*+\/\n?/);
  670. if (comment) {
  671. return new(tree.Comment)(comment, false, i, env.currentFileInfo);
  672. }
  673. },
  674. comments: function () {
  675. var comment, comments = [];
  676. while(true) {
  677. comment = this.comment();
  678. if (!comment) {
  679. break;
  680. }
  681. comments.push(comment);
  682. }
  683. return comments;
  684. },
  685. //
  686. // Entities are tokens which can be found inside an Expression
  687. //
  688. entities: {
  689. //
  690. // A string, which supports escaping " and '
  691. //
  692. // "milky way" 'he\'s the one!'
  693. //
  694. quoted: function () {
  695. var str, j = i, e, index = i;
  696. if (input.charAt(j) === '~') { j++; e = true; } // Escaped strings
  697. if (input.charAt(j) !== '"' && input.charAt(j) !== "'") { return; }
  698. if (e) { $char('~'); }
  699. str = $re(/^"((?:[^"\\\r\n]|\\.)*)"|'((?:[^'\\\r\n]|\\.)*)'/);
  700. if (str) {
  701. return new(tree.Quoted)(str[0], str[1] || str[2], e, index, env.currentFileInfo);
  702. }
  703. },
  704. //
  705. // A catch-all word, such as:
  706. //
  707. // black border-collapse
  708. //
  709. keyword: function () {
  710. var k;
  711. k = $re(/^%|^[_A-Za-z-][_A-Za-z0-9-]*/);
  712. if (k) {
  713. var color = tree.Color.fromKeyword(k);
  714. if (color) {
  715. return color;
  716. }
  717. return new(tree.Keyword)(k);
  718. }
  719. },
  720. //
  721. // A function call
  722. //
  723. // rgb(255, 0, 255)
  724. //
  725. // We also try to catch IE's `alpha()`, but let the `alpha` parser
  726. // deal with the details.
  727. //
  728. // The arguments are parsed with the `entities.arguments` parser.
  729. //
  730. call: function () {
  731. var name, nameLC, args, alpha_ret, index = i;
  732. name = /^([\w-]+|%|progid:[\w\.]+)\(/.exec(current);
  733. if (!name) { return; }
  734. name = name[1];
  735. nameLC = name.toLowerCase();
  736. if (nameLC === 'url') {
  737. return null;
  738. }
  739. i += name.length;
  740. if (nameLC === 'alpha') {
  741. alpha_ret = parsers.alpha();
  742. if(typeof alpha_ret !== 'undefined') {
  743. return alpha_ret;
  744. }
  745. }
  746. $char('('); // Parse the '(' and consume whitespace.
  747. args = this.arguments();
  748. if (! $char(')')) {
  749. return;
  750. }
  751. if (name) { return new(tree.Call)(name, args, index, env.currentFileInfo); }
  752. },
  753. arguments: function () {
  754. var args = [], arg;
  755. while (true) {
  756. arg = this.assignment() || parsers.expression();
  757. if (!arg) {
  758. break;
  759. }
  760. args.push(arg);
  761. if (! $char(',')) {
  762. break;
  763. }
  764. }
  765. return args;
  766. },
  767. literal: function () {
  768. return this.dimension() ||
  769. this.color() ||
  770. this.quoted() ||
  771. this.unicodeDescriptor();
  772. },
  773. // Assignments are argument entities for calls.
  774. // They are present in ie filter properties as shown below.
  775. //
  776. // filter: progid:DXImageTransform.Microsoft.Alpha( *opacity=50* )
  777. //
  778. assignment: function () {
  779. var key, value;
  780. key = $re(/^\w+(?=\s?=)/i);
  781. if (!key) {
  782. return;
  783. }
  784. if (!$char('=')) {
  785. return;
  786. }
  787. value = parsers.entity();
  788. if (value) {
  789. return new(tree.Assignment)(key, value);
  790. }
  791. },
  792. //
  793. // Parse url() tokens
  794. //
  795. // We use a specific rule for urls, because they don't really behave like
  796. // standard function calls. The difference is that the argument doesn't have
  797. // to be enclosed within a string, so it can't be parsed as an Expression.
  798. //
  799. url: function () {
  800. var value;
  801. if (input.charAt(i) !== 'u' || !$re(/^url\(/)) {
  802. return;
  803. }
  804. value = this.quoted() || this.variable() ||
  805. $re(/^(?:(?:\\[\(\)'"])|[^\(\)'"])+/) || "";
  806. expectChar(')');
  807. return new(tree.URL)((value.value != null || value instanceof tree.Variable)
  808. ? value : new(tree.Anonymous)(value), env.currentFileInfo);
  809. },
  810. //
  811. // A Variable entity, such as `@fink`, in
  812. //
  813. // width: @fink + 2px
  814. //
  815. // We use a different parser for variable definitions,
  816. // see `parsers.variable`.
  817. //
  818. variable: function () {
  819. var name, index = i;
  820. if (input.charAt(i) === '@' && (name = $re(/^@@?[\w-]+/))) {
  821. return new(tree.Variable)(name, index, env.currentFileInfo);
  822. }
  823. },
  824. // A variable entity useing the protective {} e.g. @{var}
  825. variableCurly: function () {
  826. var curly, index = i;
  827. if (input.charAt(i) === '@' && (curly = $re(/^@\{([\w-]+)\}/))) {
  828. return new(tree.Variable)("@" + curly[1], index, env.currentFileInfo);
  829. }
  830. },
  831. //
  832. // A Hexadecimal color
  833. //
  834. // #4F3C2F
  835. //
  836. // `rgb` and `hsl` colors are parsed through the `entities.call` parser.
  837. //
  838. color: function () {
  839. var rgb;
  840. if (input.charAt(i) === '#' && (rgb = $re(/^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})/))) {
  841. var colorCandidateString = rgb.input.match(/^#([\w]+).*/); // strip colons, brackets, whitespaces and other characters that should not definitely be part of color string
  842. colorCandidateString = colorCandidateString[1];
  843. if (!colorCandidateString.match(/^[A-Fa-f0-9]+$/)) { // verify if candidate consists only of allowed HEX characters
  844. error("Invalid HEX color code");
  845. }
  846. return new(tree.Color)(rgb[1]);
  847. }
  848. },
  849. //
  850. // A Dimension, that is, a number and a unit
  851. //
  852. // 0.5em 95%
  853. //
  854. dimension: function () {
  855. var value, c = input.charCodeAt(i);
  856. //Is the first char of the dimension 0-9, '.', '+' or '-'
  857. if ((c > 57 || c < 43) || c === 47 || c == 44) {
  858. return;
  859. }
  860. value = $re(/^([+-]?\d*\.?\d+)(%|[a-z]+)?/);
  861. if (value) {
  862. return new(tree.Dimension)(value[1], value[2]);
  863. }
  864. },
  865. //
  866. // A unicode descriptor, as is used in unicode-range
  867. //
  868. // U+0?? or U+00A1-00A9
  869. //
  870. unicodeDescriptor: function () {
  871. var ud;
  872. ud = $re(/^U\+[0-9a-fA-F?]+(\-[0-9a-fA-F?]+)?/);
  873. if (ud) {
  874. return new(tree.UnicodeDescriptor)(ud[0]);
  875. }
  876. },
  877. //
  878. // JavaScript code to be evaluated
  879. //
  880. // `window.location.href`
  881. //
  882. javascript: function () {
  883. var str, j = i, e;
  884. if (input.charAt(j) === '~') { j++; e = true; } // Escaped strings
  885. if (input.charAt(j) !== '`') { return; }
  886. if (env.javascriptEnabled !== undefined && !env.javascriptEnabled) {
  887. error("You are using JavaScript, which has been disabled.");
  888. }
  889. if (e) { $char('~'); }
  890. str = $re(/^`([^`]*)`/);
  891. if (str) {
  892. return new(tree.JavaScript)(str[1], i, e);
  893. }
  894. }
  895. },
  896. //
  897. // The variable part of a variable definition. Used in the `rule` parser
  898. //
  899. // @fink:
  900. //
  901. variable: function () {
  902. var name;
  903. if (input.charAt(i) === '@' && (name = $re(/^(@[\w-]+)\s*:/))) { return name[1]; }
  904. },
  905. //
  906. // The variable part of a variable definition. Used in the `rule` parser
  907. //
  908. // @fink();
  909. //
  910. rulesetCall: function () {
  911. var name;
  912. if (input.charAt(i) === '@' && (name = $re(/^(@[\w-]+)\s*\(\s*\)\s*;/))) {
  913. return new tree.RulesetCall(name[1]);
  914. }
  915. },
  916. //
  917. // extend syntax - used to extend selectors
  918. //
  919. extend: function(isRule) {
  920. var elements, e, index = i, option, extendList, extend;
  921. if (!(isRule ? $re(/^&:extend\(/) : $re(/^:extend\(/))) { return; }
  922. do {
  923. option = null;
  924. elements = null;
  925. while (! (option = $re(/^(all)(?=\s*(\)|,))/))) {
  926. e = this.element();
  927. if (!e) { break; }
  928. if (elements) { elements.push(e); } else { elements = [ e ]; }
  929. }
  930. option = option && option[1];
  931. if (!elements)
  932. error("Missing target selector for :extend().");
  933. extend = new(tree.Extend)(new(tree.Selector)(elements), option, index);
  934. if (extendList) { extendList.push(extend); } else { extendList = [ extend ]; }
  935. } while($char(","));
  936. expect(/^\)/);
  937. if (isRule) {
  938. expect(/^;/);
  939. }
  940. return extendList;
  941. },
  942. //
  943. // extendRule - used in a rule to extend all the parent selectors
  944. //
  945. extendRule: function() {
  946. return this.extend(true);
  947. },
  948. //
  949. // Mixins
  950. //
  951. mixin: {
  952. //
  953. // A Mixin call, with an optional argument list
  954. //
  955. // #mixins > .square(#fff);
  956. // .rounded(4px, black);
  957. // .button;
  958. //
  959. // The `while` loop is there because mixins can be
  960. // namespaced, but we only support the child and descendant
  961. // selector for now.
  962. //
  963. call: function () {
  964. var s = input.charAt(i), important = false, index = i, elemIndex,
  965. elements, elem, e, c, args;
  966. if (s !== '.' && s !== '#') { return; }
  967. save(); // stop us absorbing part of an invalid selector
  968. while (true) {
  969. elemIndex = i;
  970. e = $re(/^[#.](?:[\w-]|\\(?:[A-Fa-f0-9]{1,6} ?|[^A-Fa-f0-9]))+/);
  971. if (!e) {
  972. break;
  973. }
  974. elem = new(tree.Element)(c, e, elemIndex, env.currentFileInfo);
  975. if (elements) { elements.push(elem); } else { elements = [ elem ]; }
  976. c = $char('>');
  977. }
  978. if (elements) {
  979. if ($char('(')) {
  980. args = this.args(true).args;
  981. expectChar(')');
  982. }
  983. if (parsers.important()) {
  984. important = true;
  985. }
  986. if (parsers.end()) {
  987. forget();
  988. return new(tree.mixin.Call)(elements, args, index, env.currentFileInfo, important);
  989. }
  990. }
  991. restore();
  992. },
  993. args: function (isCall) {
  994. var parsers = parser.parsers, entities = parsers.entities,
  995. returner = { args:null, variadic: false },
  996. expressions = [], argsSemiColon = [], argsComma = [],
  997. isSemiColonSeperated, expressionContainsNamed, name, nameLoop, value, arg;
  998. save();
  999. while (true) {
  1000. if (isCall) {
  1001. arg = parsers.detachedRuleset() || parsers.expression();
  1002. } else {
  1003. parsers.comments();
  1004. if (input.charAt(i) === '.' && $re(/^\.{3}/)) {
  1005. returner.variadic = true;
  1006. if ($char(";") && !isSemiColonSeperated) {
  1007. isSemiColonSeperated = true;
  1008. }
  1009. (isSemiColonSeperated ? argsSemiColon : argsComma)
  1010. .push({ variadic: true });
  1011. break;
  1012. }
  1013. arg = entities.variable() || entities.literal() || entities.keyword();
  1014. }
  1015. if (!arg) {
  1016. break;
  1017. }
  1018. nameLoop = null;
  1019. if (arg.throwAwayComments) {
  1020. arg.throwAwayComments();
  1021. }
  1022. value = arg;
  1023. var val = null;
  1024. if (isCall) {
  1025. // Variable
  1026. if (arg.value && arg.value.length == 1) {
  1027. val = arg.value[0];
  1028. }
  1029. } else {
  1030. val = arg;
  1031. }
  1032. if (val && val instanceof tree.Variable) {
  1033. if ($char(':')) {
  1034. if (expressions.length > 0) {
  1035. if (isSemiColonSeperated) {
  1036. error("Cannot mix ; and , as delimiter types");
  1037. }
  1038. expressionContainsNamed = true;
  1039. }
  1040. // we do not support setting a ruleset as a default variable - it doesn't make sense
  1041. // However if we do want to add it, there is nothing blocking it, just don't error
  1042. // and remove isCall dependency below
  1043. value = (isCall && parsers.detachedRuleset()) || parsers.expression();
  1044. if (!value) {
  1045. if (isCall) {
  1046. error("could not understand value for named argument");
  1047. } else {
  1048. restore();
  1049. returner.args = [];
  1050. return returner;
  1051. }
  1052. }
  1053. nameLoop = (name = val.name);
  1054. } else if (!isCall && $re(/^\.{3}/)) {
  1055. returner.variadic = true;
  1056. if ($char(";") && !isSemiColonSeperated) {
  1057. isSemiColonSeperated = true;
  1058. }
  1059. (isSemiColonSeperated ? argsSemiColon : argsComma)
  1060. .push({ name: arg.name, variadic: true });
  1061. break;
  1062. } else if (!isCall) {
  1063. name = nameLoop = val.name;
  1064. value = null;
  1065. }
  1066. }
  1067. if (value) {
  1068. expressions.push(value);
  1069. }
  1070. argsComma.push({ name:nameLoop, value:value });
  1071. if ($char(',')) {
  1072. continue;
  1073. }
  1074. if ($char(';') || isSemiColonSeperated) {
  1075. if (expressionContainsNamed) {
  1076. error("Cannot mix ; and , as delimiter types");
  1077. }
  1078. isSemiColonSeperated = true;
  1079. if (expressions.length > 1) {
  1080. value = new(tree.Value)(expressions);
  1081. }
  1082. argsSemiColon.push({ name:name, value:value });
  1083. name = null;
  1084. expressions = [];
  1085. expressionContainsNamed = false;
  1086. }
  1087. }
  1088. forget();
  1089. returner.args = isSemiColonSeperated ? argsSemiColon : argsComma;
  1090. return returner;
  1091. },
  1092. //
  1093. // A Mixin definition, with a list of parameters
  1094. //
  1095. // .rounded (@radius: 2px, @color) {
  1096. // ...
  1097. // }
  1098. //
  1099. // Until we have a finer grained state-machine, we have to
  1100. // do a look-ahead, to…

Large files files are truncated, but you can click here to view the full file