index.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604
  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. source: options.source,
  69. rules: rulesList,
  70. parsingErrors: errorsList
  71. }
  72. };
  73. }
  74. /**
  75. * Opening brace.
  76. */
  77. function open() {
  78. return match(/^{\s*/);
  79. }
  80. /**
  81. * Closing brace.
  82. */
  83. function close() {
  84. return match(/^}/);
  85. }
  86. /**
  87. * Parse ruleset.
  88. */
  89. function rules() {
  90. var node;
  91. var rules = [];
  92. whitespace();
  93. comments(rules);
  94. while (css.length && css.charAt(0) != '}' && (node = atrule() || rule())) {
  95. if (node !== false) {
  96. rules.push(node);
  97. comments(rules);
  98. }
  99. }
  100. return rules;
  101. }
  102. /**
  103. * Match `re` and return captures.
  104. */
  105. function match(re) {
  106. var m = re.exec(css);
  107. if (!m) return;
  108. var str = m[0];
  109. updatePosition(str);
  110. css = css.slice(str.length);
  111. return m;
  112. }
  113. /**
  114. * Parse whitespace.
  115. */
  116. function whitespace() {
  117. match(/^\s*/);
  118. }
  119. /**
  120. * Parse comments;
  121. */
  122. function comments(rules) {
  123. var c;
  124. rules = rules || [];
  125. while (c = comment()) {
  126. if (c !== false) {
  127. rules.push(c);
  128. }
  129. }
  130. return rules;
  131. }
  132. /**
  133. * Parse comment.
  134. */
  135. function comment() {
  136. var pos = position();
  137. if ('/' != css.charAt(0) || '*' != css.charAt(1)) return;
  138. var i = 2;
  139. while ("" != css.charAt(i) && ('*' != css.charAt(i) || '/' != css.charAt(i + 1))) ++i;
  140. i += 2;
  141. if ("" === css.charAt(i-1)) {
  142. return error('End of comment missing');
  143. }
  144. var str = css.slice(2, i - 2);
  145. column += 2;
  146. updatePosition(str);
  147. css = css.slice(i);
  148. column += 2;
  149. return pos({
  150. type: 'comment',
  151. comment: str
  152. });
  153. }
  154. /**
  155. * Parse selector.
  156. */
  157. function selector() {
  158. var m = match(/^([^{]+)/);
  159. if (!m) return;
  160. /* @fix Remove all comments from selectors
  161. * http://ostermiller.org/findcomment.html */
  162. return trim(m[0])
  163. .replace(/\/\*([^*]|[\r\n]|(\*+([^*/]|[\r\n])))*\*\/+/g, '')
  164. .replace(/"(?:\\"|[^"])*"|'(?:\\'|[^'])*'/g, function(m) {
  165. return m.replace(/,/g, '\u200C');
  166. })
  167. .split(/\s*(?![^(]*\)),\s*/)
  168. .map(function(s) {
  169. return s.replace(/\u200C/g, ',');
  170. });
  171. }
  172. /**
  173. * Parse declaration.
  174. */
  175. function declaration() {
  176. var pos = position();
  177. // prop
  178. var prop = match(/^(\*?[-#\/\*\\\w]+(\[[0-9a-z_-]+\])?)\s*/);
  179. if (!prop) return;
  180. prop = trim(prop[0]);
  181. // :
  182. if (!match(/^:\s*/)) return error("property missing ':'");
  183. // val
  184. var val = match(/^((?:'(?:\\'|.)*?'|"(?:\\"|.)*?"|\([^\)]*?\)|[^};])+)/);
  185. var ret = pos({
  186. type: 'declaration',
  187. property: prop.replace(commentre, ''),
  188. value: val ? trim(val[0]).replace(commentre, '') : ''
  189. });
  190. // ;
  191. match(/^[;\s]*/);
  192. return ret;
  193. }
  194. /**
  195. * Parse declarations.
  196. */
  197. function declarations() {
  198. var decls = [];
  199. if (!open()) return error("missing '{'");
  200. comments(decls);
  201. // declarations
  202. var decl;
  203. while (decl = declaration()) {
  204. if (decl !== false) {
  205. decls.push(decl);
  206. comments(decls);
  207. }
  208. }
  209. if (!close()) return error("missing '}'");
  210. return decls;
  211. }
  212. /**
  213. * Parse keyframe.
  214. */
  215. function keyframe() {
  216. var m;
  217. var vals = [];
  218. var pos = position();
  219. while (m = match(/^((\d+\.\d+|\.\d+|\d+)%?|[a-z]+)\s*/)) {
  220. vals.push(m[1]);
  221. match(/^,\s*/);
  222. }
  223. if (!vals.length) return;
  224. return pos({
  225. type: 'keyframe',
  226. values: vals,
  227. declarations: declarations()
  228. });
  229. }
  230. /**
  231. * Parse keyframes.
  232. */
  233. function atkeyframes() {
  234. var pos = position();
  235. var m = match(/^@([-\w]+)?keyframes\s*/);
  236. if (!m) return;
  237. var vendor = m[1];
  238. // identifier
  239. var m = match(/^([-\w]+)\s*/);
  240. if (!m) return error("@keyframes missing name");
  241. var name = m[1];
  242. if (!open()) return error("@keyframes missing '{'");
  243. var frame;
  244. var frames = comments();
  245. while (frame = keyframe()) {
  246. frames.push(frame);
  247. frames = frames.concat(comments());
  248. }
  249. if (!close()) return error("@keyframes missing '}'");
  250. return pos({
  251. type: 'keyframes',
  252. name: name,
  253. vendor: vendor,
  254. keyframes: frames
  255. });
  256. }
  257. /**
  258. * Parse supports.
  259. */
  260. function atsupports() {
  261. var pos = position();
  262. var m = match(/^@supports *([^{]+)/);
  263. if (!m) return;
  264. var supports = trim(m[1]);
  265. if (!open()) return error("@supports missing '{'");
  266. var style = comments().concat(rules());
  267. if (!close()) return error("@supports missing '}'");
  268. return pos({
  269. type: 'supports',
  270. supports: supports,
  271. rules: style
  272. });
  273. }
  274. /**
  275. * Parse host.
  276. */
  277. function athost() {
  278. var pos = position();
  279. var m = match(/^@host\s*/);
  280. if (!m) return;
  281. if (!open()) return error("@host missing '{'");
  282. var style = comments().concat(rules());
  283. if (!close()) return error("@host missing '}'");
  284. return pos({
  285. type: 'host',
  286. rules: style
  287. });
  288. }
  289. /**
  290. * Parse media.
  291. */
  292. function atmedia() {
  293. var pos = position();
  294. var m = match(/^@media *([^{]+)/);
  295. if (!m) return;
  296. var media = trim(m[1]);
  297. if (!open()) return error("@media missing '{'");
  298. var style = comments().concat(rules());
  299. if (!close()) return error("@media missing '}'");
  300. return pos({
  301. type: 'media',
  302. media: media,
  303. rules: style
  304. });
  305. }
  306. /**
  307. * Parse custom-media.
  308. */
  309. function atcustommedia() {
  310. var pos = position();
  311. var m = match(/^@custom-media\s+(--[^\s]+)\s*([^{;]+);/);
  312. if (!m) return;
  313. return pos({
  314. type: 'custom-media',
  315. name: trim(m[1]),
  316. media: trim(m[2])
  317. });
  318. }
  319. /**
  320. * Parse paged media.
  321. */
  322. function atpage() {
  323. var pos = position();
  324. var m = match(/^@page */);
  325. if (!m) return;
  326. var sel = selector() || [];
  327. if (!open()) return error("@page missing '{'");
  328. var decls = comments();
  329. // declarations
  330. var decl;
  331. while (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. var decl;
  372. while (decl = declaration()) {
  373. decls.push(decl);
  374. decls = decls.concat(comments());
  375. }
  376. if (!close()) return error("@font-face missing '}'");
  377. return pos({
  378. type: 'font-face',
  379. declarations: decls
  380. });
  381. }
  382. /**
  383. * Parse import
  384. */
  385. var atimport = _compileAtrule('import');
  386. /**
  387. * Parse charset
  388. */
  389. var atcharset = _compileAtrule('charset');
  390. /**
  391. * Parse namespace
  392. */
  393. var atnamespace = _compileAtrule('namespace');
  394. /**
  395. * Parse non-block at-rules
  396. */
  397. function _compileAtrule(name) {
  398. var re = new RegExp('^@' + name + '\\s*([^;]+);');
  399. return function() {
  400. var pos = position();
  401. var m = match(re);
  402. if (!m) return;
  403. var ret = { type: name };
  404. ret[name] = m[1].trim();
  405. return pos(ret);
  406. }
  407. }
  408. /**
  409. * Parse at rule.
  410. */
  411. function atrule() {
  412. if (css[0] != '@') return;
  413. return atkeyframes()
  414. || atmedia()
  415. || atcustommedia()
  416. || atsupports()
  417. || atimport()
  418. || atcharset()
  419. || atnamespace()
  420. || atdocument()
  421. || atpage()
  422. || athost()
  423. || atfontface();
  424. }
  425. /**
  426. * Parse rule.
  427. */
  428. function rule() {
  429. var pos = position();
  430. var sel = selector();
  431. if (!sel) return error('selector missing');
  432. comments();
  433. return pos({
  434. type: 'rule',
  435. selectors: sel,
  436. declarations: declarations()
  437. });
  438. }
  439. return addParent(stylesheet());
  440. };
  441. /**
  442. * Trim `str`.
  443. */
  444. function trim(str) {
  445. return str ? str.replace(/^\s+|\s+$/g, '') : '';
  446. }
  447. /**
  448. * Adds non-enumerable parent node reference to each node.
  449. */
  450. function addParent(obj, parent) {
  451. var isNode = obj && typeof obj.type === 'string';
  452. var childParent = isNode ? obj : parent;
  453. for (var k in obj) {
  454. var value = obj[k];
  455. if (Array.isArray(value)) {
  456. value.forEach(function(v) { addParent(v, childParent); });
  457. } else if (value && typeof value === 'object') {
  458. addParent(value, childParent);
  459. }
  460. }
  461. if (isNode) {
  462. Object.defineProperty(obj, 'parent', {
  463. configurable: true,
  464. writable: true,
  465. enumerable: false,
  466. value: parent || null
  467. });
  468. }
  469. return obj;
  470. }