123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576 |
- 'use strict';
- const Root = require('./root');
- const Value = require('./value');
- const AtWord = require('./atword');
- const Colon = require('./colon');
- const Comma = require('./comma');
- const Comment = require('./comment');
- const Func = require('./function');
- const Numbr = require('./number');
- const Operator = require('./operator');
- const Paren = require('./paren');
- const Str = require('./string');
- const Word = require('./word');
- const UnicodeRange = require('./unicode-range');
- const tokenize = require('./tokenize');
- const flatten = require('flatten');
- const indexesOf = require('indexes-of');
- const uniq = require('uniq');
- const ParserError = require('./errors/ParserError');
- function sortAscending (list) {
- return list.sort((a, b) => a - b);
- }
- module.exports = class Parser {
- constructor (input, options) {
- const defaults = { loose: false };
- // cache needs to be an array for values with more than 1 level of function nesting
- this.cache = [];
- this.input = input;
- this.options = Object.assign({}, defaults, options);
- this.position = 0;
- // we'll use this to keep track of the paren balance
- this.unbalanced = 0;
- this.root = new Root();
- let value = new Value();
- this.root.append(value);
- this.current = value;
- this.tokens = tokenize(input, this.options);
- }
- parse () {
- return this.loop();
- }
- colon () {
- let token = this.currToken;
- this.newNode(new Colon({
- value: token[1],
- source: {
- start: {
- line: token[2],
- column: token[3]
- },
- end: {
- line: token[4],
- column: token[5]
- }
- },
- sourceIndex: token[6]
- }));
- this.position ++;
- }
- comma () {
- let token = this.currToken;
- this.newNode(new Comma({
- value: token[1],
- source: {
- start: {
- line: token[2],
- column: token[3]
- },
- end: {
- line: token[4],
- column: token[5]
- }
- },
- sourceIndex: token[6]
- }));
- this.position ++;
- }
- comment () {
- let inline = false,
- value = this.currToken[1].replace(/\/\*|\*\//g, ''),
- node;
- if (this.options.loose && value.startsWith("//")) {
- value = value.substring(2);
- inline = true;
- }
- node = new Comment({
- value: value,
- inline: inline,
- source: {
- start: {
- line: this.currToken[2],
- column: this.currToken[3]
- },
- end: {
- line: this.currToken[4],
- column: this.currToken[5]
- }
- },
- sourceIndex: this.currToken[6]
- });
- this.newNode(node);
- this.position++;
- }
- error (message, token) {
- throw new ParserError(message + ` at line: ${token[2]}, column ${token[3]}`);
- }
- loop () {
- while (this.position < this.tokens.length) {
- this.parseTokens();
- }
- if (!this.current.last && this.spaces) {
- this.current.raws.before += this.spaces;
- }
- else if (this.spaces) {
- this.current.last.raws.after += this.spaces;
- }
- this.spaces = '';
- return this.root;
- }
- operator () {
- // if a +|- operator is followed by a non-word character (. is allowed) and
- // is preceded by a non-word character. (5+5)
- let char = this.currToken[1],
- node;
- if (char === '+' || char === '-') {
- // only inspect if the operator is not the first token, and we're only
- // within a calc() function: the only spec-valid place for math expressions
- if (!this.options.loose) {
- if (this.position > 0) {
- if (this.current.type === 'func' && this.current.value === 'calc') {
- // allow operators to be proceeded by spaces and opening parens
- if (this.prevToken[0] !== 'space' && this.prevToken[0] !== '(') {
- this.error('Syntax Error', this.currToken);
- }
- // valid: calc(1 - +2)
- // invalid: calc(1 -+2)
- else if (this.nextToken[0] !== 'space' && this.nextToken[0] !== 'word') {
- this.error('Syntax Error', this.currToken);
- }
- // valid: calc(1 - +2)
- // valid: calc(-0.5 + 2)
- // invalid: calc(1 -2)
- else if (this.nextToken[0] === 'word' && this.current.last.type !== 'operator' &&
- this.current.last.value !== '(') {
- this.error('Syntax Error', this.currToken);
- }
- }
- // if we're not in a function and someone has doubled up on operators,
- // or they're trying to perform a calc outside of a calc
- // eg. +-4px or 5+ 5, throw an error
- else if (this.nextToken[0] === 'space'
- || this.nextToken[0] === 'operator'
- || this.prevToken[0] === 'operator') {
- this.error('Syntax Error', this.currToken);
- }
- }
- }
- if (!this.options.loose) {
- if (this.nextToken[0] === 'word') {
- return this.word();
- }
- }
- else {
- if ((!this.current.nodes.length || (this.current.last && this.current.last.type === 'operator')) && this.nextToken[0] === 'word') {
- return this.word();
- }
- }
- }
- node = new Operator({
- value: this.currToken[1],
- source: {
- start: {
- line: this.currToken[2],
- column: this.currToken[3]
- },
- end: {
- line: this.currToken[2],
- column: this.currToken[3]
- }
- },
- sourceIndex: this.currToken[4]
- });
- this.position ++;
- return this.newNode(node);
- }
- parseTokens () {
- switch (this.currToken[0]) {
- case 'space':
- this.space();
- break;
- case 'colon':
- this.colon();
- break;
- case 'comma':
- this.comma();
- break;
- case 'comment':
- this.comment();
- break;
- case '(':
- this.parenOpen();
- break;
- case ')':
- this.parenClose();
- break;
- case 'atword':
- case 'word':
- this.word();
- break;
- case 'operator':
- this.operator();
- break;
- case 'string':
- this.string();
- break;
- case 'unicoderange':
- this.unicodeRange();
- break;
- default:
- this.word();
- break;
- }
- }
- parenOpen () {
- let unbalanced = 1,
- pos = this.position + 1,
- token = this.currToken,
- last;
- // check for balanced parens
- while (pos < this.tokens.length && unbalanced) {
- let tkn = this.tokens[pos];
- if (tkn[0] === '(') {
- unbalanced++;
- }
- if (tkn[0] === ')') {
- unbalanced--;
- }
- pos ++;
- }
- if (unbalanced) {
- this.error('Expected closing parenthesis', token);
- }
- // ok, all parens are balanced. continue on
- last = this.current.last;
- if (last && last.type === 'func' && last.unbalanced < 0) {
- last.unbalanced = 0; // ok we're ready to add parens now
- this.current = last;
- }
- this.current.unbalanced ++;
- this.newNode(new Paren({
- value: token[1],
- source: {
- start: {
- line: token[2],
- column: token[3]
- },
- end: {
- line: token[4],
- column: token[5]
- }
- },
- sourceIndex: token[6]
- }));
- this.position ++;
- // url functions get special treatment, and anything between the function
- // parens get treated as one word, if the contents aren't not a string.
- if (this.current.type === 'func' && this.current.unbalanced &&
- this.current.value === 'url' && this.currToken[0] !== 'string' &&
- this.currToken[0] !== ')' && !this.options.loose) {
- let nextToken = this.nextToken,
- value = this.currToken[1],
- start = {
- line: this.currToken[2],
- column: this.currToken[3]
- };
- while (nextToken && nextToken[0] !== ')' && this.current.unbalanced) {
- this.position ++;
- value += this.currToken[1];
- nextToken = this.nextToken;
- }
- if (this.position !== this.tokens.length - 1) {
- // skip the following word definition, or it'll be a duplicate
- this.position ++;
- this.newNode(new Word({
- value,
- source: {
- start,
- end: {
- line: this.currToken[4],
- column: this.currToken[5]
- }
- },
- sourceIndex: this.currToken[6]
- }));
- }
- }
- }
- parenClose () {
- let token = this.currToken;
- this.newNode(new Paren({
- value: token[1],
- source: {
- start: {
- line: token[2],
- column: token[3]
- },
- end: {
- line: token[4],
- column: token[5]
- }
- },
- sourceIndex: token[6]
- }));
- this.position ++;
- if (this.position >= this.tokens.length - 1 && !this.current.unbalanced) {
- return;
- }
- this.current.unbalanced --;
- if (this.current.unbalanced < 0) {
- this.error('Expected opening parenthesis', token);
- }
- if (!this.current.unbalanced && this.cache.length) {
- this.current = this.cache.pop();
- }
- }
- space () {
- let token = this.currToken;
- // Handle space before and after the selector
- if (this.position === (this.tokens.length - 1) || this.nextToken[0] === ',' || this.nextToken[0] === ')') {
- this.current.last.raws.after += token[1];
- this.position ++;
- }
- else {
- this.spaces = token[1];
- this.position ++;
- }
- }
- unicodeRange () {
- let token = this.currToken;
- this.newNode(new UnicodeRange({
- value: token[1],
- source: {
- start: {
- line: token[2],
- column: token[3]
- },
- end: {
- line: token[4],
- column: token[5]
- }
- },
- sourceIndex: token[6]
- }));
- this.position ++;
- }
- splitWord () {
- let nextToken = this.nextToken,
- word = this.currToken[1],
- rNumber = /^[\+\-]?((\d+(\.\d*)?)|(\.\d+))([eE][\+\-]?\d+)?/,
- // treat css-like groupings differently so they can be inspected,
- // but don't address them as anything but a word, but allow hex values
- // to pass through.
- rNoFollow = /^(?!\#([a-z0-9]+))[\#\{\}]/gi,
- hasAt, indices;
- if (!rNoFollow.test(word)) {
- while (nextToken && nextToken[0] === 'word') {
- this.position ++;
- let current = this.currToken[1];
- word += current;
- nextToken = this.nextToken;
- }
- }
- hasAt = indexesOf(word, '@');
- indices = sortAscending(uniq(flatten([[0], hasAt])));
- indices.forEach((ind, i) => {
- let index = indices[i + 1] || word.length,
- value = word.slice(ind, index),
- node;
- if (~hasAt.indexOf(ind)) {
- node = new AtWord({
- value: value.slice(1),
- source: {
- start: {
- line: this.currToken[2],
- column: this.currToken[3] + ind
- },
- end: {
- line: this.currToken[4],
- column: this.currToken[3] + (index - 1)
- }
- },
- sourceIndex: this.currToken[6] + indices[i]
- });
- }
- else if (rNumber.test(this.currToken[1])) {
- let unit = value.replace(rNumber, '');
- node = new Numbr({
- value: value.replace(unit, ''),
- source: {
- start: {
- line: this.currToken[2],
- column: this.currToken[3] + ind
- },
- end: {
- line: this.currToken[4],
- column: this.currToken[3] + (index - 1)
- }
- },
- sourceIndex: this.currToken[6] + indices[i],
- unit
- });
- }
- else {
- node = new (nextToken && nextToken[0] === '(' ? Func : Word)({
- value,
- source: {
- start: {
- line: this.currToken[2],
- column: this.currToken[3] + ind
- },
- end: {
- line: this.currToken[4],
- column: this.currToken[3] + (index - 1)
- }
- },
- sourceIndex: this.currToken[6] + indices[i]
- });
- if (node.constructor.name === 'Word') {
- node.isHex = /^#(.+)/.test(value);
- node.isColor = /^#([0-9a-f]{3}|[0-9a-f]{4}|[0-9a-f]{6}|[0-9a-f]{8})$/i.test(value);
- }
- else {
- this.cache.push(this.current);
- }
- }
- this.newNode(node);
- });
- this.position ++;
- }
- string () {
- let token = this.currToken,
- value = this.currToken[1],
- rQuote = /^(\"|\')/,
- quoted = rQuote.test(value),
- quote = '',
- node;
- if (quoted) {
- quote = value.match(rQuote)[0];
- // set value to the string within the quotes
- // quotes are stored in raws
- value = value.slice(1, value.length - 1);
- }
- node = new Str({
- value,
- source: {
- start: {
- line: token[2],
- column: token[3]
- },
- end: {
- line: token[4],
- column: token[5]
- }
- },
- sourceIndex: token[6],
- quoted
- });
- node.raws.quote = quote;
- this.newNode(node);
- this.position++;
- }
- word () {
- return this.splitWord();
- }
- newNode (node) {
- if (this.spaces) {
- node.raws.before += this.spaces;
- this.spaces = '';
- }
- return this.current.append(node);
- }
- get currToken () {
- return this.tokens[this.position];
- }
- get nextToken () {
- return this.tokens[this.position + 1];
- }
- get prevToken () {
- return this.tokens[this.position - 1];
- }
- };
|