/* This Source Code Form is subject to the terms of the Mozilla Public *License,v.2.0.IfacopyoftheMPLwasnotdistributedwiththis
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
// This regexp matches a URL token. It puts the "url(", any // leading whitespace, and any opening quote into |leader|; the // URL text itself into |body|, and any trailing quote, trailing // whitespace, and the ")" into |trailer|. const URL_REGEX =
/^(?<leader>url\([ \t\r\n\f]*(["']?))(?<body>.*?)(?<trailer>\2[ \t\r\n\f]*\))$/i;
// Very long text properties should be truncated using CSS to avoid creating // extremely tall propertyvalue containers. 5000 characters is an arbitrary // limit. Assuming an average ruleview can hold 50 characters per line, this // should start truncating properties which would otherwise be 100 lines long. const TRUNCATE_LENGTH_THRESHOLD = 5000; const TRUNCATE_NODE_CLASSNAME = "propertyvalue-long-text";
// This symbol is used in stack entries for the `tokenType` property of the object we set // as a key in `tokensByPart`, for token/part pairs that were already processed in child // stack entries and which shouldn't be processed as individual entries. const AGGREGATED_TOKEN_TYPE = Symbol("AGGREGATED_TOKEN_TYPE");
const angleOK = function (angle) { returnnew angleUtils.CssAngle(angle).valid;
};
let spaceNeeded = false;
let token; while ((token = tokenStream.nextToken())) { const tokenType = token.tokenType; if (tokenType === "Comment") { // This doesn't change spaceNeeded, because we didn't emit // anything to the output. continue;
}
if ( this.#stack.length && // Don't add the token text to the current stack if we have a function or an // opening parenthesis, as we're going to create a new stack entry for those (with // the tokenText being the initial text value in it)
tokenType !== "Function" &&
tokenType !== "ParenthesisBlock"
) { const stackEntry = this.#stack.at(-1);
stackEntry.text += tokenText; // We only want to add the token text to substituted text when there was one // deeper subtitution function (see #onCloseParenthesis) if (stackEntry.substitutedText !== null) {
stackEntry.substitutedText += tokenText;
}
}
case"CloseParenthesis": { if (options.expectFont && fontFamilyNameIndex !== null) { this.#wrapFontFamilyName(fontFamilyNameIndex, options); // reset the variable so we can handle following names
fontFamilyNameIndex = null;
} this.#appendTextNode(")", token); this.#onCloseParenthesis(options); break;
}
case"Comma": case"Delim": if (
(token.tokenType === "Comma" || token.text === "!") &&
options.expectFont &&
fontFamilyNameIndex !== null
) { this.#wrapFontFamilyName(fontFamilyNameIndex, options); // reset the variable so we can handle following names
fontFamilyNameIndex = null;
}
if (options.expectFont && fontFamilyNameIndex !== null) { this.#wrapFontFamilyName(fontFamilyNameIndex, options);
}
// We might never encounter a matching closing parenthesis for a function and still // have a "valid" value (e.g. `background: linear-gradient(90deg, red, blue"`) // In such case, go through the stack and handle each items until we have nothing left. if (this.#stack.length) { while (this.#stack.length !== 0) { this.#onCloseParenthesis(options);
}
}
let result = this.#toDOM();
if (options.expectFilter && !options.filterSwatch) {
result = this.#wrapFilter(text, options, result);
}
return result;
}
/** *Addastackentryinthis.#stack * *@param{object}entryData:Anobjectthatwillbespreadintothestackentry.
*/
#createStackEntry(entryData) { const stackEntry = { // The parsed parts of the function that will be rendered on screen. // This can hold Element or Text instances
parts: [], // A <(Element|Text),object> Map, whose keys are element in `parts`, // and values are usually the token they represents (multiple part can represent // a single token). // When a set of tokens (e.g. a stack entry, a font family name, …) was already // handled (e.g. in #onCloseParenthesis or in #wrapFontFamilyName) the value will, // be an object with an AGGREGATED_TOKEN_TYPE tokenType and a `data` property that will // hold all or a subset of the properties that can be found in a stack entry
tokensByPart: new WeakMap(), // Function name if token is a function, null otherwise.
functionName: null, // Lowercase function name if token is a function, null otherwise. // Precomputed because this can be a hot path.
lowerCaseFunctionName: null, // Will hold the names of the functions that are used inside the current one
nestedFunctions: [], // Boolean indicating if the function accepts color parameters // if token is a function, null otherwise.
isColorTakingFunction: null, // Will hold the text for the stack entry, i.e. the whole function call // (e.g. `min(10px, max(1em, var(--w, 20w)))`),
text: "", // Will hold the substituted text for the stack entry, i.e. the whole function call // with subtitution functions (for now `var()`, but later `attr()` and `env()`) // being replaced by their returned value // (e.g. for `min(10px, max(1em, var(--w, 20w)))` with `--w:30%`, this will be // `min(10px, max(1em, 30%))`), // Initial value is null so it can properly be replaced by `text` when there // isn't any subtitution functions in the stack (we can't just use an empty string // as some function can subtitute to an empty string)
substitutedText: null, // Used to know if a comma was found in the entry. Useful for functions like `var()` // or `attr()` to know if they have a fallback.
sawComma: false,
...entryData,
}; this.#stack.push(stackEntry);
}
// eslint-disable-next-line complexity
#onCloseParenthesis(options) { if (!this.#stack.length) { return;
}
const stackEntry = this.#stack.pop();
let { lowerCaseFunctionName, parts, text } = stackEntry; if (lowerCaseFunctionName === "attr") {
parts = this.#onCloseParenthesisForAttr(stackEntry, options);
} elseif (lowerCaseFunctionName === "cubic-bezier") {
parts = this.#onCloseParenthesisForCubicBezier(stackEntry, options);
} elseif (lowerCaseFunctionName === "light-dark") {
parts = this.#onCloseParenthesisForLightDark(stackEntry, options);
} elseif (lowerCaseFunctionName === "linear") {
parts = this.#onCloseParenthesisForLinear(stackEntry, options);
} elseif (lowerCaseFunctionName === "url") {
parts = this.#onCloseParenthesisForUrl(stackEntry, options);
} elseif (lowerCaseFunctionName === "var") {
parts = this.#onCloseParenthesisForVar(stackEntry, options);
} elseif (BASIC_SHAPE_FUNCTIONS.has(lowerCaseFunctionName)) {
parts = this.#onCloseParenthesisForBasicShape(stackEntry, options);
} elseif (
(options.supportsColor ||
((options.expectFilter || options.isVariable) && this.#stack.length !== 0 && this.#stack.at(-1).isColorTakingFunction)) &&
InspectorUtils.isValidCSSColor( // use the substituted text when we have one, as it allows us to still get the // color swatch when we have CSS variables parameters
stackEntry.substitutedText ?? stackEntry.text
)
) { const colorFunctionEntry = this.#stack.findLast(
entry => entry.isColorTakingFunction
); const colorObj =
options.colorObj || new colorUtils.CssColor(stackEntry.substitutedText ?? stackEntry.text); const colorContainerEl = this.#createColorContainerElement(
colorObj,
{
...options,
colorFunction: colorFunctionEntry?.functionName,
},
stackEntry.parts
);
parts = [colorContainerEl];
}
// Put all the parts in the "new" last stack, or the main parsed array if there // is no more entry in the stack this.#getCurrentStackParts().push(...parts);
if (this.#stack.length) { const lastStackEntry = this.#stack.at(-1); // This needs to be done before we update lastStackEntry.text if ( // Only compute the substituted text if a stack entry has substituted text…
stackEntry.substitutedText || // …or if substituted text was already consumed in a "child" stack entry
lastStackEntry.substitutedText
) { // if that's the first substituted function we encounter, we need to initialize // the value if (lastStackEntry.substitutedText === null) {
lastStackEntry.substitutedText = lastStackEntry.text;
}
// substitutedText is only computed for some functions, so fall back to text when // it doesn't exist const textToAdd = stackEntry.substitutedText ?? text;
lastStackEntry.substitutedText += textToAdd;
} // Then update the authored text
lastStackEntry.text += text;
if (stackEntry.lowerCaseFunctionName) { // Set the nested functions by adding the one for the stack entry we just handled
lastStackEntry.nestedFunctions = [
stackEntry.lowerCaseFunctionName,
...stackEntry.nestedFunctions,
];
} else { // If we closed a parenthesis block, just copy the nested functions we had
lastStackEntry.nestedFunctions = Array.from(stackEntry.nestedFunctions);
}
const compoundEntryToken = { // Associate AGGREGATED_TOKEN_TYPE to the part so consumers can know the part was for // a previous stack entry and shouldn't be considered.
tokenType: AGGREGATED_TOKEN_TYPE,
data: stackEntry,
}; for (const part of parts) {
lastStackEntry.tokensByPart.set(part, compoundEntryToken);
}
}
}
let separatorIndex = null; for (let i = 0; i < stackEntryParts.length; i++) { const token = stackEntry.tokensByPart.get(stackEntryParts[i]); if (token?.tokenType === "Comma") { if (separatorIndex === null) {
separatorIndex = i;
} else { // light-dark takes exactly two parameters, so if we don't get exactly 1 separator // at this point, that means that the value is valid at parse time, but is invalid // at computed value time. // TODO: We might want to add a class to indicate that this is invalid at computed // value time (See Bug 1910845) return stackEntryParts;
}
}
}
if (separatorIndex === null) { return stackEntryParts;
}
let startIndex;
let endIndex; if (options.isDarkColorScheme) { // If we're using a dark color scheme, we want to mark the first param as // not used.
// The first "part" is `light-dark(`, so we can start after that. // We want to filter out white space character before the first parameter for (let i = 1; i < separatorIndex; i++) { const token = stackEntry.tokensByPart.get(stackEntryParts[i]); if (token?.tokenType !== "WhiteSpace") {
startIndex = i; break;
}
}
// same for the end of the parameter, we want to filter out whitespaces // after the parameter and before the comma
endIndex = separatorIndex - 1; for (let i = endIndex; i >= startIndex; i--) { const token = stackEntry.tokensByPart.get(stackEntryParts[i]); if (token?.tokenType !== "WhiteSpace") { // We found a non-whitespace part, we need to include it, so increment the endIndex
endIndex = i + 1; break;
}
}
} else { // If we're not using a dark color scheme, we want to mark the second param as // not used.
// We want to filter out white space character after the comma and before the // second parameter for (let i = separatorIndex + 1; i < stackEntryParts.length; i++) { const token = stackEntry.tokensByPart.get(stackEntryParts[i]); if (token?.tokenType !== "WhiteSpace") {
startIndex = i; break;
}
}
// same for the end of the parameter, we want to filter out whitespaces // after the parameter and before the closing parenthesis (which is not yet // included in stackEntryParts) for ( // we don't start at the last part, but the one before that, as the last part will // always be the closing parenthesis for the function, and it shouldn't be included // in the unmatched span.
let i = stackEntryParts.length - 2;
i > separatorIndex;
i--
) { const token = stackEntry.tokensByPart.get(stackEntryParts[i]); if (token?.tokenType !== "WhiteSpace") { // We found a non-whitespace part, we need to include it, so increment the endIndex
endIndex = i + 1; break;
}
}
}
const parts = stackEntryParts.slice(startIndex, endIndex);
// If the item we need to mark is already an element (e.g. a parsed color), // just add a class to it. if (parts.length === 1 && Element.isInstance(parts[0])) {
parts[0].classList.add(options.unmatchedClass);
} else { // Otherwise, we need to wrap our parts into a specific element so we can // style them const node = this.#createNode("span", { class: options.unmatchedClass,
});
node.append(...parts);
stackEntryParts.splice(startIndex, parts.length, node);
}
let attrNameIndex = null;
let attrTypeIndex = null;
let commaIndex = null; for (let i = 0; i < stackEntry.parts.length; i++) { const part = stackEntry.parts[i]; if (!stackEntry.tokensByPart.has(part)) { continue;
} const token = stackEntry.tokensByPart.get(part);
// The attribute name is the first Ident if (token.tokenType === "Ident" && attrNameIndex === null) {
attrNameIndex = i;
} elseif ( // If we have another Ident or a closed stack entry before the comma, then that's // the attr type.
attrNameIndex !== null &&
attrTypeIndex === null && // Here we're looking for <attr-type> which might be an Ident (raw-string, // number, px, …), a % (Delim) or the `type()` function (which will be represented // as an aggregated token at this point)
(token.tokenType === "Ident" ||
(token.tokenType === "Delim" && token.text === "%") ||
token.tokenType === AGGREGATED_TOKEN_TYPE)
) {
attrTypeIndex = i;
} elseif (token.tokenType === "Comma") {
commaIndex = i; break;
}
}
// This shouldn't happen, but let's be safe if (attrNameIndex === null) { return stackEntry.parts;
}
// Get the attribute name part, which should be the first Ident const attrNamePart = stackEntry.parts[attrNameIndex]; const attrName = attrNamePart.textContent; // and its value const attrValue = options.getAttributeValue(attrName);
// as well as the first attribute (might contain attribute name + typing information), // with specific style if the attribute isn't set const attrFirstParamNode = this.#createNode("span", { class: "inspector-attr-param",
});
// > When an <attr-type> is set, attr() will try to parse the attribute into that // > specified <attr-type> and return it. // > If the attribute cannot be parsed into the given <attr-type>, the <fallback-value> // > will be returned instead. // > When no <attr-type> is set, the attribute will be parsed into a CSS string. // > If no <fallback-value> is set, the return value will default to an empty string // > when no <attr-type> is set or the guaranteed-invalid value when an <attr-type> is set.
let fallbackValueIsUsed = attrValue === null;
let attrTypeMismatchText; if (attrTypeIndex !== null && attrValue !== null) { const part = stackEntry.parts[attrTypeIndex]; const token = stackEntry.tokensByPart.get(part); // First, we want to handle <attr-type> other than `type()`, i.e. Idents (`raw-string`, // `number`, `px`, …) and `%` if (
token.tokenType === "Ident" ||
(token.tokenType === "Delim" && token.text === "%")
) { // For `number` and units, the spec says: // > If given as the number keyword, it causes the attribute’s literal value […] // to be parsed as a <number-token>. // > Values that fail to parse trigger fallback. // […] // > If given as an <attr-unit> value, the value is first parsed as if number // > keyword was specified, then the resulting numeric value is turned into a // > dimension with the corresponding unit, or a percentage if % was given. // > Same as for number <attr-type>, values that do not correspond to the // > <number-token> production trigger fallback.
// So we need to check that the attribute value is actually a number. And that's // pretty much it: for <attr-unit>, if the given unit is not known, the declaration // is invalid and won't be parsed anyway
if (
token.text !== "raw-string" &&
!InspectorUtils.valueMatchesSyntax(this.#doc, attrValue, "<number>")
) {
fallbackValueIsUsed = true;
attrTypeMismatchText = STYLE_INSPECTOR_L10N.getFormatStr( "rule.attributeNotNumber",
`"${attrValue}"`
);
}
} elseif (
token.tokenType === AGGREGATED_TOKEN_TYPE &&
token.data.lowerCaseFunctionName === "type"
) { // Here we have a type() function. We need to extract its content to see if // the attribute value can be parsed with this type. // We can take a small shortcut here: we have the text of the type() function so… const syntax = token.data.text
.slice( // …we can just remove the leading "type(" 5, // …as well as the trailing ")"
-1
)
.trim(); if (!InspectorUtils.valueMatchesSyntax(this.#doc, attrValue, syntax)) {
fallbackValueIsUsed = true;
attrTypeMismatchText = STYLE_INSPECTOR_L10N.getFormatStr( "rule.attributeUnmatchedType",
`"${attrValue}"`,
`"${syntax}"`
);
}
}
}
// First, we want to render the attribute name on its own element const attrNameNode = this.#createNode( "span",
{ class: "inspector-attr-name",
},
attrName
);
stackEntry.parts[attrNameIndex] = attrNameNode;
if (fallbackValueIsUsed) {
attrFirstParamNode.classList.add(options.unmatchedClass);
}
if (attrValue === null) {
attrFirstParamNode.setAttribute( "data-attribute",
STYLE_INSPECTOR_L10N.getFormatStr("rule.attributeUnset", attrName)
);
} elseif (attrTypeMismatchText) {
attrFirstParamNode.setAttribute("data-attribute", attrTypeMismatchText);
} else { // Otherwise we set it on the attribute name only
attrNameNode.setAttribute("data-attribute", `"${attrValue}"`);
}
// Let's put all the parts starting with the attribute name until the comma
let attrFirstParamChildCount = 0;
let attrFirstParamEndIndex; if (commaIndex === null) { // if we didn't found a comma, we want to get all the items until the closing // parenthesis, which is the last item in parts
attrFirstParamEndIndex = stackEntry.parts.length - 1;
} elseif ( // if the token before the comma is a whitespace, don't include it in the first param node
stackEntry.tokensByPart.get(stackEntry.parts[commaIndex - 1])
?.tokenType === "WhiteSpace"
) {
attrFirstParamEndIndex = commaIndex - 1;
} else {
attrFirstParamEndIndex = commaIndex;
}
for (let i = attrNameIndex; i < attrFirstParamEndIndex; i++) {
attrFirstParamNode.append(stackEntry.parts[i]);
attrFirstParamChildCount++;
}
// We don't have to do anything more when there's no fallback value, i.e. if we didn't // found a comma if (commaIndex === null) { return stackEntry.parts;
}
// we need to update the comma index, as we added attrFirstParamNode in parts and // removed all the elements we put in it.
commaIndex = commaIndex + 1 - attrFirstParamChildCount;
let fallbackStartIndex = null; // Then we want to find the part that correspond to the first non whitespace token, // which will be the start of the fallback param for (let i = commaIndex + 1; i < stackEntry.parts.length; i++) { const part = stackEntry.parts[i]; if (!stackEntry.tokensByPart.has(part)) { continue;
} const token = stackEntry.tokensByPart.get(part); if ( // we might get into a part that was already handled, for example a nested function, // and in such case, it should be part of the fallback element
token.tokenType === AGGREGATED_TOKEN_TYPE ||
token.tokenType !== "WhiteSpace"
) {
fallbackStartIndex = i; break;
}
}
// This shouldn't happen, but let's be safe an bail if we didn't find the fallback part if (fallbackStartIndex === null) { return stackEntry.parts;
}
// The last part is the closing bracket, so let's put the index before it.
let fallbackEndTokenIndex = stackEntry.parts.length - 2; for (let i = fallbackEndTokenIndex; i >= fallbackStartIndex; i--) { const part = stackEntry.parts[i]; if (!stackEntry.tokensByPart.has(part)) { continue;
} const token = stackEntry.tokensByPart.get(part); if ( // we might get into a part that was already handled, for example a nested function, // and in such case, it should be part of the fallback element
token.tokenType === AGGREGATED_TOKEN_TYPE ||
token.tokenType !== "WhiteSpace"
) {
fallbackEndTokenIndex = i; break;
}
}
// So, at this point, we have the fallback parts that we want to put in their own elements const partsToWrap = stackEntry.parts.splice(
fallbackStartIndex,
fallbackEndTokenIndex - fallbackStartIndex + 1
);
// Let's retrieve the index in `parts` where the coordinates start
let coordStartIdx = null;
let previousToken; for (let i = 0; i < stackEntry.parts.length; i++) { const part = stackEntry.parts[i]; const token = stackEntry.tokensByPart.get(part); // Multiple consecutive parts can reference the same token, so let's find the first // part that refers to a token that is not the initial function. if (
token.tokenType === "Function" &&
(!previousToken || token === previousToken)
) {
coordStartIdx = i + 1;
previousToken = token;
valContainer.append(part);
} elseif (coordStartIdx !== null) { // we already found the coordinate, and the token does not represent the initial // function, so we can stop looping break;
}
}
// That shouldn't happen, but let's be safe if (coordStartIdx === null) { return stackEntry.parts;
}
// Let's iterate through points in reverse as we're going to mutate stackEntry.parts // and the indexes in `points` refer to the original indexes for (let i = points.length - 1; i >= 0; i--) { const point = points[i]; const xNode = this.#createNode("span", { class: "inspector-shape-point", "data-point": i, "data-pair": "x",
}); for (const idx of point.x) {
xNode.append(stackEntry.parts[idx]);
} const yNode = this.#createNode("span", { class: "inspector-shape-point", "data-point": i, "data-pair": "y",
}); for (const idx of point.y) {
yNode.append(stackEntry.parts[idx]);
} const coordNode = this.#createNode("span", { class: "inspector-shape-point", "data-point": i,
});
coordNode.append(xNode); // Put the parts between the x and y points for (let j = point.x.at(-1) + 1; j < point.y[0]; j++) {
coordNode.append(stackEntry.parts[j]);
}
coordNode.append(yNode);
stackEntry.parts.splice(
point.x[0],
point.y.at(-1) - point.x[0] + 1,
coordNode
);
}
// circle() can take a radius which is before `at`, which can be a length, percentage, // or a keyword (closest-corner, closest-side, farthest-corner, farthest-side) if( !seenAtKeyword&& (token.tokenType==="Number"|| token.tokenType==="Dimension"|| token.tokenType==="Percentage"|| token.tokenType==="Ident"|| // a collapsed function call (e.g. `var(…)`) counts as a single argument token.tokenType===AGGREGATED_TOKEN_TYPE) ){ // we have a single radius, the array will contain all the indexes of parts that // refer to it. radiusPartsIndexes.push(i); }
// after that `at` keyword, the position of the circle is defined. It can be represented // by 1, 2 or 4 length, percentage or keyword (e.g. start, center, …) // So let's collect all those here if( seenAtKeyword&& (token.tokenType==="Number"|| token.tokenType==="Dimension"|| token.tokenType==="Percentage"|| token.tokenType==="Ident"|| // a collapsed function call (e.g. `var(…)`) counts as a single argument token.tokenType===AGGREGATED_TOKEN_TYPE) ){ if(token!==previousToken){ positionsPartsIndexes.push([i]); }else{ // if the token for the current part is the same one as the previous part, then // it represent the same position, so we add the part index to the last position // item we added. positionsPartsIndexes.at(-1).push(i); } }
previousToken=token; }
// We're going to mutate stackEntry.parts, so let's go through the parts in reverse // as the indexes in radiusIndexes and positionIndexes refer to the original indexes // So first, let's handle positions if there are some if(positionsPartsIndexes.length){ constcenterEl=this.#createNode("span",{ class:"inspector-shape-point", "data-point":"center", }); for(leti=positionsPartsIndexes.length-1;i>=0;i--){ constpointEl=this.#createNode("span",{ class:"inspector-shape-point", "data-point":"center", }); if(i===0){ pointEl.setAttribute("data-pair","x"); }elseif(positionsPartsIndexes.length===2){ // Here we're not handling the first item, and there's only 2 items, so we know // we have the y coord pointEl.setAttribute("data-pair","y"); }elseif(i===2){ // If there's more than 2 position, that means we have a <position-four> type, // where there's both x,y positions + offsets (e.g. `left 10px top 15px`) // In such case, the first item is x (already handled in the first if block), // and the third item is y pointEl.setAttribute("data-pair","y"); }
// append any parts between this point and the previous one into centerEl constpreviousIndexes=positionsPartsIndexes[i-1]; if(previousIndexes){ for(letj=indexes[0]-1;j>previousIndexes.at(-1);j--){ centerEl.prepend(stackEntry.parts[j]); stackEntry.parts.splice(j,1); } } } stackEntry.parts.splice(positionsPartsIndexes[0][0],0,centerEl); }
// Handle radius size if there's one if(radiusPartsIndexes.length){ constradiusEl=this.#createNode("span",{ class:"inspector-shape-point", "data-point":"radius", }); for(leti=radiusPartsIndexes.length-1;i>=0;i--){ constidx=radiusPartsIndexes[i]; radiusEl.prepend(stackEntry.parts[idx]); stackEntry.parts.splice(idx,1); } stackEntry.parts.splice(radiusPartsIndexes[0],0,radiusEl); }
returnstackEntry.parts; }
/** *Calledwhenwegottheclosingbracketforthe`ellipse()`function. *ItwillappendaCSSshapeshighlightertogglenexttothevalue,andparsethevalue *intospans,eachcontainingapointthatcanbehoveredover. * *@param{object}stackEntry *Thelastiteminthis.#stack *@param{number}coordsStartIdx *TheindexinstackEntry.partsatwhichthecoordinatesfortheellipsestart *@returns{Array<Element|Text>}Thepartsthatwerehandled
*/ // eslint-disable-next-line complexity
#onCloseParenthesisForEllipseShape(stackEntry, coordsStartIdx) { const radiiPartsIndexes = []; const positionsPartsIndexes = [];
let seenAtKeyword = false;
let previousToken; for (let i = coordsStartIdx; i < stackEntry.parts.length; i++) { const part = stackEntry.parts[i]; const token = stackEntry.tokensByPart.get(part);
// ellipse() can take two radii before `at`, which can be a lengths, percentages, // or a keywords (closest-corner, closest-side, farthest-corner, farthest-side) if (
!seenAtKeyword &&
(token.tokenType === "Number" ||
token.tokenType === "Dimension" ||
token.tokenType === "Percentage" ||
token.tokenType === "Ident" || // a collapsed function call (e.g. `var(…)`) counts as a single argument
token.tokenType === AGGREGATED_TOKEN_TYPE)
) { if (token !== previousToken) {
radiiPartsIndexes.push([i]);
} else { // if the token for the current part is the same one as the previous part, then // it represent the same radius, so we add the part index to the last radius // item we added.
radiiPartsIndexes.at(-1).push(i);
}
}
// after that `at` keyword, the position of the ellipse is defined. It can be represented // by 1, 2 or 4 length, percentage or keyword (e.g. start, center, …) // So let's collect all those here if (
seenAtKeyword &&
(token.tokenType === "Number" ||
token.tokenType === "Dimension" ||
token.tokenType === "Percentage" ||
token.tokenType === "Ident" || // a collapsed function call (e.g. `var(…)`) counts as a single argument
token.tokenType === AGGREGATED_TOKEN_TYPE)
) { if (token !== previousToken) {
positionsPartsIndexes.push([i]);
} else { // if the token for the current part is the same one as the previous part, then // it represent the same position, so we add the part index to the last position // item we added.
positionsPartsIndexes.at(-1).push(i);
}
}
previousToken = token;
}
// We're going to mutate stackEntry.parts, so let's go through the parts in reverse // as the indexes in radiusIndexes and positionIndexes refer to the original indexes // So first, let's handle positions if there are some if (positionsPartsIndexes.length) { const centerEl = this.#createNode("span", { class: "inspector-shape-point", "data-point": "center",
}); for (let i = positionsPartsIndexes.length - 1; i >= 0; i--) { const pointEl = this.#createNode("span", { class: "inspector-shape-point", "data-point": "center",
}); if (i === 0) {
pointEl.setAttribute("data-pair", "x");
} elseif (positionsPartsIndexes.length === 2) { // Here we're not handling the first item, and there's only 2 items, so we know // we have the y coord
pointEl.setAttribute("data-pair", "y");
} elseif (i === 2) { // If there's more than 2 position, that means we have a <position-four> type, // where there's both x,y positions + offsets (e.g. `left 10px top 15px`) // In such case, the first item is x (already handled in the first if block), // and the third item is y
pointEl.setAttribute("data-pair", "y");
}
const indexes = positionsPartsIndexes[i]; for (const idx of indexes) {
pointEl.append(stackEntry.parts[idx]);
} // we're iterating the parts in reverse, so we need to prepend in centerEl
centerEl.prepend(pointEl); // We can remove as many items as we have indexes here, because if we have // multiple parts refering to the same position, their indexes should be consecutive.
stackEntry.parts.splice(indexes[0], indexes.length);
// prepend any parts (e.g. whitespaces) between this point and the previous one // into centerEl const previousIndexes = positionsPartsIndexes[i - 1]; if (previousIndexes) { for (let j = indexes[0] - 1; j > previousIndexes.at(-1); j--) {
centerEl.prepend(stackEntry.parts[j]);
stackEntry.parts.splice(j, 1);
}
}
}
stackEntry.parts.splice(positionsPartsIndexes[0][0], 0, centerEl);
}
// Handle radius size if there are some if (radiiPartsIndexes.length) { for (let i = radiiPartsIndexes.length - 1; i >= 0; i--) { const radiusEl = this.#createNode("span", { class: "inspector-shape-point", // we should only have 2 radii, the first one being rx and the second one ry "data-point": i === 0 ? "rx" : "ry",
});
const indexes = radiiPartsIndexes[i]; for (const idx of indexes) {
radiusEl.append(stackEntry.parts[idx]);
} // We can remove as many items as we have indexes here, because if we have // multiple parts refering to the same radius, their indexes should be consecutive.
stackEntry.parts.splice(indexes[0], indexes.length, radiusEl);
}
}
return stackEntry.parts;
}
/** *Calledwhenwegottheclosingbracketforthe`inset()`function. *ItwillappendaCSSshapeshighlightertogglenexttothevalue,andparsethevalue *intospans,eachcontainingapointthatcanbehoveredover. * *@param{object}stackEntry *Thelastiteminthis.#stack *@param{number}coordsStartIdx *TheindexinstackEntry.partsatwhichthecoordinatesfortheinsetstart *@returns{Array<Element|Text>}Thepartsthatwerehandled
*/
#onCloseParenthesisForInsetShape(stackEntry, coordsStartIdx) { const insetPointsPartsIndexes = [];
let previousToken; for (let i = coordsStartIdx; i < stackEntry.parts.length; i++) { const part = stackEntry.parts[i]; const token = stackEntry.tokensByPart.get(part);
if (token.tokenType === "Ident" && token.text === "round") { // Once we see the `round` keyword, we can stop looping, we have all the coordinates // we need break;
}
if (
token.tokenType !== "Number" &&
token.tokenType !== "Dimension" &&
token.tokenType !== "Percentage" && // a collapsed function call (e.g. `var(…)`) counts as a single argument
token.tokenType !== AGGREGATED_TOKEN_TYPE
) { continue;
}
// Let's iterate through points in reverse as we're going to mutate stackEntry.parts // and the indexes in `points` refer to the original indexes for (let i = insetPointsPartsIndexes.length - 1; i >= 0; i--) { const pointPartsIndexes = insetPointsPartsIndexes[i]; const shapePointNode = this.#createNode("span", { class: "inspector-shape-point",
});
// insetPoints contains the 4 different possible inset points in the order they are // defined. By taking the modulo of the index in insetPoints with the number of nodes, // we can get which node represents each point (e.g. if there is only 1 node, it // represents all 4 points). The exception is "left" when there are 3 nodes. In that // case, it is nodes[1] that represents the left point rather than nodes[0]. if (insetPointsPartsIndexes.length === 1) {
shapePointNode.classList.add(...insetPoints);
} elseif (insetPointsPartsIndexes.length === 2) { if (i === 0) {
shapePointNode.classList.add(insetPoints[0], insetPoints[2]);
} else {
shapePointNode.classList.add(insetPoints[1], insetPoints[3]);
}
} elseif (insetPointsPartsIndexes.length === 3) { if (i === 1) {
shapePointNode.classList.add(insetPoints[1], insetPoints[3]);
} else {
shapePointNode.classList.add(insetPoints[i]);
}
} elseif (insetPointsPartsIndexes.length === 4) {
shapePointNode.classList.add(insetPoints[i]);
}
for (const idx of pointPartsIndexes) {
shapePointNode.append(stackEntry.parts[idx]);
}
// url() with quoted strings are not mapped as UnquotedUrl, instead, we get a "Function" // token with "url" (the one we're closing here), and later, a "QuotedString" token // which contains the actual URL. // So here, we only need to loop through the parts to find the one which holds the // QuotedString token and wrap it in an anchor.
let url; for (let i = 0; i < stackEntry.parts.length; i++) { const part = stackEntry.parts[i]; const token = stackEntry.tokensByPart.get(part); if (token?.tokenType !== "QuotedString") { continue;
}
// url() only takes a string, so we'll only have a single part refering to the url token
url = token.value; break;
}
let varNameIndex = null;
let varName = null;
let fallbackStartIndex = null; for (let i = 0; i < stackEntry.parts.length; i++) { const part = stackEntry.parts[i]; const token = stackEntry.tokensByPart.get(part);
// The variable name is the first Ident we find if (varNameIndex === null && token.tokenType === "Ident") {
varNameIndex = i;
varName = token.text;
} elseif (token.tokenType === "Comma") { // Anything between the first comma and the end of the function is considered a // fallback value.
fallbackStartIndex = i + 1; break;
}
}
// Shouldn't happen, but let's be safe if (varNameIndex === null) { return stackEntry.parts;
}
const varData = options.getVariableData(varName); const varValue = typeof varData.value === "string"
? varData.value
: varData.registeredProperty?.initialValue;
let varStartingStyleValue; if (options.inStartingStyleRule) {
varStartingStyleValue = typeof varData.startingStyle === "string"
? varData.startingStyle
: // If the variable is not set in starting style, then it will default to either: // - a declaration in a "regular" rule // - or if there's no declaration in regular rule, to the registered property initial-value.
varValue;
}
let varSubstitutedValue = options.inStartingStyleRule
? varStartingStyleValue
: varValue; const variableExists = typeof varSubstitutedValue === "string"; // TODO: we should also check if the variable is not guaranteed invalid (see Bug 1904013) const shouldUseFallback = !variableExists; const varComputedValue = varData.computedValue; const varNameNodeOptions = {}; const varFallbackNodeOptions = {};
if (variableExists) { // The variable value is valid, store the substituted value in a data attribute to // be reused by the variable tooltip.
varNameNodeOptions["data-variable"] = varSubstitutedValue;
varNameNodeOptions.class = options.matchedVariableClass;
varFallbackNodeOptions.class = options.unmatchedClass;
// Display computed value when it exists, is different from the substituted value // we computed, and we're not inside a starting-style rule if (
!options.inStartingStyleRule && typeof varComputedValue === "string" &&
varComputedValue !== varSubstitutedValue
) {
varNameNodeOptions["data-variable-computed"] = varComputedValue;
}
// Display starting-style value when not in a starting style rule if (
!options.inStartingStyleRule && typeof varData.startingStyle === "string"
) {
varNameNodeOptions["data-starting-style-variable"] =
varData.startingStyle;
}
if (varData.registeredProperty) { const { initialValue, syntax, inherits } = varData.registeredProperty;
varNameNodeOptions["data-registered-property-initial-value"] =
initialValue;
varNameNodeOptions["data-registered-property-syntax"] = syntax; // createNode does not handle `false`, let's stringify the boolean.
varNameNodeOptions["data-registered-property-inherits"] = `${inherits}`;
}
} else { // The variable is not set and does not have an initial value, mark it unmatched.
varNameNodeOptions.class = options.unmatchedClass;
varNameNodeOptions["data-variable"] = STYLE_INSPECTOR_L10N.getFormatStr( "rule.variableUnset",
varName
);
}
// From https://drafts.csswg.org/css-variables/#using-variables: // > var(--a,) is a valid function, specifying that if the --a custom property is // > invalid or missing, the var() should be replaced with nothing. // // So if we saw a comma, initialize the value with an empty string
let fallbackSubstitutedValue = fallbackStartIndex !== null ? "" : null;
if (fallbackStartIndex !== null) { // We want to wrap the fallback into a span, so let's find the last non whitespace // token before the closing parenthesis now
let fallbackEndIndex = null; for ( // we can start at the part before the last one, as the last one will always be // the closing parenthesis
let i = stackEntry.parts.length - 2;
i >= fallbackStartIndex;
i--
) { const part = stackEntry.parts[i]; const token = stackEntry.tokensByPart.get(part); if (token.tokenType !== "WhiteSpace") {
fallbackEndIndex = i; break;
}
}
const fallbackNode = this.#createNode("span", varFallbackNodeOptions);
let previousToken; for (let i = fallbackStartIndex; i <= fallbackEndIndex; i++) { const part = stackEntry.parts[i]; const token = stackEntry.tokensByPart.get(part);
fallbackNode.append(part); if (previousToken === token) { continue;
} if (token?.tokenType === AGGREGATED_TOKEN_TYPE) {
fallbackSubstitutedValue +=
token.data.substitutedText ?? token.data.text;
} else {
fallbackSubstitutedValue += part.textContent;
}
previousToken = token;
}
stackEntry.parts.splice(
fallbackStartIndex,
fallbackEndIndex - fallbackStartIndex + 1,
fallbackNode
);
}
// Now that we went through the fallback, we can re-compute varSubstitutedValue // to potentially include the fallback value. if (shouldUseFallback) { // If the fallback should be used (i.e. the variable value is guaranteed invalid) // but none was found, then the substituted value should be an empty string, as // defined in https://drafts.csswg.org/css-variables/#guaranteed-invalid: // > The guaranteed-invalid value serializes as the empty string if (fallbackSubstitutedValue === null) {
varSubstitutedValue = "";
} else {
varSubstitutedValue = fallbackSubstitutedValue;
}
}
// TODO: We should handle the following case (see Bug 2006565) // From https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Values/var#invalid_values: // > var() functions can resolve to invalid values if: // > - […] // > - The custom property is defined but its value is an invalid value for the // property it is used in. // > When this happens, the property is treated as if it has value unset
// Add click listener to stop event propagation when shift key is pressed // in order to prevent the value input to be focused. // Bug 711942 will add a tooltip to edit angle values and we should // be able to move this listener to Tooltip.js when it'll be implemented.
swatch.addEventListener("click", function (event) { if (event.shiftKey) {
event.stopPropagation();
}
});
container.appendChild(swatch);
}
/** *CheckifaCSSpropertysupportsaspecificvalue. * *@param{string}name *CSSPropertynametocheck *@param{string}value *CSSPropertyvaluetocheck *@param{object}options *Optionsobject.Forvalidoptionsanddefaultvaluessee#mergeOptions().
*/
#cssPropertySupportsValue(name, value, options = {}) { if (
options.isValid || // The filter property is special in that we want to show the swatch even if the // value is invalid, because this way the user can easily use the editor to fix it.
options.expectFilter
) { returntrue;
}
// Checking pair as a CSS declaration string to account for "!important" in value. const declaration = `${name}:${value}`; returnthis.#doc.defaultView.CSS.supports(declaration);
}
#createColorContainerElement(colorObj, options, children) {
let color = colorObj.authored; const containerEl = this.#createNode("span", { "data-color": color,
});
if (options.colorSwatchClass) {
let attributes = { class: options.colorSwatchClass,
style: "background-color:" + color,
};
// Color swatches next to values trigger the color editor everywhere aside from // the Computed panel where values are read-only. if (!options.colorSwatchReadOnly) {
attributes = { ...attributes, tabindex: "0", role: "button" };
}
// The swatch is a <span> instead of a <button> intentionally. See Bug 1597125. // It is made keyboard accessible via `tabindex` and has keydown handlers // attached for pressing SPACE and RETURN in SwatchBasedEditorTooltip.js const swatch = this.#createNode("span", attributes); this.#colorSwatches.set(swatch, colorObj); if (options.colorFunction) {
swatch.dataset.colorFunction = options.colorFunction;
}
swatch.addEventListener("mousedown", this.#onColorSwatchMouseDown);
containerEl.appendChild(swatch);
containerEl.classList.add("color-swatch-container");
}
let colorUnit = options.defaultColorUnit; if (!options.useDefaultColorUnit) { // If we're not being asked to convert the color to the default color type // specified by the user, then force the CssColor instance to be set to the type // of the current color. // Not having a type means that the default color type will be automatically used.
colorUnit = colorUtils.classifyColor(color);
}
color = colorObj.toString(colorUnit);
containerEl.dataset.color = color;
const value = this.#createNode("span", { class: options.filterClass,
});
value.appendChild(nodes);
container.appendChild(value);
return container;
}
#onColorSwatchMouseDown = event => { if (!event.shiftKey) { return;
}
// Prevent click event to be fired to not show the tooltip
event.stopPropagation(); // Prevent text selection but switch the focus
event.preventDefault();
event.target.focus({ focusVisible: false });
const swatch = event.target; const color = this.#colorSwatches.get(swatch); const val = color.nextColorUnit();
const unitChangeEvent = new swatch.documentGlobal.CustomEvent( "unit-change"
);
swatch.dispatchEvent(unitChangeEvent);
};
/** *Ahelperfunctionthatsanitizesapossibly-unterminatedURL.
*/
#sanitizeURL(url) { // Re-lex the URL and add any needed termination characters. const urlTokenizer = new InspectorCSSParserWrapper(url, {
trackEOFChars: true,
}); // Just read until EOF; there will only be a single token. while (urlTokenizer.nextToken()) { // Nothing.
}
// Sanitize the URL. Note that if we modify the URL, we just // leave the termination characters. This isn't strictly // "as-authored", but it makes a bit more sense.
match = this.#sanitizeURL(match); const urlParts = URL_REGEX.exec(match);
// Bail out if that didn't match anything. if (!urlParts) { return [this.#doc.createTextNode(match)];
}
const parts = this.#getCurrentStackParts(); // We have the beginning of the font family, we need to find the end. // Loop through the parts in reverse to find the first non whitespace character. // By default, let's consider the last part as the end of the font name
let fontFamilyEndPartIndex = parts.length - 1; for (let i = parts.length - 1; i >= fontFamilyStartPartIndex; i--) { const part = parts[i]; if (part.textContent.trim() !== "") {
fontFamilyEndPartIndex = i; break;
}
}
// Wrap the parts in a dedicated element const fontFamilyNode = this.#createNode("span", { class: options.fontFamilyClass,
});
// If the family name is quoted, we need to put the quotes outside of the family node // So first let's compute the family name (it might be made out of multiple parts at // the moment, e.g. if we have `Helvetica Black`)
let familyName = ""; for (let i = fontFamilyStartPartIndex; i <= fontFamilyEndPartIndex; i++) {
familyName += parts[i].textContent;
}
// We'll associate the new part we create with this aggregated token, so other functions // know those shouldn't be processed. const aggregatedToken = this.#stack.length
? {
tokenType: AGGREGATED_TOKEN_TYPE,
data: {
text: familyName,
},
}
: null; const stackEntry = this.#stack.length ? this.#stack.at(-1) : null; if (stackEntry) {
stackEntry.tokensByPart.set(fontFamilyNode, aggregatedToken);
}
// Extracting opening and closing quotes, as well as the font name const quoteRegex = /^(?<open>['"])(?<name>[^'"]*)(?<close>['"])$/g; const regexResult = quoteRegex.exec(familyName); // If it's actually wrapped in quote if (regexResult !== null) { // Then, first append the closing quote, as we're going to modify parts and we rely // on the indexes to insert the name at the right spot const part = this.#doc.createTextNode(regexResult.groups.close);
parts.splice(fontFamilyEndPartIndex + 1, 0, part);
if (stackEntry) {
stackEntry.tokensByPart.set(part, aggregatedToken);
} // Update the family name with the non-quoted one
familyName = regexResult.groups.name;
}
fontFamilyNode.append(familyName);
// Then we want to insert our container, and remove the parts that are representing it const fontFamilyNodeChildCount =
fontFamilyEndPartIndex - fontFamilyStartPartIndex + 1;
parts.splice(
fontFamilyStartPartIndex,
fontFamilyNodeChildCount,
fontFamilyNode
);
// Finally we insert the opening quote if (regexResult !== null) { const part = this.#doc.createTextNode(regexResult.groups.open);
parts.splice(fontFamilyStartPartIndex, 0, part);
if (stackEntry) {
stackEntry.tokensByPart.set(part, aggregatedToken);
}
}
}
/** *Createanelementrepresentingasimpletext. * *@param{string}text *Texttoappend *@returns{Text|Element}ReturnsaText,or,ifthetextisgreaterthanthetruncate *threshold,aNodewithaspecificclasstotriggerCSS"truncation".
*/
#createTextElement(text) { if (text.length > TRUNCATE_LENGTH_THRESHOLD) { // If the text is too long, force creating a node, which will add the // necessary classname to truncate the property correctly. returnthis.#createNode("span", {}, text);
}
/** *Appendatextnodetotheoutput.Ifthepreviouslyoutputitemwasatext *nodethenweappendthetexttothatnode. * *@param{string}text *Texttoappend *@param{object}token *@return{number}Theindexofthenewpartinthepartsarray
*/
#appendTextNode(text, token) { if (text.length > TRUNCATE_LENGTH_THRESHOLD) { // If the text is too long, force creating a node, which will add the // necessary classname to truncate the property correctly. returnthis.#appendNode("span", {}, text, token);
}
for (const item in overrides) {
defaults[item] = overrides[item];
} return defaults;
}
}
module.exports = OutputParser;
Messung V0.5 in Prozent
¤ 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.0.68Bemerkung:
(Wie Sie bei der Firma Beratungs- und Dienstleistungen beauftragen können 2026-08-25)
¤
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 und die Messung sind noch experimentell.