123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603 |
- 'use strict'
- let Declaration = require('./declaration')
- let tokenizer = require('./tokenize')
- let Comment = require('./comment')
- let AtRule = require('./at-rule')
- let Root = require('./root')
- let Rule = require('./rule')
- const SAFE_COMMENT_NEIGHBOR = {
- empty: true,
- space: true
- }
- function findLastWithPosition(tokens) {
- for (let i = tokens.length - 1; i >= 0; i--) {
- let token = tokens[i]
- let pos = token[3] || token[2]
- if (pos) return pos
- }
- }
- class Parser {
- constructor(input) {
- this.input = input
- this.root = new Root()
- this.current = this.root
- this.spaces = ''
- this.semicolon = false
- this.customProperty = false
- this.createTokenizer()
- this.root.source = { input, start: { column: 1, line: 1, offset: 0 } }
- }
- atrule(token) {
- let node = new AtRule()
- node.name = token[1].slice(1)
- if (node.name === '') {
- this.unnamedAtrule(node, token)
- }
- this.init(node, token[2])
- let type
- let prev
- let shift
- let last = false
- let open = false
- let params = []
- let brackets = []
- while (!this.tokenizer.endOfFile()) {
- token = this.tokenizer.nextToken()
- type = token[0]
- if (type === '(' || type === '[') {
- brackets.push(type === '(' ? ')' : ']')
- } else if (type === '{' && brackets.length > 0) {
- brackets.push('}')
- } else if (type === brackets[brackets.length - 1]) {
- brackets.pop()
- }
- if (brackets.length === 0) {
- if (type === ';') {
- node.source.end = this.getPosition(token[2])
- this.semicolon = true
- break
- } else if (type === '{') {
- open = true
- break
- } else if (type === '}') {
- if (params.length > 0) {
- shift = params.length - 1
- prev = params[shift]
- while (prev && prev[0] === 'space') {
- prev = params[--shift]
- }
- if (prev) {
- node.source.end = this.getPosition(prev[3] || prev[2])
- }
- }
- this.end(token)
- break
- } else {
- params.push(token)
- }
- } else {
- params.push(token)
- }
- if (this.tokenizer.endOfFile()) {
- last = true
- break
- }
- }
- node.raws.between = this.spacesAndCommentsFromEnd(params)
- if (params.length) {
- node.raws.afterName = this.spacesAndCommentsFromStart(params)
- this.raw(node, 'params', params)
- if (last) {
- token = params[params.length - 1]
- node.source.end = this.getPosition(token[3] || token[2])
- this.spaces = node.raws.between
- node.raws.between = ''
- }
- } else {
- node.raws.afterName = ''
- node.params = ''
- }
- if (open) {
- node.nodes = []
- this.current = node
- }
- }
- checkMissedSemicolon(tokens) {
- let colon = this.colon(tokens)
- if (colon === false) return
- let founded = 0
- let token
- for (let j = colon - 1; j >= 0; j--) {
- token = tokens[j]
- if (token[0] !== 'space') {
- founded += 1
- if (founded === 2) break
- }
- }
- // If the token is a word, e.g. `!important`, `red` or any other valid property's value.
- // Then we need to return the colon after that word token. [3] is the "end" colon of that word.
- // And because we need it after that one we do +1 to get the next one.
- throw this.input.error(
- 'Missed semicolon',
- token[0] === 'word' ? token[3] + 1 : token[2]
- )
- }
- colon(tokens) {
- let brackets = 0
- let token, type, prev
- for (let [i, element] of tokens.entries()) {
- token = element
- type = token[0]
- if (type === '(') {
- brackets += 1
- }
- if (type === ')') {
- brackets -= 1
- }
- if (brackets === 0 && type === ':') {
- if (!prev) {
- this.doubleColon(token)
- } else if (prev[0] === 'word' && prev[1] === 'progid') {
- continue
- } else {
- return i
- }
- }
- prev = token
- }
- return false
- }
- comment(token) {
- let node = new Comment()
- this.init(node, token[2])
- node.source.end = this.getPosition(token[3] || token[2])
- let text = token[1].slice(2, -2)
- if (/^\s*$/.test(text)) {
- node.text = ''
- node.raws.left = text
- node.raws.right = ''
- } else {
- let match = text.match(/^(\s*)([^]*\S)(\s*)$/)
- node.text = match[2]
- node.raws.left = match[1]
- node.raws.right = match[3]
- }
- }
- createTokenizer() {
- this.tokenizer = tokenizer(this.input)
- }
- decl(tokens, customProperty) {
- let node = new Declaration()
- this.init(node, tokens[0][2])
- let last = tokens[tokens.length - 1]
- if (last[0] === ';') {
- this.semicolon = true
- tokens.pop()
- }
- node.source.end = this.getPosition(
- last[3] || last[2] || findLastWithPosition(tokens)
- )
- while (tokens[0][0] !== 'word') {
- if (tokens.length === 1) this.unknownWord(tokens)
- node.raws.before += tokens.shift()[1]
- }
- node.source.start = this.getPosition(tokens[0][2])
- node.prop = ''
- while (tokens.length) {
- let type = tokens[0][0]
- if (type === ':' || type === 'space' || type === 'comment') {
- break
- }
- node.prop += tokens.shift()[1]
- }
- node.raws.between = ''
- let token
- while (tokens.length) {
- token = tokens.shift()
- if (token[0] === ':') {
- node.raws.between += token[1]
- break
- } else {
- if (token[0] === 'word' && /\w/.test(token[1])) {
- this.unknownWord([token])
- }
- node.raws.between += token[1]
- }
- }
- if (node.prop[0] === '_' || node.prop[0] === '*') {
- node.raws.before += node.prop[0]
- node.prop = node.prop.slice(1)
- }
- let firstSpaces = []
- let next
- while (tokens.length) {
- next = tokens[0][0]
- if (next !== 'space' && next !== 'comment') break
- firstSpaces.push(tokens.shift())
- }
- this.precheckMissedSemicolon(tokens)
- for (let i = tokens.length - 1; i >= 0; i--) {
- token = tokens[i]
- if (token[1].toLowerCase() === '!important') {
- node.important = true
- let string = this.stringFrom(tokens, i)
- string = this.spacesFromEnd(tokens) + string
- if (string !== ' !important') node.raws.important = string
- break
- } else if (token[1].toLowerCase() === 'important') {
- let cache = tokens.slice(0)
- let str = ''
- for (let j = i; j > 0; j--) {
- let type = cache[j][0]
- if (str.trim().indexOf('!') === 0 && type !== 'space') {
- break
- }
- str = cache.pop()[1] + str
- }
- if (str.trim().indexOf('!') === 0) {
- node.important = true
- node.raws.important = str
- tokens = cache
- }
- }
- if (token[0] !== 'space' && token[0] !== 'comment') {
- break
- }
- }
- let hasWord = tokens.some(i => i[0] !== 'space' && i[0] !== 'comment')
- if (hasWord) {
- node.raws.between += firstSpaces.map(i => i[1]).join('')
- firstSpaces = []
- }
- this.raw(node, 'value', firstSpaces.concat(tokens), customProperty)
- if (node.value.includes(':') && !customProperty) {
- this.checkMissedSemicolon(tokens)
- }
- }
- doubleColon(token) {
- throw this.input.error(
- 'Double colon',
- { offset: token[2] },
- { offset: token[2] + token[1].length }
- )
- }
- emptyRule(token) {
- let node = new Rule()
- this.init(node, token[2])
- node.selector = ''
- node.raws.between = ''
- this.current = node
- }
- end(token) {
- if (this.current.nodes && this.current.nodes.length) {
- this.current.raws.semicolon = this.semicolon
- }
- this.semicolon = false
- this.current.raws.after = (this.current.raws.after || '') + this.spaces
- this.spaces = ''
- if (this.current.parent) {
- this.current.source.end = this.getPosition(token[2])
- this.current = this.current.parent
- } else {
- this.unexpectedClose(token)
- }
- }
- endFile() {
- if (this.current.parent) this.unclosedBlock()
- if (this.current.nodes && this.current.nodes.length) {
- this.current.raws.semicolon = this.semicolon
- }
- this.current.raws.after = (this.current.raws.after || '') + this.spaces
- }
- freeSemicolon(token) {
- this.spaces += token[1]
- if (this.current.nodes) {
- let prev = this.current.nodes[this.current.nodes.length - 1]
- if (prev && prev.type === 'rule' && !prev.raws.ownSemicolon) {
- prev.raws.ownSemicolon = this.spaces
- this.spaces = ''
- }
- }
- }
- // Helpers
- getPosition(offset) {
- let pos = this.input.fromOffset(offset)
- return {
- column: pos.col,
- line: pos.line,
- offset
- }
- }
- init(node, offset) {
- this.current.push(node)
- node.source = {
- input: this.input,
- start: this.getPosition(offset)
- }
- node.raws.before = this.spaces
- this.spaces = ''
- if (node.type !== 'comment') this.semicolon = false
- }
- other(start) {
- let end = false
- let type = null
- let colon = false
- let bracket = null
- let brackets = []
- let customProperty = start[1].startsWith('--')
- let tokens = []
- let token = start
- while (token) {
- type = token[0]
- tokens.push(token)
- if (type === '(' || type === '[') {
- if (!bracket) bracket = token
- brackets.push(type === '(' ? ')' : ']')
- } else if (customProperty && colon && type === '{') {
- if (!bracket) bracket = token
- brackets.push('}')
- } else if (brackets.length === 0) {
- if (type === ';') {
- if (colon) {
- this.decl(tokens, customProperty)
- return
- } else {
- break
- }
- } else if (type === '{') {
- this.rule(tokens)
- return
- } else if (type === '}') {
- this.tokenizer.back(tokens.pop())
- end = true
- break
- } else if (type === ':') {
- colon = true
- }
- } else if (type === brackets[brackets.length - 1]) {
- brackets.pop()
- if (brackets.length === 0) bracket = null
- }
- token = this.tokenizer.nextToken()
- }
- if (this.tokenizer.endOfFile()) end = true
- if (brackets.length > 0) this.unclosedBracket(bracket)
- if (end && colon) {
- if (!customProperty) {
- while (tokens.length) {
- token = tokens[tokens.length - 1][0]
- if (token !== 'space' && token !== 'comment') break
- this.tokenizer.back(tokens.pop())
- }
- }
- this.decl(tokens, customProperty)
- } else {
- this.unknownWord(tokens)
- }
- }
- parse() {
- let token
- while (!this.tokenizer.endOfFile()) {
- token = this.tokenizer.nextToken()
- switch (token[0]) {
- case 'space':
- this.spaces += token[1]
- break
- case ';':
- this.freeSemicolon(token)
- break
- case '}':
- this.end(token)
- break
- case 'comment':
- this.comment(token)
- break
- case 'at-word':
- this.atrule(token)
- break
- case '{':
- this.emptyRule(token)
- break
- default:
- this.other(token)
- break
- }
- }
- this.endFile()
- }
- precheckMissedSemicolon(/* tokens */) {
- // Hook for Safe Parser
- }
- raw(node, prop, tokens, customProperty) {
- let token, type
- let length = tokens.length
- let value = ''
- let clean = true
- let next, prev
- for (let i = 0; i < length; i += 1) {
- token = tokens[i]
- type = token[0]
- if (type === 'space' && i === length - 1 && !customProperty) {
- clean = false
- } else if (type === 'comment') {
- prev = tokens[i - 1] ? tokens[i - 1][0] : 'empty'
- next = tokens[i + 1] ? tokens[i + 1][0] : 'empty'
- if (!SAFE_COMMENT_NEIGHBOR[prev] && !SAFE_COMMENT_NEIGHBOR[next]) {
- if (value.slice(-1) === ',') {
- clean = false
- } else {
- value += token[1]
- }
- } else {
- clean = false
- }
- } else {
- value += token[1]
- }
- }
- if (!clean) {
- let raw = tokens.reduce((all, i) => all + i[1], '')
- node.raws[prop] = { raw, value }
- }
- node[prop] = value
- }
- rule(tokens) {
- tokens.pop()
- let node = new Rule()
- this.init(node, tokens[0][2])
- node.raws.between = this.spacesAndCommentsFromEnd(tokens)
- this.raw(node, 'selector', tokens)
- this.current = node
- }
- spacesAndCommentsFromEnd(tokens) {
- let lastTokenType
- let spaces = ''
- while (tokens.length) {
- lastTokenType = tokens[tokens.length - 1][0]
- if (lastTokenType !== 'space' && lastTokenType !== 'comment') break
- spaces = tokens.pop()[1] + spaces
- }
- return spaces
- }
- // Errors
- spacesAndCommentsFromStart(tokens) {
- let next
- let spaces = ''
- while (tokens.length) {
- next = tokens[0][0]
- if (next !== 'space' && next !== 'comment') break
- spaces += tokens.shift()[1]
- }
- return spaces
- }
- spacesFromEnd(tokens) {
- let lastTokenType
- let spaces = ''
- while (tokens.length) {
- lastTokenType = tokens[tokens.length - 1][0]
- if (lastTokenType !== 'space') break
- spaces = tokens.pop()[1] + spaces
- }
- return spaces
- }
- stringFrom(tokens, from) {
- let result = ''
- for (let i = from; i < tokens.length; i++) {
- result += tokens[i][1]
- }
- tokens.splice(from, tokens.length - from)
- return result
- }
- unclosedBlock() {
- let pos = this.current.source.start
- throw this.input.error('Unclosed block', pos.line, pos.column)
- }
- unclosedBracket(bracket) {
- throw this.input.error(
- 'Unclosed bracket',
- { offset: bracket[2] },
- { offset: bracket[2] + 1 }
- )
- }
- unexpectedClose(token) {
- throw this.input.error(
- 'Unexpected }',
- { offset: token[2] },
- { offset: token[2] + 1 }
- )
- }
- unknownWord(tokens) {
- throw this.input.error(
- 'Unknown word',
- { offset: tokens[0][2] },
- { offset: tokens[0][2] + tokens[0][1].length }
- )
- }
- unnamedAtrule(node, token) {
- throw this.input.error(
- 'At-rule without name',
- { offset: token[2] },
- { offset: token[2] + token[1].length }
- )
- }
- }
- module.exports = Parser
|