1bf248099f0782ae9e1d6dfd7436f0b220b5824dd0d95eca45b50d01cb69ad3cf1a925383935ec92b094acd3458204ee33a1c641559273afa29084f37a342e 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481
  1. /*jshint node:true */
  2. /*
  3. The MIT License (MIT)
  4. Copyright (c) 2007-2018 Einar Lielmanis, Liam Newman, and contributors.
  5. Permission is hereby granted, free of charge, to any person
  6. obtaining a copy of this software and associated documentation files
  7. (the "Software"), to deal in the Software without restriction,
  8. including without limitation the rights to use, copy, modify, merge,
  9. publish, distribute, sublicense, and/or sell copies of the Software,
  10. and to permit persons to whom the Software is furnished to do so,
  11. subject to the following conditions:
  12. The above copyright notice and this permission notice shall be
  13. included in all copies or substantial portions of the Software.
  14. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
  15. EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
  16. MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
  17. NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
  18. BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
  19. ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
  20. CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  21. SOFTWARE.
  22. */
  23. 'use strict';
  24. var Options = require('./options').Options;
  25. var Output = require('../core/output').Output;
  26. var InputScanner = require('../core/inputscanner').InputScanner;
  27. var Directives = require('../core/directives').Directives;
  28. var directives_core = new Directives(/\/\*/, /\*\//);
  29. var lineBreak = /\r\n|[\r\n]/;
  30. var allLineBreaks = /\r\n|[\r\n]/g;
  31. // tokenizer
  32. var whitespaceChar = /\s/;
  33. var whitespacePattern = /(?:\s|\n)+/g;
  34. var block_comment_pattern = /\/\*(?:[\s\S]*?)((?:\*\/)|$)/g;
  35. var comment_pattern = /\/\/(?:[^\n\r\u2028\u2029]*)/g;
  36. function Beautifier(source_text, options) {
  37. this._source_text = source_text || '';
  38. // Allow the setting of language/file-type specific options
  39. // with inheritance of overall settings
  40. this._options = new Options(options);
  41. this._ch = null;
  42. this._input = null;
  43. // https://developer.mozilla.org/en-US/docs/Web/CSS/At-rule
  44. this.NESTED_AT_RULE = {
  45. "@page": true,
  46. "@font-face": true,
  47. "@keyframes": true,
  48. // also in CONDITIONAL_GROUP_RULE below
  49. "@media": true,
  50. "@supports": true,
  51. "@document": true
  52. };
  53. this.CONDITIONAL_GROUP_RULE = {
  54. "@media": true,
  55. "@supports": true,
  56. "@document": true
  57. };
  58. }
  59. Beautifier.prototype.eatString = function(endChars) {
  60. var result = '';
  61. this._ch = this._input.next();
  62. while (this._ch) {
  63. result += this._ch;
  64. if (this._ch === "\\") {
  65. result += this._input.next();
  66. } else if (endChars.indexOf(this._ch) !== -1 || this._ch === "\n") {
  67. break;
  68. }
  69. this._ch = this._input.next();
  70. }
  71. return result;
  72. };
  73. // Skips any white space in the source text from the current position.
  74. // When allowAtLeastOneNewLine is true, will output new lines for each
  75. // newline character found; if the user has preserve_newlines off, only
  76. // the first newline will be output
  77. Beautifier.prototype.eatWhitespace = function(allowAtLeastOneNewLine) {
  78. var result = whitespaceChar.test(this._input.peek());
  79. var isFirstNewLine = true;
  80. while (whitespaceChar.test(this._input.peek())) {
  81. this._ch = this._input.next();
  82. if (allowAtLeastOneNewLine && this._ch === '\n') {
  83. if (this._options.preserve_newlines || isFirstNewLine) {
  84. isFirstNewLine = false;
  85. this._output.add_new_line(true);
  86. }
  87. }
  88. }
  89. return result;
  90. };
  91. // Nested pseudo-class if we are insideRule
  92. // and the next special character found opens
  93. // a new block
  94. Beautifier.prototype.foundNestedPseudoClass = function() {
  95. var openParen = 0;
  96. var i = 1;
  97. var ch = this._input.peek(i);
  98. while (ch) {
  99. if (ch === "{") {
  100. return true;
  101. } else if (ch === '(') {
  102. // pseudoclasses can contain ()
  103. openParen += 1;
  104. } else if (ch === ')') {
  105. if (openParen === 0) {
  106. return false;
  107. }
  108. openParen -= 1;
  109. } else if (ch === ";" || ch === "}") {
  110. return false;
  111. }
  112. i++;
  113. ch = this._input.peek(i);
  114. }
  115. return false;
  116. };
  117. Beautifier.prototype.print_string = function(output_string) {
  118. this._output.set_indent(this._indentLevel);
  119. this._output.non_breaking_space = true;
  120. this._output.add_token(output_string);
  121. };
  122. Beautifier.prototype.preserveSingleSpace = function(isAfterSpace) {
  123. if (isAfterSpace) {
  124. this._output.space_before_token = true;
  125. }
  126. };
  127. Beautifier.prototype.indent = function() {
  128. this._indentLevel++;
  129. };
  130. Beautifier.prototype.outdent = function() {
  131. if (this._indentLevel > 0) {
  132. this._indentLevel--;
  133. }
  134. };
  135. /*_____________________--------------------_____________________*/
  136. Beautifier.prototype.beautify = function() {
  137. if (this._options.disabled) {
  138. return this._source_text;
  139. }
  140. var source_text = this._source_text;
  141. var eol = this._options.eol;
  142. if (eol === 'auto') {
  143. eol = '\n';
  144. if (source_text && lineBreak.test(source_text || '')) {
  145. eol = source_text.match(lineBreak)[0];
  146. }
  147. }
  148. // HACK: newline parsing inconsistent. This brute force normalizes the this._input.
  149. source_text = source_text.replace(allLineBreaks, '\n');
  150. // reset
  151. var baseIndentString = source_text.match(/^[\t ]*/)[0];
  152. this._output = new Output(this._options, baseIndentString);
  153. this._input = new InputScanner(source_text);
  154. this._indentLevel = 0;
  155. this._nestedLevel = 0;
  156. this._ch = null;
  157. var parenLevel = 0;
  158. var insideRule = false;
  159. // This is the value side of a property value pair (blue in the following ex)
  160. // label { content: blue }
  161. var insidePropertyValue = false;
  162. var enteringConditionalGroup = false;
  163. var insideAtExtend = false;
  164. var insideAtImport = false;
  165. var topCharacter = this._ch;
  166. var whitespace;
  167. var isAfterSpace;
  168. var previous_ch;
  169. while (true) {
  170. whitespace = this._input.read(whitespacePattern);
  171. isAfterSpace = whitespace !== '';
  172. previous_ch = topCharacter;
  173. this._ch = this._input.next();
  174. if (this._ch === '\\' && this._input.hasNext()) {
  175. this._ch += this._input.next();
  176. }
  177. topCharacter = this._ch;
  178. if (!this._ch) {
  179. break;
  180. } else if (this._ch === '/' && this._input.peek() === '*') {
  181. // /* css comment */
  182. // Always start block comments on a new line.
  183. // This handles scenarios where a block comment immediately
  184. // follows a property definition on the same line or where
  185. // minified code is being beautified.
  186. this._output.add_new_line();
  187. this._input.back();
  188. var comment = this._input.read(block_comment_pattern);
  189. // Handle ignore directive
  190. var directives = directives_core.get_directives(comment);
  191. if (directives && directives.ignore === 'start') {
  192. comment += directives_core.readIgnored(this._input);
  193. }
  194. this.print_string(comment);
  195. // Ensures any new lines following the comment are preserved
  196. this.eatWhitespace(true);
  197. // Block comments are followed by a new line so they don't
  198. // share a line with other properties
  199. this._output.add_new_line();
  200. } else if (this._ch === '/' && this._input.peek() === '/') {
  201. // // single line comment
  202. // Preserves the space before a comment
  203. // on the same line as a rule
  204. this._output.space_before_token = true;
  205. this._input.back();
  206. this.print_string(this._input.read(comment_pattern));
  207. // Ensures any new lines following the comment are preserved
  208. this.eatWhitespace(true);
  209. } else if (this._ch === '@') {
  210. this.preserveSingleSpace(isAfterSpace);
  211. // deal with less propery mixins @{...}
  212. if (this._input.peek() === '{') {
  213. this.print_string(this._ch + this.eatString('}'));
  214. } else {
  215. this.print_string(this._ch);
  216. // strip trailing space, if present, for hash property checks
  217. var variableOrRule = this._input.peekUntilAfter(/[: ,;{}()[\]\/='"]/g);
  218. if (variableOrRule.match(/[ :]$/)) {
  219. // we have a variable or pseudo-class, add it and insert one space before continuing
  220. variableOrRule = this.eatString(": ").replace(/\s$/, '');
  221. this.print_string(variableOrRule);
  222. this._output.space_before_token = true;
  223. }
  224. variableOrRule = variableOrRule.replace(/\s$/, '');
  225. if (variableOrRule === 'extend') {
  226. insideAtExtend = true;
  227. } else if (variableOrRule === 'import') {
  228. insideAtImport = true;
  229. }
  230. // might be a nesting at-rule
  231. if (variableOrRule in this.NESTED_AT_RULE) {
  232. this._nestedLevel += 1;
  233. if (variableOrRule in this.CONDITIONAL_GROUP_RULE) {
  234. enteringConditionalGroup = true;
  235. }
  236. // might be less variable
  237. } else if (!insideRule && parenLevel === 0 && variableOrRule.indexOf(':') !== -1) {
  238. insidePropertyValue = true;
  239. this.indent();
  240. }
  241. }
  242. } else if (this._ch === '#' && this._input.peek() === '{') {
  243. this.preserveSingleSpace(isAfterSpace);
  244. this.print_string(this._ch + this.eatString('}'));
  245. } else if (this._ch === '{') {
  246. if (insidePropertyValue) {
  247. insidePropertyValue = false;
  248. this.outdent();
  249. }
  250. // when entering conditional groups, only rulesets are allowed
  251. if (enteringConditionalGroup) {
  252. enteringConditionalGroup = false;
  253. insideRule = (this._indentLevel >= this._nestedLevel);
  254. } else {
  255. // otherwise, declarations are also allowed
  256. insideRule = (this._indentLevel >= this._nestedLevel - 1);
  257. }
  258. if (this._options.newline_between_rules && insideRule) {
  259. if (this._output.previous_line && this._output.previous_line.item(-1) !== '{') {
  260. this._output.ensure_empty_line_above('/', ',');
  261. }
  262. }
  263. this._output.space_before_token = true;
  264. // The difference in print_string and indent order is necessary to indent the '{' correctly
  265. if (this._options.brace_style === 'expand') {
  266. this._output.add_new_line();
  267. this.print_string(this._ch);
  268. this.indent();
  269. this._output.set_indent(this._indentLevel);
  270. } else {
  271. this.indent();
  272. this.print_string(this._ch);
  273. }
  274. this.eatWhitespace(true);
  275. this._output.add_new_line();
  276. } else if (this._ch === '}') {
  277. this.outdent();
  278. this._output.add_new_line();
  279. if (previous_ch === '{') {
  280. this._output.trim(true);
  281. }
  282. insideAtImport = false;
  283. insideAtExtend = false;
  284. if (insidePropertyValue) {
  285. this.outdent();
  286. insidePropertyValue = false;
  287. }
  288. this.print_string(this._ch);
  289. insideRule = false;
  290. if (this._nestedLevel) {
  291. this._nestedLevel--;
  292. }
  293. this.eatWhitespace(true);
  294. this._output.add_new_line();
  295. if (this._options.newline_between_rules && !this._output.just_added_blankline()) {
  296. if (this._input.peek() !== '}') {
  297. this._output.add_new_line(true);
  298. }
  299. }
  300. } else if (this._ch === ":") {
  301. if ((insideRule || enteringConditionalGroup) && !(this._input.lookBack("&") || this.foundNestedPseudoClass()) && !this._input.lookBack("(") && !insideAtExtend && parenLevel === 0) {
  302. // 'property: value' delimiter
  303. // which could be in a conditional group query
  304. this.print_string(':');
  305. if (!insidePropertyValue) {
  306. insidePropertyValue = true;
  307. this._output.space_before_token = true;
  308. this.eatWhitespace(true);
  309. this.indent();
  310. }
  311. } else {
  312. // sass/less parent reference don't use a space
  313. // sass nested pseudo-class don't use a space
  314. // preserve space before pseudoclasses/pseudoelements, as it means "in any child"
  315. if (this._input.lookBack(" ")) {
  316. this._output.space_before_token = true;
  317. }
  318. if (this._input.peek() === ":") {
  319. // pseudo-element
  320. this._ch = this._input.next();
  321. this.print_string("::");
  322. } else {
  323. // pseudo-class
  324. this.print_string(':');
  325. }
  326. }
  327. } else if (this._ch === '"' || this._ch === '\'') {
  328. this.preserveSingleSpace(isAfterSpace);
  329. this.print_string(this._ch + this.eatString(this._ch));
  330. this.eatWhitespace(true);
  331. } else if (this._ch === ';') {
  332. if (parenLevel === 0) {
  333. if (insidePropertyValue) {
  334. this.outdent();
  335. insidePropertyValue = false;
  336. }
  337. insideAtExtend = false;
  338. insideAtImport = false;
  339. this.print_string(this._ch);
  340. this.eatWhitespace(true);
  341. // This maintains single line comments on the same
  342. // line. Block comments are also affected, but
  343. // a new line is always output before one inside
  344. // that section
  345. if (this._input.peek() !== '/') {
  346. this._output.add_new_line();
  347. }
  348. } else {
  349. this.print_string(this._ch);
  350. this.eatWhitespace(true);
  351. this._output.space_before_token = true;
  352. }
  353. } else if (this._ch === '(') { // may be a url
  354. if (this._input.lookBack("url")) {
  355. this.print_string(this._ch);
  356. this.eatWhitespace();
  357. parenLevel++;
  358. this.indent();
  359. this._ch = this._input.next();
  360. if (this._ch === ')' || this._ch === '"' || this._ch === '\'') {
  361. this._input.back();
  362. } else if (this._ch) {
  363. this.print_string(this._ch + this.eatString(')'));
  364. if (parenLevel) {
  365. parenLevel--;
  366. this.outdent();
  367. }
  368. }
  369. } else {
  370. this.preserveSingleSpace(isAfterSpace);
  371. this.print_string(this._ch);
  372. this.eatWhitespace();
  373. parenLevel++;
  374. this.indent();
  375. }
  376. } else if (this._ch === ')') {
  377. if (parenLevel) {
  378. parenLevel--;
  379. this.outdent();
  380. }
  381. this.print_string(this._ch);
  382. } else if (this._ch === ',') {
  383. this.print_string(this._ch);
  384. this.eatWhitespace(true);
  385. if (this._options.selector_separator_newline && !insidePropertyValue && parenLevel === 0 && !insideAtImport) {
  386. this._output.add_new_line();
  387. } else {
  388. this._output.space_before_token = true;
  389. }
  390. } else if ((this._ch === '>' || this._ch === '+' || this._ch === '~') && !insidePropertyValue && parenLevel === 0) {
  391. //handle combinator spacing
  392. if (this._options.space_around_combinator) {
  393. this._output.space_before_token = true;
  394. this.print_string(this._ch);
  395. this._output.space_before_token = true;
  396. } else {
  397. this.print_string(this._ch);
  398. this.eatWhitespace();
  399. // squash extra whitespace
  400. if (this._ch && whitespaceChar.test(this._ch)) {
  401. this._ch = '';
  402. }
  403. }
  404. } else if (this._ch === ']') {
  405. this.print_string(this._ch);
  406. } else if (this._ch === '[') {
  407. this.preserveSingleSpace(isAfterSpace);
  408. this.print_string(this._ch);
  409. } else if (this._ch === '=') { // no whitespace before or after
  410. this.eatWhitespace();
  411. this.print_string('=');
  412. if (whitespaceChar.test(this._ch)) {
  413. this._ch = '';
  414. }
  415. } else if (this._ch === '!' && !this._input.lookBack("\\")) { // !important
  416. this.print_string(' ');
  417. this.print_string(this._ch);
  418. } else {
  419. this.preserveSingleSpace(isAfterSpace);
  420. this.print_string(this._ch);
  421. }
  422. }
  423. var sweetCode = this._output.get_code(eol);
  424. return sweetCode;
  425. };
  426. module.exports.Beautifier = Beautifier;