index.js 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888
  1. // Credits: https://github.com/reworkcss/css/tree/master/lib/parse
  2. // Licensed under MIT
  3. // Forge: modified while{} loop declarations on some lines to reduce errors on logging
  4. // http://www.w3.org/TR/CSS21/grammar.html
  5. // https://github.com/visionmedia/css-parse/pull/49#issuecomment-30088027
  6. const commentre = /\/\*[^*]*\*+([^/*][^*]*\*+)*\//g;
  7. export function parse(css, options) {
  8. options = options || {};
  9. /**
  10. * Positional.
  11. */
  12. var lineno = 1;
  13. var column = 1;
  14. /**
  15. * Update lineno and column based on `str`.
  16. */
  17. function updatePosition(str) {
  18. var lines = str.match(/\n/g);
  19. if (lines) lineno += lines.length;
  20. var i = str.lastIndexOf("\n");
  21. column = ~i ? str.length - i : column + str.length;
  22. }
  23. /**
  24. * Mark position and patch `node.position`.
  25. */
  26. function position() {
  27. var start = { line: lineno, column: column };
  28. return function (node) {
  29. node.position = new Position(start);
  30. whitespace();
  31. return node;
  32. };
  33. }
  34. /**
  35. * Store position information for a node
  36. */
  37. class Position {
  38. /**
  39. * Non-enumerable source string
  40. */
  41. content = css;
  42. constructor(start) {
  43. this.start = start;
  44. this.end = { line: lineno, column: column };
  45. this.source = options.source;
  46. }
  47. }
  48. /**
  49. * Error `msg`.
  50. */
  51. var errorsList = [];
  52. function error(msg) {
  53. var err = new Error(options.source + ":" + lineno + ":" + column + ": " + msg);
  54. err.reason = msg;
  55. err.filename = options.source;
  56. err.line = lineno;
  57. err.column = column;
  58. err.source = css;
  59. if (options.silent) {
  60. errorsList.push(err);
  61. } else {
  62. throw err;
  63. }
  64. }
  65. /**
  66. * Parse stylesheet.
  67. */
  68. function stylesheet() {
  69. var rulesList = rules();
  70. return {
  71. type: "stylesheet",
  72. stylesheet: {
  73. source: options.source,
  74. rules: rulesList,
  75. parsingErrors: errorsList,
  76. },
  77. };
  78. }
  79. /**
  80. * Opening brace.
  81. */
  82. function open() {
  83. return match(/^{\s*/);
  84. }
  85. /**
  86. * Closing brace.
  87. */
  88. function close() {
  89. return match(/^}/);
  90. }
  91. /**
  92. * Parse ruleset.
  93. */
  94. function rules() {
  95. var node;
  96. var rules = [];
  97. whitespace();
  98. comments(rules);
  99. while (css.length && css.charAt(0) != "}" && (node = atrule() || rule())) {
  100. if (node !== false) {
  101. rules.push(node);
  102. comments(rules);
  103. }
  104. }
  105. return rules;
  106. }
  107. /**
  108. * Match `re` and return captures.
  109. */
  110. function match(re) {
  111. var m = re.exec(css);
  112. if (!m) return;
  113. var str = m[0];
  114. updatePosition(str);
  115. css = css.slice(str.length);
  116. return m;
  117. }
  118. /**
  119. * Parse whitespace.
  120. */
  121. function whitespace() {
  122. match(/^\s*/);
  123. }
  124. /**
  125. * Parse comments;
  126. */
  127. function comments(rules) {
  128. rules = rules || [];
  129. for (var c; (c = comment()); ) {
  130. if (c !== false) {
  131. rules.push(c);
  132. }
  133. }
  134. return rules;
  135. }
  136. /**
  137. * Parse comment.
  138. */
  139. function comment() {
  140. var pos = position();
  141. if ("/" != css.charAt(0) || "*" != css.charAt(1)) return;
  142. var i = 2;
  143. while ("" != css.charAt(i) && ("*" != css.charAt(i) || "/" != css.charAt(i + 1))) ++i;
  144. i += 2;
  145. if ("" === css.charAt(i - 1)) {
  146. return error("End of comment missing");
  147. }
  148. var str = css.slice(2, i - 2);
  149. column += 2;
  150. updatePosition(str);
  151. css = css.slice(i);
  152. column += 2;
  153. return pos({
  154. type: "comment",
  155. comment: str,
  156. });
  157. }
  158. /**
  159. * Parse selector.
  160. */
  161. function selector() {
  162. var m = match(/^([^{]+)/);
  163. if (!m) return;
  164. /* @fix Remove all comments from selectors
  165. * http://ostermiller.org/findcomment.html */
  166. return trim(m[0])
  167. .replace(/\/\*([^*]|[\r\n]|(\*+([^*/]|[\r\n])))*\*\/+/g, "")
  168. .replace(/"(?:\\"|[^"])*"|'(?:\\'|[^'])*'/g, function (m) {
  169. return m.replace(/,/g, "\u200C");
  170. })
  171. .split(/\s*(?![^(]*\)),\s*/)
  172. .map(function (s) {
  173. return s.replace(/\u200C/g, ",");
  174. });
  175. }
  176. /**
  177. * Parse declaration.
  178. */
  179. function declaration() {
  180. var pos = position();
  181. // prop
  182. var prop = match(/^(\*?[-#\/\*\\\w]+(\[[0-9a-z_-]+\])?)\s*/);
  183. if (!prop) return;
  184. prop = trim(prop[0]);
  185. // :
  186. if (!match(/^:\s*/)) return error("property missing ':'");
  187. // val
  188. var val = match(/^((?:'(?:\\'|.)*?'|"(?:\\"|.)*?"|\([^\)]*?\)|[^};])+)/);
  189. var ret = pos({
  190. type: "declaration",
  191. property: prop.replace(commentre, ""),
  192. value: val ? trim(val[0]).replace(commentre, "") : "",
  193. });
  194. // ;
  195. match(/^[;\s]*/);
  196. return ret;
  197. }
  198. /**
  199. * Parse declarations.
  200. */
  201. function declarations() {
  202. var decls = [];
  203. if (!open()) return error("missing '{'");
  204. comments(decls);
  205. // declarations
  206. for (var decl; (decl = declaration()); ) {
  207. if (decl !== false) {
  208. decls.push(decl);
  209. comments(decls);
  210. }
  211. }
  212. if (!close()) return error("missing '}'");
  213. return decls;
  214. }
  215. /**
  216. * Parse keyframe.
  217. */
  218. function keyframe() {
  219. var vals = [];
  220. var pos = position();
  221. for (var m; (m = match(/^((\d+\.\d+|\.\d+|\d+)%?|[a-z]+)\s*/)); ) {
  222. vals.push(m[1]);
  223. match(/^,\s*/);
  224. }
  225. if (!vals.length) return;
  226. return pos({
  227. type: "keyframe",
  228. values: vals,
  229. declarations: declarations(),
  230. });
  231. }
  232. /**
  233. * Parse keyframes.
  234. */
  235. function atkeyframes() {
  236. var pos = position();
  237. var m = match(/^@([-\w]+)?keyframes\s*/);
  238. if (!m) return;
  239. var vendor = m[1];
  240. // identifier
  241. var m = match(/^([-\w]+)\s*/);
  242. if (!m) return error("@keyframes missing name");
  243. var name = m[1];
  244. if (!open()) return error("@keyframes missing '{'");
  245. var frames = comments();
  246. for (var frame; (frame = keyframe()); ) {
  247. frames.push(frame);
  248. frames = frames.concat(comments());
  249. }
  250. if (!close()) return error("@keyframes missing '}'");
  251. return pos({
  252. type: "keyframes",
  253. name: name,
  254. vendor: vendor,
  255. keyframes: frames,
  256. });
  257. }
  258. /**
  259. * Parse supports.
  260. */
  261. function atsupports() {
  262. var pos = position();
  263. var m = match(/^@supports *([^{]+)/);
  264. if (!m) return;
  265. var supports = trim(m[1]);
  266. if (!open()) return error("@supports missing '{'");
  267. var style = comments().concat(rules());
  268. if (!close()) return error("@supports missing '}'");
  269. return pos({
  270. type: "supports",
  271. supports: supports,
  272. rules: style,
  273. });
  274. }
  275. /**
  276. * Parse host.
  277. */
  278. function athost() {
  279. var pos = position();
  280. var m = match(/^@host\s*/);
  281. if (!m) return;
  282. if (!open()) return error("@host missing '{'");
  283. var style = comments().concat(rules());
  284. if (!close()) return error("@host missing '}'");
  285. return pos({
  286. type: "host",
  287. rules: style,
  288. });
  289. }
  290. /**
  291. * Parse media.
  292. */
  293. function atmedia() {
  294. var pos = position();
  295. var m = match(/^@media *([^{]+)/);
  296. if (!m) return;
  297. var media = trim(m[1]);
  298. if (!open()) return error("@media missing '{'");
  299. var style = comments().concat(rules());
  300. if (!close()) return error("@media missing '}'");
  301. return pos({
  302. type: "media",
  303. media: media,
  304. rules: style,
  305. });
  306. }
  307. /**
  308. * Parse custom-media.
  309. */
  310. function atcustommedia() {
  311. var pos = position();
  312. var m = match(/^@custom-media\s+(--[^\s]+)\s*([^{;]+);/);
  313. if (!m) return;
  314. return pos({
  315. type: "custom-media",
  316. name: trim(m[1]),
  317. media: trim(m[2]),
  318. });
  319. }
  320. /**
  321. * Parse paged media.
  322. */
  323. function atpage() {
  324. var pos = position();
  325. var m = match(/^@page */);
  326. if (!m) return;
  327. var sel = selector() || [];
  328. if (!open()) return error("@page missing '{'");
  329. var decls = comments();
  330. // declarations
  331. for (var decl; (decl = declaration()); ) {
  332. decls.push(decl);
  333. decls = decls.concat(comments());
  334. }
  335. if (!close()) return error("@page missing '}'");
  336. return pos({
  337. type: "page",
  338. selectors: sel,
  339. declarations: decls,
  340. });
  341. }
  342. /**
  343. * Parse document.
  344. */
  345. function atdocument() {
  346. var pos = position();
  347. var m = match(/^@([-\w]+)?document *([^{]+)/);
  348. if (!m) return;
  349. var vendor = trim(m[1]);
  350. var doc = trim(m[2]);
  351. if (!open()) return error("@document missing '{'");
  352. var style = comments().concat(rules());
  353. if (!close()) return error("@document missing '}'");
  354. return pos({
  355. type: "document",
  356. document: doc,
  357. vendor: vendor,
  358. rules: style,
  359. });
  360. }
  361. /**
  362. * Parse font-face.
  363. */
  364. function atfontface() {
  365. var pos = position();
  366. var m = match(/^@font-face\s*/);
  367. if (!m) return;
  368. if (!open()) return error("@font-face missing '{'");
  369. var decls = comments();
  370. // declarations
  371. for (var decl; (decl = declaration()); ) {
  372. decls.push(decl);
  373. decls = decls.concat(comments());
  374. }
  375. if (!close()) return error("@font-face missing '}'");
  376. return pos({
  377. type: "font-face",
  378. declarations: decls,
  379. });
  380. }
  381. /**
  382. * Parse import
  383. */
  384. var atimport = _compileAtrule("import");
  385. /**
  386. * Parse charset
  387. */
  388. var atcharset = _compileAtrule("charset");
  389. /**
  390. * Parse namespace
  391. */
  392. var atnamespace = _compileAtrule("namespace");
  393. /**
  394. * Parse non-block at-rules
  395. */
  396. function _compileAtrule(name) {
  397. var re = new RegExp("^@" + name + "\\s*([^;]+);");
  398. return function () {
  399. var pos = position();
  400. var m = match(re);
  401. if (!m) return;
  402. var ret = { type: name };
  403. ret[name] = m[1].trim();
  404. return pos(ret);
  405. };
  406. }
  407. /**
  408. * Parse at rule.
  409. */
  410. function atrule() {
  411. if (css[0] != "@") return;
  412. return (
  413. atkeyframes() ||
  414. atmedia() ||
  415. atcustommedia() ||
  416. atsupports() ||
  417. atimport() ||
  418. atcharset() ||
  419. atnamespace() ||
  420. atdocument() ||
  421. atpage() ||
  422. athost() ||
  423. atfontface()
  424. );
  425. }
  426. /**
  427. * Parse rule.
  428. */
  429. function rule() {
  430. var pos = position();
  431. var sel = selector();
  432. if (!sel) return error("selector missing");
  433. comments();
  434. return pos({
  435. type: "rule",
  436. selectors: sel,
  437. declarations: declarations(),
  438. });
  439. }
  440. return addParent(stylesheet());
  441. }
  442. /**
  443. * Trim `str`.
  444. */
  445. export function trim(str) {
  446. return str ? str.replace(/^\s+|\s+$/g, "") : "";
  447. }
  448. /**
  449. * Adds non-enumerable parent node reference to each node.
  450. */
  451. export function addParent(obj, parent) {
  452. var isNode = obj && typeof obj.type === "string";
  453. var childParent = isNode ? obj : parent;
  454. for (var k in obj) {
  455. var value = obj[k];
  456. if (Array.isArray(value)) {
  457. value.forEach(function (v) {
  458. addParent(v, childParent);
  459. });
  460. } else if (value && typeof value === "object") {
  461. addParent(value, childParent);
  462. }
  463. }
  464. if (isNode) {
  465. Object.defineProperty(obj, "parent", {
  466. configurable: true,
  467. writable: true,
  468. enumerable: false,
  469. value: parent || null,
  470. });
  471. }
  472. return obj;
  473. }
  474. // Credits: https://github.com/reworkcss/css/blob/master/lib/stringify
  475. // Forge: derived the identity.js module only
  476. export class Compiler {
  477. constructor(options) {
  478. options ||= {};
  479. this.indentation = typeof options.indent === "string" ? options.indent : " ";
  480. }
  481. /**
  482. * Emit `str`
  483. */
  484. emit(str) {
  485. return str;
  486. }
  487. /**
  488. * Visit `node`.
  489. */
  490. visit(node) {
  491. return this[node.type](node);
  492. }
  493. /**
  494. * Map visit over array of `nodes`, optionally using a `delim`
  495. */
  496. mapVisit(nodes, delim) {
  497. var buf = "";
  498. delim = delim || "";
  499. for (var i = 0, length = nodes.length; i < length; i++) {
  500. buf += this.visit(nodes[i]);
  501. if (delim && i < length - 1) buf += this.emit(delim);
  502. }
  503. return buf;
  504. }
  505. /**
  506. * Compile `node`.
  507. */
  508. compile(node) {
  509. return this.stylesheet(node);
  510. }
  511. /**
  512. * Visit stylesheet node.
  513. */
  514. stylesheet(node) {
  515. return this.mapVisit(node.stylesheet.rules, "\n\n");
  516. }
  517. /**
  518. * Visit comment node.
  519. */
  520. comment(node) {
  521. return this.emit(this.indent() + "/*" + node.comment + "*/", node.position);
  522. }
  523. /**
  524. * Visit import node.
  525. */
  526. import(node) {
  527. return this.emit("@import " + node.import + ";", node.position);
  528. }
  529. /**
  530. * Visit media node.
  531. */
  532. media(node) {
  533. return (
  534. this.emit("@media " + node.media, node.position) +
  535. this.emit(" {\n" + this.indent(1)) +
  536. this.mapVisit(node.rules, "\n\n") +
  537. this.emit(this.indent(-1) + "\n}")
  538. );
  539. }
  540. /**
  541. * Visit document node.
  542. */
  543. document(node) {
  544. var doc = "@" + (node.vendor || "") + "document " + node.document;
  545. return (
  546. this.emit(doc, node.position) +
  547. this.emit(" " + " {\n" + this.indent(1)) +
  548. this.mapVisit(node.rules, "\n\n") +
  549. this.emit(this.indent(-1) + "\n}")
  550. );
  551. }
  552. /**
  553. * Visit charset node.
  554. */
  555. charset(node) {
  556. return this.emit("@charset " + node.charset + ";", node.position);
  557. }
  558. /**
  559. * Visit namespace node.
  560. */
  561. namespace(node) {
  562. return this.emit("@namespace " + node.namespace + ";", node.position);
  563. }
  564. /**
  565. * Visit supports node.
  566. */
  567. supports(node) {
  568. return (
  569. this.emit("@supports " + node.supports, node.position) +
  570. this.emit(" {\n" + this.indent(1)) +
  571. this.mapVisit(node.rules, "\n\n") +
  572. this.emit(this.indent(-1) + "\n}")
  573. );
  574. }
  575. /**
  576. * Visit keyframes node.
  577. */
  578. keyframes(node) {
  579. return (
  580. this.emit("@" + (node.vendor || "") + "keyframes " + node.name, node.position) +
  581. this.emit(" {\n" + this.indent(1)) +
  582. this.mapVisit(node.keyframes, "\n") +
  583. this.emit(this.indent(-1) + "}")
  584. );
  585. }
  586. /**
  587. * Visit keyframe node.
  588. */
  589. keyframe(node) {
  590. var decls = node.declarations;
  591. return (
  592. this.emit(this.indent()) +
  593. this.emit(node.values.join(", "), node.position) +
  594. this.emit(" {\n" + this.indent(1)) +
  595. this.mapVisit(decls, "\n") +
  596. this.emit(this.indent(-1) + "\n" + this.indent() + "}\n")
  597. );
  598. }
  599. /**
  600. * Visit page node.
  601. */
  602. page(node) {
  603. var sel = node.selectors.length ? node.selectors.join(", ") + " " : "";
  604. return (
  605. this.emit("@page " + sel, node.position) +
  606. this.emit("{\n") +
  607. this.emit(this.indent(1)) +
  608. this.mapVisit(node.declarations, "\n") +
  609. this.emit(this.indent(-1)) +
  610. this.emit("\n}")
  611. );
  612. }
  613. /**
  614. * Visit font-face node.
  615. */
  616. ["font-face"] = function (node) {
  617. return (
  618. this.emit("@font-face ", node.position) +
  619. this.emit("{\n") +
  620. this.emit(this.indent(1)) +
  621. this.mapVisit(node.declarations, "\n") +
  622. this.emit(this.indent(-1)) +
  623. this.emit("\n}")
  624. );
  625. };
  626. /**
  627. * Visit host node.
  628. */
  629. host(node) {
  630. return (
  631. this.emit("@host", node.position) +
  632. this.emit(" {\n" + this.indent(1)) +
  633. this.mapVisit(node.rules, "\n\n") +
  634. this.emit(this.indent(-1) + "\n}")
  635. );
  636. }
  637. /**
  638. * Visit custom-media node.
  639. */
  640. ["custom-media"] = function (node) {
  641. return this.emit("@custom-media " + node.name + " " + node.media + ";", node.position);
  642. };
  643. /**
  644. * Visit rule node.
  645. */
  646. rule(node) {
  647. var indent = this.indent();
  648. var decls = node.declarations;
  649. if (!decls.length) return "";
  650. return (
  651. this.emit(
  652. node.selectors
  653. .map(function (s) {
  654. return indent + s;
  655. })
  656. .join(",\n"),
  657. node.position
  658. ) +
  659. this.emit(" {\n") +
  660. this.emit(this.indent(1)) +
  661. this.mapVisit(decls, "\n") +
  662. this.emit(this.indent(-1)) +
  663. this.emit("\n" + this.indent() + "}")
  664. );
  665. }
  666. /**
  667. * Visit declaration node.
  668. */
  669. declaration(node) {
  670. return (
  671. this.emit(this.indent()) +
  672. this.emit(node.property + ": " + node.value, node.position) +
  673. this.emit(";")
  674. );
  675. }
  676. /**
  677. * Increase, decrease or return current indentation.
  678. */
  679. indent(level) {
  680. this.level = this.level || 1;
  681. if (null != level) {
  682. this.level += level;
  683. return "";
  684. }
  685. return Array(this.level).join(this.indentation);
  686. }
  687. }
  688. // Credits: https://github.com/reworkcss/css/tree/master/lib/stringify
  689. // Licensed under MIT
  690. // Forge: removed unused options
  691. /**
  692. * Stringfy the given AST `node`.
  693. *
  694. * @param {Object} node
  695. * @param {Object} [_options]
  696. * @return {String}
  697. * @api public
  698. */
  699. export function stringify(node, _options) {
  700. _options ||= {};
  701. var compiler = new Compiler(_options);
  702. var code = compiler.compile(node);
  703. return code;
  704. }