index.js 8.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494
  1. module.exports = function(css, options){
  2. options = options || {};
  3. /**
  4. * Positional.
  5. */
  6. var lineno = 1;
  7. var column = 1;
  8. /**
  9. * Update lineno and column based on `str`.
  10. */
  11. function updatePosition(str) {
  12. var lines = str.match(/\n/g);
  13. if (lines) lineno += lines.length;
  14. var i = str.lastIndexOf('\n');
  15. column = ~i ? str.length - i : column + str.length;
  16. }
  17. /**
  18. * Mark position and patch `node.position`.
  19. */
  20. function position() {
  21. var start = { line: lineno, column: column };
  22. if (!options.position) return positionNoop;
  23. return function(node){
  24. node.position = {
  25. start: start,
  26. end: { line: lineno, column: column },
  27. source: options.source
  28. };
  29. whitespace();
  30. return node;
  31. }
  32. }
  33. /**
  34. * Return `node`.
  35. */
  36. function positionNoop(node) {
  37. whitespace();
  38. return node;
  39. }
  40. /**
  41. * Error `msg`.
  42. */
  43. function error(msg) {
  44. var err = new Error(msg + ' near line ' + lineno + ':' + column);
  45. err.filename = options.source;
  46. err.line = lineno;
  47. err.column = column;
  48. err.source = css;
  49. throw err;
  50. }
  51. /**
  52. * Parse stylesheet.
  53. */
  54. function stylesheet() {
  55. return {
  56. type: 'stylesheet',
  57. stylesheet: {
  58. rules: rules()
  59. }
  60. };
  61. }
  62. /**
  63. * Opening brace.
  64. */
  65. function open() {
  66. return match(/^{\s*/);
  67. }
  68. /**
  69. * Closing brace.
  70. */
  71. function close() {
  72. return match(/^}/);
  73. }
  74. /**
  75. * Parse ruleset.
  76. */
  77. function rules() {
  78. var node;
  79. var rules = [];
  80. whitespace();
  81. comments(rules);
  82. while (css.charAt(0) != '}' && (node = atrule() || rule())) {
  83. rules.push(node);
  84. comments(rules);
  85. }
  86. return rules;
  87. }
  88. /**
  89. * Match `re` and return captures.
  90. */
  91. function match(re) {
  92. var m = re.exec(css);
  93. if (!m) return;
  94. var str = m[0];
  95. updatePosition(str);
  96. css = css.slice(str.length);
  97. return m;
  98. }
  99. /**
  100. * Parse whitespace.
  101. */
  102. function whitespace() {
  103. match(/^\s*/);
  104. }
  105. /**
  106. * Parse comments;
  107. */
  108. function comments(rules) {
  109. var c;
  110. rules = rules || [];
  111. while (c = comment()) rules.push(c);
  112. return rules;
  113. }
  114. /**
  115. * Parse comment.
  116. */
  117. function comment() {
  118. var pos = position();
  119. if ('/' != css.charAt(0) || '*' != css.charAt(1)) return;
  120. var i = 2;
  121. while (null != css.charAt(i) && ('*' != css.charAt(i) || '/' != css.charAt(i + 1))) ++i;
  122. i += 2;
  123. var str = css.slice(2, i - 2);
  124. column += 2;
  125. updatePosition(str);
  126. css = css.slice(i);
  127. column += 2;
  128. return pos({
  129. type: 'comment',
  130. comment: str
  131. });
  132. }
  133. /**
  134. * Parse selector.
  135. */
  136. function selector() {
  137. var m = match(/^([^{]+)/);
  138. if (!m) return;
  139. return trim(m[0]).split(/\s*,\s*/);
  140. }
  141. /**
  142. * Parse declaration.
  143. */
  144. function declaration() {
  145. var pos = position();
  146. // prop
  147. var prop = match(/^(\*?[-#\/\*\w]+(\[[0-9a-z_-]+\])?)\s*/);
  148. if (!prop) return;
  149. prop = trim(prop[0]);
  150. // :
  151. if (!match(/^:\s*/)) return error("property missing ':'");
  152. // val
  153. var val = match(/^((?:'(?:\\'|.)*?'|"(?:\\"|.)*?"|\([^\)]*?\)|[^};])+)/);
  154. if (!val) return error('property missing value');
  155. var ret = pos({
  156. type: 'declaration',
  157. property: prop,
  158. value: trim(val[0])
  159. });
  160. // ;
  161. match(/^[;\s]*/);
  162. return ret;
  163. }
  164. /**
  165. * Parse declarations.
  166. */
  167. function declarations() {
  168. var decls = [];
  169. if (!open()) return error("missing '{'");
  170. comments(decls);
  171. // declarations
  172. var decl;
  173. while (decl = declaration()) {
  174. decls.push(decl);
  175. comments(decls);
  176. }
  177. if (!close()) return error("missing '}'");
  178. return decls;
  179. }
  180. /**
  181. * Parse keyframe.
  182. */
  183. function keyframe() {
  184. var m;
  185. var vals = [];
  186. var pos = position();
  187. while (m = match(/^((\d+\.\d+|\.\d+|\d+)%?|[a-z]+)\s*/)) {
  188. vals.push(m[1]);
  189. match(/^,\s*/);
  190. }
  191. if (!vals.length) return;
  192. return pos({
  193. type: 'keyframe',
  194. values: vals,
  195. declarations: declarations()
  196. });
  197. }
  198. /**
  199. * Parse keyframes.
  200. */
  201. function atkeyframes() {
  202. var pos = position();
  203. var m = match(/^@([-\w]+)?keyframes */);
  204. if (!m) return;
  205. var vendor = m[1];
  206. // identifier
  207. var m = match(/^([-\w]+)\s*/);
  208. if (!m) return error("@keyframes missing name");
  209. var name = m[1];
  210. if (!open()) return error("@keyframes missing '{'");
  211. var frame;
  212. var frames = comments();
  213. while (frame = keyframe()) {
  214. frames.push(frame);
  215. frames = frames.concat(comments());
  216. }
  217. if (!close()) return error("@keyframes missing '}'");
  218. return pos({
  219. type: 'keyframes',
  220. name: name,
  221. vendor: vendor,
  222. keyframes: frames
  223. });
  224. }
  225. /**
  226. * Parse supports.
  227. */
  228. function atsupports() {
  229. var pos = position();
  230. var m = match(/^@supports *([^{]+)/);
  231. if (!m) return;
  232. var supports = trim(m[1]);
  233. if (!open()) return error("@supports missing '{'");
  234. var style = comments().concat(rules());
  235. if (!close()) return error("@supports missing '}'");
  236. return pos({
  237. type: 'supports',
  238. supports: supports,
  239. rules: style
  240. });
  241. }
  242. /**
  243. * Parse host.
  244. */
  245. function athost() {
  246. var pos = position();
  247. var m = match(/^@host */);
  248. if (!m) return;
  249. if (!open()) return error("@host missing '{'");
  250. var style = comments().concat(rules());
  251. if (!close()) return error("@host missing '}'");
  252. return pos({
  253. type: 'host',
  254. rules: style
  255. });
  256. }
  257. /**
  258. * Parse media.
  259. */
  260. function atmedia() {
  261. var pos = position();
  262. var m = match(/^@media *([^{]+)/);
  263. if (!m) return;
  264. var media = trim(m[1]);
  265. if (!open()) return error("@media missing '{'");
  266. var style = comments().concat(rules());
  267. if (!close()) return error("@media missing '}'");
  268. return pos({
  269. type: 'media',
  270. media: media,
  271. rules: style
  272. });
  273. }
  274. /**
  275. * Parse paged media.
  276. */
  277. function atpage() {
  278. var pos = position();
  279. var m = match(/^@page */);
  280. if (!m) return;
  281. var sel = selector() || [];
  282. if (!open()) return error("@page missing '{'");
  283. var decls = comments();
  284. // declarations
  285. var decl;
  286. while (decl = declaration()) {
  287. decls.push(decl);
  288. decls = decls.concat(comments());
  289. }
  290. if (!close()) return error("@page missing '}'");
  291. return pos({
  292. type: 'page',
  293. selectors: sel,
  294. declarations: decls
  295. });
  296. }
  297. /**
  298. * Parse document.
  299. */
  300. function atdocument() {
  301. var pos = position();
  302. var m = match(/^@([-\w]+)?document *([^{]+)/);
  303. if (!m) return;
  304. var vendor = trim(m[1]);
  305. var doc = trim(m[2]);
  306. if (!open()) return error("@document missing '{'");
  307. var style = comments().concat(rules());
  308. if (!close()) return error("@document missing '}'");
  309. return pos({
  310. type: 'document',
  311. document: doc,
  312. vendor: vendor,
  313. rules: style
  314. });
  315. }
  316. /**
  317. * Parse import
  318. */
  319. function atimport() {
  320. return _atrule('import');
  321. }
  322. /**
  323. * Parse charset
  324. */
  325. function atcharset() {
  326. return _atrule('charset');
  327. }
  328. /**
  329. * Parse namespace
  330. */
  331. function atnamespace() {
  332. return _atrule('namespace')
  333. }
  334. /**
  335. * Parse non-block at-rules
  336. */
  337. function _atrule(name) {
  338. var pos = position();
  339. var m = match(new RegExp('^@' + name + ' *([^;\\n]+);'));
  340. if (!m) return;
  341. var ret = { type: name };
  342. ret[name] = trim(m[1]);
  343. return pos(ret);
  344. }
  345. /**
  346. * Parse at rule.
  347. */
  348. function atrule() {
  349. if (css[0] != '@') return;
  350. return atkeyframes()
  351. || atmedia()
  352. || atsupports()
  353. || atimport()
  354. || atcharset()
  355. || atnamespace()
  356. || atdocument()
  357. || atpage()
  358. || athost();
  359. }
  360. /**
  361. * Parse rule.
  362. */
  363. function rule() {
  364. var pos = position();
  365. var sel = selector();
  366. if (!sel) return;
  367. comments();
  368. return pos({
  369. type: 'rule',
  370. selectors: sel,
  371. declarations: declarations()
  372. });
  373. }
  374. return stylesheet();
  375. };
  376. /**
  377. * Trim `str`.
  378. */
  379. function trim(str) {
  380. return str ? str.replace(/^\s+|\s+$/g, '') : '';
  381. }