AK-21/Graphite-Industrial-Intelligence
0
1'use strict'2 3let AtRule = require('./at-rule')4let Comment = require('./comment')5let Declaration = require('./declaration')6let Root = require('./root')7let Rule = require('./rule')8let tokenizer = require('./tokenize')9 10const SAFE_COMMENT_NEIGHBOR = {11 empty: true,12 space: true13}14 15function findLastWithPosition(tokens) {16 for (let i = tokens.length - 1; i >= 0; i--) {17 let token = tokens[i]18 let pos = token[3] || token[2]19 if (pos) return pos20 }21}22 23function tokensToString(tokens, from, to) {24 let result = ''25 for (let i = from; i < to; i++) result += tokens[i][1]26 return result27}28 29class Parser {30 constructor(input) {31 this.input = input32 33 this.root = new Root()34 this.current = this.root35 this.spaces = ''36 this.semicolon = false37 38 this.createTokenizer()39 this.root.source = { input, start: { column: 1, line: 1, offset: 0 } }40 }41 42 atrule(token) {43 let node = new AtRule()44 node.name = token[1].slice(1)45 if (node.name === '') {46 this.unnamedAtrule(node, token)47 }48 this.init(node, token[2])49 50 let type51 let prev52 let shift53 let last = false54 let open = false55 let params = []56 let brackets = []57 58 while (!this.tokenizer.endOfFile()) {59 token = this.tokenizer.nextToken()60 type = token[0]61 62 if (type === '(' || type === '[') {63 brackets.push(type === '(' ? ')' : ']')64 } else if (type === '{' && brackets.length > 0) {65 brackets.push('}')66 } else if (type === brackets[brackets.length - 1]) {67 brackets.pop()68 }69 70 if (brackets.length === 0) {71 if (type === ';') {72 node.source.end = this.getPosition(token[2])73 node.source.end.offset++74 this.semicolon = true75 break76 } else if (type === '{') {77 open = true78 break79 } else if (type === '}') {80 if (params.length > 0) {81 shift = params.length - 182 prev = params[shift]83 while (prev && prev[0] === 'space') {84 prev = params[--shift]85 }86 if (prev) {87 node.source.end = this.getPosition(prev[3] || prev[2])88 node.source.end.offset++89 }90 }91 this.end(token)92 break93 } else {94 params.push(token)95 }96 } else {97 params.push(token)98 }99 100 if (this.tokenizer.endOfFile()) {101 last = true102 break103 }104 }105 106 node.raws.between = this.spacesAndCommentsFromEnd(params)107 if (params.length) {108 node.raws.afterName = this.spacesAndCommentsFromStart(params)109 this.raw(node, 'params', params)110 if (last) {111 token = params[params.length - 1]112 node.source.end = this.getPosition(token[3] || token[2])113 node.source.end.offset++114 this.spaces = node.raws.between115 node.raws.between = ''116 }117 } else {118 node.raws.afterName = ''119 node.params = ''120 }121 122 if (open) {123 node.nodes = []124 this.current = node125 }126 }127 128 checkMissedSemicolon(tokens) {129 let colon = this.colon(tokens)130 if (colon === false) return131 132 let founded = 0133 let token134 for (let j = colon - 1; j >= 0; j--) {135 token = tokens[j]136 if (token[0] !== 'space') {137 founded += 1138 if (founded === 2) break139 }140 }141 // If the token is a word, e.g. `!important`, `red` or any other valid142 // property's value. Then we need to return the colon after that word143 // token. [3] is the "end" colon of that word. And because we need it144 // after that one we do +1 to get the next one.145 throw this.input.error(146 'Missed semicolon',147 token[0] === 'word' ? token[3] + 1 : token[2]148 )149 }150 151 colon(tokens) {152 let brackets = 0153 let prev, token, type154 for (let [i, element] of tokens.entries()) {155 token = element156 type = token[0]157 158 if (type === '(') {159 brackets += 1160 }161 if (type === ')') {162 brackets -= 1163 }164 if (brackets === 0 && type === ':') {165 if (!prev) {166 this.doubleColon(token)167 } else if (prev[0] === 'word' && prev[1] === 'progid') {168 continue169 } else {170 return i171 }172 }173 174 prev = token175 }176 return false177 }178 179 comment(token) {180 let node = new Comment()181 this.init(node, token[2])182 node.source.end = this.getPosition(token[3] || token[2])183 node.source.end.offset++184 185 let text = token[1].slice(2, -2)186 if (!text.trim()) {187 node.text = ''188 node.raws.left = text189 node.raws.right = ''190 } else {191 let match = text.match(/^(\s*)([^]*\S)(\s*)$/)192 node.text = match[2]193 node.raws.left = match[1]194 node.raws.right = match[3]195 }196 }197 198 createTokenizer() {199 this.tokenizer = tokenizer(this.input)200 }201 202 decl(tokens, customProperty) {203 let node = new Declaration()204 this.init(node, tokens[0][2])205 206 let last = tokens[tokens.length - 1]207 if (last[0] === ';') {208 this.semicolon = true209 tokens.pop()210 }211 212 node.source.end = this.getPosition(213 last[3] || last[2] || findLastWithPosition(tokens)214 )215 node.source.end.offset++216 217 let start = 0218 while (tokens[start][0] !== 'word') {219 if (start === tokens.length - 1) this.unknownWord([tokens[start]])220 start++221 }222 node.raws.before += tokensToString(tokens, 0, start)223 node.source.start = this.getPosition(tokens[start][2])224 225 let propStart = start226 while (start < tokens.length) {227 let type = tokens[start][0]228 if (type === ':' || type === 'space' || type === 'comment') {229 break230 }231 start++232 }233 node.prop = tokensToString(tokens, propStart, start)234 235 let betweenStart = start236 let token237 while (start < tokens.length) {238 token = tokens[start]239 start++240 if (token[0] === ':') break241 if (token[0] === 'word' && /\w/.test(token[1])) {242 this.unknownWord([token])243 }244 }245 node.raws.between = tokensToString(tokens, betweenStart, start)246 247 if (node.prop[0] === '_' || node.prop[0] === '*') {248 node.raws.before += node.prop[0]249 node.prop = node.prop.slice(1)250 }251 252 let firstSpacesStart = start253 while (start < tokens.length) {254 let next = tokens[start][0]255 if (next !== 'space' && next !== 'comment') break256 start++257 }258 let firstSpaces = tokens.slice(firstSpacesStart, start)259 260 tokens = tokens.slice(start)261 262 this.precheckMissedSemicolon(tokens)263 264 for (let i = tokens.length - 1; i >= 0; i--) {265 token = tokens[i]266 if (token[1].toLowerCase() === '!important') {267 node.important = true268 let string = this.stringFrom(tokens, i)269 string = this.spacesFromEnd(tokens) + string270 if (string !== ' !important') node.raws.important = string271 break272 } else if (token[1].toLowerCase() === 'important') {273 let cache = tokens.slice(0)274 let str = ''275 for (let j = i; j > 0; j--) {276 let type = cache[j][0]277 if (str.trim().startsWith('!') && type !== 'space') {278 break279 }280 str = cache.pop()[1] + str281 }282 if (str.trim().startsWith('!')) {283 node.important = true284 node.raws.important = str285 tokens = cache286 }287 }288 289 if (token[0] !== 'space' && token[0] !== 'comment') {290 break291 }292 }293 294 let hasWord = tokens.some(i => i[0] !== 'space' && i[0] !== 'comment')295 296 if (hasWord) {297 node.raws.between += firstSpaces.map(i => i[1]).join('')298 firstSpaces = []299 }300 this.raw(node, 'value', firstSpaces.concat(tokens), customProperty)301 302 if (node.value.includes(':') && !customProperty) {303 this.checkMissedSemicolon(tokens)304 }305 }306 307 doubleColon(token) {308 throw this.input.error(309 'Double colon',310 { offset: token[2] },311 { offset: token[2] + token[1].length }312 )313 }314 315 emptyRule(token) {316 let node = new Rule()317 this.init(node, token[2])318 node.selector = ''319 node.raws.between = ''320 this.current = node321 }322 323 end(token) {324 if (this.current.nodes && this.current.nodes.length) {325 this.current.raws.semicolon = this.semicolon326 }327 this.semicolon = false328 329 this.current.raws.after = (this.current.raws.after || '') + this.spaces330 this.spaces = ''331 332 if (this.current.parent) {333 this.current.source.end = this.getPosition(token[2])334 this.current.source.end.offset++335 this.current = this.current.parent336 } else {337 this.unexpectedClose(token)338 }339 }340 341 endFile() {342 if (this.current.parent) this.unclosedBlock()343 if (this.current.nodes && this.current.nodes.length) {344 this.current.raws.semicolon = this.semicolon345 }346 this.current.raws.after = (this.current.raws.after || '') + this.spaces347 this.root.source.end = this.getPosition(this.tokenizer.position())348 }349 350 freeSemicolon(token) {351 this.spaces += token[1]352 if (this.current.nodes) {353 let prev = this.current.nodes[this.current.nodes.length - 1]354 if (prev && prev.type === 'rule' && !prev.raws.ownSemicolon) {355 prev.raws.ownSemicolon = this.spaces356 this.spaces = ''357 prev.source.end = this.getPosition(token[2])358 prev.source.end.offset += prev.raws.ownSemicolon.length359 }360 }361 }362 363 // Helpers364 365 getPosition(offset) {366 let pos = this.input.fromOffset(offset)367 return {368 column: pos.col,369 line: pos.line,370 offset371 }372 }373 374 init(node, offset) {375 this.current.push(node)376 node.source = {377 input: this.input,378 start: this.getPosition(offset)379 }380 node.raws.before = this.spaces381 this.spaces = ''382 if (node.type !== 'comment') this.semicolon = false383 }384 385 other(start) {386 let end = false387 let type = null388 let colon = false389 let bracket = null390 let brackets = []391 let customProperty = start[1].startsWith('--')392 393 let tokens = []394 let token = start395 while (token) {396 type = token[0]397 tokens.push(token)398 399 if (type === '(' || type === '[') {400 if (!bracket) bracket = token401 brackets.push(type === '(' ? ')' : ']')402 } else if (customProperty && colon && type === '{') {403 if (!bracket) bracket = token404 brackets.push('}')405 } else if (brackets.length === 0) {406 if (type === ';') {407 if (colon) {408 this.decl(tokens, customProperty)409 return410 } else {411 break412 }413 } else if (type === '{') {414 this.rule(tokens)415 return416 } else if (type === '}') {417 this.tokenizer.back(tokens.pop())418 end = true419 break420 } else if (type === ':') {421 colon = true422 }423 } else if (type === brackets[brackets.length - 1]) {424 brackets.pop()425 if (brackets.length === 0) bracket = null426 }427 428 token = this.tokenizer.nextToken()429 }430 431 if (this.tokenizer.endOfFile()) end = true432 if (brackets.length > 0) this.unclosedBracket(bracket)433 434 if (end && colon) {435 if (!customProperty) {436 while (tokens.length) {437 token = tokens[tokens.length - 1][0]438 if (token !== 'space' && token !== 'comment') break439 this.tokenizer.back(tokens.pop())440 }441 }442 this.decl(tokens, customProperty)443 } else {444 this.unknownWord(tokens)445 }446 }447 448 parse() {449 let token450 while (!this.tokenizer.endOfFile()) {451 token = this.tokenizer.nextToken()452 453 switch (token[0]) {454 case 'space':455 this.spaces += token[1]456 break457 458 case ';':459 this.freeSemicolon(token)460 break461 462 case '}':463 this.end(token)464 break465 466 case 'comment':467 this.comment(token)468 break469 470 case 'at-word':471 this.atrule(token)472 break473 474 case '{':475 this.emptyRule(token)476 break477 478 default:479 this.other(token)480 break481 }482 }483 this.endFile()484 }485 486 precheckMissedSemicolon(/* tokens */) {487 // Hook for Safe Parser488 }489 490 raw(node, prop, tokens, customProperty) {491 let token, type492 let length = tokens.length493 let value = ''494 let clean = true495 let next, prev496 497 for (let i = 0; i < length; i += 1) {498 token = tokens[i]499 type = token[0]500 if (type === 'space' && i === length - 1 && !customProperty) {501 clean = false502 } else if (type === 'comment') {503 prev = tokens[i - 1] ? tokens[i - 1][0] : 'empty'504 next = tokens[i + 1] ? tokens[i + 1][0] : 'empty'505 if (!SAFE_COMMENT_NEIGHBOR[prev] && !SAFE_COMMENT_NEIGHBOR[next]) {506 if (value.slice(-1) === ',') {507 clean = false508 } else {509 value += token[1]510 }511 } else {512 clean = false513 }514 } else {515 value += token[1]516 }517 }518 if (!clean) {519 let raw = tokens.reduce((all, i) => all + i[1], '')520 node.raws[prop] = { raw, value }521 }522 node[prop] = value523 }524 525 rule(tokens) {526 tokens.pop()527 528 let node = new Rule()529 this.init(node, tokens[0][2])530 531 node.raws.between = this.spacesAndCommentsFromEnd(tokens)532 this.raw(node, 'selector', tokens)533 this.current = node534 }535 536 spacesAndCommentsFromEnd(tokens) {537 let lastTokenType538 let spaces = ''539 while (tokens.length) {540 lastTokenType = tokens[tokens.length - 1][0]541 if (lastTokenType !== 'space' && lastTokenType !== 'comment') break542 spaces = tokens.pop()[1] + spaces543 }544 return spaces545 }546 547 // Errors548 549 spacesAndCommentsFromStart(tokens) {550 let next551 let spaces = ''552 while (tokens.length) {553 next = tokens[0][0]554 if (next !== 'space' && next !== 'comment') break555 spaces += tokens.shift()[1]556 }557 return spaces558 }559 560 spacesFromEnd(tokens) {561 let lastTokenType562 let spaces = ''563 while (tokens.length) {564 lastTokenType = tokens[tokens.length - 1][0]565 if (lastTokenType !== 'space') break566 spaces = tokens.pop()[1] + spaces567 }568 return spaces569 }570 571 stringFrom(tokens, from) {572 let result = ''573 for (let i = from; i < tokens.length; i++) {574 result += tokens[i][1]575 }576 tokens.splice(from, tokens.length - from)577 return result578 }579 580 unclosedBlock() {581 let pos = this.current.source.start582 throw this.input.error('Unclosed block', pos.line, pos.column)583 }584 585 unclosedBracket(bracket) {586 throw this.input.error(587 'Unclosed bracket',588 { offset: bracket[2] },589 { offset: bracket[2] + 1 }590 )591 }592 593 unexpectedClose(token) {594 throw this.input.error(595 'Unexpected }',596 { offset: token[2] },597 { offset: token[2] + 1 }598 )599 }600 601 unknownWord(tokens) {602 throw this.input.error(603 'Unknown word ' + tokens[0][1],604 { offset: tokens[0][2] },605 { offset: tokens[0][2] + tokens[0][1].length }606 )607 }608 609 unnamedAtrule(node, token) {610 throw this.input.error(611 'At-rule without name',612 { offset: token[2] },613 { offset: token[2] + token[1].length }614 )615 }616}617 618module.exports = Parser619 