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.
 
 
 

659 lines
16 KiB

/*
MIT License http://www.opensource.org/licenses/mit-license.php
Author Tobias Koppers @sokra
*/
"use strict";
/**
* @typedef {Object} CssTokenCallbacks
* @property {function(string, number): boolean} isSelector
* @property {function(string, number, number, number, number): number=} url
* @property {function(string, number, number): number=} string
* @property {function(string, number, number): number=} leftParenthesis
* @property {function(string, number, number): number=} rightParenthesis
* @property {function(string, number, number): number=} pseudoFunction
* @property {function(string, number, number): number=} function
* @property {function(string, number, number): number=} pseudoClass
* @property {function(string, number, number): number=} atKeyword
* @property {function(string, number, number): number=} class
* @property {function(string, number, number): number=} identifier
* @property {function(string, number, number): number=} id
* @property {function(string, number, number): number=} leftCurlyBracket
* @property {function(string, number, number): number=} rightCurlyBracket
* @property {function(string, number, number): number=} semicolon
* @property {function(string, number, number): number=} comma
*/
/** @typedef {function(string, number, CssTokenCallbacks): number} CharHandler */
// spec: https://drafts.csswg.org/css-syntax/
const CC_LINE_FEED = "\n".charCodeAt(0);
const CC_CARRIAGE_RETURN = "\r".charCodeAt(0);
const CC_FORM_FEED = "\f".charCodeAt(0);
const CC_TAB = "\t".charCodeAt(0);
const CC_SPACE = " ".charCodeAt(0);
const CC_SLASH = "/".charCodeAt(0);
const CC_BACK_SLASH = "\\".charCodeAt(0);
const CC_ASTERISK = "*".charCodeAt(0);
const CC_LEFT_PARENTHESIS = "(".charCodeAt(0);
const CC_RIGHT_PARENTHESIS = ")".charCodeAt(0);
const CC_LEFT_CURLY = "{".charCodeAt(0);
const CC_RIGHT_CURLY = "}".charCodeAt(0);
const CC_QUOTATION_MARK = '"'.charCodeAt(0);
const CC_APOSTROPHE = "'".charCodeAt(0);
const CC_FULL_STOP = ".".charCodeAt(0);
const CC_COLON = ":".charCodeAt(0);
const CC_SEMICOLON = ";".charCodeAt(0);
const CC_COMMA = ",".charCodeAt(0);
const CC_PERCENTAGE = "%".charCodeAt(0);
const CC_AT_SIGN = "@".charCodeAt(0);
const CC_LOW_LINE = "_".charCodeAt(0);
const CC_LOWER_A = "a".charCodeAt(0);
const CC_LOWER_U = "u".charCodeAt(0);
const CC_LOWER_E = "e".charCodeAt(0);
const CC_LOWER_Z = "z".charCodeAt(0);
const CC_UPPER_A = "A".charCodeAt(0);
const CC_UPPER_E = "E".charCodeAt(0);
const CC_UPPER_Z = "Z".charCodeAt(0);
const CC_0 = "0".charCodeAt(0);
const CC_9 = "9".charCodeAt(0);
const CC_NUMBER_SIGN = "#".charCodeAt(0);
const CC_PLUS_SIGN = "+".charCodeAt(0);
const CC_HYPHEN_MINUS = "-".charCodeAt(0);
const CC_LESS_THAN_SIGN = "<".charCodeAt(0);
const CC_GREATER_THAN_SIGN = ">".charCodeAt(0);
const _isNewLine = cc => {
return (
cc === CC_LINE_FEED || cc === CC_CARRIAGE_RETURN || cc === CC_FORM_FEED
);
};
/** @type {CharHandler} */
const consumeSpace = (input, pos, callbacks) => {
let cc;
do {
pos++;
cc = input.charCodeAt(pos);
} while (_isWhiteSpace(cc));
return pos;
};
const _isWhiteSpace = cc => {
return (
cc === CC_LINE_FEED ||
cc === CC_CARRIAGE_RETURN ||
cc === CC_FORM_FEED ||
cc === CC_TAB ||
cc === CC_SPACE
);
};
/** @type {CharHandler} */
const consumeSingleCharToken = (input, pos, callbacks) => {
return pos + 1;
};
/** @type {CharHandler} */
const consumePotentialComment = (input, pos, callbacks) => {
pos++;
if (pos === input.length) return pos;
let cc = input.charCodeAt(pos);
if (cc !== CC_ASTERISK) return pos;
for (;;) {
pos++;
if (pos === input.length) return pos;
cc = input.charCodeAt(pos);
while (cc === CC_ASTERISK) {
pos++;
if (pos === input.length) return pos;
cc = input.charCodeAt(pos);
if (cc === CC_SLASH) return pos + 1;
}
}
};
/** @type {function(number): CharHandler} */
const consumeString = end => (input, pos, callbacks) => {
const start = pos;
pos = _consumeString(input, pos, end);
if (callbacks.string !== undefined) {
pos = callbacks.string(input, start, pos);
}
return pos;
};
const _consumeString = (input, pos, end) => {
pos++;
for (;;) {
if (pos === input.length) return pos;
const cc = input.charCodeAt(pos);
if (cc === end) return pos + 1;
if (_isNewLine(cc)) {
// bad string
return pos;
}
if (cc === CC_BACK_SLASH) {
// we don't need to fully parse the escaped code point
// just skip over a potential new line
pos++;
if (pos === input.length) return pos;
pos++;
} else {
pos++;
}
}
};
const _isIdentifierStartCode = cc => {
return (
cc === CC_LOW_LINE ||
(cc >= CC_LOWER_A && cc <= CC_LOWER_Z) ||
(cc >= CC_UPPER_A && cc <= CC_UPPER_Z) ||
cc > 0x80
);
};
const _isDigit = cc => {
return cc >= CC_0 && cc <= CC_9;
};
const _startsIdentifier = (input, pos) => {
const cc = input.charCodeAt(pos);
if (cc === CC_HYPHEN_MINUS) {
if (pos === input.length) return false;
const cc = input.charCodeAt(pos + 1);
if (cc === CC_HYPHEN_MINUS) return true;
if (cc === CC_BACK_SLASH) {
const cc = input.charCodeAt(pos + 2);
return !_isNewLine(cc);
}
return _isIdentifierStartCode(cc);
}
if (cc === CC_BACK_SLASH) {
const cc = input.charCodeAt(pos + 1);
return !_isNewLine(cc);
}
return _isIdentifierStartCode(cc);
};
/** @type {CharHandler} */
const consumeNumberSign = (input, pos, callbacks) => {
const start = pos;
pos++;
if (pos === input.length) return pos;
if (callbacks.isSelector(input, pos) && _startsIdentifier(input, pos)) {
pos = _consumeIdentifier(input, pos);
if (callbacks.id !== undefined) {
return callbacks.id(input, start, pos);
}
}
return pos;
};
/** @type {CharHandler} */
const consumeMinus = (input, pos, callbacks) => {
const start = pos;
pos++;
if (pos === input.length) return pos;
const cc = input.charCodeAt(pos);
if (cc === CC_FULL_STOP || _isDigit(cc)) {
return consumeNumericToken(input, pos, callbacks);
} else if (cc === CC_HYPHEN_MINUS) {
pos++;
if (pos === input.length) return pos;
const cc = input.charCodeAt(pos);
if (cc === CC_GREATER_THAN_SIGN) {
return pos + 1;
} else {
pos = _consumeIdentifier(input, pos);
if (callbacks.identifier !== undefined) {
return callbacks.identifier(input, start, pos);
}
}
} else if (cc === CC_BACK_SLASH) {
if (pos + 1 === input.length) return pos;
const cc = input.charCodeAt(pos + 1);
if (_isNewLine(cc)) return pos;
pos = _consumeIdentifier(input, pos);
if (callbacks.identifier !== undefined) {
return callbacks.identifier(input, start, pos);
}
} else if (_isIdentifierStartCode(cc)) {
pos++;
pos = _consumeIdentifier(input, pos);
if (callbacks.identifier !== undefined) {
return callbacks.identifier(input, start, pos);
}
}
return pos;
};
/** @type {CharHandler} */
const consumeDot = (input, pos, callbacks) => {
const start = pos;
pos++;
if (pos === input.length) return pos;
const cc = input.charCodeAt(pos);
if (_isDigit(cc)) return consumeNumericToken(input, pos - 2, callbacks);
if (!callbacks.isSelector(input, pos) || !_startsIdentifier(input, pos))
return pos;
pos = _consumeIdentifier(input, pos);
if (callbacks.class !== undefined) return callbacks.class(input, start, pos);
return pos;
};
/** @type {CharHandler} */
const consumeNumericToken = (input, pos, callbacks) => {
pos = _consumeNumber(input, pos);
if (pos === input.length) return pos;
if (_startsIdentifier(input, pos)) return _consumeIdentifier(input, pos);
const cc = input.charCodeAt(pos);
if (cc === CC_PERCENTAGE) return pos + 1;
return pos;
};
/** @type {CharHandler} */
const consumeOtherIdentifier = (input, pos, callbacks) => {
const start = pos;
pos = _consumeIdentifier(input, pos);
if (
pos !== input.length &&
!callbacks.isSelector(input, pos) &&
input.charCodeAt(pos) === CC_LEFT_PARENTHESIS
) {
pos++;
if (callbacks.function !== undefined) {
return callbacks.function(input, start, pos);
}
} else {
if (callbacks.identifier !== undefined) {
return callbacks.identifier(input, start, pos);
}
}
return pos;
};
/** @type {CharHandler} */
const consumePotentialUrl = (input, pos, callbacks) => {
const start = pos;
pos = _consumeIdentifier(input, pos);
if (pos === start + 3 && input.slice(start, pos + 1) === "url(") {
pos++;
let cc = input.charCodeAt(pos);
while (_isWhiteSpace(cc)) {
pos++;
if (pos === input.length) return pos;
cc = input.charCodeAt(pos);
}
if (cc === CC_QUOTATION_MARK || cc === CC_APOSTROPHE) {
pos++;
const contentStart = pos;
pos = _consumeString(input, pos, cc);
const contentEnd = pos - 1;
cc = input.charCodeAt(pos);
while (_isWhiteSpace(cc)) {
pos++;
if (pos === input.length) return pos;
cc = input.charCodeAt(pos);
}
if (cc !== CC_RIGHT_PARENTHESIS) return pos;
pos++;
if (callbacks.url !== undefined)
return callbacks.url(input, start, pos, contentStart, contentEnd);
return pos;
} else {
const contentStart = pos;
let contentEnd;
for (;;) {
if (cc === CC_BACK_SLASH) {
pos++;
if (pos === input.length) return pos;
pos++;
} else if (_isWhiteSpace(cc)) {
contentEnd = pos;
do {
pos++;
if (pos === input.length) return pos;
cc = input.charCodeAt(pos);
} while (_isWhiteSpace(cc));
if (cc !== CC_RIGHT_PARENTHESIS) return pos;
pos++;
if (callbacks.url !== undefined) {
return callbacks.url(input, start, pos, contentStart, contentEnd);
}
return pos;
} else if (cc === CC_RIGHT_PARENTHESIS) {
contentEnd = pos;
pos++;
if (callbacks.url !== undefined) {
return callbacks.url(input, start, pos, contentStart, contentEnd);
}
return pos;
} else if (cc === CC_LEFT_PARENTHESIS) {
return pos;
} else {
pos++;
}
if (pos === input.length) return pos;
cc = input.charCodeAt(pos);
}
}
} else {
if (callbacks.identifier !== undefined) {
return callbacks.identifier(input, start, pos);
}
return pos;
}
};
/** @type {CharHandler} */
const consumePotentialPseudo = (input, pos, callbacks) => {
const start = pos;
pos++;
if (!callbacks.isSelector(input, pos) || !_startsIdentifier(input, pos))
return pos;
pos = _consumeIdentifier(input, pos);
let cc = input.charCodeAt(pos);
if (cc === CC_LEFT_PARENTHESIS) {
pos++;
if (callbacks.pseudoFunction !== undefined) {
return callbacks.pseudoFunction(input, start, pos);
}
return pos;
}
if (callbacks.pseudoClass !== undefined) {
return callbacks.pseudoClass(input, start, pos);
}
return pos;
};
/** @type {CharHandler} */
const consumeLeftParenthesis = (input, pos, callbacks) => {
pos++;
if (callbacks.leftParenthesis !== undefined) {
return callbacks.leftParenthesis(input, pos - 1, pos);
}
return pos;
};
/** @type {CharHandler} */
const consumeRightParenthesis = (input, pos, callbacks) => {
pos++;
if (callbacks.rightParenthesis !== undefined) {
return callbacks.rightParenthesis(input, pos - 1, pos);
}
return pos;
};
/** @type {CharHandler} */
const consumeLeftCurlyBracket = (input, pos, callbacks) => {
pos++;
if (callbacks.leftCurlyBracket !== undefined) {
return callbacks.leftCurlyBracket(input, pos - 1, pos);
}
return pos;
};
/** @type {CharHandler} */
const consumeRightCurlyBracket = (input, pos, callbacks) => {
pos++;
if (callbacks.rightCurlyBracket !== undefined) {
return callbacks.rightCurlyBracket(input, pos - 1, pos);
}
return pos;
};
/** @type {CharHandler} */
const consumeSemicolon = (input, pos, callbacks) => {
pos++;
if (callbacks.semicolon !== undefined) {
return callbacks.semicolon(input, pos - 1, pos);
}
return pos;
};
/** @type {CharHandler} */
const consumeComma = (input, pos, callbacks) => {
pos++;
if (callbacks.comma !== undefined) {
return callbacks.comma(input, pos - 1, pos);
}
return pos;
};
const _consumeIdentifier = (input, pos) => {
for (;;) {
const cc = input.charCodeAt(pos);
if (cc === CC_BACK_SLASH) {
pos++;
if (pos === input.length) return pos;
pos++;
} else if (
_isIdentifierStartCode(cc) ||
_isDigit(cc) ||
cc === CC_HYPHEN_MINUS
) {
pos++;
} else {
return pos;
}
}
};
const _consumeNumber = (input, pos) => {
pos++;
if (pos === input.length) return pos;
let cc = input.charCodeAt(pos);
while (_isDigit(cc)) {
pos++;
if (pos === input.length) return pos;
cc = input.charCodeAt(pos);
}
if (cc === CC_FULL_STOP && pos + 1 !== input.length) {
const next = input.charCodeAt(pos + 1);
if (_isDigit(next)) {
pos += 2;
cc = input.charCodeAt(pos);
while (_isDigit(cc)) {
pos++;
if (pos === input.length) return pos;
cc = input.charCodeAt(pos);
}
}
}
if (cc === CC_LOWER_E || cc === CC_UPPER_E) {
if (pos + 1 !== input.length) {
const next = input.charCodeAt(pos + 2);
if (_isDigit(next)) {
pos += 2;
} else if (
(next === CC_HYPHEN_MINUS || next === CC_PLUS_SIGN) &&
pos + 2 !== input.length
) {
const next = input.charCodeAt(pos + 2);
if (_isDigit(next)) {
pos += 3;
} else {
return pos;
}
} else {
return pos;
}
}
} else {
return pos;
}
cc = input.charCodeAt(pos);
while (_isDigit(cc)) {
pos++;
if (pos === input.length) return pos;
cc = input.charCodeAt(pos);
}
return pos;
};
/** @type {CharHandler} */
const consumeLessThan = (input, pos, callbacks) => {
if (input.slice(pos + 1, pos + 4) === "!--") return pos + 4;
return pos + 1;
};
/** @type {CharHandler} */
const consumeAt = (input, pos, callbacks) => {
const start = pos;
pos++;
if (pos === input.length) return pos;
if (_startsIdentifier(input, pos)) {
pos = _consumeIdentifier(input, pos);
if (callbacks.atKeyword !== undefined) {
pos = callbacks.atKeyword(input, start, pos);
}
}
return pos;
};
const CHAR_MAP = Array.from({ length: 0x80 }, (_, cc) => {
// https://drafts.csswg.org/css-syntax/#consume-token
switch (cc) {
case CC_LINE_FEED:
case CC_CARRIAGE_RETURN:
case CC_FORM_FEED:
case CC_TAB:
case CC_SPACE:
return consumeSpace;
case CC_QUOTATION_MARK:
case CC_APOSTROPHE:
return consumeString(cc);
case CC_NUMBER_SIGN:
return consumeNumberSign;
case CC_SLASH:
return consumePotentialComment;
// case CC_LEFT_SQUARE:
// case CC_RIGHT_SQUARE:
// case CC_COMMA:
// case CC_COLON:
// return consumeSingleCharToken;
case CC_COMMA:
return consumeComma;
case CC_SEMICOLON:
return consumeSemicolon;
case CC_LEFT_PARENTHESIS:
return consumeLeftParenthesis;
case CC_RIGHT_PARENTHESIS:
return consumeRightParenthesis;
case CC_LEFT_CURLY:
return consumeLeftCurlyBracket;
case CC_RIGHT_CURLY:
return consumeRightCurlyBracket;
case CC_COLON:
return consumePotentialPseudo;
case CC_PLUS_SIGN:
return consumeNumericToken;
case CC_FULL_STOP:
return consumeDot;
case CC_HYPHEN_MINUS:
return consumeMinus;
case CC_LESS_THAN_SIGN:
return consumeLessThan;
case CC_AT_SIGN:
return consumeAt;
case CC_LOWER_U:
return consumePotentialUrl;
case CC_LOW_LINE:
return consumeOtherIdentifier;
default:
if (_isDigit(cc)) return consumeNumericToken;
if (
(cc >= CC_LOWER_A && cc <= CC_LOWER_Z) ||
(cc >= CC_UPPER_A && cc <= CC_UPPER_Z)
) {
return consumeOtherIdentifier;
}
return consumeSingleCharToken;
}
});
/**
* @param {string} input input css
* @param {CssTokenCallbacks} callbacks callbacks
* @returns {void}
*/
module.exports = (input, callbacks) => {
let pos = 0;
while (pos < input.length) {
const cc = input.charCodeAt(pos);
if (cc < 0x80) {
pos = CHAR_MAP[cc](input, pos, callbacks);
} else {
pos++;
}
}
};
module.exports.eatComments = (input, pos) => {
loop: for (;;) {
const cc = input.charCodeAt(pos);
if (cc === CC_SLASH) {
if (pos === input.length) return pos;
let cc = input.charCodeAt(pos + 1);
if (cc !== CC_ASTERISK) return pos;
pos++;
for (;;) {
pos++;
if (pos === input.length) return pos;
cc = input.charCodeAt(pos);
while (cc === CC_ASTERISK) {
pos++;
if (pos === input.length) return pos;
cc = input.charCodeAt(pos);
if (cc === CC_SLASH) {
pos++;
continue loop;
}
}
}
}
return pos;
}
};
module.exports.eatWhitespaceAndComments = (input, pos) => {
loop: for (;;) {
const cc = input.charCodeAt(pos);
if (cc === CC_SLASH) {
if (pos === input.length) return pos;
let cc = input.charCodeAt(pos + 1);
if (cc !== CC_ASTERISK) return pos;
pos++;
for (;;) {
pos++;
if (pos === input.length) return pos;
cc = input.charCodeAt(pos);
while (cc === CC_ASTERISK) {
pos++;
if (pos === input.length) return pos;
cc = input.charCodeAt(pos);
if (cc === CC_SLASH) {
pos++;
continue loop;
}
}
}
} else if (_isWhiteSpace(cc)) {
pos++;
continue;
}
return pos;
}
};