PageRenderTime 51ms CodeModel.GetById 15ms RepoModel.GetById 0ms 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
  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 make sure we don't have a mixin call.
  1101. // See the `rule` function for more information.
  1102. //
  1103. // We start by matching `.rounded (`, and then proceed on to
  1104. // the argument list, which has optional default values.
  1105. // We store the parameters in `params`, with a `value` key,
  1106. // if there is a value, such as in the case of `@radius`.
  1107. //
  1108. // Once we've got our params list, and a closing `)`, we parse
  1109. // the `{...}` block.
  1110. //
  1111. definition: function () {
  1112. var name, params = [], match, ruleset, cond, variadic = false;
  1113. if ((input.charAt(i) !== '.' && input.charAt(i) !== '#') ||
  1114. peek(/^[^{]*\}/)) {
  1115. return;
  1116. }
  1117. save();
  1118. match = $re(/^([#.](?:[\w-]|\\(?:[A-Fa-f0-9]{1,6} ?|[^A-Fa-f0-9]))+)\s*\(/);
  1119. if (match) {
  1120. name = match[1];
  1121. var argInfo = this.args(false);
  1122. params = argInfo.args;
  1123. variadic = argInfo.variadic;
  1124. // .mixincall("@{a}");
  1125. // looks a bit like a mixin definition..
  1126. // also
  1127. // .mixincall(@a: {rule: set;});
  1128. // so we have to be nice and restore
  1129. if (!$char(')')) {
  1130. furthest = i;
  1131. restore();
  1132. return;
  1133. }
  1134. parsers.comments();
  1135. if ($re(/^when/)) { // Guard
  1136. cond = expect(parsers.conditions, 'expected condition');
  1137. }
  1138. ruleset = parsers.block();
  1139. if (ruleset) {
  1140. forget();
  1141. return new(tree.mixin.Definition)(name, params, ruleset, cond, variadic);
  1142. } else {
  1143. restore();
  1144. }
  1145. } else {
  1146. forget();
  1147. }
  1148. }
  1149. },
  1150. //
  1151. // Entities are the smallest recognized token,
  1152. // and can be found inside a rule's value.
  1153. //
  1154. entity: function () {
  1155. var entities = this.entities;
  1156. return entities.literal() || entities.variable() || entities.url() ||
  1157. entities.call() || entities.keyword() || entities.javascript() ||
  1158. this.comment();
  1159. },
  1160. //
  1161. // A Rule terminator. Note that we use `peek()` to check for '}',
  1162. // because the `block` rule will be expecting it, but we still need to make sure
  1163. // it's there, if ';' was ommitted.
  1164. //
  1165. end: function () {
  1166. return $char(';') || peekChar('}');
  1167. },
  1168. //
  1169. // IE's alpha function
  1170. //
  1171. // alpha(opacity=88)
  1172. //
  1173. alpha: function () {
  1174. var value;
  1175. if (! $re(/^\(opacity=/i)) { return; }
  1176. value = $re(/^\d+/) || this.entities.variable();
  1177. if (value) {
  1178. expectChar(')');
  1179. return new(tree.Alpha)(value);
  1180. }
  1181. },
  1182. //
  1183. // A Selector Element
  1184. //
  1185. // div
  1186. // + h1
  1187. // #socks
  1188. // input[type="text"]
  1189. //
  1190. // Elements are the building blocks for Selectors,
  1191. // they are made out of a `Combinator` (see combinator rule),
  1192. // and an element name, such as a tag a class, or `*`.
  1193. //
  1194. element: function () {
  1195. var e, c, v, index = i;
  1196. c = this.combinator();
  1197. e = $re(/^(?:\d+\.\d+|\d+)%/) || $re(/^(?:[.#]?|:*)(?:[\w-]|[^\x00-\x9f]|\\(?:[A-Fa-f0-9]{1,6} ?|[^A-Fa-f0-9]))+/) ||
  1198. $char('*') || $char('&') || this.attribute() || $re(/^\([^()@]+\)/) || $re(/^[\.#](?=@)/) ||
  1199. this.entities.variableCurly();
  1200. if (! e) {
  1201. save();
  1202. if ($char('(')) {
  1203. if ((v = this.selector()) && $char(')')) {
  1204. e = new(tree.Paren)(v);
  1205. forget();
  1206. } else {
  1207. restore();
  1208. }
  1209. } else {
  1210. forget();
  1211. }
  1212. }
  1213. if (e) { return new(tree.Element)(c, e, index, env.currentFileInfo); }
  1214. },
  1215. //
  1216. // Combinators combine elements together, in a Selector.
  1217. //
  1218. // Because our parser isn't white-space sensitive, special care
  1219. // has to be taken, when parsing the descendant combinator, ` `,
  1220. // as it's an empty space. We have to check the previous character
  1221. // in the input, to see if it's a ` ` character. More info on how
  1222. // we deal with this in *combinator.js*.
  1223. //
  1224. combinator: function () {
  1225. var c = input.charAt(i);
  1226. if (c === '/') {
  1227. save();
  1228. var slashedCombinator = $re(/^\/[a-z]+\//i);
  1229. if (slashedCombinator) {
  1230. forget();
  1231. return new(tree.Combinator)(slashedCombinator);
  1232. }
  1233. restore();
  1234. }
  1235. if (c === '>' || c === '+' || c === '~' || c === '|' || c === '^') {
  1236. i++;
  1237. if (c === '^' && input.charAt(i) === '^') {
  1238. c = '^^';
  1239. i++;
  1240. }
  1241. while (isWhitespace(input, i)) { i++; }
  1242. return new(tree.Combinator)(c);
  1243. } else if (isWhitespace(input, i - 1)) {
  1244. return new(tree.Combinator)(" ");
  1245. } else {
  1246. return new(tree.Combinator)(null);
  1247. }
  1248. },
  1249. //
  1250. // A CSS selector (see selector below)
  1251. // with less extensions e.g. the ability to extend and guard
  1252. //
  1253. lessSelector: function () {
  1254. return this.selector(true);
  1255. },
  1256. //
  1257. // A CSS Selector
  1258. //
  1259. // .class > div + h1
  1260. // li a:hover
  1261. //
  1262. // Selectors are made out of one or more Elements, see above.
  1263. //
  1264. selector: function (isLess) {
  1265. var index = i, $re = _$re, elements, extendList, c, e, extend, when, condition;
  1266. while ((isLess && (extend = this.extend())) || (isLess && (when = $re(/^when/))) || (e = this.element())) {
  1267. if (when) {
  1268. condition = expect(this.conditions, 'expected condition');
  1269. } else if (condition) {
  1270. error("CSS guard can only be used at the end of selector");
  1271. } else if (extend) {
  1272. if (extendList) { extendList.push(extend); } else { extendList = [ extend ]; }
  1273. } else {
  1274. if (extendList) { error("Extend can only be used at the end of selector"); }
  1275. c = input.charAt(i);
  1276. if (elements) { elements.push(e); } else { elements = [ e ]; }
  1277. e = null;
  1278. }
  1279. if (c === '{' || c === '}' || c === ';' || c === ',' || c === ')') {
  1280. break;
  1281. }
  1282. }
  1283. if (elements) { return new(tree.Selector)(elements, extendList, condition, index, env.currentFileInfo); }
  1284. if (extendList) { error("Extend must be used to extend a selector, it cannot be used on its own"); }
  1285. },
  1286. attribute: function () {
  1287. if (! $char('[')) { return; }
  1288. var entities = this.entities,
  1289. key, val, op;
  1290. if (!(key = entities.variableCurly())) {
  1291. key = expect(/^(?:[_A-Za-z0-9-\*]*\|)?(?:[_A-Za-z0-9-]|\\.)+/);
  1292. }
  1293. op = $re(/^[|~*$^]?=/);
  1294. if (op) {
  1295. val = entities.quoted() || $re(/^[0-9]+%/) || $re(/^[\w-]+/) || entities.variableCurly();
  1296. }
  1297. expectChar(']');
  1298. return new(tree.Attribute)(key, op, val);
  1299. },
  1300. //
  1301. // The `block` rule is used by `ruleset` and `mixin.definition`.
  1302. // It's a wrapper around the `primary` rule, with added `{}`.
  1303. //
  1304. block: function () {
  1305. var content;
  1306. if ($char('{') && (content = this.primary()) && $char('}')) {
  1307. return content;
  1308. }
  1309. },
  1310. blockRuleset: function() {
  1311. var block = this.block();
  1312. if (block) {
  1313. block = new tree.Ruleset(null, block);
  1314. }
  1315. return block;
  1316. },
  1317. detachedRuleset: function() {
  1318. var blockRuleset = this.blockRuleset();
  1319. if (blockRuleset) {
  1320. return new tree.DetachedRuleset(blockRuleset);
  1321. }
  1322. },
  1323. //
  1324. // div, .class, body > p {...}
  1325. //
  1326. ruleset: function () {
  1327. var selectors, s, rules, debugInfo;
  1328. save();
  1329. if (env.dumpLineNumbers) {
  1330. debugInfo = getDebugInfo(i, input, env);
  1331. }
  1332. while (true) {
  1333. s = this.lessSelector();
  1334. if (!s) {
  1335. break;
  1336. }
  1337. if (selectors) { selectors.push(s); } else { selectors = [ s ]; }
  1338. this.comments();
  1339. if (s.condition && selectors.length > 1) {
  1340. error("Guards are only currently allowed on a single selector.");
  1341. }
  1342. if (! $char(',')) { break; }
  1343. if (s.condition) {
  1344. error("Guards are only currently allowed on a single selector.");
  1345. }
  1346. this.comments();
  1347. }
  1348. if (selectors && (rules = this.block())) {
  1349. forget();
  1350. var ruleset = new(tree.Ruleset)(selectors, rules, env.strictImports);
  1351. if (env.dumpLineNumbers) {
  1352. ruleset.debugInfo = debugInfo;
  1353. }
  1354. return ruleset;
  1355. } else {
  1356. // Backtrack
  1357. furthest = i;
  1358. restore();
  1359. }
  1360. },
  1361. rule: function (tryAnonymous) {
  1362. var name, value, startOfRule = i, c = input.charAt(startOfRule), important, merge, isVariable;
  1363. if (c === '.' || c === '#' || c === '&') { return; }
  1364. save();
  1365. name = this.variable() || this.ruleProperty();
  1366. if (name) {
  1367. isVariable = typeof name === "string";
  1368. if (isVariable) {
  1369. value = this.detachedRuleset();
  1370. }
  1371. this.comments();
  1372. if (!value) {
  1373. // prefer to try to parse first if its a variable or we are compressing
  1374. // but always fallback on the other one
  1375. value = !tryAnonymous && (env.compress || isVariable) ?
  1376. (this.value() || this.anonymousValue()) :
  1377. (this.anonymousValue() || this.value());
  1378. important = this.important();
  1379. // a name returned by this.ruleProperty() is always an array of the form:
  1380. // [string-1, ..., string-n, ""] or [string-1, ..., string-n, "+"]
  1381. // where each item is a tree.Keyword or tree.Variable
  1382. merge = !isVariable && name.pop().value;
  1383. }
  1384. if (value && this.end()) {
  1385. forget();
  1386. return new (tree.Rule)(name, value, important, merge, startOfRule, env.currentFileInfo);
  1387. } else {
  1388. furthest = i;
  1389. restore();
  1390. if (value && !tryAnonymous) {
  1391. return this.rule(true);
  1392. }
  1393. }
  1394. } else {
  1395. forget();
  1396. }
  1397. },
  1398. anonymousValue: function () {
  1399. var match;
  1400. match = /^([^@+\/'"*`(;{}-]*);/.exec(current);
  1401. if (match) {
  1402. i += match[0].length - 1;
  1403. return new(tree.Anonymous)(match[1]);
  1404. }
  1405. },
  1406. //
  1407. // An @import directive
  1408. //
  1409. // @import "lib";
  1410. //
  1411. // Depending on our environment, importing is done differently:
  1412. // In the browser, it's an XHR request, in Node, it would be a
  1413. // file-system operation. The function used for importing is
  1414. // stored in `import`, which we pass to the Import constructor.
  1415. //
  1416. "import": function () {
  1417. var path, features, index = i;
  1418. var dir = $re(/^@import?\s+/);
  1419. if (dir) {
  1420. var options = (dir ? this.importOptions() : null) || {};
  1421. if ((path = this.entities.quoted() || this.entities.url())) {
  1422. features = this.mediaFeatures();
  1423. if (!$(';')) {
  1424. i = index;
  1425. error("missing semi-colon or unrecognised media features on import");
  1426. }
  1427. features = features && new(tree.Value)(features);
  1428. return new(tree.Import)(path, features, options, index, env.currentFileInfo);
  1429. }
  1430. else
  1431. {
  1432. i = index;
  1433. error("malformed import statement");
  1434. }
  1435. }
  1436. },
  1437. importOptions: function() {
  1438. var o, options = {}, optionName, value;
  1439. // list of options, surrounded by parens
  1440. if (! $char('(')) { return null; }
  1441. do {
  1442. o = this.importOption();
  1443. if (o) {
  1444. optionName = o;
  1445. value = true;
  1446. switch(optionName) {
  1447. case "css":
  1448. optionName = "less";
  1449. value = false;
  1450. break;
  1451. case "once":
  1452. optionName = "multiple";
  1453. value = false;
  1454. break;
  1455. }
  1456. options[optionName] = value;
  1457. if (! $char(',')) { break; }
  1458. }
  1459. } while (o);
  1460. expectChar(')');
  1461. return options;
  1462. },
  1463. importOption: function() {
  1464. var opt = $re(/^(less|css|multiple|once|inline|reference)/);
  1465. if (opt) {
  1466. return opt[1];
  1467. }
  1468. },
  1469. mediaFeature: function () {
  1470. var entities = this.entities, nodes = [], e, p;
  1471. do {
  1472. e = entities.keyword() || entities.variable();
  1473. if (e) {
  1474. nodes.push(e);
  1475. } else if ($char('(')) {
  1476. p = this.property();
  1477. e = this.value();
  1478. if ($char(')')) {
  1479. if (p && e) {
  1480. nodes.push(new(tree.Paren)(new(tree.Rule)(p, e, null, null, i, env.currentFileInfo, true)));
  1481. } else if (e) {
  1482. nodes.push(new(tree.Paren)(e));
  1483. } else {
  1484. return null;
  1485. }
  1486. } else { return null; }
  1487. }
  1488. } while (e);
  1489. if (nodes.length > 0) {
  1490. return new(tree.Expression)(nodes);
  1491. }
  1492. },
  1493. mediaFeatures: function () {
  1494. var entities = this.entities, features = [], e;
  1495. do {
  1496. e = this.mediaFeature();
  1497. if (e) {
  1498. features.push(e);
  1499. if (! $char(',')) { break; }
  1500. } else {
  1501. e = entities.variable();
  1502. if (e) {
  1503. features.push(e);
  1504. if (! $char(',')) { break; }
  1505. }
  1506. }
  1507. } while (e);
  1508. return features.length > 0 ? features : null;
  1509. },
  1510. media: function () {
  1511. var features, rules, media, debugInfo;
  1512. if (env.dumpLineNumbers) {
  1513. debugInfo = getDebugInfo(i, input, env);
  1514. }
  1515. if ($re(/^@media/)) {
  1516. features = this.mediaFeatures();
  1517. rules = this.block();
  1518. if (rules) {
  1519. media = new(tree.Media)(rules, features, i, env.currentFileInfo);
  1520. if (env.dumpLineNumbers) {
  1521. media.debugInfo = debugInfo;
  1522. }
  1523. return media;
  1524. }
  1525. }
  1526. },
  1527. //
  1528. // A CSS Directive
  1529. //
  1530. // @charset "utf-8";
  1531. //
  1532. directive: function () {
  1533. var index = i, name, value, rules, nonVendorSpecificName,
  1534. hasIdentifier, hasExpression, hasUnknown, hasBlock = true;
  1535. if (input.charAt(i) !== '@') { return; }
  1536. value = this['import']() || this.media();
  1537. if (value) {
  1538. return value;
  1539. }
  1540. save();
  1541. name = $re(/^@[a-z-]+/);
  1542. if (!name) { return; }
  1543. nonVendorSpecificName = name;
  1544. if (name.charAt(1) == '-' && name.indexOf('-', 2) > 0) {
  1545. nonVendorSpecificName = "@" + name.slice(name.indexOf('-', 2) + 1);
  1546. }
  1547. switch(nonVendorSpecificName) {
  1548. /*
  1549. case "@font-face":
  1550. case "@viewport":
  1551. case "@top-left":
  1552. case "@top-left-corner":
  1553. case "@top-center":
  1554. case "@top-right":
  1555. case "@top-right-corner":
  1556. case "@bottom-left":
  1557. case "@bottom-left-corner":
  1558. case "@bottom-center":
  1559. case "@bottom-right":
  1560. case "@bottom-right-corner":
  1561. case "@left-top":
  1562. case "@left-middle":
  1563. case "@left-bottom":
  1564. case "@right-top":
  1565. case "@right-middle":
  1566. case "@right-bottom":
  1567. hasBlock = true;
  1568. break;
  1569. */
  1570. case "@charset":
  1571. hasIdentifier = true;
  1572. hasBlock = false;
  1573. break;
  1574. case "@namespace":
  1575. hasExpression = true;
  1576. hasBlock = false;
  1577. break;
  1578. case "@keyframes":
  1579. hasIdentifier = true;
  1580. break;
  1581. case "@host":
  1582. case "@page":
  1583. case "@document":
  1584. case "@supports":
  1585. hasUnknown = true;
  1586. break;
  1587. }
  1588. this.comments();
  1589. if (hasIdentifier) {
  1590. value = this.entity();
  1591. if (!value) {
  1592. error("expected " + name + " identifier");
  1593. }
  1594. } else if (hasExpression) {
  1595. value = this.expression();
  1596. if (!value) {
  1597. error("expected " + name + " expression");
  1598. }
  1599. } else if (hasUnknown) {
  1600. value = ($re(/^[^{;]+/) || '').trim();
  1601. if (value) {
  1602. value = new(tree.Anonymous)(value);
  1603. }
  1604. }
  1605. this.comments();
  1606. if (hasBlock) {
  1607. rules = this.blockRuleset();
  1608. }
  1609. if (rules || (!hasBlock && value && $char(';'))) {
  1610. forget();
  1611. return new(tree.Directive)(name, value, rules, index, env.currentFileInfo,
  1612. env.dumpLineNumbers ? getDebugInfo(index, input, env) : null);
  1613. }
  1614. restore();
  1615. },
  1616. //
  1617. // A Value is a comma-delimited list of Expressions
  1618. //
  1619. // font-family: Baskerville, Georgia, serif;
  1620. //
  1621. // In a Rule, a Value represents everything after the `:`,
  1622. // and before the `;`.
  1623. //
  1624. value: function () {
  1625. var e, expressions = [];
  1626. do {
  1627. e = this.expression();
  1628. if (e) {
  1629. expressions.push(e);
  1630. if (! $char(',')) { break; }
  1631. }
  1632. } while(e);
  1633. if (expressions.length > 0) {
  1634. return new(tree.Value)(expressions);
  1635. }
  1636. },
  1637. important: function () {
  1638. if (input.charAt(i) === '!') {
  1639. return $re(/^! *important/);
  1640. }
  1641. },
  1642. sub: function () {
  1643. var a, e;
  1644. if ($char('(')) {
  1645. a = this.addition();
  1646. if (a) {
  1647. e = new(tree.Expression)([a]);
  1648. expectChar(')');
  1649. e.parens = true;
  1650. return e;
  1651. }
  1652. }
  1653. },
  1654. multiplication: function () {
  1655. var m, a, op, operation, isSpaced;
  1656. m = this.operand();
  1657. if (m) {
  1658. isSpaced = isWhitespace(input, i - 1);
  1659. while (true) {
  1660. if (peek(/^\/[*\/]/)) {
  1661. break;
  1662. }
  1663. save();
  1664. op = $char('/') || $char('*');
  1665. if (!op) { forget(); break; }
  1666. a = this.operand();
  1667. if (!a) { restore(); break; }
  1668. forget();
  1669. m.parensInOp = true;
  1670. a.parensInOp = true;
  1671. operation = new(tree.Operation)(op, [operation || m, a], isSpaced);
  1672. isSpaced = isWhitespace(input, i - 1);
  1673. }
  1674. return operation || m;
  1675. }
  1676. },
  1677. addition: function () {
  1678. var m, a, op, operation, isSpaced;
  1679. m = this.multiplication();
  1680. if (m) {
  1681. isSpaced = isWhitespace(input, i - 1);
  1682. while (true) {
  1683. op = $re(/^[-+]\s+/) || (!isSpaced && ($char('+') || $char('-')));
  1684. if (!op) {
  1685. break;
  1686. }
  1687. a = this.multiplication();
  1688. if (!a) {
  1689. break;
  1690. }
  1691. m.parensInOp = true;
  1692. a.parensInOp = true;
  1693. operation = new(tree.Operation)(op, [operation || m, a], isSpaced);
  1694. isSpaced = isWhitespace(input, i - 1);
  1695. }
  1696. return operation || m;
  1697. }
  1698. },
  1699. conditions: function () {
  1700. var a, b, index = i, condition;
  1701. a = this.condition();
  1702. if (a) {
  1703. while (true) {
  1704. if (!peek(/^,\s*(not\s*)?\(/) || !$char(',')) {
  1705. break;
  1706. }
  1707. b = this.condition();
  1708. if (!b) {
  1709. break;
  1710. }
  1711. condition = new(tree.Condition)('or', condition || a, b, index);
  1712. }
  1713. return condition || a;
  1714. }
  1715. },
  1716. condition: function () {
  1717. var entities = this.entities, index = i, negate = false,
  1718. a, b, c, op;
  1719. if ($re(/^not/)) { negate = true; }
  1720. expectChar('(');
  1721. a = this.addition() || entities.keyword() || entities.quoted();
  1722. if (a) {
  1723. op = $re(/^(?:>=|<=|=<|[<=>])/);
  1724. if (op) {
  1725. b = this.addition() || entities.keyword() || entities.quoted();
  1726. if (b) {
  1727. c = new(tree.Condition)(op, a, b, index, negate);
  1728. } else {
  1729. error('expected expression');
  1730. }
  1731. } else {
  1732. c = new(tree.Condition)('=', a, new(tree.Keyword)('true'), index, negate);
  1733. }
  1734. expectChar(')');
  1735. return $re(/^and/) ? new(tree.Condition)('and', c, this.condition()) : c;
  1736. }
  1737. },
  1738. //
  1739. // An operand is anything that can be part of an operation,
  1740. // such as a Color, or a Variable
  1741. //
  1742. operand: function () {
  1743. var entities = this.entities,
  1744. p = input.charAt(i + 1), negate;
  1745. if (input.charAt(i) === '-' && (p === '@' || p === '(')) { negate = $char('-'); }
  1746. var o = this.sub() || entities.dimension() ||
  1747. entities.color() || entities.variable() ||
  1748. entities.call();
  1749. if (negate) {
  1750. o.parensInOp = true;
  1751. o = new(tree.Negative)(o);
  1752. }
  1753. return o;
  1754. },
  1755. //
  1756. // Expressions either represent mathematical operations,
  1757. // or white-space delimited Entities.
  1758. //
  1759. // 1px solid black
  1760. // @var * 2
  1761. //
  1762. expression: function () {
  1763. var entities = [], e, delim;
  1764. do {
  1765. e = this.addition() || this.entity();
  1766. if (e) {
  1767. entities.push(e);
  1768. // operations do not allow keyword "/" dimension (e.g. small/20px) so we support that here
  1769. if (!peek(/^\/[\/*]/)) {
  1770. delim = $char('/');
  1771. if (delim) {
  1772. entities.push(new(tree.Anonymous)(delim));
  1773. }
  1774. }
  1775. }
  1776. } while (e);
  1777. if (entities.length > 0) {
  1778. return new(tree.Expression)(entities);
  1779. }
  1780. },
  1781. property: function () {
  1782. var name = $re(/^(\*?-?[_a-zA-Z0-9-]+)\s*:/);
  1783. if (name) {
  1784. return name[1];
  1785. }
  1786. },
  1787. ruleProperty: function () {
  1788. var c = current, name = [], index = [], length = 0, s, k;
  1789. function match(re) {
  1790. var a = re.exec(c);
  1791. if (a) {
  1792. index.push(i + length);
  1793. length += a[0].length;
  1794. c = c.slice(a[1].length);
  1795. return name.push(a[1]);
  1796. }
  1797. }
  1798. function cutOutBlockComments() {
  1799. //match block comments
  1800. var a = /^\s*\/\*(?:[^*]|\*+[^\/*])*\*+\//.exec(c);
  1801. if (a) {
  1802. length += a[0].length;
  1803. c = c.slice(a[0].length);
  1804. return true;
  1805. }
  1806. return false;
  1807. }
  1808. match(/^(\*?)/);
  1809. while (match(/^((?:[\w-]+)|(?:@\{[\w-]+\}))/)); // !
  1810. while (cutOutBlockComments());
  1811. if ((name.length > 1) && match(/^\s*((?:\+_|\+)?)\s*:/)) {
  1812. // at last, we have the complete match now. move forward,
  1813. // convert name particles to tree objects and return:
  1814. skipWhitespace(length);
  1815. if (name[0] === '') {
  1816. name.shift();
  1817. index.shift();
  1818. }
  1819. for (k = 0; k < name.length; k++) {
  1820. s = name[k];
  1821. name[k] = (s.charAt(0) !== '@')
  1822. ? new(tree.Keyword)(s)
  1823. : new(tree.Variable)('@' + s.slice(2, -1),
  1824. index[k], env.currentFileInfo);
  1825. }
  1826. return name;
  1827. }
  1828. }
  1829. }
  1830. };
  1831. return parser;
  1832. };
  1833. less.Parser.serializeVars = function(vars) {
  1834. var s = '';
  1835. for (var name in vars) {
  1836. if (Object.hasOwnProperty.call(vars, name)) {
  1837. var value = vars[name];
  1838. s += ((name[0] === '@') ? '' : '@') + name +': '+ value +
  1839. ((('' + value).slice(-1) === ';') ? '' : ';');
  1840. }
  1841. }
  1842. return s;
  1843. };