summaryrefslogtreecommitdiffstats
path: root/devtools/client/shared
diff options
context:
space:
mode:
Diffstat (limited to 'devtools/client/shared')
-rw-r--r--devtools/client/shared/components/object-inspector/utils/node.js5
-rw-r--r--devtools/client/shared/css-angle.js11
-rw-r--r--devtools/client/shared/output-parser.js316
-rw-r--r--devtools/client/shared/screenshot.js14
-rw-r--r--devtools/client/shared/sourceeditor/css-autocompleter.js578
-rw-r--r--devtools/client/shared/sourceeditor/editor.js324
-rw-r--r--devtools/client/shared/test/browser_filter-editor-01.js5
-rw-r--r--devtools/client/shared/test/browser_outputparser.js16
-rw-r--r--devtools/client/shared/test/xpcshell/test_parseDeclarations.js23
-rw-r--r--devtools/client/shared/test/xpcshell/test_rewriteDeclarations.js4
-rw-r--r--devtools/client/shared/widgets/CubicBezierWidget.js18
-rw-r--r--devtools/client/shared/widgets/FilterWidget.js69
-rw-r--r--devtools/client/shared/widgets/LinearEasingFunctionWidget.js16
13 files changed, 867 insertions, 532 deletions
diff --git a/devtools/client/shared/components/object-inspector/utils/node.js b/devtools/client/shared/components/object-inspector/utils/node.js
index 0d049e1c4b..63d358e580 100644
--- a/devtools/client/shared/components/object-inspector/utils/node.js
+++ b/devtools/client/shared/components/object-inspector/utils/node.js
@@ -283,10 +283,7 @@ function nodeHasEntries(item) {
className === "MIDIInputMap" ||
className === "MIDIOutputMap" ||
className === "HighlightRegistry" ||
- // @backward-compat { version 125 } Support for enumerate CustomStateSet items was
- // added in 125. When connecting to older server, we don't want to show the <entries>
- // node for them. The extra check can be removed once 125 hits release.
- (className === "CustomStateSet" && Array.isArray(value.preview?.items))
+ className === "CustomStateSet"
);
}
diff --git a/devtools/client/shared/css-angle.js b/devtools/client/shared/css-angle.js
index 903b7813ad..d89cba5b7f 100644
--- a/devtools/client/shared/css-angle.js
+++ b/devtools/client/shared/css-angle.js
@@ -6,7 +6,9 @@
const SPECIALVALUES = new Set(["initial", "inherit", "unset"]);
-const { getCSSLexer } = require("resource://devtools/shared/css/lexer.js");
+const {
+ InspectorCSSParserWrapper,
+} = require("resource://devtools/shared/css/lexer.js");
loader.lazyRequireGetter(
this,
@@ -68,13 +70,14 @@ CssAngle.prototype = {
},
get valid() {
- const token = getCSSLexer(this.authored).nextToken();
+ const token = new InspectorCSSParserWrapper(this.authored).nextToken();
if (!token) {
return false;
}
+
return (
- token.tokenType === "dimension" &&
- token.text.toLowerCase() in this.ANGLEUNIT
+ token.tokenType === "Dimension" &&
+ token.unit.toLowerCase() in this.ANGLEUNIT
);
},
diff --git a/devtools/client/shared/output-parser.js b/devtools/client/shared/output-parser.js
index bd514096b3..5202ae3a8e 100644
--- a/devtools/client/shared/output-parser.js
+++ b/devtools/client/shared/output-parser.js
@@ -8,7 +8,9 @@ const {
angleUtils,
} = require("resource://devtools/client/shared/css-angle.js");
const { colorUtils } = require("resource://devtools/shared/css/color.js");
-const { getCSSLexer } = require("resource://devtools/shared/css/lexer.js");
+const {
+ InspectorCSSParserWrapper,
+} = require("resource://devtools/shared/css/lexer.js");
const {
appendText,
} = require("resource://devtools/client/inspector/shared/utils.js");
@@ -67,6 +69,13 @@ const BACKDROP_FILTER_ENABLED = Services.prefs.getBoolPref(
);
const HTML_NS = "http://www.w3.org/1999/xhtml";
+// 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
@@ -175,7 +184,7 @@ class OutputParser {
* @param {Boolean} stopAtComma
* If true, stop at a comma.
* @return {Object}
- * An object of the form {tokens, functionData, sawComma, sawVariable}.
+ * An object of the form {tokens, functionData, sawComma, sawVariable, depth}.
* |tokens| is a list of the non-comment, non-whitespace tokens
* that were seen. The stopping token (paren or comma) will not
* be included.
@@ -184,6 +193,7 @@ class OutputParser {
* not be included.
* |sawComma| is true if the stop was due to a comma, or false otherwise.
* |sawVariable| is true if a variable was seen while parsing the text.
+ * |depth| is the number of unclosed parenthesis remaining when we return.
*/
#parseMatchingParens(text, tokenStream, options, stopAtComma) {
let depth = 1;
@@ -196,24 +206,22 @@ class OutputParser {
if (!token) {
break;
}
- if (token.tokenType === "comment") {
+ if (token.tokenType === "Comment") {
continue;
}
- if (token.tokenType === "symbol") {
- if (stopAtComma && depth === 1 && token.text === ",") {
- return { tokens, functionData, sawComma: true, sawVariable };
- } else if (token.text === "(") {
- ++depth;
- } else if (token.text === ")") {
- --depth;
- if (depth === 0) {
- break;
- }
+ if (stopAtComma && depth === 1 && token.tokenType === "Comma") {
+ return { tokens, functionData, sawComma: true, sawVariable, depth };
+ } else if (token.tokenType === "ParenthesisBlock") {
+ ++depth;
+ } else if (token.tokenType === "CloseParenthesis") {
+ --depth;
+ if (depth === 0) {
+ break;
}
} else if (
- token.tokenType === "function" &&
- token.text === "var" &&
+ token.tokenType === "Function" &&
+ token.value === "var" &&
options.getVariableValue
) {
sawVariable = true;
@@ -224,24 +232,24 @@ class OutputParser {
options
);
functionData.push({ node, value, fallbackValue });
- } else if (token.tokenType === "function") {
+ } else if (token.tokenType === "Function") {
++depth;
}
if (
- token.tokenType !== "function" ||
- token.text !== "var" ||
+ token.tokenType !== "Function" ||
+ token.value !== "var" ||
!options.getVariableValue
) {
functionData.push(text.substring(token.startOffset, token.endOffset));
}
- if (token.tokenType !== "whitespace") {
+ if (token.tokenType !== "WhiteSpace") {
tokens.push(token);
}
}
- return { tokens, functionData, sawComma: false, sawVariable };
+ return { tokens, functionData, sawComma: false, sawVariable, depth };
}
/**
@@ -389,19 +397,23 @@ class OutputParser {
}
const lowerCaseTokenText = token.text?.toLowerCase();
- if (token.tokenType === "comment") {
+ if (token.tokenType === "Comment") {
// This doesn't change spaceNeeded, because we didn't emit
// anything to the output.
continue;
}
switch (token.tokenType) {
- case "function": {
- const isColorTakingFunction =
- COLOR_TAKING_FUNCTIONS.includes(lowerCaseTokenText);
+ case "Function": {
+ const functionName = token.value;
+ const lowerCaseFunctionName = functionName.toLowerCase();
+
+ const isColorTakingFunction = COLOR_TAKING_FUNCTIONS.includes(
+ lowerCaseFunctionName
+ );
if (
isColorTakingFunction ||
- ANGLE_TAKING_FUNCTIONS.includes(lowerCaseTokenText)
+ ANGLE_TAKING_FUNCTIONS.includes(lowerCaseFunctionName)
) {
// The function can accept a color or an angle argument, and we know
// it isn't special in some other way. So, we let it
@@ -414,10 +426,13 @@ class OutputParser {
outerMostFunctionTakesColor = isColorTakingFunction;
}
if (isColorTakingFunction) {
- colorFunctions.push({ parenDepth, functionName: token.text });
+ colorFunctions.push({ parenDepth, functionName });
}
++parenDepth;
- } else if (lowerCaseTokenText === "var" && options.getVariableValue) {
+ } else if (
+ lowerCaseFunctionName === "var" &&
+ options.getVariableValue
+ ) {
const { node: variableNode, value } = this.#parseVariable(
token,
text,
@@ -434,20 +449,17 @@ class OutputParser {
this.#parsed.push(variableNode);
}
} else {
- const { functionData, sawVariable } = this.#parseMatchingParens(
- text,
- tokenStream,
- options
- );
-
- const functionName = text.substring(
- token.startOffset,
- token.endOffset
- );
+ const {
+ functionData,
+ sawVariable,
+ tokens: functionArgTokens,
+ depth,
+ } = this.#parseMatchingParens(text, tokenStream, options);
if (sawVariable) {
const computedFunctionText =
functionName +
+ "(" +
functionData
.map(data => {
if (typeof data === "string") {
@@ -466,6 +478,7 @@ class OutputParser {
colorFunction: colorFunctions.at(-1)?.functionName,
valueParts: [
functionName,
+ "(",
...functionData.map(data => data.node || data),
")",
],
@@ -473,7 +486,7 @@ class OutputParser {
} else {
// If function contains variable, we need to add both strings
// and nodes.
- this.#appendTextNode(functionName);
+ this.#appendTextNode(functionName + "(");
for (const data of functionData) {
if (typeof data === "string") {
this.#appendTextNode(data);
@@ -486,16 +499,40 @@ class OutputParser {
} else {
// If no variable in function, join the text together and add
// to DOM accordingly.
- const functionText = functionName + functionData.join("") + ")";
+ const functionText =
+ functionName +
+ "(" +
+ functionData.join("") +
+ // only append closing parenthesis if the authored text actually had it
+ // In such case, we should probably indicate that there's a "syntax error"
+ // See Bug 1891461.
+ (depth == 0 ? ")" : "");
+
+ if (lowerCaseFunctionName === "url" && options.urlClass) {
+ // url() with quoted strings are not mapped as UnquotedUrl,
+ // instead, we get a "Function" token with "url" function name,
+ // and later, a "QuotedString" token, which contains the actual URL.
+ let url;
+ for (const argToken of functionArgTokens) {
+ if (argToken.tokenType === "QuotedString") {
+ url = argToken.value;
+ break;
+ }
+ }
- if (
+ if (url !== undefined) {
+ this.#appendURL(functionText, url, options);
+ } else {
+ this.#appendTextNode(functionText);
+ }
+ } else if (
options.expectCubicBezier &&
- lowerCaseTokenText === "cubic-bezier"
+ lowerCaseFunctionName === "cubic-bezier"
) {
this.#appendCubicBezier(functionText, options);
} else if (
options.expectLinearEasing &&
- lowerCaseTokenText === "linear"
+ lowerCaseFunctionName === "linear"
) {
this.#appendLinear(functionText, options);
} else if (
@@ -508,7 +545,7 @@ class OutputParser {
});
} else if (
options.expectShape &&
- BASIC_SHAPE_FUNCTIONS.includes(lowerCaseTokenText)
+ BASIC_SHAPE_FUNCTIONS.includes(lowerCaseFunctionName)
) {
this.#appendShape(functionText, options);
} else {
@@ -519,7 +556,7 @@ class OutputParser {
break;
}
- case "ident":
+ case "Ident":
if (
options.expectCubicBezier &&
BEZIER_KEYWORDS.includes(lowerCaseTokenText)
@@ -553,8 +590,8 @@ class OutputParser {
}
break;
- case "id":
- case "hash": {
+ case "IDHash":
+ case "Hash": {
const original = text.substring(token.startOffset, token.endOffset);
if (colorOK() && InspectorUtils.isValidCSSColor(original)) {
if (spaceNeeded) {
@@ -571,7 +608,7 @@ class OutputParser {
}
break;
}
- case "dimension":
+ case "Dimension":
const value = text.substring(token.startOffset, token.endOffset);
if (angleOK(value)) {
this.#appendAngle(value, options);
@@ -579,16 +616,16 @@ class OutputParser {
this.#appendTextNode(value);
}
break;
- case "url":
- case "bad_url":
+ case "UnquotedUrl":
+ case "BadUrl":
this.#appendURL(
text.substring(token.startOffset, token.endOffset),
- token.text,
+ token.value,
options
);
break;
- case "string":
+ case "QuotedString":
if (options.expectFont) {
fontFamilyNameParts.push(
text.substring(token.startOffset, token.endOffset)
@@ -600,7 +637,7 @@ class OutputParser {
}
break;
- case "whitespace":
+ case "WhiteSpace":
if (options.expectFont) {
fontFamilyNameParts.push(" ");
} else {
@@ -610,32 +647,44 @@ class OutputParser {
}
break;
- case "symbol":
- if (token.text === "(") {
- ++parenDepth;
- } else if (token.text === ")") {
- --parenDepth;
+ case "ParenthesisBlock":
+ ++parenDepth;
+ this.#appendTextNode(
+ text.substring(token.startOffset, token.endOffset)
+ );
+ break;
- if (colorFunctions.at(-1)?.parenDepth == parenDepth) {
- colorFunctions.pop();
- }
+ case "CloseParenthesis":
+ --parenDepth;
- if (stopAtCloseParen && parenDepth === 0) {
- done = true;
- break;
- }
+ if (colorFunctions.at(-1)?.parenDepth == parenDepth) {
+ colorFunctions.pop();
+ }
- if (parenDepth === 0) {
- outerMostFunctionTakesColor = false;
- }
- } else if (
- (token.text === "," || token.text === "!") &&
+ if (stopAtCloseParen && parenDepth === 0) {
+ done = true;
+ break;
+ }
+
+ if (parenDepth === 0) {
+ outerMostFunctionTakesColor = false;
+ }
+ this.#appendTextNode(
+ text.substring(token.startOffset, token.endOffset)
+ );
+ break;
+
+ case "Comma":
+ case "Delim":
+ if (
+ (token.tokenType === "Comma" || token.text === "!") &&
options.expectFont &&
fontFamilyNameParts.length !== 0
) {
this.#appendFontFamily(fontFamilyNameParts.join(""), options);
fontFamilyNameParts = [];
}
+
// falls through
default:
this.#appendTextNode(
@@ -647,15 +696,15 @@ class OutputParser {
// If this token might possibly introduce token pasting when
// color-cycling, require a space.
spaceNeeded =
- token.tokenType === "ident" ||
- token.tokenType === "at" ||
- token.tokenType === "id" ||
- token.tokenType === "hash" ||
- token.tokenType === "number" ||
- token.tokenType === "dimension" ||
- token.tokenType === "percentage" ||
- token.tokenType === "dimension";
- previousWasBang = token.tokenType === "symbol" && token.text === "!";
+ token.tokenType === "Ident" ||
+ token.tokenType === "AtKeyword" ||
+ token.tokenType === "IDHash" ||
+ token.tokenType === "Hash" ||
+ token.tokenType === "Number" ||
+ token.tokenType === "Dimension" ||
+ token.tokenType === "Percentage" ||
+ token.tokenType === "Dimension";
+ previousWasBang = token.tokenType === "Delim" && token.text === "!";
}
if (options.expectFont && fontFamilyNameParts.length !== 0) {
@@ -686,7 +735,7 @@ class OutputParser {
text = text.trim();
this.#parsed.length = 0;
- const tokenStream = getCSSLexer(text);
+ const tokenStream = new InspectorCSSParserWrapper(text);
return this.#doParse(text, options, tokenStream, false);
}
@@ -884,7 +933,7 @@ class OutputParser {
*/
// eslint-disable-next-line complexity
#addPolygonPointNodes(coords, container) {
- const tokenStream = getCSSLexer(coords);
+ const tokenStream = new InspectorCSSParserWrapper(coords);
let token = tokenStream.nextToken();
let coord = "";
let i = 0;
@@ -897,7 +946,7 @@ class OutputParser {
});
while (token) {
- if (token.tokenType === "symbol" && token.text === ",") {
+ if (token.tokenType === "Comma") {
// Comma separating coordinate pairs; add coordNode to container and reset vars
if (!isXCoord) {
// Y coord not added to coordNode yet
@@ -933,19 +982,19 @@ class OutputParser {
class: "ruleview-shape-point",
"data-point": `${i}`,
});
- } else if (token.tokenType === "symbol" && token.text === "(") {
+ } else if (token.tokenType === "ParenthesisBlock") {
depth++;
coord += coords.substring(token.startOffset, token.endOffset);
- } else if (token.tokenType === "symbol" && token.text === ")") {
+ } else if (token.tokenType === "CloseParenthesis") {
depth--;
coord += coords.substring(token.startOffset, token.endOffset);
- } else if (token.tokenType === "whitespace" && coord === "") {
+ } else if (token.tokenType === "WhiteSpace" && coord === "") {
// Whitespace at beginning of coord; add to container
appendText(
container,
coords.substring(token.startOffset, token.endOffset)
);
- } else if (token.tokenType === "whitespace" && depth === 0) {
+ } else if (token.tokenType === "WhiteSpace" && depth === 0) {
// Whitespace signifying end of coord
const node = this.#createNode(
"span",
@@ -964,10 +1013,10 @@ class OutputParser {
coord = "";
isXCoord = !isXCoord;
} else if (
- token.tokenType === "number" ||
- token.tokenType === "dimension" ||
- token.tokenType === "percentage" ||
- token.tokenType === "function"
+ token.tokenType === "Number" ||
+ token.tokenType === "Dimension" ||
+ token.tokenType === "Percentage" ||
+ token.tokenType === "Function"
) {
if (isXCoord && coord && depth === 0) {
// Whitespace is not necessary between x/y coords.
@@ -986,11 +1035,11 @@ class OutputParser {
}
coord += coords.substring(token.startOffset, token.endOffset);
- if (token.tokenType === "function") {
+ if (token.tokenType === "Function") {
depth++;
}
} else if (
- token.tokenType === "ident" &&
+ token.tokenType === "Ident" &&
(token.text === "nonzero" || token.text === "evenodd")
) {
// A fill-rule (nonzero or evenodd).
@@ -1034,7 +1083,7 @@ class OutputParser {
*/
// eslint-disable-next-line complexity
#addCirclePointNodes(coords, container) {
- const tokenStream = getCSSLexer(coords);
+ const tokenStream = new InspectorCSSParserWrapper(coords);
let token = tokenStream.nextToken();
let depth = 0;
let coord = "";
@@ -1044,20 +1093,20 @@ class OutputParser {
"data-point": "center",
});
while (token) {
- if (token.tokenType === "symbol" && token.text === "(") {
+ if (token.tokenType === "ParenthesisBlock") {
depth++;
coord += coords.substring(token.startOffset, token.endOffset);
- } else if (token.tokenType === "symbol" && token.text === ")") {
+ } else if (token.tokenType === "CloseParenthesis") {
depth--;
coord += coords.substring(token.startOffset, token.endOffset);
- } else if (token.tokenType === "whitespace" && coord === "") {
+ } else if (token.tokenType === "WhiteSpace" && coord === "") {
// Whitespace at beginning of coord; add to container
appendText(
container,
coords.substring(token.startOffset, token.endOffset)
);
} else if (
- token.tokenType === "whitespace" &&
+ token.tokenType === "WhiteSpace" &&
point === "radius" &&
depth === 0
) {
@@ -1078,7 +1127,7 @@ class OutputParser {
point = "cx";
coord = "";
depth = 0;
- } else if (token.tokenType === "whitespace" && depth === 0) {
+ } else if (token.tokenType === "WhiteSpace" && depth === 0) {
// Whitespace signifying end of cx/cy
const node = this.#createNode(
"span",
@@ -1097,7 +1146,7 @@ class OutputParser {
point = point === "cx" ? "cy" : "cx";
coord = "";
depth = 0;
- } else if (token.tokenType === "ident" && token.text === "at") {
+ } else if (token.tokenType === "Ident" && token.text === "at") {
// "at"; Add radius to container if not already done so
if (point === "radius" && coord) {
const node = this.#createNode(
@@ -1118,10 +1167,10 @@ class OutputParser {
coord = "";
depth = 0;
} else if (
- token.tokenType === "number" ||
- token.tokenType === "dimension" ||
- token.tokenType === "percentage" ||
- token.tokenType === "function"
+ token.tokenType === "Number" ||
+ token.tokenType === "Dimension" ||
+ token.tokenType === "Percentage" ||
+ token.tokenType === "Function"
) {
if (point === "cx" && coord && depth === 0) {
// Center coords don't require whitespace between x/y. So if current point is
@@ -1142,7 +1191,7 @@ class OutputParser {
}
coord += coords.substring(token.startOffset, token.endOffset);
- if (token.tokenType === "function") {
+ if (token.tokenType === "Function") {
depth++;
}
} else {
@@ -1195,7 +1244,7 @@ class OutputParser {
*/
// eslint-disable-next-line complexity
#addEllipsePointNodes(coords, container) {
- const tokenStream = getCSSLexer(coords);
+ const tokenStream = new InspectorCSSParserWrapper(coords);
let token = tokenStream.nextToken();
let depth = 0;
let coord = "";
@@ -1205,19 +1254,19 @@ class OutputParser {
"data-point": "center",
});
while (token) {
- if (token.tokenType === "symbol" && token.text === "(") {
+ if (token.tokenType === "ParenthesisBlock") {
depth++;
coord += coords.substring(token.startOffset, token.endOffset);
- } else if (token.tokenType === "symbol" && token.text === ")") {
+ } else if (token.tokenType === "CloseParenthesis") {
depth--;
coord += coords.substring(token.startOffset, token.endOffset);
- } else if (token.tokenType === "whitespace" && coord === "") {
+ } else if (token.tokenType === "WhiteSpace" && coord === "") {
// Whitespace at beginning of coord; add to container
appendText(
container,
coords.substring(token.startOffset, token.endOffset)
);
- } else if (token.tokenType === "whitespace" && depth === 0) {
+ } else if (token.tokenType === "WhiteSpace" && depth === 0) {
if (point === "rx" || point === "ry") {
// Whitespace signifying end of rx/ry
const node = this.#createNode(
@@ -1256,7 +1305,7 @@ class OutputParser {
coord = "";
depth = 0;
}
- } else if (token.tokenType === "ident" && token.text === "at") {
+ } else if (token.tokenType === "Ident" && token.text === "at") {
// "at"; Add radius to container if not already done so
if (point === "ry" && coord) {
const node = this.#createNode(
@@ -1277,10 +1326,10 @@ class OutputParser {
coord = "";
depth = 0;
} else if (
- token.tokenType === "number" ||
- token.tokenType === "dimension" ||
- token.tokenType === "percentage" ||
- token.tokenType === "function"
+ token.tokenType === "Number" ||
+ token.tokenType === "Dimension" ||
+ token.tokenType === "Percentage" ||
+ token.tokenType === "Function"
) {
if (point === "rx" && coord && depth === 0) {
// Radius coords don't require whitespace between x/y.
@@ -1313,7 +1362,7 @@ class OutputParser {
}
coord += coords.substring(token.startOffset, token.endOffset);
- if (token.tokenType === "function") {
+ if (token.tokenType === "Function") {
depth++;
}
} else {
@@ -1366,7 +1415,7 @@ class OutputParser {
// eslint-disable-next-line complexity
#addInsetPointNodes(coords, container) {
const insetPoints = ["top", "right", "bottom", "left"];
- const tokenStream = getCSSLexer(coords);
+ const tokenStream = new InspectorCSSParserWrapper(coords);
let token = tokenStream.nextToken();
let depth = 0;
let coord = "";
@@ -1383,16 +1432,16 @@ class OutputParser {
if (round) {
// Everything that comes after "round" should just be plain text
otherText[i].push(coords.substring(token.startOffset, token.endOffset));
- } else if (token.tokenType === "symbol" && token.text === "(") {
+ } else if (token.tokenType === "ParenthesisBlock") {
depth++;
coord += coords.substring(token.startOffset, token.endOffset);
- } else if (token.tokenType === "symbol" && token.text === ")") {
+ } else if (token.tokenType === "CloseParenthesis") {
depth--;
coord += coords.substring(token.startOffset, token.endOffset);
- } else if (token.tokenType === "whitespace" && coord === "") {
+ } else if (token.tokenType === "WhiteSpace" && coord === "") {
// Whitespace at beginning of coord; add to container
otherText[i].push(coords.substring(token.startOffset, token.endOffset));
- } else if (token.tokenType === "whitespace" && depth === 0) {
+ } else if (token.tokenType === "WhiteSpace" && depth === 0) {
// Whitespace signifying end of coord; create node and push to nodes
const node = this.#createNode(
"span",
@@ -1407,10 +1456,10 @@ class OutputParser {
otherText[i] = [coords.substring(token.startOffset, token.endOffset)];
depth = 0;
} else if (
- token.tokenType === "number" ||
- token.tokenType === "dimension" ||
- token.tokenType === "percentage" ||
- token.tokenType === "function"
+ token.tokenType === "Number" ||
+ token.tokenType === "Dimension" ||
+ token.tokenType === "Percentage" ||
+ token.tokenType === "Function"
) {
if (coord && depth === 0) {
// Inset coords don't require whitespace between each coord.
@@ -1428,10 +1477,10 @@ class OutputParser {
}
coord += coords.substring(token.startOffset, token.endOffset);
- if (token.tokenType === "function") {
+ if (token.tokenType === "Function") {
depth++;
}
- } else if (token.tokenType === "ident" && token.text === "round") {
+ } else if (token.tokenType === "Ident" && token.text === "round") {
if (coord && depth === 0) {
// Whitespace is not necessary before "round"; create a new node for the coord
const node = this.#createNode(
@@ -1730,13 +1779,15 @@ class OutputParser {
*/
#sanitizeURL(url) {
// Re-lex the URL and add any needed termination characters.
- const urlTokenizer = getCSSLexer(url);
+ const urlTokenizer = new InspectorCSSParserWrapper(url, {
+ trackEOFChars: true,
+ });
// Just read until EOF; there will only be a single token.
while (urlTokenizer.nextToken()) {
// Nothing.
}
- return urlTokenizer.performEOFFixup(url, true);
+ return urlTokenizer.performEOFFixup(url);
}
/**
@@ -1756,14 +1807,7 @@ class OutputParser {
// leave the termination characters. This isn't strictly
// "as-authored", but it makes a bit more sense.
match = this.#sanitizeURL(match);
- // 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|. We considered adding
- // functionality for this to CSSLexer, in some way, but this
- // seemed simpler on the whole.
- const urlParts =
- /^(url\([ \t\r\n\f]*(["']?))(.*?)(\2[ \t\r\n\f]*\))$/i.exec(match);
+ const urlParts = URL_REGEX.exec(match);
// Bail out if that didn't match anything.
if (!urlParts) {
@@ -1771,7 +1815,7 @@ class OutputParser {
return;
}
- const [, leader, , body, trailer] = urlParts;
+ const { leader, body, trailer } = urlParts.groups;
this.#appendTextNode(leader);
diff --git a/devtools/client/shared/screenshot.js b/devtools/client/shared/screenshot.js
index ca746f66e7..4031708293 100644
--- a/devtools/client/shared/screenshot.js
+++ b/devtools/client/shared/screenshot.js
@@ -327,9 +327,9 @@ function saveToClipboard(base64URI) {
let _outputDirectory = null;
/**
- * Returns the default directory for screenshots.
- * If a specific directory for screenshots is not defined,
- * it falls back to the system downloads directory.
+ * Returns the default directory for DevTools screenshots.
+ * For consistency with the Firefox Screenshots feature, this will default to
+ * the preferred downloads directory.
*
* @return {Promise<String>} Resolves the path as a string
*/
@@ -338,13 +338,7 @@ async function getOutputDirectory() {
return _outputDirectory;
}
- try {
- // This will throw if there is not a screenshot directory set for the platform
- _outputDirectory = Services.dirsvc.get("Scrnshts", Ci.nsIFile).path;
- } catch (e) {
- _outputDirectory = await lazy.Downloads.getPreferredDownloadsDirectory();
- }
-
+ _outputDirectory = await lazy.Downloads.getPreferredDownloadsDirectory();
return _outputDirectory;
}
diff --git a/devtools/client/shared/sourceeditor/css-autocompleter.js b/devtools/client/shared/sourceeditor/css-autocompleter.js
index 7db3cbbc0f..4266bd02b8 100644
--- a/devtools/client/shared/sourceeditor/css-autocompleter.js
+++ b/devtools/client/shared/sourceeditor/css-autocompleter.js
@@ -28,15 +28,14 @@ const {
* The file 'css-parsing-utils' helps to convert the CSS into meaningful tokens,
* each having a certain type associated with it. These tokens help us to figure
* out the currently edited word and to write a CSS state machine to figure out
- * what the user is currently editing. By that, I mean, whether he is editing a
- * selector or a property or a value, or even fine grained information like an
- * id in the selector.
+ * what the user is currently editing (e.g. a selector or a property or a value,
+ * or even fine grained information like an id in the selector).
*
* The `resolveState` method iterated over the tokens spitted out by the
* tokenizer, using switch cases, follows a state machine logic and finally
* figures out these informations:
* - The state of the CSS at the cursor (one out of CSS_STATES)
- * - The current token that is being edited `cmpleting`
+ * - The current token that is being edited `completing`
* - If the state is "selector", the selector state (one of SELECTOR_STATES)
* - If the state is "selector", the current selector till the cursor
* - If the state is "value", the corresponding property name
@@ -214,30 +213,26 @@ CSSCompleter.prototype = {
// From CSS_STATES.property, we can either go to CSS_STATES.value
// state when we hit the first ':' or CSS_STATES.selector if "}" is
// reached.
- if (token.tokenType === "symbol") {
- switch (token.text) {
- case ":":
- scopeStack.push(":");
- if (tokens[cursor - 2].tokenType != "whitespace") {
- propertyName = tokens[cursor - 2].text;
- } else {
- propertyName = tokens[cursor - 3].text;
- }
- _state = CSS_STATES.value;
- break;
+ if (token.tokenType === "Colon") {
+ scopeStack.push(":");
+ if (tokens[cursor - 2].tokenType != "WhiteSpace") {
+ propertyName = tokens[cursor - 2].text;
+ } else {
+ propertyName = tokens[cursor - 3].text;
+ }
+ _state = CSS_STATES.value;
+ }
- case "}":
- if (/[{f]/.test(peek(scopeStack))) {
- const popped = scopeStack.pop();
- if (popped == "f") {
- _state = CSS_STATES.frame;
- } else {
- selector = "";
- selectors = [];
- _state = CSS_STATES.null;
- }
- }
- break;
+ if (token.tokenType === "CloseCurlyBracket") {
+ if (/[{f]/.test(peek(scopeStack))) {
+ const popped = scopeStack.pop();
+ if (popped == "f") {
+ _state = CSS_STATES.frame;
+ } else {
+ selector = "";
+ selectors = [];
+ _state = CSS_STATES.null;
+ }
}
}
break;
@@ -245,31 +240,27 @@ CSSCompleter.prototype = {
case CSS_STATES.value:
// From CSS_STATES.value, we can go to one of CSS_STATES.property,
// CSS_STATES.frame, CSS_STATES.selector and CSS_STATES.null
- if (token.tokenType === "symbol") {
- switch (token.text) {
- case ";":
- if (/[:]/.test(peek(scopeStack))) {
- scopeStack.pop();
- _state = CSS_STATES.property;
- }
- break;
+ if (token.tokenType === "Semicolon") {
+ if (/[:]/.test(peek(scopeStack))) {
+ scopeStack.pop();
+ _state = CSS_STATES.property;
+ }
+ }
- case "}":
- if (peek(scopeStack) == ":") {
- scopeStack.pop();
- }
+ if (token.tokenType === "CloseCurlyBracket") {
+ if (peek(scopeStack) == ":") {
+ scopeStack.pop();
+ }
- if (/[{f]/.test(peek(scopeStack))) {
- const popped = scopeStack.pop();
- if (popped == "f") {
- _state = CSS_STATES.frame;
- } else {
- selector = "";
- selectors = [];
- _state = CSS_STATES.null;
- }
- }
- break;
+ if (/[{f]/.test(peek(scopeStack))) {
+ const popped = scopeStack.pop();
+ if (popped == "f") {
+ _state = CSS_STATES.frame;
+ } else {
+ selector = "";
+ selectors = [];
+ _state = CSS_STATES.null;
+ }
}
}
break;
@@ -277,7 +268,7 @@ CSSCompleter.prototype = {
case CSS_STATES.selector:
// From CSS_STATES.selector, we can only go to CSS_STATES.property
// when we hit "{"
- if (token.tokenType === "symbol" && token.text == "{") {
+ if (token.tokenType === "CurlyBracketBlock") {
scopeStack.push("{");
_state = CSS_STATES.property;
selectors.push(selector);
@@ -290,74 +281,87 @@ CSSCompleter.prototype = {
case SELECTOR_STATES.class:
case SELECTOR_STATES.tag:
switch (token.tokenType) {
- case "hash":
- case "id":
+ case "Hash":
+ case "IDHash":
selectorState = SELECTOR_STATES.id;
- selector += "#" + token.text;
+ selector += token.text;
break;
- case "symbol":
+ case "Delim":
if (token.text == ".") {
selectorState = SELECTOR_STATES.class;
selector += ".";
if (
cursor <= tokIndex &&
- tokens[cursor].tokenType == "ident"
+ tokens[cursor].tokenType == "Ident"
) {
token = tokens[cursor++];
selector += token.text;
}
} else if (token.text == "#") {
+ // Lonely # char, that doesn't produce a Hash nor IDHash
selectorState = SELECTOR_STATES.id;
selector += "#";
- } else if (/[>~+]/.test(token.text)) {
+ } else if (
+ token.text == "+" ||
+ token.text == "~" ||
+ token.text == ">"
+ ) {
selectorState = SELECTOR_STATES.null;
selector += token.text;
- } else if (token.text == ",") {
- selectorState = SELECTOR_STATES.null;
- selectors.push(selector);
- selector = "";
- } else if (token.text == ":") {
- selectorState = SELECTOR_STATES.pseudo;
- selector += ":";
- if (cursor > tokIndex) {
- break;
- }
+ }
+ break;
- token = tokens[cursor++];
- switch (token.tokenType) {
- case "function":
- if (token.text == "not") {
- selectorBeforeNot = selector;
- selector = "";
- scopeStack.push("(");
- } else {
- selector += token.text + "(";
- }
- selectorState = SELECTOR_STATES.null;
- break;
-
- case "ident":
+ case "Comma":
+ selectorState = SELECTOR_STATES.null;
+ selectors.push(selector);
+ selector = "";
+ break;
+
+ case "Colon":
+ selectorState = SELECTOR_STATES.pseudo;
+ selector += ":";
+ if (cursor > tokIndex) {
+ break;
+ }
+
+ token = tokens[cursor++];
+ switch (token.tokenType) {
+ case "Function":
+ if (token.value == "not") {
+ selectorBeforeNot = selector;
+ selector = "";
+ scopeStack.push("(");
+ } else {
selector += token.text;
- break;
- }
- } else if (token.text == "[") {
- selectorState = SELECTOR_STATES.attribute;
- scopeStack.push("[");
- selector += "[";
- } else if (token.text == ")") {
- if (peek(scopeStack) == "(") {
- scopeStack.pop();
- selector = selectorBeforeNot + "not(" + selector + ")";
- selectorBeforeNot = null;
- } else {
- selector += ")";
- }
- selectorState = SELECTOR_STATES.null;
+ }
+ selectorState = SELECTOR_STATES.null;
+ break;
+
+ case "Ident":
+ selector += token.text;
+ break;
+ }
+ break;
+
+ case "SquareBracketBlock":
+ selectorState = SELECTOR_STATES.attribute;
+ scopeStack.push("[");
+ selector += "[";
+ break;
+
+ case "CloseParenthesis":
+ if (peek(scopeStack) == "(") {
+ scopeStack.pop();
+ selector = selectorBeforeNot + "not(" + selector + ")";
+ selectorBeforeNot = null;
+ } else {
+ selector += ")";
}
+ selectorState = SELECTOR_STATES.null;
break;
- case "whitespace":
+ case "WhiteSpace":
selectorState = SELECTOR_STATES.null;
selector && (selector += " ");
break;
@@ -369,81 +373,94 @@ CSSCompleter.prototype = {
// SELECTOR_STATES.id, SELECTOR_STATES.class or
// SELECTOR_STATES.tag
switch (token.tokenType) {
- case "hash":
- case "id":
+ case "Hash":
+ case "IDHash":
selectorState = SELECTOR_STATES.id;
- selector += "#" + token.text;
+ selector += token.text;
break;
- case "ident":
+ case "Ident":
selectorState = SELECTOR_STATES.tag;
selector += token.text;
break;
- case "symbol":
+ case "Delim":
if (token.text == ".") {
selectorState = SELECTOR_STATES.class;
selector += ".";
if (
cursor <= tokIndex &&
- tokens[cursor].tokenType == "ident"
+ tokens[cursor].tokenType == "Ident"
) {
token = tokens[cursor++];
selector += token.text;
}
} else if (token.text == "#") {
+ // Lonely # char, that doesn't produce a Hash nor IDHash
selectorState = SELECTOR_STATES.id;
selector += "#";
} else if (token.text == "*") {
selectorState = SELECTOR_STATES.tag;
selector += "*";
- } else if (/[>~+]/.test(token.text)) {
+ } else if (
+ token.text == "+" ||
+ token.text == "~" ||
+ token.text == ">"
+ ) {
selector += token.text;
- } else if (token.text == ",") {
- selectorState = SELECTOR_STATES.null;
- selectors.push(selector);
- selector = "";
- } else if (token.text == ":") {
- selectorState = SELECTOR_STATES.pseudo;
- selector += ":";
- if (cursor > tokIndex) {
- break;
- }
+ }
+ break;
- token = tokens[cursor++];
- switch (token.tokenType) {
- case "function":
- if (token.text == "not") {
- selectorBeforeNot = selector;
- selector = "";
- scopeStack.push("(");
- } else {
- selector += token.text + "(";
- }
- selectorState = SELECTOR_STATES.null;
- break;
-
- case "ident":
+ case "Comma":
+ selectorState = SELECTOR_STATES.null;
+ selectors.push(selector);
+ selector = "";
+ break;
+
+ case "Colon":
+ selectorState = SELECTOR_STATES.pseudo;
+ selector += ":";
+ if (cursor > tokIndex) {
+ break;
+ }
+
+ token = tokens[cursor++];
+ switch (token.tokenType) {
+ case "Function":
+ if (token.value == "not") {
+ selectorBeforeNot = selector;
+ selector = "";
+ scopeStack.push("(");
+ } else {
selector += token.text;
- break;
- }
- } else if (token.text == "[") {
- selectorState = SELECTOR_STATES.attribute;
- scopeStack.push("[");
- selector += "[";
- } else if (token.text == ")") {
- if (peek(scopeStack) == "(") {
- scopeStack.pop();
- selector = selectorBeforeNot + "not(" + selector + ")";
- selectorBeforeNot = null;
- } else {
- selector += ")";
- }
- selectorState = SELECTOR_STATES.null;
+ }
+ selectorState = SELECTOR_STATES.null;
+ break;
+
+ case "Ident":
+ selector += token.text;
+ break;
}
break;
- case "whitespace":
+ case "SquareBracketBlock":
+ selectorState = SELECTOR_STATES.attribute;
+ scopeStack.push("[");
+ selector += "[";
+ break;
+
+ case "CloseParenthesis":
+ if (peek(scopeStack) == "(") {
+ scopeStack.pop();
+ selector = selectorBeforeNot + "not(" + selector + ")";
+ selectorBeforeNot = null;
+ } else {
+ selector += ")";
+ }
+ selectorState = SELECTOR_STATES.null;
+ break;
+
+ case "WhiteSpace":
selector && (selector += " ");
break;
}
@@ -451,46 +468,55 @@ CSSCompleter.prototype = {
case SELECTOR_STATES.pseudo:
switch (token.tokenType) {
- case "symbol":
- if (/[>~+]/.test(token.text)) {
+ case "Delim":
+ if (
+ token.text == "+" ||
+ token.text == "~" ||
+ token.text == ">"
+ ) {
selectorState = SELECTOR_STATES.null;
selector += token.text;
- } else if (token.text == ",") {
- selectorState = SELECTOR_STATES.null;
- selectors.push(selector);
- selector = "";
- } else if (token.text == ":") {
- selectorState = SELECTOR_STATES.pseudo;
- selector += ":";
- if (cursor > tokIndex) {
- break;
- }
+ }
+ break;
+
+ case "Comma":
+ selectorState = SELECTOR_STATES.null;
+ selectors.push(selector);
+ selector = "";
+ break;
+
+ case "Colon":
+ selectorState = SELECTOR_STATES.pseudo;
+ selector += ":";
+ if (cursor > tokIndex) {
+ break;
+ }
- token = tokens[cursor++];
- switch (token.tokenType) {
- case "function":
- if (token.text == "not") {
- selectorBeforeNot = selector;
- selector = "";
- scopeStack.push("(");
- } else {
- selector += token.text + "(";
- }
- selectorState = SELECTOR_STATES.null;
- break;
-
- case "ident":
+ token = tokens[cursor++];
+ switch (token.tokenType) {
+ case "Function":
+ if (token.value == "not") {
+ selectorBeforeNot = selector;
+ selector = "";
+ scopeStack.push("(");
+ } else {
selector += token.text;
- break;
- }
- } else if (token.text == "[") {
- selectorState = SELECTOR_STATES.attribute;
- scopeStack.push("[");
- selector += "[";
+ }
+ selectorState = SELECTOR_STATES.null;
+ break;
+
+ case "Ident":
+ selector += token.text;
+ break;
}
break;
+ case "SquareBracketBlock":
+ selectorState = SELECTOR_STATES.attribute;
+ scopeStack.push("[");
+ selector += "[";
+ break;
- case "whitespace":
+ case "WhiteSpace":
selectorState = SELECTOR_STATES.null;
selector && (selector += " ");
break;
@@ -499,29 +525,40 @@ CSSCompleter.prototype = {
case SELECTOR_STATES.attribute:
switch (token.tokenType) {
- case "symbol":
- if (/[~|^$*]/.test(token.text)) {
- selector += token.text;
- token = tokens[cursor++];
- } else if (token.text == "=") {
+ case "IncludeMatch":
+ case "DashMatch":
+ case "PrefixMatch":
+ case "IncludeSuffixMatchMatch":
+ case "SubstringMatch":
+ selector += token.text;
+ token = tokens[cursor++];
+ break;
+
+ case "Delim":
+ if (token.text == "=") {
selectorState = SELECTOR_STATES.value;
selector += token.text;
- } else if (token.text == "]") {
- if (peek(scopeStack) == "[") {
- scopeStack.pop();
- }
+ }
+ break;
- selectorState = SELECTOR_STATES.null;
- selector += "]";
+ case "CloseSquareBracket":
+ if (peek(scopeStack) == "[") {
+ scopeStack.pop();
}
+
+ selectorState = SELECTOR_STATES.null;
+ selector += "]";
break;
- case "ident":
- case "string":
+ case "Ident":
selector += token.text;
break;
- case "whitespace":
+ case "QuotedString":
+ selector += token.value;
+ break;
+
+ case "WhiteSpace":
selector && (selector += " ");
break;
}
@@ -529,23 +566,24 @@ CSSCompleter.prototype = {
case SELECTOR_STATES.value:
switch (token.tokenType) {
- case "string":
- case "ident":
+ case "Ident":
selector += token.text;
break;
- case "symbol":
- if (token.text == "]") {
- if (peek(scopeStack) == "[") {
- scopeStack.pop();
- }
+ case "QuotedString":
+ selector += token.value;
+ break;
- selectorState = SELECTOR_STATES.null;
- selector += "]";
+ case "CloseSquareBracket":
+ if (peek(scopeStack) == "[") {
+ scopeStack.pop();
}
+
+ selectorState = SELECTOR_STATES.null;
+ selector += "]";
break;
- case "whitespace":
+ case "WhiteSpace":
selector && (selector += " ");
break;
}
@@ -557,29 +595,30 @@ CSSCompleter.prototype = {
// From CSS_STATES.null state, we can go to either CSS_STATES.media or
// CSS_STATES.selector.
switch (token.tokenType) {
- case "hash":
- case "id":
+ case "Hash":
+ case "IDHash":
selectorState = SELECTOR_STATES.id;
- selector = "#" + token.text;
+ selector = token.text;
_state = CSS_STATES.selector;
break;
- case "ident":
+ case "Ident":
selectorState = SELECTOR_STATES.tag;
selector = token.text;
_state = CSS_STATES.selector;
break;
- case "symbol":
+ case "Delim":
if (token.text == ".") {
selectorState = SELECTOR_STATES.class;
selector = ".";
_state = CSS_STATES.selector;
- if (cursor <= tokIndex && tokens[cursor].tokenType == "ident") {
+ if (cursor <= tokIndex && tokens[cursor].tokenType == "Ident") {
token = tokens[cursor++];
selector += token.text;
}
} else if (token.text == "#") {
+ // Lonely # char, that doesn't produce a Hash nor IDHash
selectorState = SELECTOR_STATES.id;
selector = "#";
_state = CSS_STATES.selector;
@@ -587,45 +626,52 @@ CSSCompleter.prototype = {
selectorState = SELECTOR_STATES.tag;
selector = "*";
_state = CSS_STATES.selector;
- } else if (token.text == ":") {
- _state = CSS_STATES.selector;
- selectorState = SELECTOR_STATES.pseudo;
- selector += ":";
- if (cursor > tokIndex) {
- break;
- }
+ }
+ break;
- token = tokens[cursor++];
- switch (token.tokenType) {
- case "function":
- if (token.text == "not") {
- selectorBeforeNot = selector;
- selector = "";
- scopeStack.push("(");
- } else {
- selector += token.text + "(";
- }
- selectorState = SELECTOR_STATES.null;
- break;
+ case "Colon":
+ _state = CSS_STATES.selector;
+ selectorState = SELECTOR_STATES.pseudo;
+ selector += ":";
+ if (cursor > tokIndex) {
+ break;
+ }
- case "ident":
+ token = tokens[cursor++];
+ switch (token.tokenType) {
+ case "Function":
+ if (token.value == "not") {
+ selectorBeforeNot = selector;
+ selector = "";
+ scopeStack.push("(");
+ } else {
selector += token.text;
- break;
- }
- } else if (token.text == "[") {
- _state = CSS_STATES.selector;
- selectorState = SELECTOR_STATES.attribute;
- scopeStack.push("[");
- selector += "[";
- } else if (token.text == "}") {
- if (peek(scopeStack) == "@m") {
- scopeStack.pop();
- }
+ }
+ selectorState = SELECTOR_STATES.null;
+ break;
+
+ case "Ident":
+ selector += token.text;
+ break;
+ }
+ break;
+
+ case "CloseSquareBracket":
+ _state = CSS_STATES.selector;
+ selectorState = SELECTOR_STATES.attribute;
+ scopeStack.push("[");
+ selector += "[";
+ break;
+
+ case "CurlyBracketBlock":
+ if (peek(scopeStack) == "@m") {
+ scopeStack.pop();
}
break;
- case "at":
- _state = token.text.startsWith("m")
+ case "AtKeyword":
+ // XXX: We should probably handle other at-rules (@container, @property, …)
+ _state = token.value.startsWith("m")
? CSS_STATES.media
: CSS_STATES.keyframes;
break;
@@ -635,7 +681,7 @@ CSSCompleter.prototype = {
case CSS_STATES.media:
// From CSS_STATES.media, we can only go to CSS_STATES.null state when
// we hit the first '{'
- if (token.tokenType == "symbol" && token.text == "{") {
+ if (token.tokenType == "CurlyBracketBlock") {
scopeStack.push("@m");
_state = CSS_STATES.null;
}
@@ -644,7 +690,7 @@ CSSCompleter.prototype = {
case CSS_STATES.keyframes:
// From CSS_STATES.keyframes, we can only go to CSS_STATES.frame state
// when we hit the first '{'
- if (token.tokenType == "symbol" && token.text == "{") {
+ if (token.tokenType == "CurlyBracketBlock") {
scopeStack.push("@k");
_state = CSS_STATES.frame;
}
@@ -654,17 +700,15 @@ CSSCompleter.prototype = {
// From CSS_STATES.frame, we can either go to CSS_STATES.property
// state when we hit the first '{' or to CSS_STATES.selector when we
// hit '}'
- if (token.tokenType == "symbol") {
- if (token.text == "{") {
- scopeStack.push("f");
- _state = CSS_STATES.property;
- } else if (token.text == "}") {
- if (peek(scopeStack) == "@k") {
- scopeStack.pop();
- }
-
- _state = CSS_STATES.null;
+ if (token.tokenType == "CurlyBracketBlock") {
+ scopeStack.push("f");
+ _state = CSS_STATES.property;
+ } else if (token.tokenType == "CloseCurlyBracket") {
+ if (peek(scopeStack) == "@k") {
+ scopeStack.pop();
}
+
+ _state = CSS_STATES.null;
}
break;
}
@@ -688,6 +732,8 @@ CSSCompleter.prototype = {
this.nullStates.push([tokenLine, tokenCh, [...scopeStack]]);
}
}
+ // ^ while loop end
+
this.state = _state;
this.propertyName = _state == CSS_STATES.value ? propertyName : null;
this.selectorState = _state == CSS_STATES.selector ? selectorState : null;
@@ -701,10 +747,18 @@ CSSCompleter.prototype = {
}
this.selectors = selectors;
- if (token && token.tokenType != "whitespace") {
+ if (token && token.tokenType != "WhiteSpace") {
let text;
- if (token.tokenType == "dimension" || !token.text) {
+ if (token.tokenType == "Dimension" || !token.text) {
text = source.substring(token.startOffset, token.endOffset);
+ } else if (
+ token.tokenType === "IDHash" ||
+ token.tokenType === "Hash" ||
+ token.tokenType === "AtKeyword" ||
+ token.tokenType === "Function" ||
+ token.tokenType === "QuotedString"
+ ) {
+ text = token.value;
} else {
text = token.text;
}
@@ -1047,10 +1101,10 @@ CSSCompleter.prototype = {
}
let prevToken = undefined;
- const tokens = cssTokenizer(lineText);
+ const tokensIterator = cssTokenizer(lineText);
let found = false;
const ech = line == caret.line ? caret.ch : 0;
- for (let token of tokens) {
+ for (let token of tokensIterator) {
// If the line is completely spaces, handle it differently
if (lineText.trim() == "") {
limitedSource += lineText;
@@ -1061,8 +1115,8 @@ CSSCompleter.prototype = {
);
}
- // Whitespace cannot change state.
- if (token.tokenType == "whitespace") {
+ // WhiteSpace cannot change state.
+ if (token.tokenType == "WhiteSpace") {
prevToken = token;
continue;
}
@@ -1072,7 +1126,7 @@ CSSCompleter.prototype = {
ch: token.endOffset + ech,
});
if (check(forwState)) {
- if (prevToken && prevToken.tokenType == "whitespace") {
+ if (prevToken && prevToken.tokenType == "WhiteSpace") {
token = prevToken;
}
location = {
@@ -1123,8 +1177,8 @@ CSSCompleter.prototype = {
limitedSource = limitedSource.slice(0, -1 * length);
}
- // Whitespace cannot change state.
- if (token.tokenType == "whitespace") {
+ // WhiteSpace cannot change state.
+ if (token.tokenType == "WhiteSpace") {
continue;
}
@@ -1133,7 +1187,7 @@ CSSCompleter.prototype = {
ch: token.startOffset,
});
if (check(backState)) {
- if (tokens[i + 1] && tokens[i + 1].tokenType == "whitespace") {
+ if (tokens[i + 1] && tokens[i + 1].tokenType == "WhiteSpace") {
token = tokens[i + 1];
}
location = {
diff --git a/devtools/client/shared/sourceeditor/editor.js b/devtools/client/shared/sourceeditor/editor.js
index 3487acffa4..90e9f6e373 100644
--- a/devtools/client/shared/sourceeditor/editor.js
+++ b/devtools/client/shared/sourceeditor/editor.js
@@ -168,6 +168,7 @@ class Editor extends EventEmitter {
#win;
#lineGutterMarkers = new Map();
#lineContentMarkers = new Map();
+ #lineContentEventHandlers = {};
#updateListener = null;
@@ -676,7 +677,9 @@ class Editor extends EventEmitter {
}
}),
lineNumberMarkersCompartment.of([]),
- lineContentMarkerCompartment.of(this.#lineContentMarkersExtension([])),
+ lineContentMarkerCompartment.of(
+ this.#lineContentMarkersExtension({ markers: [] })
+ ),
// keep last so other extension take precedence
codemirror.minimalSetup,
];
@@ -696,29 +699,51 @@ class Editor extends EventEmitter {
/**
* This creates the extension used to manage the rendering of markers
* for in editor line content.
- * @param {Array} markers - The current list of markers
+ * @param {Array} markers - The current list of markers
+ * @param {Object} domEventHandlers - A dictionary of handlers for the DOM events
+ * See https://codemirror.net/docs/ref/#view.PluginSpec.eventHandlers
* @returns {Array<ViewPlugin>} showLineContentDecorations - An extension which is an array containing the view
* which manages the rendering of the line content markers.
*/
- #lineContentMarkersExtension(markers) {
+ #lineContentMarkersExtension({ markers, domEventHandlers }) {
const {
- codemirrorView: { Decoration, ViewPlugin },
- codemirrorState: { RangeSetBuilder },
+ codemirrorView: { Decoration, ViewPlugin, WidgetType },
+ codemirrorState: { RangeSetBuilder, RangeSet },
} = this.#CodeMirror6;
+ class LineContentWidget extends WidgetType {
+ constructor(line, createElementNode) {
+ super();
+ this.toDOM = () => createElementNode(line);
+ }
+ }
+
// Build and return the decoration set
function buildDecorations(view) {
+ if (!markers) {
+ return RangeSet.empty;
+ }
const builder = new RangeSetBuilder();
for (const { from, to } of view.visibleRanges) {
for (let pos = from; pos <= to; ) {
const line = view.state.doc.lineAt(pos);
- for (const { lineClassName, condition } of markers) {
- if (condition(line.number)) {
- builder.add(
- line.from,
- line.from,
- Decoration.line({ class: lineClassName })
- );
+ for (const marker of markers) {
+ if (marker.condition(line.number)) {
+ if (marker.lineClassName) {
+ const classDecoration = Decoration.line({
+ class: marker.lineClassName,
+ });
+ builder.add(line.from, line.from, classDecoration);
+ }
+ if (marker.createLineElementNode) {
+ const nodeDecoration = Decoration.widget({
+ widget: new LineContentWidget(
+ line.number,
+ marker.createLineElementNode
+ ),
+ });
+ builder.add(line.to, line.to, nodeDecoration);
+ }
}
}
pos = line.to + 1;
@@ -727,9 +752,9 @@ class Editor extends EventEmitter {
return builder.finish();
}
- // The view which handles rendering and updating the
+ // The view which handles events, rendering and updating the
// markers decorations
- const showLineContentDecorations = ViewPlugin.fromClass(
+ const lineContentMarkersView = ViewPlugin.fromClass(
class {
decorations;
constructor(view) {
@@ -741,10 +766,46 @@ class Editor extends EventEmitter {
}
}
},
- { decorations: v => v.decorations }
+ {
+ decorations: v => v.decorations,
+ eventHandlers: domEventHandlers || this.#lineContentEventHandlers,
+ }
);
- return [showLineContentDecorations];
+ return [lineContentMarkersView];
+ }
+
+ /**
+ *
+ * @param {Object} domEventHandlers - A dictionary of handlers for the DOM events
+ */
+ setContentEventListeners(domEventHandlers) {
+ const cm = editors.get(this);
+
+ for (const eventName in domEventHandlers) {
+ const handler = domEventHandlers[eventName];
+ domEventHandlers[eventName] = (event, editor) => {
+ // Wait a cycle so the codemirror updates to the current cursor position,
+ // information, TODO: Currently noticed this issue with CM6, not ideal but should
+ // investigate further Bug 1890895.
+ event.target.ownerGlobal.setTimeout(() => {
+ const view = editor.viewState;
+ const head = view.state.selection.main.head;
+ const cursor = view.state.doc.lineAt(head);
+ const column = head - cursor.from;
+ handler(event, view, cursor.number, column);
+ }, 0);
+ };
+ }
+
+ // Cache the handlers related to the editor content
+ this.#lineContentEventHandlers = domEventHandlers;
+
+ cm.dispatch({
+ effects: this.#compartments.lineContentMarkerCompartment.reconfigure(
+ this.#lineContentMarkersExtension({ domEventHandlers })
+ ),
+ });
}
/**
@@ -754,6 +815,9 @@ class Editor extends EventEmitter {
* @property {string} marker.lineClassName - The css class to add to the line
* @property {function} marker.condition - The condition that decides if the marker/class gets added or removed.
* The line is passed as an argument.
+ * @property {function} marker.createLineElementNode - This should return the DOM element which
+ * is used for the marker. The line number is passed as a parameter.
+ * This is optional.
*/
setLineContentMarker(marker) {
const cm = editors.get(this);
@@ -761,9 +825,9 @@ class Editor extends EventEmitter {
cm.dispatch({
effects: this.#compartments.lineContentMarkerCompartment.reconfigure(
- this.#lineContentMarkersExtension(
- Array.from(this.#lineContentMarkers.values())
- )
+ this.#lineContentMarkersExtension({
+ markers: Array.from(this.#lineContentMarkers.values()),
+ })
),
});
}
@@ -778,9 +842,9 @@ class Editor extends EventEmitter {
cm.dispatch({
effects: this.#compartments.lineContentMarkerCompartment.reconfigure(
- this.#lineContentMarkersExtension(
- Array.from(this.#lineContentMarkers.values())
- )
+ this.#lineContentMarkersExtension({
+ markers: Array.from(this.#lineContentMarkers.values()),
+ })
),
});
}
@@ -869,31 +933,31 @@ class Editor extends EventEmitter {
// (representing the lines in the current viewport) and generate a new rangeset for updating the line gutter
// based on the conditions defined in the markers(for each line) provided.
const builder = new RangeSetBuilder();
- for (const { from, to } of cm.visibleRanges) {
- for (let pos = from; pos <= to; ) {
- const line = cm.state.doc.lineAt(pos);
- for (const {
- lineClassName,
- condition,
- createLineElementNode,
- } of markers) {
- if (typeof condition !== "function") {
- throw new Error("The `condition` is not a valid function");
- }
- if (condition(line.number)) {
- builder.add(
- line.from,
- line.to,
- new LineGutterMarker(
- lineClassName,
- line.number,
- createLineElementNode
- )
- );
- }
+ const { from, to } = cm.viewport;
+ let pos = from;
+ while (pos <= to) {
+ const line = cm.state.doc.lineAt(pos);
+ for (const {
+ lineClassName,
+ condition,
+ createLineElementNode,
+ } of markers) {
+ if (typeof condition !== "function") {
+ throw new Error("The `condition` is not a valid function");
+ }
+ if (condition(line.number)) {
+ builder.add(
+ line.from,
+ line.to,
+ new LineGutterMarker(
+ lineClassName,
+ line.number,
+ createLineElementNode
+ )
+ );
}
- pos = line.to + 1;
}
+ pos = line.to + 1;
}
// To update the state with the newly generated marker range set, a dispatch is called on the view
@@ -907,6 +971,61 @@ class Editor extends EventEmitter {
}
/**
+ * Gets the position information for the current selection
+ * @returns {Object} cursor - The location information for the current selection
+ * cursor.from - An object with the starting line / column of the selection
+ * cursor.to - An object with the end line / column of the selection
+ */
+ getSelectionCursor() {
+ const cm = editors.get(this);
+ if (this.config.cm6) {
+ const selection = cm.state.selection.ranges[0];
+ const lineFrom = cm.state.doc.lineAt(selection.from);
+ const lineTo = cm.state.doc.lineAt(selection.to);
+ return {
+ from: {
+ line: lineFrom.number,
+ ch: selection.from - lineFrom.from,
+ },
+ to: {
+ line: lineTo.number,
+ ch: selection.to - lineTo.from,
+ },
+ };
+ }
+ return {
+ from: cm.getCursor("from"),
+ to: cm.getCursor("to"),
+ };
+ }
+
+ /**
+ * Gets the text content for the current selection
+ * @returns {String}
+ */
+ getSelectedText() {
+ const cm = editors.get(this);
+ if (this.config.cm6) {
+ const selection = cm.state.selection.ranges[0];
+ return cm.state.doc.sliceString(selection.from, selection.to);
+ }
+ return cm.getSelection().trim();
+ }
+
+ /**
+ * Check that text is selected
+ * @returns {Boolean}
+ */
+ isTextSelected() {
+ const cm = editors.get(this);
+ if (this.config.cm6) {
+ const selection = cm.state.selection.ranges[0];
+ return selection.from !== selection.to;
+ }
+ return cm.somethingSelected();
+ }
+
+ /**
* Returns a boolean indicating whether the editor is ready to
* use. Use appendTo(el).then(() => {}) for most cases
*/
@@ -1860,6 +1979,119 @@ class Editor extends EventEmitter {
}
/**
+ * This checks if the specified position (top/left) is within the current viewpport
+ * bounds. it helps determine is scrolling should happen.
+ * @param {Object} cm - The codemirror instance
+ * @param {Number} line - The line in the source
+ * @param {Number} column - The column in the source
+ * @returns {Boolean}
+ */
+ #isVisible(cm, line, column) {
+ let inXView, inYView;
+
+ function withinBounds(x, min, max) {
+ return x >= min && x <= max;
+ }
+
+ if (this.config.cm6) {
+ const pos = this.#posToOffset(cm.state.doc, line, column);
+ const coords = pos && cm.coordsAtPos(pos);
+ if (!coords) {
+ return false;
+ }
+ const { scrollTop, scrollLeft, clientHeight, clientWidth } = cm.scrollDOM;
+
+ inXView = withinBounds(coords.left, scrollLeft, scrollLeft + clientWidth);
+ inYView = withinBounds(coords.top, scrollTop, scrollTop + clientHeight);
+ } else {
+ const { top, left } = cm.charCoords({ line, ch: column }, "local");
+ const scrollArea = cm.getScrollInfo();
+ const charWidth = cm.defaultCharWidth();
+ const fontHeight = cm.defaultTextHeight();
+ const { scrollTop, scrollLeft } = cm.doc;
+
+ inXView = withinBounds(
+ left,
+ scrollLeft,
+ // Note: 30 might relate to the margin on one of the scroll bar elements.
+ // See comment https://github.com/firefox-devtools/debugger/pull/5182#discussion_r163439209
+ scrollLeft + (scrollArea.clientWidth - 30) - charWidth
+ );
+ inYView = withinBounds(
+ top,
+ scrollTop,
+ scrollTop + scrollArea.clientHeight - fontHeight
+ );
+ }
+ return inXView && inYView;
+ }
+
+ /**
+ * Converts line/col to CM6 offset position
+ * @param {Object} doc - the codemirror document
+ * @param {Number} line - The line in the source
+ * @param {Number} col - The column in the source
+ * @returns {Number}
+ */
+ #posToOffset(doc, line, col) {
+ if (!this.config.cm6) {
+ throw new Error("This function is only compatible with CM6");
+ }
+ try {
+ const offset = doc.line(line);
+ return offset.from + col;
+ } catch (e) {
+ // Line likey does not exist in viewport yet
+ console.warn(e.message);
+ }
+ return null;
+ }
+
+ /**
+ * Scrolls the editor to the specified line and column
+ * @param {Number} line - The line in the source
+ * @param {Number} column - The column in the source
+ */
+ scrollTo(line, column) {
+ const cm = editors.get(this);
+ if (this.config.cm6) {
+ const {
+ codemirrorView: { EditorView },
+ } = this.#CodeMirror6;
+
+ if (!this.#isVisible(cm, line, column)) {
+ const offset = this.#posToOffset(cm.state.doc, line, column);
+ if (!offset) {
+ return;
+ }
+ cm.dispatch({
+ effects: EditorView.scrollIntoView(offset, {
+ x: "nearest",
+ y: "center",
+ }),
+ });
+ }
+ } else {
+ // For all cases where these are on the first line and column,
+ // avoid the possibly slow computation of cursor location on large bundles.
+ if (!line && !column) {
+ cm.scrollTo(0, 0);
+ return;
+ }
+
+ const { top, left } = cm.charCoords({ line, ch: column }, "local");
+
+ if (!this.#isVisible(cm, line, column)) {
+ const scroller = cm.getScrollerElement();
+ const centeredX = Math.max(left - scroller.offsetWidth / 2, 0);
+ const centeredY = Math.max(top - scroller.offsetHeight / 2, 0);
+
+ cm.scrollTo(centeredX, centeredY);
+ }
+ }
+ }
+
+ /**
* Extends an instance of the Editor object with additional
* functions. Each function will be called with context as
* the first argument. Context is a {ed, cm} object where
@@ -1901,6 +2133,8 @@ class Editor extends EventEmitter {
this.#ownerDoc = null;
this.#updateListener = null;
this.#lineGutterMarkers.clear();
+ this.#lineContentMarkers.clear();
+ this.#lineContentEventHandlers = {};
if (this.#prefObserver) {
this.#prefObserver.off(KEYMAP_PREF, this.setKeyMap);
diff --git a/devtools/client/shared/test/browser_filter-editor-01.js b/devtools/client/shared/test/browser_filter-editor-01.js
index 557a02857c..106f89dbc4 100644
--- a/devtools/client/shared/test/browser_filter-editor-01.js
+++ b/devtools/client/shared/test/browser_filter-editor-01.js
@@ -10,15 +10,14 @@ const {
} = require("resource://devtools/client/shared/widgets/FilterWidget.js");
const TEST_URI = CHROME_URL_ROOT + "doc_filter-editor-01.html";
-const { getCSSLexer } = require("resource://devtools/shared/css/lexer.js");
// Verify that the given string consists of a valid CSS URL token.
// Return true on success, false on error.
function verifyURL(string) {
- const lexer = getCSSLexer(string);
+ const lexer = new InspectorCSSParser(string);
const token = lexer.nextToken();
- if (!token || token.tokenType !== "url") {
+ if (!token || token.tokenType !== "UnquotedUrl") {
return false;
}
diff --git a/devtools/client/shared/test/browser_outputparser.js b/devtools/client/shared/test/browser_outputparser.js
index 7e0c1b7e00..a2fcce8f78 100644
--- a/devtools/client/shared/test/browser_outputparser.js
+++ b/devtools/client/shared/test/browser_outputparser.js
@@ -700,6 +700,22 @@ function testParseVariable(doc, parser) {
")</span>" +
"</span>",
},
+ {
+ text: "rgb(var(--not-seen), 0, 0)",
+ variables: {},
+ expected:
+ // prettier-ignore
+ `rgb(` +
+ `<span>` +
+ `var(` +
+ `<span class="unmatched-class" data-variable="--not-seen is not set">` +
+ `--not-seen` +
+ `</span>` +
+ `)` +
+ `</span>` +
+ `, 0, 0` +
+ `)`,
+ },
];
for (const test of TESTS) {
diff --git a/devtools/client/shared/test/xpcshell/test_parseDeclarations.js b/devtools/client/shared/test/xpcshell/test_parseDeclarations.js
index 593087d46b..373d5acc56 100644
--- a/devtools/client/shared/test/xpcshell/test_parseDeclarations.js
+++ b/devtools/client/shared/test/xpcshell/test_parseDeclarations.js
@@ -750,7 +750,7 @@ const TEST_DATA = [
expected: [
{
name: "color",
- value: "blue \\9 no_need",
+ value: "blue \\9 no\\_need",
priority: "",
offsets: [0, 23],
declarationText: "color: blue \\9 no\\_need",
@@ -1609,18 +1609,27 @@ function assertOutput(input, actual, expected) {
"Check that the output item has the expected name, " +
"value and priority"
);
- Assert.equal(expected[i].name, actual[i].name);
- Assert.equal(expected[i].value, actual[i].value);
- Assert.equal(expected[i].priority, actual[i].priority);
- deepEqual(expected[i].offsets, actual[i].offsets);
+ Assert.equal(actual[i].name, expected[i].name, "Expected name");
+ Assert.equal(actual[i].value, expected[i].value, "Expected value");
+ Assert.equal(
+ actual[i].priority,
+ expected[i].priority,
+ "Expected priority"
+ );
+ deepEqual(actual[i].offsets, expected[i].offsets, "Expected offsets");
if ("commentOffsets" in expected[i]) {
- deepEqual(expected[i].commentOffsets, actual[i].commentOffsets);
+ deepEqual(
+ actual[i].commentOffsets,
+ expected[i].commentOffsets,
+ "Expected commentOffsets"
+ );
}
if (expected[i].declarationText) {
Assert.equal(
input.substring(expected[i].offsets[0], expected[i].offsets[1]),
- expected[i].declarationText
+ expected[i].declarationText,
+ "Expected declarationText"
);
}
}
diff --git a/devtools/client/shared/test/xpcshell/test_rewriteDeclarations.js b/devtools/client/shared/test/xpcshell/test_rewriteDeclarations.js
index 228d2dc79d..9842db7c02 100644
--- a/devtools/client/shared/test/xpcshell/test_rewriteDeclarations.js
+++ b/devtools/client/shared/test/xpcshell/test_rewriteDeclarations.js
@@ -483,9 +483,7 @@ const TEST_DATA = [
enabled: true,
},
expected: "something: \\\\;color: red;",
- // The lexer rewrites the token before we see it. However this is
- // so obscure as to be inconsequential.
- changed: { 0: "\uFFFD\\" },
+ changed: { 0: "\\\\" },
},
// Termination insertion corner case.
diff --git a/devtools/client/shared/widgets/CubicBezierWidget.js b/devtools/client/shared/widgets/CubicBezierWidget.js
index 39407d4711..df41949df5 100644
--- a/devtools/client/shared/widgets/CubicBezierWidget.js
+++ b/devtools/client/shared/widgets/CubicBezierWidget.js
@@ -31,7 +31,9 @@ const {
PRESETS,
DEFAULT_PRESET_CATEGORY,
} = require("resource://devtools/client/shared/widgets/CubicBezierPresets.js");
-const { getCSSLexer } = require("resource://devtools/shared/css/lexer.js");
+const {
+ InspectorCSSParserWrapper,
+} = require("resource://devtools/shared/css/lexer.js");
const XHTML_NS = "http://www.w3.org/1999/xhtml";
/**
@@ -918,13 +920,13 @@ function parseTimingFunction(value) {
return PREDEFINED[value];
}
- const tokenStream = getCSSLexer(value);
+ const tokenStream = new InspectorCSSParserWrapper(value);
const getNextToken = () => {
while (true) {
const token = tokenStream.nextToken();
if (
!token ||
- (token.tokenType !== "whitespace" && token.tokenType !== "comment")
+ (token.tokenType !== "WhiteSpace" && token.tokenType !== "Comment")
) {
return token;
}
@@ -932,24 +934,20 @@ function parseTimingFunction(value) {
};
let token = getNextToken();
- if (token.tokenType !== "function" || token.text !== "cubic-bezier") {
+ if (token.tokenType !== "Function" || token.value !== "cubic-bezier") {
return undefined;
}
const result = [];
for (let i = 0; i < 4; ++i) {
token = getNextToken();
- if (!token || token.tokenType !== "number") {
+ if (!token || token.tokenType !== "Number") {
return undefined;
}
result.push(token.number);
token = getNextToken();
- if (
- !token ||
- token.tokenType !== "symbol" ||
- token.text !== (i == 3 ? ")" : ",")
- ) {
+ if (!token || token.tokenType !== (i == 3 ? "CloseParenthesis" : "Comma")) {
return undefined;
}
}
diff --git a/devtools/client/shared/widgets/FilterWidget.js b/devtools/client/shared/widgets/FilterWidget.js
index bb23bdfeca..487cc9ad0b 100644
--- a/devtools/client/shared/widgets/FilterWidget.js
+++ b/devtools/client/shared/widgets/FilterWidget.js
@@ -814,7 +814,7 @@ CSSFilterEditorWidget.prototype = {
return;
}
- for (let { name, value, quote } of tokenizeFilterValue(cssValue)) {
+ for (let { name, value } of tokenizeFilterValue(cssValue)) {
// If the specified value is invalid, replace it with the
// default.
if (name !== "url") {
@@ -823,7 +823,7 @@ CSSFilterEditorWidget.prototype = {
}
}
- this.add(name, value, quote, true);
+ this.add(name, value, true);
}
this.emit("updated", this.getCssValue());
@@ -838,9 +838,6 @@ CSSFilterEditorWidget.prototype = {
* @param {String} value
* value of the filter (e.g. 30px, 20%)
* If this is |null|, then a default value may be supplied.
- * @param {String} quote
- * For a url filter, the quoting style. This can be a
- * single quote, a double quote, or empty.
* @return {Number}
* The index of the new filter in the current list of filters
* @param {Boolean}
@@ -848,7 +845,7 @@ CSSFilterEditorWidget.prototype = {
* you're calling add in a loop and wait to emit a single event after
* the loop yourself, set this parameter to true.
*/
- add(name, value, quote, noEvent) {
+ add(name, value, noEvent) {
const def = this._definition(name);
if (!def) {
return false;
@@ -868,11 +865,6 @@ CSSFilterEditorWidget.prototype = {
} else {
value = def.range[0] + unitLabel;
}
-
- if (name === "url") {
- // Default quote.
- quote = '"';
- }
}
let unit = def.type === "string" ? "" : (/[a-zA-Z%]+/.exec(value) || [])[0];
@@ -894,7 +886,7 @@ CSSFilterEditorWidget.prototype = {
}
}
- const index = this.filters.push({ value, unit, name, quote }) - 1;
+ const index = this.filters.push({ value, unit, name }) - 1;
if (!noEvent) {
this.emit("updated", this.getCssValue());
}
@@ -916,22 +908,12 @@ CSSFilterEditorWidget.prototype = {
return null;
}
- // Just return the value+unit for non-url functions.
- if (filter.name !== "url") {
- return filter.value + filter.unit;
+ // Just return the value url functions.
+ if (filter.name === "url") {
+ return filter.value;
}
- // url values need to be quoted and escaped.
- if (filter.quote === "'") {
- return "'" + filter.value.replace(/\'/g, "\\'") + "'";
- } else if (filter.quote === '"') {
- return '"' + filter.value.replace(/\"/g, '\\"') + '"';
- }
-
- // Unquoted. This approach might change the original input -- for
- // example the original might be over-quoted. But, this is
- // correct and probably good enough.
- return filter.value.replace(/[\\ \t()"']/g, "\\$&");
+ return filter.value + filter.unit;
},
removeAt(index) {
@@ -1051,27 +1033,34 @@ function tokenizeFilterValue(css) {
for (const token of cssTokenizer(css)) {
switch (state) {
case "initial":
- if (token.tokenType === "function") {
- name = token.text;
+ if (token.tokenType === "Function") {
+ name = token.value;
contents = "";
state = "function";
depth = 1;
- } else if (token.tokenType === "url" || token.tokenType === "bad_url") {
- // Extract the quoting style from the url.
- const originalText = css.substring(
- token.startOffset,
- token.endOffset
- );
- const [, quote] = /^url\([ \t\r\n\f]*(["']?)/i.exec(originalText);
-
- filters.push({ name: "url", value: token.text.trim(), quote });
+ } else if (
+ token.tokenType === "UnquotedUrl" ||
+ token.tokenType === "BadUrl"
+ ) {
+ const url = token.text
+ .substring(
+ // token text starts with `url(`
+ 4,
+ // unquoted url also include the closing parenthesis
+ token.tokenType == "UnquotedUrl"
+ ? token.text.length - 1
+ : undefined
+ )
+ .trim();
+
+ filters.push({ name: "url", value: url });
// Leave state as "initial" because the URL token includes
// the trailing close paren.
}
break;
case "function":
- if (token.tokenType === "symbol" && token.text === ")") {
+ if (token.tokenType === "CloseParenthesis") {
--depth;
if (depth === 0) {
filters.push({ name, value: contents.trim() });
@@ -1081,8 +1070,8 @@ function tokenizeFilterValue(css) {
}
contents += css.substring(token.startOffset, token.endOffset);
if (
- token.tokenType === "function" ||
- (token.tokenType === "symbol" && token.text === "(")
+ token.tokenType === "Function" ||
+ token.tokenType === "Parenthesis"
) {
++depth;
}
diff --git a/devtools/client/shared/widgets/LinearEasingFunctionWidget.js b/devtools/client/shared/widgets/LinearEasingFunctionWidget.js
index e6d2e604df..5ea3b33d15 100644
--- a/devtools/client/shared/widgets/LinearEasingFunctionWidget.js
+++ b/devtools/client/shared/widgets/LinearEasingFunctionWidget.js
@@ -9,7 +9,7 @@
*/
const EventEmitter = require("devtools/shared/event-emitter");
-const { getCSSLexer } = require("devtools/shared/css/lexer");
+const { InspectorCSSParserWrapper } = require("devtools/shared/css/lexer");
const { throttle } = require("devtools/shared/throttle");
const XHTML_NS = "http://www.w3.org/1999/xhtml";
const SVG_NS = "http://www.w3.org/2000/svg";
@@ -578,13 +578,13 @@ class TimingFunctionPreviewWidget {
*/
function parseTimingFunction(value) {
value = value.trim();
- const tokenStream = getCSSLexer(value);
+ const tokenStream = new InspectorCSSParserWrapper(value);
const getNextToken = () => {
while (true) {
const token = tokenStream.nextToken();
if (
!token ||
- (token.tokenType !== "whitespace" && token.tokenType !== "comment")
+ (token.tokenType !== "WhiteSpace" && token.tokenType !== "Comment")
) {
return token;
}
@@ -592,7 +592,7 @@ function parseTimingFunction(value) {
};
let token = getNextToken();
- if (!token || token.tokenType !== "function" || token.text !== "linear") {
+ if (!token || token.tokenType !== "Function" || token.value !== "linear") {
return undefined;
}
@@ -601,11 +601,11 @@ function parseTimingFunction(value) {
let largestInput = -Infinity;
while ((token = getNextToken())) {
- if (token.text === ")") {
+ if (token.tokenType === "CloseParenthesis") {
break;
}
- if (token.tokenType === "number") {
+ if (token.tokenType === "Number") {
// [parsing step 4.1]
const point = { input: null, output: token.number };
// [parsing step 4.2]
@@ -614,7 +614,7 @@ function parseTimingFunction(value) {
// get nextToken to see if there's a linear stop length
token = getNextToken();
// [parsing step 4.3]
- if (token && token.tokenType === "percentage") {
+ if (token && token.tokenType === "Percentage") {
// [parsing step 4.3.1]
point.input = Math.max(token.number, largestInput);
// [parsing step 4.3.2]
@@ -624,7 +624,7 @@ function parseTimingFunction(value) {
token = getNextToken();
// [parsing step 4.3.3]
- if (token && token.tokenType === "percentage") {
+ if (token && token.tokenType === "Percentage") {
// [parsing step 4.3.3.1]
const extraPoint = { input: null, output: point.output };
// [parsing step 4.3.3.2]