index.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602
  1. // http://www.w3.org/TR/CSS21/grammar.html
  2. // https://github.com/visionmedia/css-parse/pull/49#issuecomment-30088027
  3. var commentre = /\/\*[^*]*\*+([^/*][^*]*\*+)*\//g
  4. module.exports = function(css, options){
  5. options = options || {};
  6. /**
  7. * Positional.
  8. */
  9. var lineno = 1;
  10. var column = 1;
  11. /**
  12. * Update lineno and column based on `str`.
  13. */
  14. function updatePosition(str) {
  15. var lines = str.match(/\n/g);
  16. if (lines) lineno += lines.length;
  17. var i = str.lastIndexOf('\n');
  18. column = ~i ? str.length - i : column + str.length;
  19. }
  20. /**
  21. * Mark position and patch `node.position`.
  22. */
  23. function position() {
  24. var start = { line: lineno, column: column };
  25. return function(node){
  26. node.position = new Position(start);
  27. whitespace();
  28. return node;
  29. };
  30. }
  31. /**
  32. * Store position information for a node
  33. */
  34. function Position(start) {
  35. this.start = start;
  36. this.end = { line: lineno, column: column };
  37. this.source = options.source;
  38. }
  39. /**
  40. * Non-enumerable source string
  41. */
  42. Position.prototype.content = css;
  43. /**
  44. * Error `msg`.
  45. */
  46. var errorsList = [];
  47. function error(msg) {
  48. var err = new Error(options.source + ':' + lineno + ':' + column + ': ' + msg);
  49. err.reason = msg;
  50. err.filename = options.source;
  51. err.line = lineno;
  52. err.column = column;
  53. err.source = css;
  54. if (options.silent) {
  55. errorsList.push(err);
  56. } else {
  57. throw err;
  58. }
  59. }
  60. /**
  61. * Parse stylesheet.
  62. */
  63. function stylesheet() {
  64. var rulesList = rules();
  65. return {
  66. type: 'stylesheet',
  67. stylesheet: {
  68. rules: rulesList,
  69. parsingErrors: errorsList
  70. }
  71. };
  72. }
  73. /**
  74. * Opening brace.
  75. */
  76. function open() {
  77. return match(/^{\s*/);
  78. }
  79. /**
  80. * Closing brace.
  81. */
  82. function close() {
  83. return match(/^}/);
  84. }
  85. /**
  86. * Parse ruleset.
  87. */
  88. function rules() {
  89. var node;
  90. var rules = [];
  91. whitespace();
  92. comments(rules);
  93. while (css.length && css.charAt(0) != '}' && (node = atrule() || rule())) {
  94. if (node !== false) {
  95. rules.push(node);
  96. comments(rules);
  97. }
  98. }
  99. return rules;
  100. }
  101. /**
  102. * Match `re` and return captures.
  103. */
  104. function match(re) {
  105. var m = re.exec(css);
  106. if (!m) return;
  107. var str = m[0];
  108. updatePosition(str);
  109. css = css.slice(str.length);
  110. return m;
  111. }
  112. /**
  113. * Parse whitespace.
  114. */
  115. function whitespace() {
  116. match(/^\s*/);
  117. }
  118. /**
  119. * Parse comments;
  120. */
  121. function comments(rules) {
  122. var c;
  123. rules = rules || [];
  124. while (c = comment()) {
  125. if (c !== false) {
  126. rules.push(c);
  127. }
  128. }
  129. return rules;
  130. }
  131. /**
  132. * Parse comment.
  133. */
  134. function comment() {
  135. var pos = position();
  136. if ('/' != css.charAt(0) || '*' != css.charAt(1)) return;
  137. var i = 2;
  138. while ("" != css.charAt(i) && ('*' != css.charAt(i) || '/' != css.charAt(i + 1))) ++i;
  139. i += 2;
  140. if ("" === css.charAt(i-1)) {
  141. return error('End of comment missing');
  142. }
  143. var str = css.slice(2, i - 2);
  144. column += 2;
  145. updatePosition(str);
  146. css = css.slice(i);
  147. column += 2;
  148. return pos({
  149. type: 'comment',
  150. comment: str
  151. });
  152. }
  153. /**
  154. * Parse selector.
  155. */
  156. function selector() {
  157. var m = match(/^([^{]+)/);
  158. if (!m) return;
  159. /* @fix Remove all comments from selectors
  160. * http://ostermiller.org/findcomment.html */
  161. return trim(m[0])
  162. .replace(/\/\*([^*]|[\r\n]|(\*+([^*/]|[\r\n])))*\*\/+/g, '')
  163. .replace(/"(?:\\"|[^"])*"|'(?:\\'|[^'])*'/g, function(m) {
  164. return m.replace(/,/g, '\u200C');
  165. })
  166. .split(/\s*(?![^(]*\)),\s*/)
  167. .map(function(s) {
  168. return s.replace(/\u200C/g, ',');
  169. });
  170. }
  171. /**
  172. * Parse declaration.
  173. */
  174. function declaration() {
  175. var pos = position();
  176. // prop
  177. var prop = match(/^(\*?[-#\/\*\\\w]+(\[[0-9a-z_-]+\])?)\s*/);
  178. if (!prop) return;
  179. prop = trim(prop[0]);
  180. // :
  181. if (!match(/^:\s*/)) return error("property missing ':'");
  182. // val
  183. var val = match(/^((?:'(?:\\'|.)*?'|"(?:\\"|.)*?"|\([^\)]*?\)|[^};])+)/);
  184. var ret = pos({
  185. type: 'declaration',
  186. property: prop.replace(commentre, ''),
  187. value: val ? trim(val[0]).replace(commentre, '') : ''
  188. });
  189. // ;
  190. match(/^[;\s]*/);
  191. return ret;
  192. }
  193. /**
  194. * Parse declarations.
  195. */
  196. function declarations() {
  197. var decls = [];
  198. if (!open()) return error("missing '{'");
  199. comments(decls);
  200. // declarations
  201. var decl;
  202. while (decl = declaration()) {
  203. if (decl !== false) {
  204. decls.push(decl);
  205. comments(decls);
  206. }
  207. }
  208. if (!close()) return error("missing '}'");
  209. return decls;
  210. }
  211. /**
  212. * Parse keyframe.
  213. */
  214. function keyframe() {
  215. var m;
  216. var vals = [];
  217. var pos = position();
  218. while (m = match(/^((\d+\.\d+|\.\d+|\d+)%?|[a-z]+)\s*/)) {
  219. vals.push(m[1]);
  220. match(/^,\s*/);
  221. }
  222. if (!vals.length) return;
  223. return pos({
  224. type: 'keyframe',
  225. values: vals,
  226. declarations: declarations()
  227. });
  228. }
  229. /**
  230. * Parse keyframes.
  231. */
  232. function atkeyframes() {
  233. var pos = position();
  234. var m = match(/^@([-\w]+)?keyframes\s*/);
  235. if (!m) return;
  236. var vendor = m[1];
  237. // identifier
  238. var m = match(/^([-\w]+)\s*/);
  239. if (!m) return error("@keyframes missing name");
  240. var name = m[1];
  241. if (!open()) return error("@keyframes missing '{'");
  242. var frame;
  243. var frames = comments();
  244. while (frame = keyframe()) {
  245. frames.push(frame);
  246. frames = frames.concat(comments());
  247. }
  248. if (!close()) return error("@keyframes missing '}'");
  249. return pos({
  250. type: 'keyframes',
  251. name: name,
  252. vendor: vendor,
  253. keyframes: frames
  254. });
  255. }
  256. /**
  257. * Parse supports.
  258. */
  259. function atsupports() {
  260. var pos = position();
  261. var m = match(/^@supports *([^{]+)/);
  262. if (!m) return;
  263. var supports = trim(m[1]);
  264. if (!open()) return error("@supports missing '{'");
  265. var style = comments().concat(rules());
  266. if (!close()) return error("@supports missing '}'");
  267. return pos({
  268. type: 'supports',
  269. supports: supports,
  270. rules: style
  271. });
  272. }
  273. /**
  274. * Parse host.
  275. */
  276. function athost() {
  277. var pos = position();
  278. var m = match(/^@host\s*/);
  279. if (!m) return;
  280. if (!open()) return error("@host missing '{'");
  281. var style = comments().concat(rules());
  282. if (!close()) return error("@host missing '}'");
  283. return pos({
  284. type: 'host',
  285. rules: style
  286. });
  287. }
  288. /**
  289. * Parse media.
  290. */
  291. function atmedia() {
  292. var pos = position();
  293. var m = match(/^@media *([^{]+)/);
  294. if (!m) return;
  295. var media = trim(m[1]);
  296. if (!open()) return error("@media missing '{'");
  297. var style = comments().concat(rules());
  298. if (!close()) return error("@media missing '}'");
  299. return pos({
  300. type: 'media',
  301. media: media,
  302. rules: style
  303. });
  304. }
  305. /**
  306. * Parse custom-media.
  307. */
  308. function atcustommedia() {
  309. var pos = position();
  310. var m = match(/^@custom-media\s+(--[^\s]+)\s*([^{;]+);/);
  311. if (!m) return;
  312. return pos({
  313. type: 'custom-media',
  314. name: trim(m[1]),
  315. media: trim(m[2])
  316. });
  317. }
  318. /**
  319. * Parse paged media.
  320. */
  321. function atpage() {
  322. var pos = position();
  323. var m = match(/^@page */);
  324. if (!m) return;
  325. var sel = selector() || [];
  326. if (!open()) return error("@page missing '{'");
  327. var decls = comments();
  328. // declarations
  329. var decl;
  330. while (decl = declaration()) {
  331. decls.push(decl);
  332. decls = decls.concat(comments());
  333. }
  334. if (!close()) return error("@page missing '}'");
  335. return pos({
  336. type: 'page',
  337. selectors: sel,
  338. declarations: decls
  339. });
  340. }
  341. /**
  342. * Parse document.
  343. */
  344. function atdocument() {
  345. var pos = position();
  346. var m = match(/^@([-\w]+)?document *([^{]+)/);
  347. if (!m) return;
  348. var vendor = trim(m[1]);
  349. var doc = trim(m[2]);
  350. if (!open()) return error("@document missing '{'");
  351. var style = comments().concat(rules());
  352. if (!close()) return error("@document missing '}'");
  353. return pos({
  354. type: 'document',
  355. document: doc,
  356. vendor: vendor,
  357. rules: style
  358. });
  359. }
  360. /**
  361. * Parse font-face.
  362. */
  363. function atfontface() {
  364. var pos = position();
  365. var m = match(/^@font-face\s*/);
  366. if (!m) return;
  367. if (!open()) return error("@font-face missing '{'");
  368. var decls = comments();
  369. // declarations
  370. var decl;
  371. while (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 atkeyframes()
  413. || atmedia()
  414. || atcustommedia()
  415. || atsupports()
  416. || atimport()
  417. || atcharset()
  418. || atnamespace()
  419. || atdocument()
  420. || atpage()
  421. || athost()
  422. || atfontface();
  423. }
  424. /**
  425. * Parse rule.
  426. */
  427. function rule() {
  428. var pos = position();
  429. var sel = selector();
  430. if (!sel) return error('selector missing');
  431. comments();
  432. return pos({
  433. type: 'rule',
  434. selectors: sel,
  435. declarations: declarations()
  436. });
  437. }
  438. return addParent(stylesheet());
  439. };
  440. /**
  441. * Trim `str`.
  442. */
  443. function trim(str) {
  444. return str ? str.replace(/^\s+|\s+$/g, '') : '';
  445. }
  446. /**
  447. * Adds non-enumerable parent node reference to each node.
  448. */
  449. function addParent(obj, parent) {
  450. var isNode = obj && typeof obj.type === 'string';
  451. var childParent = isNode ? obj : parent;
  452. for (var k in obj) {
  453. var value = obj[k];
  454. if (Array.isArray(value)) {
  455. value.forEach(function(v) { addParent(v, childParent); });
  456. } else if (value && typeof value === 'object') {
  457. addParent(value, childParent);
  458. }
  459. }
  460. if (isNode) {
  461. Object.defineProperty(obj, 'parent', {
  462. configurable: true,
  463. writable: true,
  464. enumerable: false,
  465. value: parent || null
  466. });
  467. }
  468. return obj;
  469. }