/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
// This file holds various CSS parsing and rewriting utilities. // Some entry points of note are: // parseDeclarations - parse a CSS rule into declarations // parsePseudoClassesAndAttributes - parse selector and extract // pseudo-classes // parseSingleValue - parse a single CSS property value
// When commenting out a declaration, we put this character into the // comment opener so that future parses of the commented text know to // bypass the property name validity heuristic. const COMMENT_PARSING_HEURISTIC_BYPASS_CHAR =
(exports.COMMENT_PARSING_HEURISTIC_BYPASS_CHAR = "!");
/** * A generator function that lexes a CSS source string, yielding the * CSS tokens. Comment tokens are dropped. * * @param {String} CSS source string * @yield {CSSToken} The next CSSToken that is lexed * @see CSSToken for details about the returned tokens
*/ function* cssTokenizer(string) { const lexer = new InspectorCSSParserWrapper(string); while (true) { const token = lexer.nextToken(); if (!token) { break;
} // None of the existing consumers want comments. if (token.tokenType !== "Comment") {
yield token;
}
}
}
/** * Pass |string| to the CSS lexer and return an array of all the * returned tokens. Comment tokens are not included. In addition to * the usual information, each token will have starting and ending * line and column information attached. Specifically, each token * has an additional "loc" attribute. This attribute is an object * of the form {line: L, column: C}. Lines and columns are both zero * based. * * It's best not to add new uses of this function. In general it is * simpler and better to use the CSSToken offsets, rather than line * and column. Also, this function lexes the entire input string at * once, rather than lazily yielding a token stream. Use * |cssTokenizer| or |getCSSLexer| instead. * * @param{String} string The input string. * @return {Array} An array of tokens (@see CSSToken) that have * line and column information.
*/ function cssTokenizerWithLineColumn(string) { const lexer = new InspectorCSSParserWrapper(string); const result = [];
let prevToken = undefined; while (true) { const token = lexer.nextToken(); const lineNumber = lexer.lineNumber; const columnNumber = lexer.columnNumber;
/** * Escape a comment body. Find the comment start and end strings in a * string and inserts backslashes so that the resulting text can * itself be put inside a comment. * * @param {String} inputString * input string * @return {String} the escaped result
*/ function escapeCSSComment(inputString) { const result = inputString.replace(/\/(\\*)\*/g, "/\\$1*"); return result.replace(/\*(\\*)\//g, "*\\$1/");
}
/** * Un-escape a comment body. This undoes any comment escaping that * was done by escapeCSSComment. That is, given input like "/\* * comment *\/", it will strip the backslashes. * * @param {String} inputString * input string * @return {String} the un-escaped result
*/ function unescapeCSSComment(inputString) { const result = inputString.replace(/\/\\(\\*)\*/g, "/$1*"); return result.replace(/\*\\(\\*)\//g, "*$1/");
}
/** * A helper function for @see parseDeclarations that handles parsing * of comment text. This wraps a recursive call to parseDeclarations * with the processing needed to ensure that offsets in the result * refer back to the original, unescaped, input string. * * @param {Function} isCssPropertyKnown * A function to check if the CSS property is known. This is either an * internal server function or from the CssPropertiesFront. * @param {String} commentText The text of the comment, without the * delimiters. * @param {Number} startOffset The offset of the comment opener * in the original text. * @param {Number} endOffset The offset of the comment closer * in the original text. * @return {array} Array of declarations of the same form as returned * by parseDeclarations.
*/ function parseCommentDeclarations(
isCssPropertyKnown,
commentText,
startOffset,
endOffset
) {
let commentOverride = false; if (commentText === "") { return [];
} elseif (commentText[0] === COMMENT_PARSING_HEURISTIC_BYPASS_CHAR) { // This is the special sign that the comment was written by // rewriteDeclarations and so we should bypass the usual // heuristic.
commentOverride = true;
commentText = commentText.substring(1);
}
// We might have rewritten an embedded comment. For example // /\* ... *\/ would turn into /* ... */. // This rewriting is necessary for proper lexing, but it means // that the offsets we get back can be off. So now we compute // a map so that we can rewrite offsets later. The map is the same // length as |rewrittenText| and tells us how to map an index // into |rewrittenText| to an index into |commentText|. // // First, we find the location of each comment starter or closer in // |rewrittenText|. At these spots we put a 1 into |rewrites|. // Then we walk the array again, using the elements to compute a // delta, which we use to make the final mapping. // // Note we allocate one extra entry because we can see an ending // offset that is equal to the length. const rewrites = new Array(rewrittenText.length + 1).fill(0);
const commentRe = /\/\\*\*|\*\\*\//g; while (true) { const matchData = commentRe.exec(rewrittenText); if (!matchData) { break;
}
rewrites[matchData.index] = 1;
}
let delta = 0; for (let i = 0; i <= rewrittenText.length; ++i) {
delta += rewrites[i]; // |startOffset| to add the offset from the comment starter, |+2| // for the length of the "/*", then |i| and |delta| as described // above.
rewrites[i] = startOffset + 2 + i + delta; if (commentOverride) {
++rewrites[i];
}
}
// Note that we pass "false" for parseComments here. It doesn't // seem worthwhile to support declarations in comments-in-comments // here, as there's no way to generate those using the tools, and // users would be crazy to write such things. const newDecls = parseDeclarationsInternal(
isCssPropertyKnown,
rewrittenText, false, true,
commentOverride
); for (const decl of newDecls) {
decl.offsets[0] = rewrites[decl.offsets[0]];
decl.offsets[1] = rewrites[decl.offsets[1]];
decl.colonOffsets[0] = rewrites[decl.colonOffsets[0]];
decl.colonOffsets[1] = rewrites[decl.colonOffsets[1]];
decl.commentOffsets = [startOffset, endOffset];
} return newDecls;
}
/** * A helper function for parseDeclarationsInternal that creates a new * empty declaration. * * @return {object} an empty declaration of the form returned by * parseDeclarations
*/ function getEmptyDeclaration() { return {
name: "",
value: "",
priority: "",
terminator: "",
offsets: [undefined, undefined],
colonOffsets: false,
};
}
/** * Like trim, but only trims CSS-allowed whitespace.
*/ function cssTrim(str) { const match = /^[ \t\r\n\f]*(.*?)[ \t\r\n\f]*$/.exec(str); if (match) { return match[1];
} return str;
}
/** * A helper function that does all the parsing work for * parseDeclarations. This is separate because it has some arguments * that don't make sense in isolation. * * The return value and arguments are like parseDeclarations, with * these additional arguments. * * @param {Function} isCssPropertyKnown * Function to check if the CSS property is known. * @param {Boolean} inComment * If true, assume that this call is parsing some text * which came from a comment in another declaration. * In this case some heuristics are used to avoid parsing * text which isn't obviously a series of declarations. * @param {Boolean} commentOverride * This only makes sense when inComment=true. * When true, assume that the comment was generated by * rewriteDeclarations, and skip the usual name-checking * heuristic.
*/ // eslint-disable-next-line complexity function parseDeclarationsInternal(
isCssPropertyKnown,
inputString,
parseComments,
inComment,
commentOverride
) { if (inputString === null || inputString === undefined) { thrownew Error("empty input string");
}
const lexer = new InspectorCSSParserWrapper(inputString, {
trackEOFChars: true,
});
let declarations = [getEmptyDeclaration()];
let lastProp = declarations[0];
// This tracks the various CSS blocks the current token is in currently. // This is a stack we push to when a block is opened, and we pop from when a block is // closed. Within a block, colons and semicolons don't advance the way they do outside // of blocks.
let currentBlocks = [];
// This tracks the "!important" parsing state. The states are: // 0 - haven't seen anything // 1 - have seen "!", looking for "important" next (possibly after // whitespace). // 2 - have seen "!important"
let importantState = 0; // This is true if we saw whitespace or comments between the "!" and // the "important".
let importantWS = false;
// This tracks the nesting parsing state
let isInNested = false;
let nestingLevel = 0;
while (true) { const token = lexer.nextToken(); if (!token) { break;
}
// Update the start and end offsets of the declaration, but only // when we see a significant token. if (token.tokenType !== "WhiteSpace" && token.tokenType !== "Comment") { if (lastProp.offsets[0] === undefined) {
lastProp.offsets[0] = token.startOffset;
}
lastProp.offsets[1] = token.endOffset;
} elseif (
lastProp.name &&
!current &&
!importantState &&
!lastProp.priority &&
lastProp.colonOffsets[1]
) { // Whitespace appearing after the ":" is attributed to it.
lastProp.colonOffsets[1] = token.endOffset;
} elseif (importantState === 1) {
importantWS = true;
}
if ( // If we're not already in a nested rule
!isInNested && // and there's an opening curly bracket
token.tokenType === "CurlyBracketBlock" && // and we're not inside a function or an attribute
!currentBlocks.length
) { // Assume we're encountering a nested rule.
if (inComment) { // If we're in a comment, we still want to retrieve all the "top" level declarations, // e.g. for `/* color: red; & > span { color: blue; } color: yellow; */`, we do want // to get the red and yellow declarations.
isInNested = true;
nestingLevel = 1; continue;
}
// If we're not in a comment, once we encounter a nested rule, we can stop; // even if there are declarations after the nested rules, they will be retrieved in // a different (CSSNestedDeclaration) rule.
declarations.pop(); break;
} elseif (isInNested) { if (token.tokenType == "CurlyBracketBlock") {
nestingLevel++;
} elseif (token.tokenType == "CloseCurlyBracket") {
nestingLevel--;
}
// If we were in a nested rule, and we saw the last closing curly bracket, // reset the state to parse possible declarations declared after the nested rule. if (nestingLevel === 0) {
isInNested = false; // We need to remove the previous pending declaration and reset the state
declarations.pop();
resetStateForNextDeclaration();
} continue;
} elseif (
token.tokenType === "CloseParenthesis" ||
token.tokenType === "CloseSquareBracket"
) { // Closing the last block that was opened.
currentBlocks.pop();
current += token.text;
} elseif (
token.tokenType === "ParenthesisBlock" ||
token.tokenType === "SquareBracketBlock"
) { // Opening a new block.
currentBlocks.push(token.text);
current += token.text;
} elseif (token.tokenType === "Function") { // Opening a function is like opening a new block, so push one to the stack.
currentBlocks.push("(");
current += token.text;
} elseif (token.tokenType === "Colon") { // Either way, a "!important" we've seen is no longer valid now.
importantState = 0;
importantWS = false; if (!lastProp.name) { // Set the current declaration name if there's no name yet
lastProp.name = cssTrim(current);
lastProp.colonOffsets = [token.startOffset, token.endOffset];
current = "";
currentBlocks = [];
// When parsing a comment body, if the left-hand-side is not a // valid property name, then drop it and stop parsing. if (
inComment &&
!commentOverride &&
!isCssPropertyKnown(lastProp.name)
) {
lastProp.name = null; break;
}
} else { // Otherwise, just append ':' to the current value (declaration value // with colons)
current += ":";
}
} elseif (token.tokenType === "Semicolon" && !currentBlocks.length) {
lastProp.terminator = ""; // When parsing a comment, if the name hasn't been set, then we // have probably just seen an ordinary semicolon used in text, // so drop this and stop parsing. if (inComment && !lastProp.name) {
current = "";
currentBlocks = []; break;
} if (importantState === 2) {
lastProp.priority = "important";
} elseif (importantState === 1) {
current += "!"; if (importantWS) {
current += " ";
}
}
lastProp.value = cssTrim(current);
resetStateForNextDeclaration();
} elseif (token.tokenType === "Ident") { if (token.text === "important" && importantState === 1) {
importantState = 2;
} else { if (importantState > 0) {
current += "!"; if (importantWS) {
current += " ";
} if (importantState === 2) {
current += "important ";
}
importantState = 0;
importantWS = false;
}
current += token.text;
}
} elseif (token.tokenType === "Delim" && token.text === "!") {
importantState = 1;
} elseif (token.tokenType === "WhiteSpace") { if (current !== "") {
current = current.trimEnd() + " ";
}
} elseif (token.tokenType === "Comment") { if (parseComments && !lastProp.name && !lastProp.value) { const commentText = inputString.substring(
token.startOffset + 2,
token.endOffset - 2
); const newDecls = parseCommentDeclarations(
isCssPropertyKnown,
commentText,
token.startOffset,
token.endOffset
);
// Insert the new declarations just before the final element. const lastDecl = declarations.pop();
declarations = [...declarations, ...newDecls, lastDecl];
} else {
current = current.trimEnd() + " ";
}
} else { if (importantState > 0) {
current += "!"; if (importantWS) {
current += " ";
} if (importantState === 2) {
current += "important ";
}
importantState = 0;
importantWS = false;
}
current += inputString.substring(token.startOffset, token.endOffset);
}
}
// Handle whatever trailing properties or values might still be there if (current) { // If nested rule doesn't have closing bracket if (isInNested && nestingLevel > 0) { // We need to remove the previous (nested) pending declaration
declarations.pop();
} elseif (!lastProp.name) { // Ignore this case in comments. if (!inComment) { // Trailing property found, e.g. p1:v1;p2:v2;p3
lastProp.name = cssTrim(current);
}
} else { // Trailing value found, i.e. value without an ending ; if (importantState === 2) {
lastProp.priority = "important";
} elseif (importantState === 1) {
current += "!";
}
lastProp.value = cssTrim(current); const terminator = lexer.performEOFFixup("");
lastProp.terminator = terminator + ";"; // If the input was unterminated, attribute the remainder to // this property. This avoids some bad behavior when rewriting // an unterminated comment. if (terminator) {
lastProp.offsets[1] = inputString.length;
}
}
}
// Remove declarations that have neither a name nor a value
declarations = declarations.filter(prop => prop.name || prop.value);
return declarations;
}
/** * Returns an array of CSS declarations given a string. * For example, parseDeclarations(isCssPropertyKnown, "width: 1px; height: 1px") * would return: * [{name:"width", value: "1px"}, {name: "height", "value": "1px"}] * * The input string is assumed to only contain declarations so { and } * characters will be treated as part of either the property or value, * depending where it's found. * * @param {Function} isCssPropertyKnown * A function to check if the CSS property is known. This is either an * internal server function or from the CssPropertiesFront. * that are supported by the server. * @param {String} inputString * An input string of CSS * @param {Boolean} parseComments * If true, try to parse the contents of comments as well. * A comment will only be parsed if it occurs outside of * the body of some other declaration. * @return {Array} an array of objects with the following signature: * [{"name": string, "value": string, "priority": string, * "terminator": string, * "offsets": [start, end], "colonOffsets": [start, end]}, * ...] * Here, "offsets" holds the offsets of the start and end * of the declaration text, in a form suitable for use with * String.substring. * "terminator" is a string to use to terminate the declaration, * usually "" to mean no additional termination is needed. * "colonOffsets" holds the start and end locations of the * ":" that separates the property name from the value. * If the declaration appears in a comment, then there will * be an additional {"commentOffsets": [start, end] property * on the object, which will hold the offsets of the start * and end of the enclosing comment.
*/ function parseDeclarations(
isCssPropertyKnown,
inputString,
parseComments = false
) { return parseDeclarationsInternal(
isCssPropertyKnown,
inputString,
parseComments, false, false
);
}
/** * Like @see parseDeclarations, but removes properties that do not * have a name.
*/ function parseNamedDeclarations(
isCssPropertyKnown,
inputString,
parseComments = false
) { return parseDeclarations(
isCssPropertyKnown,
inputString,
parseComments
).filter(item => !!item.name);
}
/** * Returns an array of the parsed CSS selector value and type given a string. * * The components making up the CSS selector can be extracted into 3 different * types: element, attribute and pseudoclass. The object that is appended to * the returned array contains the value related to one of the 3 types described * along with the actual type. * * The following are the 3 types that can be returned in the object signature: * (1) SELECTOR_ATTRIBUTE * (2) SELECTOR_ELEMENT * (3) SELECTOR_PSEUDO_CLASS * * @param {String} value * The CSS selector text. * @return {Array} an array of objects with the following signature: * [{ "value": string, "type": integer }, ...]
*/ // eslint-disable-next-line complexity function parsePseudoClassesAndAttributes(value) { if (!value) { thrownew Error("empty input string");
}
// See InspectorCSSToken dictionnary in InspectorUtils.webidl for more information // about the tokens. const tokensIterator = cssTokenizer(value); const result = [];
let current = "";
let functionCount = 0;
let hasAttribute = false;
let hasColon = false;
for (const token of tokensIterator) { if (token.tokenType === "Ident") {
current += value.substring(token.startOffset, token.endOffset);
if (hasColon && !functionCount) { if (current) {
result.push({ value: current, type: SELECTOR_PSEUDO_CLASS });
}
current = "";
hasColon = false;
}
} elseif (token.tokenType === "Colon") { if (!hasColon) { if (current) {
result.push({ value: current, type: SELECTOR_ELEMENT });
}
current = "";
hasColon = true;
}
current += token.text;
} elseif (token.tokenType === "Function") {
current += value.substring(token.startOffset, token.endOffset);
functionCount++;
} elseif (token.tokenType === "CloseParenthesis") {
current += token.text;
if (hasColon && functionCount == 1) { if (current) {
result.push({ value: current, type: SELECTOR_PSEUDO_CLASS });
}
current += token.text;
} elseif (token.tokenType === "CloseSquareBracket") {
current += token.text;
if (hasAttribute && !functionCount) { if (current) {
result.push({ value: current, type: SELECTOR_ATTRIBUTE });
}
current = "";
hasAttribute = false;
}
} else {
current += value.substring(token.startOffset, token.endOffset);
}
}
if (current) {
result.push({ value: current, type: SELECTOR_ELEMENT });
}
return result;
}
/** * Expects a single CSS value to be passed as the input and parses the value * and priority. * * @param {Function} isCssPropertyKnown * A function to check if the CSS property is known. This is either an * internal server function or from the CssPropertiesFront. * that are supported by the server. * @param {String} value * The value from the text editor. * @return {Object} an object with 'value' and 'priority' properties.
*/ function parseSingleValue(isCssPropertyKnown, value) { const declaration = parseDeclarations(
isCssPropertyKnown, "a: " + value + ";"
)[0]; return {
value: declaration ? declaration.value : "",
priority: declaration ? declaration.priority : "",
};
}
/** * Convert an angle value to degree. * * @param {Number} angleValue The angle value. * @param {CSS_ANGLEUNIT} angleUnit The angleValue's angle unit. * @return {Number} An angle value in degree.
*/ function getAngleValueInDegrees(angleValue, angleUnit) { switch (angleUnit) { case CSS_ANGLEUNIT.deg: return angleValue; case CSS_ANGLEUNIT.grad: return angleValue * 0.9; case CSS_ANGLEUNIT.rad: return (angleValue * 180) / Math.PI; case CSS_ANGLEUNIT.turn: return angleValue * 360; default: thrownew Error("No matched angle unit.");
}
}
Die Informationen auf dieser Webseite wurden
nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit,
noch Qualität der bereit gestellten Informationen zugesichert.
Bemerkung:
Die farbliche Syntaxdarstellung ist noch experimentell.