css.js 9.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329
  1. var Class = require('./Class');
  2. var trim = require('./trim');
  3. var repeat = require('./repeat');
  4. var defaults = require('./defaults');
  5. var camelCase = require('./camelCase');
  6. exports = {
  7. parse: function(css) {
  8. return new Parser(css).parse();
  9. },
  10. stringify: function(stylesheet, options) {
  11. return new Compiler(stylesheet, options).compile();
  12. }
  13. };
  14. var regComments = /(\/\*[\s\S]*?\*\/)/gi;
  15. var regOpen = /^{\s*/;
  16. var regClose = /^}/;
  17. var regWhitespace = /^\s*/;
  18. var regProperty = /^(\*?[-#/*\\\w]+(\[[0-9a-z_-]+\])?)\s*/;
  19. var regValue = /^((?:'(?:\\'|.)*?'|"(?:\\"|.)*?"|\([^)]*?\)|[^};])+)/;
  20. var regSelector = /^([^{]+)/;
  21. var regSemicolon = /^[;\s]*/;
  22. var regColon = /^:\s*/;
  23. var regMedia = /^@media *([^{]+)/;
  24. var regKeyframes = /^@([-\w]+)?keyframes\s*/;
  25. var regFontFace = /^@font-face\s*/;
  26. var regSupports = /^@supports *([^{]+)/;
  27. var regIdentifier = /^([-\w]+)\s*/;
  28. var regKeyframeSelector = /^((\d+\.\d+|\.\d+|\d+)%?|[a-z]+)\s*/;
  29. var regComma = /^,\s*/;
  30. var Parser = Class({
  31. initialize: function Parser(css) {
  32. this.input = stripCmt(css);
  33. this.open = this._createMatcher(regOpen);
  34. this.close = this._createMatcher(regClose);
  35. this.whitespace = this._createMatcher(regWhitespace);
  36. this.atImport = this._createAtRule('import');
  37. this.atCharset = this._createAtRule('charset');
  38. this.atNamespace = this._createAtRule('namespace');
  39. },
  40. parse: function() {
  41. return this.stylesheet();
  42. },
  43. stylesheet: function() {
  44. return {
  45. type: 'stylesheet',
  46. rules: this.rules()
  47. };
  48. },
  49. rules: function() {
  50. var rule;
  51. var rules = [];
  52. this.whitespace();
  53. while (
  54. this.input.length &&
  55. this.input[0] !== '}' &&
  56. (rule = this.atRule() || this.rule())
  57. ) {
  58. rules.push(rule);
  59. this.whitespace();
  60. }
  61. return rules;
  62. },
  63. atRule: function() {
  64. if (this.input[0] !== '@') return;
  65. return (
  66. this.atKeyframes() ||
  67. this.atMedia() ||
  68. this.atSupports() ||
  69. this.atImport() ||
  70. this.atCharset() ||
  71. this.atNamespace() ||
  72. this.atFontFace()
  73. );
  74. },
  75. atKeyframes: function() {
  76. var matched = this.match(regKeyframes);
  77. if (!matched) return;
  78. var vendor = matched[1] || '';
  79. matched = this.match(regIdentifier);
  80. if (!matched) throw Error('@keyframes missing name');
  81. var name = matched[1];
  82. if (!this.open()) throw Error("@keyframes missing '{'");
  83. var keyframes = [];
  84. var keyframe;
  85. while ((keyframe = this.keyframe())) {
  86. keyframes.push(keyframe);
  87. }
  88. if (!this.close()) throw Error("@keyframes missing '}'");
  89. return {
  90. type: 'keyframes',
  91. name: name,
  92. vendor: vendor,
  93. keyframes: keyframes
  94. };
  95. },
  96. keyframe: function() {
  97. var selector = [];
  98. var matched;
  99. while ((matched = this.match(regKeyframeSelector))) {
  100. selector.push(matched[1]);
  101. this.match(regComma);
  102. }
  103. if (!selector.length) return;
  104. this.whitespace();
  105. return {
  106. type: 'keyframe',
  107. selector: selector.join(', '),
  108. declarations: this.declarations()
  109. };
  110. },
  111. atSupports: function() {
  112. var matched = this.match(regSupports);
  113. if (!matched) return;
  114. var supports = trim(matched[1]);
  115. if (!this.open()) throw Error("@supports missing '{'");
  116. var rules = this.rules();
  117. if (!this.close()) throw Error("@supports missing '}'");
  118. return {
  119. type: 'supports',
  120. supports: supports,
  121. rules: rules
  122. };
  123. },
  124. atFontFace: function() {
  125. var matched = this.match(regFontFace);
  126. if (!matched) return;
  127. if (!this.open()) throw Error("@font-face missing '{'");
  128. var declaration;
  129. var declarations = [];
  130. while ((declaration = this.declaration())) {
  131. declarations.push(declaration);
  132. }
  133. if (!this.close()) throw Error("@font-face missing '}'");
  134. return {
  135. type: 'font-face',
  136. declarations: declarations
  137. };
  138. },
  139. atMedia: function() {
  140. var matched = this.match(regMedia);
  141. if (!matched) return;
  142. var media = trim(matched[1]);
  143. if (!this.open()) throw Error("@media missing '{'");
  144. this.whitespace();
  145. var rules = this.rules();
  146. if (!this.close()) throw Error("@media missing '}'");
  147. return {
  148. type: 'media',
  149. media: media,
  150. rules: rules
  151. };
  152. },
  153. rule: function() {
  154. var selector = this.selector();
  155. if (!selector) throw Error('missing selector');
  156. return {
  157. type: 'rule',
  158. selector: selector,
  159. declarations: this.declarations()
  160. };
  161. },
  162. declarations: function() {
  163. var declarations = [];
  164. if (!this.open()) throw Error("missing '{'");
  165. this.whitespace();
  166. var declaration;
  167. while ((declaration = this.declaration())) {
  168. declarations.push(declaration);
  169. }
  170. if (!this.close()) throw Error("missing '}'");
  171. this.whitespace();
  172. return declarations;
  173. },
  174. declaration: function() {
  175. var property = this.match(regProperty);
  176. if (!property) return;
  177. property = trim(property[0]);
  178. if (!this.match(regColon)) throw Error("property missing ':'");
  179. var value = this.match(regValue);
  180. this.match(regSemicolon);
  181. this.whitespace();
  182. return {
  183. type: 'declaration',
  184. property: property,
  185. value: value ? trim(value[0]) : ''
  186. };
  187. },
  188. selector: function() {
  189. var matched = this.match(regSelector);
  190. if (!matched) return;
  191. return trim(matched[0]);
  192. },
  193. match: function(reg) {
  194. var matched = reg.exec(this.input);
  195. if (!matched) return;
  196. this.input = this.input.slice(matched[0].length);
  197. return matched;
  198. },
  199. _createMatcher: function(reg) {
  200. var _this = this;
  201. return function() {
  202. return _this.match(reg);
  203. };
  204. },
  205. _createAtRule: function(name) {
  206. var reg = new RegExp('^@' + name + '\\s*([^;]+);');
  207. return function() {
  208. var matched = this.match(reg);
  209. if (!matched) return;
  210. var ret = {
  211. type: name
  212. };
  213. ret[name] = trim(matched[1]);
  214. return ret;
  215. };
  216. }
  217. });
  218. var Compiler = Class({
  219. initialize: function Compiler(input) {
  220. var options =
  221. arguments.length > 1 && arguments[1] !== undefined
  222. ? arguments[1]
  223. : {};
  224. defaults(options, {
  225. indent: ' '
  226. });
  227. this.input = input;
  228. this.indentLevel = 0;
  229. this.indentation = options.indent;
  230. },
  231. compile: function() {
  232. return this.stylesheet(this.input);
  233. },
  234. stylesheet: function(node) {
  235. return this.mapVisit(node.rules, '\n\n');
  236. },
  237. media: function(node) {
  238. return (
  239. '@media ' +
  240. node.media +
  241. ' {\n' +
  242. this.indent(1) +
  243. this.mapVisit(node.rules, '\n\n') +
  244. this.indent(-1) +
  245. '\n}'
  246. );
  247. },
  248. keyframes: function(node) {
  249. return (
  250. '@'.concat(node.vendor, 'keyframes ') +
  251. node.name +
  252. ' {\n' +
  253. this.indent(1) +
  254. this.mapVisit(node.keyframes, '\n') +
  255. this.indent(-1) +
  256. '\n}'
  257. );
  258. },
  259. supports: function(node) {
  260. return (
  261. '@supports ' +
  262. node.supports +
  263. ' {\n' +
  264. this.indent(1) +
  265. this.mapVisit(node.rules, '\n\n') +
  266. this.indent(-1) +
  267. '\n}'
  268. );
  269. },
  270. keyframe: function(node) {
  271. return this.rule(node);
  272. },
  273. mapVisit: function(nodes, delimiter) {
  274. var str = '';
  275. for (var i = 0, len = nodes.length; i < len; i++) {
  276. var node = nodes[i];
  277. str += this[camelCase(node.type)](node);
  278. if (delimiter && i < len - 1) str += delimiter;
  279. }
  280. return str;
  281. },
  282. fontFace: function(node) {
  283. return (
  284. '@font-face {\n' +
  285. this.indent(1) +
  286. this.mapVisit(node.declarations, '\n') +
  287. this.indent(-1) +
  288. '\n}'
  289. );
  290. },
  291. rule: function(node) {
  292. return (
  293. this.indent() +
  294. node.selector +
  295. ' {\n' +
  296. this.indent(1) +
  297. this.mapVisit(node.declarations, '\n') +
  298. this.indent(-1) +
  299. '\n' +
  300. this.indent() +
  301. '}'
  302. );
  303. },
  304. declaration: function(node) {
  305. return this.indent() + node.property + ': ' + node.value + ';';
  306. },
  307. import: function(node) {
  308. return '@import '.concat(node.import, ';');
  309. },
  310. charset: function(node) {
  311. return '@charset '.concat(node.charset, ';');
  312. },
  313. namespace: function(node) {
  314. return '@namespace '.concat(node.namespace, ';');
  315. },
  316. indent: function(level) {
  317. if (level) {
  318. this.indentLevel += level;
  319. return '';
  320. }
  321. return repeat(this.indentation, this.indentLevel);
  322. }
  323. });
  324. var stripCmt = function(str) {
  325. return str.replace(regComments, '');
  326. };
  327. module.exports = exports;