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.

603 lines
14 KiB

2 years ago
  1. 'use strict'
  2. let Declaration = require('./declaration')
  3. let tokenizer = require('./tokenize')
  4. let Comment = require('./comment')
  5. let AtRule = require('./at-rule')
  6. let Root = require('./root')
  7. let Rule = require('./rule')
  8. const SAFE_COMMENT_NEIGHBOR = {
  9. empty: true,
  10. space: true
  11. }
  12. function findLastWithPosition(tokens) {
  13. for (let i = tokens.length - 1; i >= 0; i--) {
  14. let token = tokens[i]
  15. let pos = token[3] || token[2]
  16. if (pos) return pos
  17. }
  18. }
  19. class Parser {
  20. constructor(input) {
  21. this.input = input
  22. this.root = new Root()
  23. this.current = this.root
  24. this.spaces = ''
  25. this.semicolon = false
  26. this.customProperty = false
  27. this.createTokenizer()
  28. this.root.source = { input, start: { offset: 0, line: 1, column: 1 } }
  29. }
  30. createTokenizer() {
  31. this.tokenizer = tokenizer(this.input)
  32. }
  33. parse() {
  34. let token
  35. while (!this.tokenizer.endOfFile()) {
  36. token = this.tokenizer.nextToken()
  37. switch (token[0]) {
  38. case 'space':
  39. this.spaces += token[1]
  40. break
  41. case ';':
  42. this.freeSemicolon(token)
  43. break
  44. case '}':
  45. this.end(token)
  46. break
  47. case 'comment':
  48. this.comment(token)
  49. break
  50. case 'at-word':
  51. this.atrule(token)
  52. break
  53. case '{':
  54. this.emptyRule(token)
  55. break
  56. default:
  57. this.other(token)
  58. break
  59. }
  60. }
  61. this.endFile()
  62. }
  63. comment(token) {
  64. let node = new Comment()
  65. this.init(node, token[2])
  66. node.source.end = this.getPosition(token[3] || token[2])
  67. let text = token[1].slice(2, -2)
  68. if (/^\s*$/.test(text)) {
  69. node.text = ''
  70. node.raws.left = text
  71. node.raws.right = ''
  72. } else {
  73. let match = text.match(/^(\s*)([^]*\S)(\s*)$/)
  74. node.text = match[2]
  75. node.raws.left = match[1]
  76. node.raws.right = match[3]
  77. }
  78. }
  79. emptyRule(token) {
  80. let node = new Rule()
  81. this.init(node, token[2])
  82. node.selector = ''
  83. node.raws.between = ''
  84. this.current = node
  85. }
  86. other(start) {
  87. let end = false
  88. let type = null
  89. let colon = false
  90. let bracket = null
  91. let brackets = []
  92. let customProperty = start[1].startsWith('--')
  93. let tokens = []
  94. let token = start
  95. while (token) {
  96. type = token[0]
  97. tokens.push(token)
  98. if (type === '(' || type === '[') {
  99. if (!bracket) bracket = token
  100. brackets.push(type === '(' ? ')' : ']')
  101. } else if (customProperty && colon && type === '{') {
  102. if (!bracket) bracket = token
  103. brackets.push('}')
  104. } else if (brackets.length === 0) {
  105. if (type === ';') {
  106. if (colon) {
  107. this.decl(tokens, customProperty)
  108. return
  109. } else {
  110. break
  111. }
  112. } else if (type === '{') {
  113. this.rule(tokens)
  114. return
  115. } else if (type === '}') {
  116. this.tokenizer.back(tokens.pop())
  117. end = true
  118. break
  119. } else if (type === ':') {
  120. colon = true
  121. }
  122. } else if (type === brackets[brackets.length - 1]) {
  123. brackets.pop()
  124. if (brackets.length === 0) bracket = null
  125. }
  126. token = this.tokenizer.nextToken()
  127. }
  128. if (this.tokenizer.endOfFile()) end = true
  129. if (brackets.length > 0) this.unclosedBracket(bracket)
  130. if (end && colon) {
  131. if (!customProperty) {
  132. while (tokens.length) {
  133. token = tokens[tokens.length - 1][0]
  134. if (token !== 'space' && token !== 'comment') break
  135. this.tokenizer.back(tokens.pop())
  136. }
  137. }
  138. this.decl(tokens, customProperty)
  139. } else {
  140. this.unknownWord(tokens)
  141. }
  142. }
  143. rule(tokens) {
  144. tokens.pop()
  145. let node = new Rule()
  146. this.init(node, tokens[0][2])
  147. node.raws.between = this.spacesAndCommentsFromEnd(tokens)
  148. this.raw(node, 'selector', tokens)
  149. this.current = node
  150. }
  151. decl(tokens, customProperty) {
  152. let node = new Declaration()
  153. this.init(node, tokens[0][2])
  154. let last = tokens[tokens.length - 1]
  155. if (last[0] === ';') {
  156. this.semicolon = true
  157. tokens.pop()
  158. }
  159. node.source.end = this.getPosition(
  160. last[3] || last[2] || findLastWithPosition(tokens)
  161. )
  162. while (tokens[0][0] !== 'word') {
  163. if (tokens.length === 1) this.unknownWord(tokens)
  164. node.raws.before += tokens.shift()[1]
  165. }
  166. node.source.start = this.getPosition(tokens[0][2])
  167. node.prop = ''
  168. while (tokens.length) {
  169. let type = tokens[0][0]
  170. if (type === ':' || type === 'space' || type === 'comment') {
  171. break
  172. }
  173. node.prop += tokens.shift()[1]
  174. }
  175. node.raws.between = ''
  176. let token
  177. while (tokens.length) {
  178. token = tokens.shift()
  179. if (token[0] === ':') {
  180. node.raws.between += token[1]
  181. break
  182. } else {
  183. if (token[0] === 'word' && /\w/.test(token[1])) {
  184. this.unknownWord([token])
  185. }
  186. node.raws.between += token[1]
  187. }
  188. }
  189. if (node.prop[0] === '_' || node.prop[0] === '*') {
  190. node.raws.before += node.prop[0]
  191. node.prop = node.prop.slice(1)
  192. }
  193. let firstSpaces = []
  194. let next
  195. while (tokens.length) {
  196. next = tokens[0][0]
  197. if (next !== 'space' && next !== 'comment') break
  198. firstSpaces.push(tokens.shift())
  199. }
  200. this.precheckMissedSemicolon(tokens)
  201. for (let i = tokens.length - 1; i >= 0; i--) {
  202. token = tokens[i]
  203. if (token[1].toLowerCase() === '!important') {
  204. node.important = true
  205. let string = this.stringFrom(tokens, i)
  206. string = this.spacesFromEnd(tokens) + string
  207. if (string !== ' !important') node.raws.important = string
  208. break
  209. } else if (token[1].toLowerCase() === 'important') {
  210. let cache = tokens.slice(0)
  211. let str = ''
  212. for (let j = i; j > 0; j--) {
  213. let type = cache[j][0]
  214. if (str.trim().indexOf('!') === 0 && type !== 'space') {
  215. break
  216. }
  217. str = cache.pop()[1] + str
  218. }
  219. if (str.trim().indexOf('!') === 0) {
  220. node.important = true
  221. node.raws.important = str
  222. tokens = cache
  223. }
  224. }
  225. if (token[0] !== 'space' && token[0] !== 'comment') {
  226. break
  227. }
  228. }
  229. let hasWord = tokens.some(i => i[0] !== 'space' && i[0] !== 'comment')
  230. if (hasWord) {
  231. node.raws.between += firstSpaces.map(i => i[1]).join('')
  232. firstSpaces = []
  233. }
  234. this.raw(node, 'value', firstSpaces.concat(tokens), customProperty)
  235. if (node.value.includes(':') && !customProperty) {
  236. this.checkMissedSemicolon(tokens)
  237. }
  238. }
  239. atrule(token) {
  240. let node = new AtRule()
  241. node.name = token[1].slice(1)
  242. if (node.name === '') {
  243. this.unnamedAtrule(node, token)
  244. }
  245. this.init(node, token[2])
  246. let type
  247. let prev
  248. let shift
  249. let last = false
  250. let open = false
  251. let params = []
  252. let brackets = []
  253. while (!this.tokenizer.endOfFile()) {
  254. token = this.tokenizer.nextToken()
  255. type = token[0]
  256. if (type === '(' || type === '[') {
  257. brackets.push(type === '(' ? ')' : ']')
  258. } else if (type === '{' && brackets.length > 0) {
  259. brackets.push('}')
  260. } else if (type === brackets[brackets.length - 1]) {
  261. brackets.pop()
  262. }
  263. if (brackets.length === 0) {
  264. if (type === ';') {
  265. node.source.end = this.getPosition(token[2])
  266. this.semicolon = true
  267. break
  268. } else if (type === '{') {
  269. open = true
  270. break
  271. } else if (type === '}') {
  272. if (params.length > 0) {
  273. shift = params.length - 1
  274. prev = params[shift]
  275. while (prev && prev[0] === 'space') {
  276. prev = params[--shift]
  277. }
  278. if (prev) {
  279. node.source.end = this.getPosition(prev[3] || prev[2])
  280. }
  281. }
  282. this.end(token)
  283. break
  284. } else {
  285. params.push(token)
  286. }
  287. } else {
  288. params.push(token)
  289. }
  290. if (this.tokenizer.endOfFile()) {
  291. last = true
  292. break
  293. }
  294. }
  295. node.raws.between = this.spacesAndCommentsFromEnd(params)
  296. if (params.length) {
  297. node.raws.afterName = this.spacesAndCommentsFromStart(params)
  298. this.raw(node, 'params', params)
  299. if (last) {
  300. token = params[params.length - 1]
  301. node.source.end = this.getPosition(token[3] || token[2])
  302. this.spaces = node.raws.between
  303. node.raws.between = ''
  304. }
  305. } else {
  306. node.raws.afterName = ''
  307. node.params = ''
  308. }
  309. if (open) {
  310. node.nodes = []
  311. this.current = node
  312. }
  313. }
  314. end(token) {
  315. if (this.current.nodes && this.current.nodes.length) {
  316. this.current.raws.semicolon = this.semicolon
  317. }
  318. this.semicolon = false
  319. this.current.raws.after = (this.current.raws.after || '') + this.spaces
  320. this.spaces = ''
  321. if (this.current.parent) {
  322. this.current.source.end = this.getPosition(token[2])
  323. this.current = this.current.parent
  324. } else {
  325. this.unexpectedClose(token)
  326. }
  327. }
  328. endFile() {
  329. if (this.current.parent) this.unclosedBlock()
  330. if (this.current.nodes && this.current.nodes.length) {
  331. this.current.raws.semicolon = this.semicolon
  332. }
  333. this.current.raws.after = (this.current.raws.after || '') + this.spaces
  334. }
  335. freeSemicolon(token) {
  336. this.spaces += token[1]
  337. if (this.current.nodes) {
  338. let prev = this.current.nodes[this.current.nodes.length - 1]
  339. if (prev && prev.type === 'rule' && !prev.raws.ownSemicolon) {
  340. prev.raws.ownSemicolon = this.spaces
  341. this.spaces = ''
  342. }
  343. }
  344. }
  345. // Helpers
  346. getPosition(offset) {
  347. let pos = this.input.fromOffset(offset)
  348. return {
  349. offset,
  350. line: pos.line,
  351. column: pos.col
  352. }
  353. }
  354. init(node, offset) {
  355. this.current.push(node)
  356. node.source = {
  357. start: this.getPosition(offset),
  358. input: this.input
  359. }
  360. node.raws.before = this.spaces
  361. this.spaces = ''
  362. if (node.type !== 'comment') this.semicolon = false
  363. }
  364. raw(node, prop, tokens, customProperty) {
  365. let token, type
  366. let length = tokens.length
  367. let value = ''
  368. let clean = true
  369. let next, prev
  370. for (let i = 0; i < length; i += 1) {
  371. token = tokens[i]
  372. type = token[0]
  373. if (type === 'space' && i === length - 1 && !customProperty) {
  374. clean = false
  375. } else if (type === 'comment') {
  376. prev = tokens[i - 1] ? tokens[i - 1][0] : 'empty'
  377. next = tokens[i + 1] ? tokens[i + 1][0] : 'empty'
  378. if (!SAFE_COMMENT_NEIGHBOR[prev] && !SAFE_COMMENT_NEIGHBOR[next]) {
  379. if (value.slice(-1) === ',') {
  380. clean = false
  381. } else {
  382. value += token[1]
  383. }
  384. } else {
  385. clean = false
  386. }
  387. } else {
  388. value += token[1]
  389. }
  390. }
  391. if (!clean) {
  392. let raw = tokens.reduce((all, i) => all + i[1], '')
  393. node.raws[prop] = { value, raw }
  394. }
  395. node[prop] = value
  396. }
  397. spacesAndCommentsFromEnd(tokens) {
  398. let lastTokenType
  399. let spaces = ''
  400. while (tokens.length) {
  401. lastTokenType = tokens[tokens.length - 1][0]
  402. if (lastTokenType !== 'space' && lastTokenType !== 'comment') break
  403. spaces = tokens.pop()[1] + spaces
  404. }
  405. return spaces
  406. }
  407. spacesAndCommentsFromStart(tokens) {
  408. let next
  409. let spaces = ''
  410. while (tokens.length) {
  411. next = tokens[0][0]
  412. if (next !== 'space' && next !== 'comment') break
  413. spaces += tokens.shift()[1]
  414. }
  415. return spaces
  416. }
  417. spacesFromEnd(tokens) {
  418. let lastTokenType
  419. let spaces = ''
  420. while (tokens.length) {
  421. lastTokenType = tokens[tokens.length - 1][0]
  422. if (lastTokenType !== 'space') break
  423. spaces = tokens.pop()[1] + spaces
  424. }
  425. return spaces
  426. }
  427. stringFrom(tokens, from) {
  428. let result = ''
  429. for (let i = from; i < tokens.length; i++) {
  430. result += tokens[i][1]
  431. }
  432. tokens.splice(from, tokens.length - from)
  433. return result
  434. }
  435. colon(tokens) {
  436. let brackets = 0
  437. let token, type, prev
  438. for (let [i, element] of tokens.entries()) {
  439. token = element
  440. type = token[0]
  441. if (type === '(') {
  442. brackets += 1
  443. }
  444. if (type === ')') {
  445. brackets -= 1
  446. }
  447. if (brackets === 0 && type === ':') {
  448. if (!prev) {
  449. this.doubleColon(token)
  450. } else if (prev[0] === 'word' && prev[1] === 'progid') {
  451. continue
  452. } else {
  453. return i
  454. }
  455. }
  456. prev = token
  457. }
  458. return false
  459. }
  460. // Errors
  461. unclosedBracket(bracket) {
  462. throw this.input.error(
  463. 'Unclosed bracket',
  464. { offset: bracket[2] },
  465. { offset: bracket[2] + 1 }
  466. )
  467. }
  468. unknownWord(tokens) {
  469. throw this.input.error(
  470. 'Unknown word',
  471. { offset: tokens[0][2] },
  472. { offset: tokens[0][2] + tokens[0][1].length }
  473. )
  474. }
  475. unexpectedClose(token) {
  476. throw this.input.error(
  477. 'Unexpected }',
  478. { offset: token[2] },
  479. { offset: token[2] + 1 }
  480. )
  481. }
  482. unclosedBlock() {
  483. let pos = this.current.source.start
  484. throw this.input.error('Unclosed block', pos.line, pos.column)
  485. }
  486. doubleColon(token) {
  487. throw this.input.error(
  488. 'Double colon',
  489. { offset: token[2] },
  490. { offset: token[2] + token[1].length }
  491. )
  492. }
  493. unnamedAtrule(node, token) {
  494. throw this.input.error(
  495. 'At-rule without name',
  496. { offset: token[2] },
  497. { offset: token[2] + token[1].length }
  498. )
  499. }
  500. precheckMissedSemicolon(/* tokens */) {
  501. // Hook for Safe Parser
  502. }
  503. checkMissedSemicolon(tokens) {
  504. let colon = this.colon(tokens)
  505. if (colon === false) return
  506. let founded = 0
  507. let token
  508. for (let j = colon - 1; j >= 0; j--) {
  509. token = tokens[j]
  510. if (token[0] !== 'space') {
  511. founded += 1
  512. if (founded === 2) break
  513. }
  514. }
  515. // If the token is a word, e.g. `!important`, `red` or any other valid property's value.
  516. // Then we need to return the colon after that word token. [3] is the "end" colon of that word.
  517. // And because we need it after that one we do +1 to get the next one.
  518. throw this.input.error(
  519. 'Missed semicolon',
  520. token[0] === 'word' ? token[3] + 1 : token[2]
  521. )
  522. }
  523. }
  524. module.exports = Parser