CoolFace
Apppublic

AK-21/Graphite-Industrial-Intelligence

sourceHugging Faceupdated 3mo agoView on Hugging Face
0likes
parser.js619 linesDownload Raw Back to lib
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