You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

379 lines
8.3 KiB

2 years ago
  1. 'use strict'
  2. let { isClean, my } = require('./symbols')
  3. let CssSyntaxError = require('./css-syntax-error')
  4. let Stringifier = require('./stringifier')
  5. let stringify = require('./stringify')
  6. function cloneNode(obj, parent) {
  7. let cloned = new obj.constructor()
  8. for (let i in obj) {
  9. if (!Object.prototype.hasOwnProperty.call(obj, i)) {
  10. /* c8 ignore next 2 */
  11. continue
  12. }
  13. if (i === 'proxyCache') continue
  14. let value = obj[i]
  15. let type = typeof value
  16. if (i === 'parent' && type === 'object') {
  17. if (parent) cloned[i] = parent
  18. } else if (i === 'source') {
  19. cloned[i] = value
  20. } else if (Array.isArray(value)) {
  21. cloned[i] = value.map(j => cloneNode(j, cloned))
  22. } else {
  23. if (type === 'object' && value !== null) value = cloneNode(value)
  24. cloned[i] = value
  25. }
  26. }
  27. return cloned
  28. }
  29. class Node {
  30. constructor(defaults = {}) {
  31. this.raws = {}
  32. this[isClean] = false
  33. this[my] = true
  34. for (let name in defaults) {
  35. if (name === 'nodes') {
  36. this.nodes = []
  37. for (let node of defaults[name]) {
  38. if (typeof node.clone === 'function') {
  39. this.append(node.clone())
  40. } else {
  41. this.append(node)
  42. }
  43. }
  44. } else {
  45. this[name] = defaults[name]
  46. }
  47. }
  48. }
  49. error(message, opts = {}) {
  50. if (this.source) {
  51. let { start, end } = this.rangeBy(opts)
  52. return this.source.input.error(
  53. message,
  54. { line: start.line, column: start.column },
  55. { line: end.line, column: end.column },
  56. opts
  57. )
  58. }
  59. return new CssSyntaxError(message)
  60. }
  61. warn(result, text, opts) {
  62. let data = { node: this }
  63. for (let i in opts) data[i] = opts[i]
  64. return result.warn(text, data)
  65. }
  66. remove() {
  67. if (this.parent) {
  68. this.parent.removeChild(this)
  69. }
  70. this.parent = undefined
  71. return this
  72. }
  73. toString(stringifier = stringify) {
  74. if (stringifier.stringify) stringifier = stringifier.stringify
  75. let result = ''
  76. stringifier(this, i => {
  77. result += i
  78. })
  79. return result
  80. }
  81. assign(overrides = {}) {
  82. for (let name in overrides) {
  83. this[name] = overrides[name]
  84. }
  85. return this
  86. }
  87. clone(overrides = {}) {
  88. let cloned = cloneNode(this)
  89. for (let name in overrides) {
  90. cloned[name] = overrides[name]
  91. }
  92. return cloned
  93. }
  94. cloneBefore(overrides = {}) {
  95. let cloned = this.clone(overrides)
  96. this.parent.insertBefore(this, cloned)
  97. return cloned
  98. }
  99. cloneAfter(overrides = {}) {
  100. let cloned = this.clone(overrides)
  101. this.parent.insertAfter(this, cloned)
  102. return cloned
  103. }
  104. replaceWith(...nodes) {
  105. if (this.parent) {
  106. let bookmark = this
  107. let foundSelf = false
  108. for (let node of nodes) {
  109. if (node === this) {
  110. foundSelf = true
  111. } else if (foundSelf) {
  112. this.parent.insertAfter(bookmark, node)
  113. bookmark = node
  114. } else {
  115. this.parent.insertBefore(bookmark, node)
  116. }
  117. }
  118. if (!foundSelf) {
  119. this.remove()
  120. }
  121. }
  122. return this
  123. }
  124. next() {
  125. if (!this.parent) return undefined
  126. let index = this.parent.index(this)
  127. return this.parent.nodes[index + 1]
  128. }
  129. prev() {
  130. if (!this.parent) return undefined
  131. let index = this.parent.index(this)
  132. return this.parent.nodes[index - 1]
  133. }
  134. before(add) {
  135. this.parent.insertBefore(this, add)
  136. return this
  137. }
  138. after(add) {
  139. this.parent.insertAfter(this, add)
  140. return this
  141. }
  142. root() {
  143. let result = this
  144. while (result.parent && result.parent.type !== 'document') {
  145. result = result.parent
  146. }
  147. return result
  148. }
  149. raw(prop, defaultType) {
  150. let str = new Stringifier()
  151. return str.raw(this, prop, defaultType)
  152. }
  153. cleanRaws(keepBetween) {
  154. delete this.raws.before
  155. delete this.raws.after
  156. if (!keepBetween) delete this.raws.between
  157. }
  158. toJSON(_, inputs) {
  159. let fixed = {}
  160. let emitInputs = inputs == null
  161. inputs = inputs || new Map()
  162. let inputsNextIndex = 0
  163. for (let name in this) {
  164. if (!Object.prototype.hasOwnProperty.call(this, name)) {
  165. /* c8 ignore next 2 */
  166. continue
  167. }
  168. if (name === 'parent' || name === 'proxyCache') continue
  169. let value = this[name]
  170. if (Array.isArray(value)) {
  171. fixed[name] = value.map(i => {
  172. if (typeof i === 'object' && i.toJSON) {
  173. return i.toJSON(null, inputs)
  174. } else {
  175. return i
  176. }
  177. })
  178. } else if (typeof value === 'object' && value.toJSON) {
  179. fixed[name] = value.toJSON(null, inputs)
  180. } else if (name === 'source') {
  181. let inputId = inputs.get(value.input)
  182. if (inputId == null) {
  183. inputId = inputsNextIndex
  184. inputs.set(value.input, inputsNextIndex)
  185. inputsNextIndex++
  186. }
  187. fixed[name] = {
  188. inputId,
  189. start: value.start,
  190. end: value.end
  191. }
  192. } else {
  193. fixed[name] = value
  194. }
  195. }
  196. if (emitInputs) {
  197. fixed.inputs = [...inputs.keys()].map(input => input.toJSON())
  198. }
  199. return fixed
  200. }
  201. positionInside(index) {
  202. let string = this.toString()
  203. let column = this.source.start.column
  204. let line = this.source.start.line
  205. for (let i = 0; i < index; i++) {
  206. if (string[i] === '\n') {
  207. column = 1
  208. line += 1
  209. } else {
  210. column += 1
  211. }
  212. }
  213. return { line, column }
  214. }
  215. positionBy(opts) {
  216. let pos = this.source.start
  217. if (opts.index) {
  218. pos = this.positionInside(opts.index)
  219. } else if (opts.word) {
  220. let index = this.toString().indexOf(opts.word)
  221. if (index !== -1) pos = this.positionInside(index)
  222. }
  223. return pos
  224. }
  225. rangeBy(opts) {
  226. let start = {
  227. line: this.source.start.line,
  228. column: this.source.start.column
  229. }
  230. let end = this.source.end
  231. ? {
  232. line: this.source.end.line,
  233. column: this.source.end.column + 1
  234. }
  235. : {
  236. line: start.line,
  237. column: start.column + 1
  238. }
  239. if (opts.word) {
  240. let index = this.toString().indexOf(opts.word)
  241. if (index !== -1) {
  242. start = this.positionInside(index)
  243. end = this.positionInside(index + opts.word.length)
  244. }
  245. } else {
  246. if (opts.start) {
  247. start = {
  248. line: opts.start.line,
  249. column: opts.start.column
  250. }
  251. } else if (opts.index) {
  252. start = this.positionInside(opts.index)
  253. }
  254. if (opts.end) {
  255. end = {
  256. line: opts.end.line,
  257. column: opts.end.column
  258. }
  259. } else if (opts.endIndex) {
  260. end = this.positionInside(opts.endIndex)
  261. } else if (opts.index) {
  262. end = this.positionInside(opts.index + 1)
  263. }
  264. }
  265. if (
  266. end.line < start.line ||
  267. (end.line === start.line && end.column <= start.column)
  268. ) {
  269. end = { line: start.line, column: start.column + 1 }
  270. }
  271. return { start, end }
  272. }
  273. getProxyProcessor() {
  274. return {
  275. set(node, prop, value) {
  276. if (node[prop] === value) return true
  277. node[prop] = value
  278. if (
  279. prop === 'prop' ||
  280. prop === 'value' ||
  281. prop === 'name' ||
  282. prop === 'params' ||
  283. prop === 'important' ||
  284. /* c8 ignore next */
  285. prop === 'text'
  286. ) {
  287. node.markDirty()
  288. }
  289. return true
  290. },
  291. get(node, prop) {
  292. if (prop === 'proxyOf') {
  293. return node
  294. } else if (prop === 'root') {
  295. return () => node.root().toProxy()
  296. } else {
  297. return node[prop]
  298. }
  299. }
  300. }
  301. }
  302. toProxy() {
  303. if (!this.proxyCache) {
  304. this.proxyCache = new Proxy(this, this.getProxyProcessor())
  305. }
  306. return this.proxyCache
  307. }
  308. addToError(error) {
  309. error.postcssNode = this
  310. if (error.stack && this.source && /\n\s{4}at /.test(error.stack)) {
  311. let s = this.source
  312. error.stack = error.stack.replace(
  313. /\n\s{4}at /,
  314. `$&${s.input.from}:${s.start.line}:${s.start.column}$&`
  315. )
  316. }
  317. return error
  318. }
  319. markDirty() {
  320. if (this[isClean]) {
  321. this[isClean] = false
  322. let next = this
  323. while ((next = next.parent)) {
  324. next[isClean] = false
  325. }
  326. }
  327. }
  328. get proxyOf() {
  329. return this
  330. }
  331. }
  332. module.exports = Node
  333. Node.default = Node