stringifier.js 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469
  1. 'use strict'
  2. // Escapes sequences that could break out of an HTML <style> context.
  3. // Uses CSS unicode escaping (\3c = '<') which is valid CSS and parsed
  4. // correctly by all compliant CSS consumers.
  5. const STYLE_TAG = /(<)(\/?style\b)/gi
  6. const COMMENT_OPEN = /(<)(!--)/g
  7. // Characters that end an at-rule name, mirroring RE_AT_END in the tokenizer.
  8. // Params starting with anything else need a space to stay separate tokens.
  9. const AT_NAME_END = /[\t\n\f\r "#'()/;[\\\]{}]/
  10. function escapeHTMLInCSS(str) {
  11. if (typeof str !== 'string') return str
  12. if (!str.includes('<')) return str
  13. return str.replace(STYLE_TAG, '\\3c $2').replace(COMMENT_OPEN, '\\3c $2')
  14. }
  15. const DEFAULT_RAW = {
  16. after: '\n',
  17. beforeClose: '\n',
  18. beforeComment: '\n',
  19. beforeDecl: '\n',
  20. beforeOpen: ' ',
  21. beforeRule: '\n',
  22. colon: ': ',
  23. commentLeft: ' ',
  24. commentRight: ' ',
  25. emptyBody: '',
  26. indent: ' ',
  27. semicolon: false
  28. }
  29. function capitalize(str) {
  30. return str[0].toUpperCase() + str.slice(1)
  31. }
  32. function atruleStart(str, node) {
  33. let name = '@' + node.name
  34. let params = node.params ? str.rawValue(node, 'params') : ''
  35. let afterName = node.raws.afterName
  36. if (typeof afterName === 'undefined') {
  37. afterName = params ? ' ' : ''
  38. } else if (afterName === '' && params && !AT_NAME_END.test(params[0])) {
  39. afterName = ' '
  40. }
  41. return name + afterName + params
  42. }
  43. // `*--x` is not a custom property: the parser checks the first token, and the
  44. // `*`/`_` hack prefix moves from `prop` into `before` only after that.
  45. function isCustomProperty(node) {
  46. if (!node.prop.startsWith('--')) return false
  47. let before = node.raws.before
  48. return typeof before === 'undefined' || !/\S$/.test(before)
  49. }
  50. function pushBody(str, stack, node) {
  51. let nodes = node.nodes
  52. let last = nodes.length - 1
  53. while (last > 0) {
  54. if (nodes[last].type !== 'comment') break
  55. last -= 1
  56. }
  57. let semicolon = str.raw(node, 'semicolon')
  58. let isDocument = node.type === 'document'
  59. for (let i = nodes.length - 1; i >= 0; i--) {
  60. let child = nodes[i]
  61. let childSemicolon = last !== i || semicolon
  62. // A childless at-rule or a custom property declaration that still has
  63. // following siblings must be terminated. Without the semicolon those
  64. // trailing comments are folded into the at-rule's prelude or the custom
  65. // property's value and disappear when the output is re-parsed.
  66. if (
  67. !childSemicolon &&
  68. i < nodes.length - 1 &&
  69. ((child.type === 'atrule' && !child.nodes) ||
  70. (child.type === 'decl' && isCustomProperty(child)))
  71. ) {
  72. childSemicolon = true
  73. }
  74. stack.push({
  75. document: isDocument,
  76. node: child,
  77. semicolon: childSemicolon
  78. })
  79. }
  80. }
  81. function pushBlock(str, stack, node, start) {
  82. let between = str.raw(node, 'between', 'beforeOpen')
  83. str.builder(escapeHTMLInCSS(start + between) + '{', node, 'start')
  84. let hasNodes = node.nodes && node.nodes.length
  85. let close = () => {
  86. let after = hasNodes
  87. ? str.raw(node, 'after')
  88. : str.raw(node, 'after', 'emptyBody')
  89. if (after) str.builder(escapeHTMLInCSS(after))
  90. str.builder('}', node, 'end')
  91. if (node.type === 'rule' && node.raws.ownSemicolon) {
  92. str.builder(escapeHTMLInCSS(node.raws.ownSemicolon), node, 'end')
  93. }
  94. }
  95. if (hasNodes) {
  96. stack.push(close)
  97. pushBody(str, stack, node)
  98. } else {
  99. close()
  100. }
  101. }
  102. class Stringifier {
  103. constructor(builder) {
  104. this.builder = builder
  105. }
  106. atrule(node, semicolon) {
  107. let start = atruleStart(this, node)
  108. if (node.nodes) {
  109. this.block(node, start)
  110. } else {
  111. let end = (node.raws.between || '') + (semicolon ? ';' : '')
  112. this.builder(escapeHTMLInCSS(start + end), node)
  113. }
  114. }
  115. beforeAfter(node, detect) {
  116. let value
  117. if (node.type === 'decl') {
  118. value = this.raw(node, null, 'beforeDecl')
  119. } else if (node.type === 'comment') {
  120. value = this.raw(node, null, 'beforeComment')
  121. } else if (detect === 'before') {
  122. value = this.raw(node, null, 'beforeRule')
  123. } else {
  124. value = this.raw(node, null, 'beforeClose')
  125. }
  126. let buf = node.parent
  127. let depth = 0
  128. while (buf && buf.type !== 'root') {
  129. depth += 1
  130. buf = buf.parent
  131. }
  132. if (value.includes('\n')) {
  133. let indent = this.raw(node, null, 'indent')
  134. if (indent.length) {
  135. for (let step = 0; step < depth; step++) value += indent
  136. }
  137. }
  138. return value
  139. }
  140. block(node, start) {
  141. let between = this.raw(node, 'between', 'beforeOpen')
  142. this.builder(escapeHTMLInCSS(start + between) + '{', node, 'start')
  143. let after
  144. if (node.nodes && node.nodes.length) {
  145. this.body(node)
  146. after = this.raw(node, 'after')
  147. } else {
  148. after = this.raw(node, 'after', 'emptyBody')
  149. }
  150. if (after) this.builder(escapeHTMLInCSS(after))
  151. this.builder('}', node, 'end')
  152. }
  153. body(node) {
  154. // Rules and at-rules are expanded into an explicit stack instead of
  155. // recursive `stringify()` calls to survive deeply nested trees.
  156. // If a subclass changes the traversal methods, its children go
  157. // through `stringify()` to keep the override in charge.
  158. let proto = Stringifier.prototype
  159. let expandable = ['atrule', 'block', 'body', 'rule', 'stringify'].every(
  160. method => this[method] === proto[method]
  161. )
  162. let stack = []
  163. pushBody(this, stack, node)
  164. while (stack.length > 0) {
  165. let entry = stack.pop()
  166. if (typeof entry === 'function') {
  167. entry()
  168. continue
  169. }
  170. let child = entry.node
  171. let before = this.raw(child, 'before')
  172. if (before) {
  173. this.builder(entry.document ? before : escapeHTMLInCSS(before))
  174. }
  175. if (expandable && child.type === 'rule') {
  176. pushBlock(this, stack, child, this.rawValue(child, 'selector'))
  177. } else if (expandable && child.type === 'atrule' && child.nodes) {
  178. pushBlock(this, stack, child, atruleStart(this, child))
  179. } else {
  180. this.stringify(child, entry.semicolon)
  181. }
  182. }
  183. }
  184. comment(node) {
  185. let left = this.raw(node, 'left', 'commentLeft')
  186. let right = this.raw(node, 'right', 'commentRight')
  187. this.builder(escapeHTMLInCSS('/*' + left + node.text + right + '*/'), node)
  188. }
  189. decl(node, semicolon) {
  190. let raws = node.raws
  191. let between = this.raw(node, 'between', 'colon')
  192. let string = node.prop + between + this.rawValue(node, 'value')
  193. if (node.important) {
  194. string += raws.important || ' !important'
  195. }
  196. if (semicolon) string += ';'
  197. this.builder(escapeHTMLInCSS(string), node)
  198. }
  199. document(node) {
  200. this.body(node)
  201. }
  202. raw(node, own, detect) {
  203. let value
  204. if (!detect) detect = own
  205. // Already had
  206. if (own) {
  207. value = node.raws[own]
  208. if (typeof value !== 'undefined') return value
  209. }
  210. let parent = node.parent
  211. if (detect === 'before') {
  212. // Hack for first rule in CSS
  213. if (!parent || (parent.type === 'root' && parent.first === node)) {
  214. return ''
  215. }
  216. // `root` nodes in `document` should use only their own raws
  217. if (parent && parent.type === 'document') {
  218. return ''
  219. }
  220. }
  221. // Floating child without parent
  222. if (!parent) return DEFAULT_RAW[detect]
  223. // Detect style by other nodes
  224. let root = node.root()
  225. let cache = root.rawCache || (root.rawCache = {})
  226. if (typeof cache[detect] !== 'undefined') {
  227. return cache[detect]
  228. }
  229. if (detect === 'before' || detect === 'after') {
  230. return this.beforeAfter(node, detect)
  231. } else {
  232. let method = 'raw' + capitalize(detect)
  233. if (this[method]) {
  234. value = this[method](root, node)
  235. } else {
  236. root.walk(i => {
  237. value = i.raws[own]
  238. if (typeof value !== 'undefined') return false
  239. })
  240. }
  241. }
  242. if (typeof value === 'undefined') value = DEFAULT_RAW[detect]
  243. cache[detect] = value
  244. return value
  245. }
  246. rawBeforeClose(root) {
  247. let value
  248. root.walk(i => {
  249. if (i.nodes && i.nodes.length > 0) {
  250. if (typeof i.raws.after !== 'undefined') {
  251. value = i.raws.after
  252. if (value.includes('\n')) {
  253. value = value.replace(/[^\n]+$/, '')
  254. }
  255. return false
  256. }
  257. }
  258. })
  259. if (value) value = value.replace(/\S/g, '')
  260. return value
  261. }
  262. rawBeforeComment(root, node) {
  263. let value
  264. root.walkComments(i => {
  265. if (typeof i.raws.before !== 'undefined') {
  266. value = i.raws.before
  267. if (value.includes('\n')) {
  268. value = value.replace(/[^\n]+$/, '')
  269. }
  270. return false
  271. }
  272. })
  273. if (typeof value === 'undefined') {
  274. value = this.raw(node, null, 'beforeDecl')
  275. } else if (value) {
  276. value = value.replace(/\S/g, '')
  277. }
  278. return value
  279. }
  280. rawBeforeDecl(root, node) {
  281. let value
  282. root.walkDecls(i => {
  283. if (typeof i.raws.before !== 'undefined') {
  284. value = i.raws.before
  285. if (value.includes('\n')) {
  286. value = value.replace(/[^\n]+$/, '')
  287. }
  288. return false
  289. }
  290. })
  291. if (typeof value === 'undefined') {
  292. value = this.raw(node, null, 'beforeRule')
  293. } else if (value) {
  294. value = value.replace(/\S/g, '')
  295. }
  296. return value
  297. }
  298. rawBeforeOpen(root) {
  299. let value
  300. root.walk(i => {
  301. if (i.type !== 'decl') {
  302. value = i.raws.between
  303. if (typeof value !== 'undefined') return false
  304. }
  305. })
  306. return value
  307. }
  308. rawBeforeRule(root) {
  309. let value
  310. root.walk(i => {
  311. if (i.nodes && (i.parent !== root || root.first !== i)) {
  312. if (typeof i.raws.before !== 'undefined') {
  313. value = i.raws.before
  314. if (value.includes('\n')) {
  315. value = value.replace(/[^\n]+$/, '')
  316. }
  317. return false
  318. }
  319. }
  320. })
  321. if (value) value = value.replace(/\S/g, '')
  322. return value
  323. }
  324. rawColon(root) {
  325. let value
  326. root.walkDecls(i => {
  327. if (typeof i.raws.between !== 'undefined') {
  328. value = i.raws.between.replace(/[^\s:]/g, '')
  329. return false
  330. }
  331. })
  332. return value
  333. }
  334. rawEmptyBody(root) {
  335. let value
  336. root.walk(i => {
  337. if (i.nodes && i.nodes.length === 0) {
  338. value = i.raws.after
  339. if (typeof value !== 'undefined') return false
  340. }
  341. })
  342. return value
  343. }
  344. rawIndent(root) {
  345. if (root.raws.indent) return root.raws.indent
  346. let value
  347. root.walk(i => {
  348. let p = i.parent
  349. if (p && p !== root && p.parent && p.parent === root) {
  350. if (typeof i.raws.before !== 'undefined') {
  351. let parts = i.raws.before.split('\n')
  352. value = parts[parts.length - 1]
  353. value = value.replace(/\S/g, '')
  354. return false
  355. }
  356. }
  357. })
  358. return value
  359. }
  360. rawSemicolon(root) {
  361. let value
  362. root.walk(i => {
  363. if (i.nodes && i.nodes.length && i.last.type === 'decl') {
  364. value = i.raws.semicolon
  365. if (typeof value !== 'undefined') return false
  366. }
  367. })
  368. return value
  369. }
  370. rawValue(node, prop) {
  371. let value = node[prop]
  372. let raw = node.raws[prop]
  373. if (raw && raw.value === value) {
  374. return raw.raw
  375. }
  376. return value
  377. }
  378. root(node) {
  379. if (node.source && node.source.input.hasBOM) {
  380. this.builder('\uFEFF', node, 'start')
  381. }
  382. this.body(node)
  383. if (node.raws.after) {
  384. let after = node.raws.after
  385. let isDocument = node.parent && node.parent.type === 'document'
  386. this.builder(isDocument ? after : escapeHTMLInCSS(after))
  387. }
  388. }
  389. rule(node) {
  390. this.block(node, this.rawValue(node, 'selector'))
  391. if (node.raws.ownSemicolon) {
  392. this.builder(escapeHTMLInCSS(node.raws.ownSemicolon), node, 'end')
  393. }
  394. }
  395. stringify(node, semicolon) {
  396. /* c8 ignore start */
  397. if (!this[node.type]) {
  398. throw new Error(
  399. 'Unknown AST node type ' +
  400. node.type +
  401. '. ' +
  402. 'Maybe you need to change PostCSS stringifier.'
  403. )
  404. }
  405. /* c8 ignore stop */
  406. this[node.type](node, semicolon)
  407. }
  408. }
  409. module.exports = Stringifier
  410. Stringifier.default = Stringifier