stringifier.js 12 KB

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