зеркало из https://github.com/mozilla/gecko-dev.git
1338 строки
45 KiB
JavaScript
1338 строки
45 KiB
JavaScript
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
|
|
|
"use strict";
|
|
|
|
const { Actor } = require("resource://devtools/shared/protocol.js");
|
|
const {
|
|
styleRuleSpec,
|
|
} = require("resource://devtools/shared/specs/style-rule.js");
|
|
|
|
const { getCSSLexer } = require("resource://devtools/shared/css/lexer.js");
|
|
const TrackChangeEmitter = require("resource://devtools/server/actors/utils/track-change-emitter.js");
|
|
const {
|
|
getRuleText,
|
|
getTextAtLineColumn,
|
|
} = require("resource://devtools/server/actors/utils/style-utils.js");
|
|
|
|
const {
|
|
style: { ELEMENT_STYLE },
|
|
} = require("resource://devtools/shared/constants.js");
|
|
|
|
loader.lazyRequireGetter(
|
|
this,
|
|
"CssLogic",
|
|
"resource://devtools/server/actors/inspector/css-logic.js",
|
|
true
|
|
);
|
|
loader.lazyRequireGetter(
|
|
this,
|
|
"SharedCssLogic",
|
|
"resource://devtools/shared/inspector/css-logic.js"
|
|
);
|
|
loader.lazyRequireGetter(
|
|
this,
|
|
"isCssPropertyKnown",
|
|
"resource://devtools/server/actors/css-properties.js",
|
|
true
|
|
);
|
|
loader.lazyRequireGetter(
|
|
this,
|
|
"isPropertyUsed",
|
|
"resource://devtools/server/actors/utils/inactive-property-helper.js",
|
|
true
|
|
);
|
|
loader.lazyRequireGetter(
|
|
this,
|
|
"parseNamedDeclarations",
|
|
"resource://devtools/shared/css/parsing-utils.js",
|
|
true
|
|
);
|
|
loader.lazyRequireGetter(
|
|
this,
|
|
["UPDATE_PRESERVING_RULES", "UPDATE_GENERAL"],
|
|
"resource://devtools/server/actors/utils/stylesheets-manager.js",
|
|
true
|
|
);
|
|
|
|
const XHTML_NS = "http://www.w3.org/1999/xhtml";
|
|
|
|
/**
|
|
* An actor that represents a CSS style object on the protocol.
|
|
*
|
|
* We slightly flatten the CSSOM for this actor, it represents
|
|
* both the CSSRule and CSSStyle objects in one actor. For nodes
|
|
* (which have a CSSStyle but no CSSRule) we create a StyleRuleActor
|
|
* with a special rule type (100).
|
|
*/
|
|
class StyleRuleActor extends Actor {
|
|
constructor(pageStyle, item, userAdded = false) {
|
|
super(pageStyle.conn, styleRuleSpec);
|
|
this.pageStyle = pageStyle;
|
|
this.rawStyle = item.style;
|
|
this._userAdded = userAdded;
|
|
this._parentSheet = null;
|
|
// Parsed CSS declarations from this.form().declarations used to check CSS property
|
|
// names and values before tracking changes. Using cached values instead of accessing
|
|
// this.form().declarations on demand because that would cause needless re-parsing.
|
|
this._declarations = [];
|
|
|
|
this._pendingDeclarationChanges = [];
|
|
this._failedToGetRuleText = false;
|
|
|
|
if (CSSRule.isInstance(item)) {
|
|
this.type = item.type;
|
|
this.ruleClassName = ChromeUtils.getClassName(item);
|
|
|
|
this.rawRule = item;
|
|
this._computeRuleIndex();
|
|
if (this.#isRuleSupported() && this.rawRule.parentStyleSheet) {
|
|
this.line = InspectorUtils.getRelativeRuleLine(this.rawRule);
|
|
this.column = InspectorUtils.getRuleColumn(this.rawRule);
|
|
this._parentSheet = this.rawRule.parentStyleSheet;
|
|
}
|
|
} else {
|
|
// Fake a rule
|
|
this.type = ELEMENT_STYLE;
|
|
this.ruleClassName = ELEMENT_STYLE;
|
|
this.rawNode = item;
|
|
this.rawRule = {
|
|
style: item.style,
|
|
toString() {
|
|
return "[element rule " + this.style + "]";
|
|
},
|
|
};
|
|
}
|
|
}
|
|
|
|
destroy() {
|
|
if (!this.rawStyle) {
|
|
return;
|
|
}
|
|
super.destroy();
|
|
this.rawStyle = null;
|
|
this.pageStyle = null;
|
|
this.rawNode = null;
|
|
this.rawRule = null;
|
|
this._declarations = null;
|
|
}
|
|
|
|
// Objects returned by this actor are owned by the PageStyleActor
|
|
// to which this rule belongs.
|
|
get marshallPool() {
|
|
return this.pageStyle;
|
|
}
|
|
|
|
// True if this rule supports as-authored styles, meaning that the
|
|
// rule text can be rewritten using setRuleText.
|
|
get canSetRuleText() {
|
|
if (this.type === ELEMENT_STYLE) {
|
|
// Element styles are always editable.
|
|
return true;
|
|
}
|
|
if (!this._parentSheet) {
|
|
return false;
|
|
}
|
|
if (InspectorUtils.hasRulesModifiedByCSSOM(this._parentSheet)) {
|
|
// If a rule has been modified via CSSOM, then we should fall back to
|
|
// non-authored editing.
|
|
// https://bugzilla.mozilla.org/show_bug.cgi?id=1224121
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Return an array with StyleRuleActor instances for each of this rule's ancestor rules
|
|
* (@media, @supports, @keyframes, etc) obtained by recursively reading rule.parentRule.
|
|
* If the rule has no ancestors, return an empty array.
|
|
*
|
|
* @return {Array}
|
|
*/
|
|
get ancestorRules() {
|
|
const ancestors = [];
|
|
let rule = this.rawRule;
|
|
|
|
while (rule.parentRule) {
|
|
ancestors.unshift(this.pageStyle._styleRef(rule.parentRule));
|
|
rule = rule.parentRule;
|
|
}
|
|
|
|
return ancestors;
|
|
}
|
|
|
|
/**
|
|
* Return an object with information about this rule used for tracking changes.
|
|
* It will be decorated with information about a CSS change before being tracked.
|
|
*
|
|
* It contains:
|
|
* - the rule selector (or generated selectror for inline styles)
|
|
* - the rule's host stylesheet (or element for inline styles)
|
|
* - the rule's ancestor rules (@media, @supports, @keyframes), if any
|
|
* - the rule's position within its ancestor tree, if any
|
|
*
|
|
* @return {Object}
|
|
*/
|
|
get metadata() {
|
|
const data = {};
|
|
data.id = this.actorID;
|
|
// Collect information about the rule's ancestors (@media, @supports, @keyframes, parent rules).
|
|
// Used to show context for this change in the UI and to match the rule for undo/redo.
|
|
data.ancestors = this.ancestorRules.map(rule => {
|
|
const ancestorData = {
|
|
id: rule.actorID,
|
|
// Array with the indexes of this rule and its ancestors within the CSS rule tree.
|
|
ruleIndex: rule._ruleIndex,
|
|
};
|
|
|
|
// Rule type as human-readable string (ex: "@media", "@supports", "@keyframes")
|
|
const typeName = SharedCssLogic.getCSSAtRuleTypeName(rule.rawRule);
|
|
if (typeName) {
|
|
ancestorData.typeName = typeName;
|
|
}
|
|
|
|
// Conditions of @container, @media and @supports rules (ex: "min-width: 1em")
|
|
if (rule.rawRule.conditionText !== undefined) {
|
|
ancestorData.conditionText = rule.rawRule.conditionText;
|
|
}
|
|
|
|
// Name of @keyframes rule; referenced by the animation-name CSS property.
|
|
if (rule.rawRule.name !== undefined) {
|
|
ancestorData.name = rule.rawRule.name;
|
|
}
|
|
|
|
// Selector of individual @keyframe rule within a @keyframes rule (ex: 0%, 100%).
|
|
if (rule.rawRule.keyText !== undefined) {
|
|
ancestorData.keyText = rule.rawRule.keyText;
|
|
}
|
|
|
|
// Selector of the rule; might be useful in case for nested rules
|
|
if (rule.rawRule.selectorText !== undefined) {
|
|
ancestorData.selectorText = rule.rawRule.selectorText;
|
|
}
|
|
|
|
return ancestorData;
|
|
});
|
|
|
|
// For changes in element style attributes, generate a unique selector.
|
|
if (this.type === ELEMENT_STYLE && this.rawNode) {
|
|
// findCssSelector() fails on XUL documents. Catch and silently ignore that error.
|
|
try {
|
|
data.selector = SharedCssLogic.findCssSelector(this.rawNode);
|
|
} catch (err) {}
|
|
|
|
data.source = {
|
|
type: "element",
|
|
// Used to differentiate between elements which match the same generated selector
|
|
// but live in different documents (ex: host document and iframe).
|
|
href: this.rawNode.baseURI,
|
|
// Element style attributes don't have a rule index; use the generated selector.
|
|
index: data.selector,
|
|
// Whether the element lives in a different frame than the host document.
|
|
isFramed: this.rawNode.ownerGlobal !== this.pageStyle.ownerWindow,
|
|
};
|
|
|
|
const nodeActor = this.pageStyle.walker.getNode(this.rawNode);
|
|
if (nodeActor) {
|
|
data.source.id = nodeActor.actorID;
|
|
}
|
|
|
|
data.ruleIndex = 0;
|
|
} else {
|
|
data.selector =
|
|
this.ruleClassName === "CSSKeyframeRule"
|
|
? this.rawRule.keyText
|
|
: this.rawRule.selectorText;
|
|
// Used to differentiate between changes to rules with identical selectors.
|
|
data.ruleIndex = this._ruleIndex;
|
|
|
|
const sheet = this._parentSheet;
|
|
const inspectorActor = this.pageStyle.inspector;
|
|
const resourceId =
|
|
this.pageStyle.styleSheetsManager.getStyleSheetResourceId(sheet);
|
|
const styleSheetIndex =
|
|
this.pageStyle.styleSheetsManager.getStyleSheetIndex(resourceId);
|
|
data.source = {
|
|
// Inline stylesheets have a null href; Use window URL instead.
|
|
type: sheet.href ? "stylesheet" : "inline",
|
|
href: sheet.href || inspectorActor.window.location.toString(),
|
|
id: resourceId,
|
|
index: styleSheetIndex,
|
|
// Whether the stylesheet lives in a different frame than the host document.
|
|
isFramed: inspectorActor.window !== inspectorActor.window.top,
|
|
};
|
|
}
|
|
|
|
return data;
|
|
}
|
|
|
|
getDocument(sheet) {
|
|
if (!sheet.associatedDocument) {
|
|
throw new Error(
|
|
"Failed trying to get the document of an invalid stylesheet"
|
|
);
|
|
}
|
|
return sheet.associatedDocument;
|
|
}
|
|
|
|
/**
|
|
* When a rule is nested in another non-at-rule (aka CSS Nesting), the client
|
|
* will need its desugared selector, i.e. the full selector, which includes ancestor
|
|
* selectors, that is computed by the platform when applying the rule.
|
|
* To compute it, the parent selector (&) is recursively replaced by the parent
|
|
* rule selector wrapped in `:is()`.
|
|
* For example, with the following nested rule: `body { & > main {} }`,
|
|
* the desugared selector will be `:is(body) > main`.
|
|
* See https://www.w3.org/TR/css-nesting-1/#nest-selector for more information.
|
|
*
|
|
* Returns an array of the desugared selectors. For example, if rule is:
|
|
*
|
|
* body {
|
|
* & > main, & section {
|
|
* }
|
|
* }
|
|
*
|
|
* this will return:
|
|
*
|
|
* [
|
|
* `:is(body) > main`,
|
|
* `:is(body) section`,
|
|
* ]
|
|
*
|
|
* @returns Array<String>
|
|
*/
|
|
getDesugaredSelectors() {
|
|
// Cache the desugared selectors as it can be expensive to compute
|
|
if (!this._desugaredSelectors) {
|
|
this._desugaredSelectors = CssLogic.getSelectors(this.rawRule, true);
|
|
}
|
|
|
|
return this._desugaredSelectors;
|
|
}
|
|
|
|
toString() {
|
|
return "[StyleRuleActor for " + this.rawRule + "]";
|
|
}
|
|
|
|
// eslint-disable-next-line complexity
|
|
form() {
|
|
const form = {
|
|
actor: this.actorID,
|
|
type: this.type,
|
|
line: this.line || undefined,
|
|
column: this.column,
|
|
traits: {
|
|
// Indicates whether StyleRuleActor implements and can use the setRuleText method.
|
|
// It cannot use it if the stylesheet was programmatically mutated via the CSSOM.
|
|
canSetRuleText: this.canSetRuleText,
|
|
},
|
|
};
|
|
|
|
// This rule was manually added by the user and may be automatically focused by the frontend.
|
|
if (this._userAdded) {
|
|
form.userAdded = true;
|
|
}
|
|
|
|
const { computeDesugaredSelector, ancestorData } =
|
|
this._getAncestorDataForForm();
|
|
form.ancestorData = ancestorData;
|
|
|
|
if (this._parentSheet) {
|
|
form.parentStyleSheet =
|
|
this.pageStyle.styleSheetsManager.getStyleSheetResourceId(
|
|
this._parentSheet
|
|
);
|
|
}
|
|
|
|
// One tricky thing here is that other methods in this actor must
|
|
// ensure that authoredText has been set before |form| is called.
|
|
// This has to be treated specially, for now, because we cannot
|
|
// synchronously compute the authored text, but |form| also cannot
|
|
// return a promise. See bug 1205868.
|
|
form.authoredText = this.authoredText;
|
|
|
|
switch (this.ruleClassName) {
|
|
case "CSSStyleRule":
|
|
form.selectors = CssLogic.getSelectors(this.rawRule);
|
|
|
|
// Only add the property when there are elements in the array to save up on serialization.
|
|
const selectorWarnings = this.rawRule.getSelectorWarnings();
|
|
if (selectorWarnings.length) {
|
|
form.selectorWarnings = selectorWarnings;
|
|
}
|
|
if (computeDesugaredSelector) {
|
|
form.desugaredSelectors = this.getDesugaredSelectors();
|
|
}
|
|
form.cssText = this.rawStyle.cssText || "";
|
|
break;
|
|
case ELEMENT_STYLE:
|
|
// Elements don't have a parent stylesheet, and therefore
|
|
// don't have an associated URI. Provide a URI for
|
|
// those.
|
|
const doc = this.rawNode.ownerDocument;
|
|
form.href = doc.location ? doc.location.href : "";
|
|
form.cssText = this.rawStyle.cssText || "";
|
|
form.authoredText = this.rawNode.getAttribute("style");
|
|
break;
|
|
case "CSSCharsetRule":
|
|
form.encoding = this.rawRule.encoding;
|
|
break;
|
|
case "CSSImportRule":
|
|
form.href = this.rawRule.href;
|
|
break;
|
|
case "CSSKeyframesRule":
|
|
form.cssText = this.rawRule.cssText;
|
|
form.name = this.rawRule.name;
|
|
break;
|
|
case "CSSKeyframeRule":
|
|
form.cssText = this.rawStyle.cssText || "";
|
|
form.keyText = this.rawRule.keyText || "";
|
|
break;
|
|
}
|
|
|
|
// Parse the text into a list of declarations so the client doesn't have to
|
|
// and so that we can safely determine if a declaration is valid rather than
|
|
// have the client guess it.
|
|
if (form.authoredText || form.cssText) {
|
|
// authoredText may be an empty string when deleting all properties; it's ok to use.
|
|
const cssText =
|
|
typeof form.authoredText === "string"
|
|
? form.authoredText
|
|
: form.cssText;
|
|
const declarations = parseNamedDeclarations(
|
|
isCssPropertyKnown,
|
|
cssText,
|
|
true
|
|
);
|
|
const el = this.pageStyle.selectedElement;
|
|
const style = this.pageStyle.cssLogic.computedStyle;
|
|
|
|
// Whether the stylesheet is a user-agent stylesheet. This affects the
|
|
// validity of some properties and property values.
|
|
const userAgent =
|
|
this._parentSheet &&
|
|
SharedCssLogic.isAgentStylesheet(this._parentSheet);
|
|
// Whether the stylesheet is a chrome stylesheet. Ditto.
|
|
//
|
|
// Note that chrome rules are also enabled in user sheets, see
|
|
// ParserContext::chrome_rules_enabled().
|
|
//
|
|
// https://searchfox.org/mozilla-central/rev/919607a3610222099fbfb0113c98b77888ebcbfb/servo/components/style/parser.rs#164
|
|
const chrome = (() => {
|
|
if (!this._parentSheet) {
|
|
return false;
|
|
}
|
|
if (SharedCssLogic.isUserStylesheet(this._parentSheet)) {
|
|
return true;
|
|
}
|
|
if (this._parentSheet.href) {
|
|
return this._parentSheet.href.startsWith("chrome:");
|
|
}
|
|
return el && el.ownerDocument.documentURI.startsWith("chrome:");
|
|
})();
|
|
// Whether the document is in quirks mode. This affects whether stuff
|
|
// like `width: 10` is valid.
|
|
const quirks =
|
|
!userAgent && el && el.ownerDocument.compatMode == "BackCompat";
|
|
const supportsOptions = { userAgent, chrome, quirks };
|
|
form.declarations = declarations.map(decl => {
|
|
// InspectorUtils.supports only supports the 1-arg version, but that's
|
|
// what we want to do anyways so that we also accept !important in the
|
|
// value.
|
|
decl.isValid = InspectorUtils.supports(
|
|
`${decl.name}:${decl.value}`,
|
|
supportsOptions
|
|
);
|
|
// TODO: convert from Object to Boolean. See Bug 1574471
|
|
decl.isUsed = isPropertyUsed(el, style, this.rawRule, decl.name);
|
|
// Check property name. All valid CSS properties support "initial" as a value.
|
|
decl.isNameValid = InspectorUtils.supports(
|
|
`${decl.name}:initial`,
|
|
supportsOptions
|
|
);
|
|
return decl;
|
|
});
|
|
|
|
// We have computed the new `declarations` array, before forgetting about
|
|
// the old declarations compute the CSS changes for pending modifications
|
|
// applied by the user. Comparing the old and new declarations arrays
|
|
// ensures we only rely on values understood by the engine and not authored
|
|
// values. See Bug 1590031.
|
|
this._pendingDeclarationChanges.forEach(change =>
|
|
this.logDeclarationChange(change, declarations, this._declarations)
|
|
);
|
|
this._pendingDeclarationChanges = [];
|
|
|
|
// Cache parsed declarations so we don't needlessly re-parse authoredText every time
|
|
// we need to check previous property names and values when tracking changes.
|
|
this._declarations = declarations;
|
|
}
|
|
|
|
return form;
|
|
}
|
|
|
|
/**
|
|
*
|
|
* @returns {Object} Object with the following properties:
|
|
* - {Array<Object>} ancestorData: An array of ancestor item data
|
|
* - {Boolean} computeDesugaredSelector: true if the rule has a non-at-rule
|
|
* parent rule (i.e. rule is likely to be a nested rule)
|
|
*/
|
|
_getAncestorDataForForm() {
|
|
const ancestorData = [];
|
|
// Flag that will be set to true if the rule has a non-at-rule parent rule
|
|
let computeDesugaredSelector = false;
|
|
|
|
// Go through all ancestor so we can build an array of all the media queries and
|
|
// layers this rule is in.
|
|
for (const ancestorRule of this.ancestorRules) {
|
|
const rawRule = ancestorRule.rawRule;
|
|
const ruleClassName = ChromeUtils.getClassName(rawRule);
|
|
const type = SharedCssLogic.CSSAtRuleClassNameType[ruleClassName];
|
|
|
|
if (ruleClassName === "CSSMediaRule" && rawRule.media?.length) {
|
|
ancestorData.push({
|
|
type,
|
|
value: Array.from(rawRule.media).join(", "),
|
|
});
|
|
} else if (ruleClassName === "CSSLayerBlockRule") {
|
|
ancestorData.push({
|
|
// we need the actorID so we can uniquely identify nameless layers on the client
|
|
actorID: ancestorRule.actorID,
|
|
type,
|
|
value: rawRule.name,
|
|
});
|
|
} else if (ruleClassName === "CSSContainerRule") {
|
|
ancestorData.push({
|
|
type,
|
|
// Send containerName and containerQuery separately (instead of conditionText)
|
|
// so the client has more flexibility to display the information.
|
|
containerName: rawRule.containerName,
|
|
containerQuery: rawRule.containerQuery,
|
|
});
|
|
} else if (ruleClassName === "CSSSupportsRule") {
|
|
ancestorData.push({
|
|
type,
|
|
conditionText: rawRule.conditionText,
|
|
});
|
|
} else if (rawRule.selectorText) {
|
|
// All the previous cases where about at-rules; this one is for regular rule
|
|
// that are ancestors because CSS nesting was used.
|
|
// In such case, we want to return the selectorText so it can be displayed in the UI.
|
|
const ancestor = {
|
|
type,
|
|
selectors: CssLogic.getSelectors(rawRule),
|
|
};
|
|
|
|
// Only add the property when there are elements in the array to save up on serialization.
|
|
const selectorWarnings = rawRule.getSelectorWarnings();
|
|
if (selectorWarnings.length) {
|
|
ancestor.selectorWarnings = selectorWarnings;
|
|
}
|
|
|
|
ancestorData.push(ancestor);
|
|
computeDesugaredSelector = true;
|
|
}
|
|
}
|
|
|
|
if (this._parentSheet) {
|
|
// Loop through all parent stylesheets to get the whole list of @import rules.
|
|
let rule = this.rawRule;
|
|
while ((rule = rule.parentStyleSheet?.ownerRule)) {
|
|
// If the rule is in a imported stylesheet with a specified layer
|
|
if (rule.layerName !== null) {
|
|
// Put the item at the top of the ancestor data array, as we're going up
|
|
// in the stylesheet hierarchy, and we want to display ancestor rules in the
|
|
// orders they're applied.
|
|
ancestorData.unshift({
|
|
type: "layer",
|
|
value: rule.layerName,
|
|
});
|
|
}
|
|
|
|
// If the rule is in a imported stylesheet with specified media/supports conditions
|
|
if (rule.media?.mediaText || rule.supportsText) {
|
|
const parts = [];
|
|
if (rule.supportsText) {
|
|
parts.push(`supports(${rule.supportsText})`);
|
|
}
|
|
|
|
if (rule.media?.mediaText) {
|
|
parts.push(rule.media.mediaText);
|
|
}
|
|
|
|
// Put the item at the top of the ancestor data array, as we're going up
|
|
// in the stylesheet hierarchy, and we want to display ancestor rules in the
|
|
// orders they're applied.
|
|
ancestorData.unshift({
|
|
type: "import",
|
|
value: parts.join(" "),
|
|
});
|
|
}
|
|
}
|
|
}
|
|
return { ancestorData, computeDesugaredSelector };
|
|
}
|
|
|
|
/**
|
|
* Send an event notifying that the location of the rule has
|
|
* changed.
|
|
*
|
|
* @param {Number} line the new line number
|
|
* @param {Number} column the new column number
|
|
*/
|
|
_notifyLocationChanged(line, column) {
|
|
this.emit("location-changed", line, column);
|
|
}
|
|
|
|
/**
|
|
* Compute the index of this actor's raw rule in its parent style
|
|
* sheet. The index is a vector where each element is the index of
|
|
* a given CSS rule in its parent. A vector is used to support
|
|
* nested rules.
|
|
*/
|
|
_computeRuleIndex() {
|
|
let rule = this.rawRule;
|
|
const result = [];
|
|
|
|
while (rule) {
|
|
let cssRules = [];
|
|
if (rule.parentRule) {
|
|
cssRules = rule.parentRule.cssRules;
|
|
} else if (rule.parentStyleSheet) {
|
|
cssRules = rule.parentStyleSheet.cssRules;
|
|
}
|
|
|
|
let found = false;
|
|
for (let i = 0; i < cssRules.length; i++) {
|
|
if (rule === cssRules.item(i)) {
|
|
found = true;
|
|
result.unshift(i);
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!found) {
|
|
this._ruleIndex = null;
|
|
return;
|
|
}
|
|
|
|
rule = rule.parentRule;
|
|
}
|
|
|
|
this._ruleIndex = result;
|
|
}
|
|
|
|
/**
|
|
* Get the rule corresponding to |this._ruleIndex| from the given
|
|
* style sheet.
|
|
*
|
|
* @param {DOMStyleSheet} sheet
|
|
* The style sheet.
|
|
* @return {CSSStyleRule} the rule corresponding to
|
|
* |this._ruleIndex|
|
|
*/
|
|
_getRuleFromIndex(parentSheet) {
|
|
let currentRule = null;
|
|
for (const i of this._ruleIndex) {
|
|
if (currentRule === null) {
|
|
currentRule = parentSheet.cssRules[i];
|
|
} else {
|
|
currentRule = currentRule.cssRules.item(i);
|
|
}
|
|
}
|
|
return currentRule;
|
|
}
|
|
|
|
/**
|
|
* Called from PageStyle actor _onStylesheetUpdated.
|
|
*/
|
|
onStyleApplied(kind) {
|
|
if (kind === UPDATE_GENERAL) {
|
|
// A general change means that the rule actors are invalidated, nothing
|
|
// to do here.
|
|
return;
|
|
}
|
|
|
|
if (this._ruleIndex) {
|
|
// The sheet was updated by this actor, in a way that preserves
|
|
// the rules. Now, recompute our new rule from the style sheet,
|
|
// so that we aren't left with a reference to a dangling rule.
|
|
const oldRule = this.rawRule;
|
|
const oldActor = this.pageStyle.refMap.get(oldRule);
|
|
this.rawRule = this._getRuleFromIndex(this._parentSheet);
|
|
if (oldActor) {
|
|
// Also tell the page style so that future calls to _styleRef
|
|
// return the same StyleRuleActor.
|
|
this.pageStyle.updateStyleRef(oldRule, this.rawRule, this);
|
|
}
|
|
const line = InspectorUtils.getRelativeRuleLine(this.rawRule);
|
|
const column = InspectorUtils.getRuleColumn(this.rawRule);
|
|
if (line !== this.line || column !== this.column) {
|
|
this._notifyLocationChanged(line, column);
|
|
}
|
|
this.line = line;
|
|
this.column = column;
|
|
}
|
|
}
|
|
|
|
#SUPPORTED_RULES_CLASSNAMES = new Set([
|
|
"CSSContainerRule",
|
|
"CSSKeyframeRule",
|
|
"CSSKeyframesRule",
|
|
"CSSLayerBlockRule",
|
|
"CSSMediaRule",
|
|
"CSSStyleRule",
|
|
"CSSSupportsRule",
|
|
]);
|
|
|
|
#isRuleSupported() {
|
|
// this.rawRule might not be an actual CSSRule (e.g. when this represent an element style),
|
|
// and in such case, ChromeUtils.getClassName will throw
|
|
try {
|
|
const ruleClassName = ChromeUtils.getClassName(this.rawRule);
|
|
return this.#SUPPORTED_RULES_CLASSNAMES.has(ruleClassName);
|
|
} catch (e) {}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Return a promise that resolves to the authored form of a rule's
|
|
* text, if available. If the authored form is not available, the
|
|
* returned promise simply resolves to the empty string. If the
|
|
* authored form is available, this also sets |this.authoredText|.
|
|
* The authored text will include invalid and otherwise ignored
|
|
* properties.
|
|
*
|
|
* @param {Boolean} skipCache
|
|
* If a value for authoredText was previously found and cached,
|
|
* ignore it and parse the stylehseet again. The authoredText
|
|
* may be outdated if a descendant of this rule has changed.
|
|
*/
|
|
async getAuthoredCssText(skipCache = false) {
|
|
if (!this.canSetRuleText || !this.#isRuleSupported()) {
|
|
return "";
|
|
}
|
|
|
|
if (!skipCache) {
|
|
if (this._failedToGetRuleText) {
|
|
return "";
|
|
}
|
|
if (typeof this.authoredText === "string") {
|
|
return this.authoredText;
|
|
}
|
|
}
|
|
|
|
try {
|
|
const resourceId =
|
|
this.pageStyle.styleSheetsManager.getStyleSheetResourceId(
|
|
this._parentSheet
|
|
);
|
|
const cssText = await this.pageStyle.styleSheetsManager.getText(
|
|
resourceId
|
|
);
|
|
const { text } = getRuleText(cssText, this.line, this.column);
|
|
// Cache the result on the rule actor to avoid parsing again next time
|
|
this._failedToGetRuleText = false;
|
|
this.authoredText = text;
|
|
} catch (e) {
|
|
this._failedToGetRuleText = true;
|
|
this.authoredText = undefined;
|
|
return "";
|
|
}
|
|
return this.authoredText;
|
|
}
|
|
|
|
/**
|
|
* Return a promise that resolves to the complete cssText of the rule as authored.
|
|
*
|
|
* Unlike |getAuthoredCssText()|, which only returns the contents of the rule, this
|
|
* method includes the CSS selectors and at-rules (@media, @supports, @keyframes, etc.)
|
|
*
|
|
* If the rule type is unrecongized, the promise resolves to an empty string.
|
|
* If the rule is an element inline style, the promise resolves with the generated
|
|
* selector that uniquely identifies the element and with the rule body consisting of
|
|
* the element's style attribute.
|
|
*
|
|
* @return {String}
|
|
*/
|
|
async getRuleText() {
|
|
// Bail out if the rule is not supported or not an element inline style.
|
|
if (!this.#isRuleSupported(true) && this.type !== ELEMENT_STYLE) {
|
|
return "";
|
|
}
|
|
|
|
let ruleBodyText;
|
|
let selectorText;
|
|
|
|
// For element inline styles, use the style attribute and generated unique selector.
|
|
if (this.type === ELEMENT_STYLE) {
|
|
ruleBodyText = this.rawNode.getAttribute("style");
|
|
selectorText = this.metadata.selector;
|
|
} else {
|
|
// Get the rule's authored text and skip any cached value.
|
|
ruleBodyText = await this.getAuthoredCssText(true);
|
|
|
|
const resourceId =
|
|
this.pageStyle.styleSheetsManager.getStyleSheetResourceId(
|
|
this._parentSheet
|
|
);
|
|
const stylesheetText = await this.pageStyle.styleSheetsManager.getText(
|
|
resourceId
|
|
);
|
|
|
|
const [start, end] = getSelectorOffsets(
|
|
stylesheetText,
|
|
this.line,
|
|
this.column
|
|
);
|
|
selectorText = stylesheetText.substring(start, end);
|
|
}
|
|
|
|
const text = `${selectorText} {${ruleBodyText}}`;
|
|
const { result } = SharedCssLogic.prettifyCSS(text);
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* Set the contents of the rule. This rewrites the rule in the
|
|
* stylesheet and causes it to be re-evaluated.
|
|
*
|
|
* @param {String} newText
|
|
* The new text of the rule
|
|
* @param {Array} modifications
|
|
* Array with modifications applied to the rule. Contains objects like:
|
|
* {
|
|
* type: "set",
|
|
* index: <number>,
|
|
* name: <string>,
|
|
* value: <string>,
|
|
* priority: <optional string>
|
|
* }
|
|
* or
|
|
* {
|
|
* type: "remove",
|
|
* index: <number>,
|
|
* name: <string>,
|
|
* }
|
|
* @returns the rule with updated properties
|
|
*/
|
|
async setRuleText(newText, modifications = []) {
|
|
if (!this.canSetRuleText) {
|
|
throw new Error("invalid call to setRuleText");
|
|
}
|
|
|
|
if (this.type === ELEMENT_STYLE) {
|
|
// For element style rules, set the node's style attribute.
|
|
this.rawNode.setAttributeDevtools("style", newText);
|
|
} else {
|
|
const resourceId =
|
|
this.pageStyle.styleSheetsManager.getStyleSheetResourceId(
|
|
this._parentSheet
|
|
);
|
|
let cssText = await this.pageStyle.styleSheetsManager.getText(resourceId);
|
|
|
|
const { offset, text } = getRuleText(cssText, this.line, this.column);
|
|
cssText =
|
|
cssText.substring(0, offset) +
|
|
newText +
|
|
cssText.substring(offset + text.length);
|
|
|
|
await this.pageStyle.styleSheetsManager.setStyleSheetText(
|
|
resourceId,
|
|
cssText,
|
|
{ kind: UPDATE_PRESERVING_RULES }
|
|
);
|
|
}
|
|
|
|
this.authoredText = newText;
|
|
await this.updateAncestorRulesAuthoredText();
|
|
this.pageStyle.refreshObservedRules(this.ancestorRules);
|
|
|
|
// Add processed modifications to the _pendingDeclarationChanges array,
|
|
// they will be emitted as CSS_CHANGE resources once `declarations` have
|
|
// been re-computed in `form`.
|
|
this._pendingDeclarationChanges.push(...modifications);
|
|
|
|
// Returning this updated actor over the protocol will update its corresponding front
|
|
// and any references to it.
|
|
return this;
|
|
}
|
|
|
|
/**
|
|
* Update the authored text of the ancestor rules. This should be called when setting
|
|
* the authored text of a (nested) rule, so all the references are properly updated.
|
|
*/
|
|
async updateAncestorRulesAuthoredText() {
|
|
return Promise.all(
|
|
this.ancestorRules.map(rule => rule.getAuthoredCssText(true))
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Modify a rule's properties. Passed an array of modifications:
|
|
* {
|
|
* type: "set",
|
|
* index: <number>,
|
|
* name: <string>,
|
|
* value: <string>,
|
|
* priority: <optional string>
|
|
* }
|
|
* or
|
|
* {
|
|
* type: "remove",
|
|
* index: <number>,
|
|
* name: <string>,
|
|
* }
|
|
*
|
|
* @returns the rule with updated properties
|
|
*/
|
|
modifyProperties(modifications) {
|
|
// Use a fresh element for each call to this function to prevent side
|
|
// effects that pop up based on property values that were already set on the
|
|
// element.
|
|
let document;
|
|
if (this.rawNode) {
|
|
document = this.rawNode.ownerDocument;
|
|
} else {
|
|
let parentStyleSheet = this._parentSheet;
|
|
while (parentStyleSheet.ownerRule) {
|
|
parentStyleSheet = parentStyleSheet.ownerRule.parentStyleSheet;
|
|
}
|
|
|
|
document = this.getDocument(parentStyleSheet);
|
|
}
|
|
|
|
const tempElement = document.createElementNS(XHTML_NS, "div");
|
|
|
|
for (const mod of modifications) {
|
|
if (mod.type === "set") {
|
|
tempElement.style.setProperty(mod.name, mod.value, mod.priority || "");
|
|
this.rawStyle.setProperty(
|
|
mod.name,
|
|
tempElement.style.getPropertyValue(mod.name),
|
|
mod.priority || ""
|
|
);
|
|
} else if (mod.type === "remove" || mod.type === "disable") {
|
|
this.rawStyle.removeProperty(mod.name);
|
|
}
|
|
}
|
|
|
|
this.pageStyle.refreshObservedRules(this.ancestorRules);
|
|
|
|
// Add processed modifications to the _pendingDeclarationChanges array,
|
|
// they will be emitted as CSS_CHANGE resources once `declarations` have
|
|
// been re-computed in `form`.
|
|
this._pendingDeclarationChanges.push(...modifications);
|
|
|
|
return this;
|
|
}
|
|
|
|
/**
|
|
* Helper function for modifySelector, inserts the new
|
|
* rule with the new selector into the parent style sheet and removes the
|
|
* current rule. Returns the newly inserted css rule or null if the rule is
|
|
* unsuccessfully inserted to the parent style sheet.
|
|
*
|
|
* @param {String} value
|
|
* The new selector value
|
|
* @param {Boolean} editAuthored
|
|
* True if the selector should be updated by editing the
|
|
* authored text; false if the selector should be updated via
|
|
* CSSOM.
|
|
*
|
|
* @returns {CSSRule}
|
|
* The new CSS rule added
|
|
*/
|
|
async _addNewSelector(value, editAuthored) {
|
|
const rule = this.rawRule;
|
|
const parentStyleSheet = this._parentSheet;
|
|
|
|
// We know the selector modification is ok, so if the client asked
|
|
// for the authored text to be edited, do it now.
|
|
if (editAuthored) {
|
|
const document = this.getDocument(this._parentSheet);
|
|
try {
|
|
document.querySelector(value);
|
|
} catch (e) {
|
|
return null;
|
|
}
|
|
|
|
const resourceId =
|
|
this.pageStyle.styleSheetsManager.getStyleSheetResourceId(
|
|
this._parentSheet
|
|
);
|
|
let authoredText = await this.pageStyle.styleSheetsManager.getText(
|
|
resourceId
|
|
);
|
|
|
|
const [startOffset, endOffset] = getSelectorOffsets(
|
|
authoredText,
|
|
this.line,
|
|
this.column
|
|
);
|
|
authoredText =
|
|
authoredText.substring(0, startOffset) +
|
|
value +
|
|
authoredText.substring(endOffset);
|
|
|
|
await this.pageStyle.styleSheetsManager.setStyleSheetText(
|
|
resourceId,
|
|
authoredText,
|
|
{ kind: UPDATE_PRESERVING_RULES }
|
|
);
|
|
} else {
|
|
// We retrieve the parent of the rule, which can be a regular stylesheet, but also
|
|
// another rule, in case the underlying rule is nested.
|
|
// If the rule is nested in another rule, we need to use its parent rule to "edit" it.
|
|
// If the rule has no parent rules, we can simply use the stylesheet.
|
|
const parent = this.rawRule.parentRule || parentStyleSheet;
|
|
const cssRules = parent.cssRules;
|
|
const cssText = rule.cssText;
|
|
const selectorText = rule.selectorText;
|
|
|
|
for (let i = 0; i < cssRules.length; i++) {
|
|
if (rule === cssRules.item(i)) {
|
|
try {
|
|
// Inserts the new style rule into the current style sheet and
|
|
// delete the current rule
|
|
const ruleText = cssText.slice(selectorText.length).trim();
|
|
parent.insertRule(value + " " + ruleText, i);
|
|
parent.deleteRule(i + 1);
|
|
break;
|
|
} catch (e) {
|
|
// The selector could be invalid, or the rule could fail to insert.
|
|
return null;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
await this.updateAncestorRulesAuthoredText();
|
|
|
|
return this._getRuleFromIndex(parentStyleSheet);
|
|
}
|
|
|
|
/**
|
|
* Take an object with instructions to modify a CSS declaration and log an object with
|
|
* normalized metadata which describes the change in the context of this rule.
|
|
*
|
|
* @param {Object} change
|
|
* Data about a modification to a declaration. @see |modifyProperties()|
|
|
* @param {Object} newDeclarations
|
|
* The current declarations array to get the latest values, names...
|
|
* @param {Object} oldDeclarations
|
|
* The previous declarations array to use to fetch old values, names...
|
|
*/
|
|
logDeclarationChange(change, newDeclarations, oldDeclarations) {
|
|
// Position of the declaration within its rule.
|
|
const index = change.index;
|
|
// Destructure properties from the previous CSS declaration at this index, if any,
|
|
// to new variable names to indicate the previous state.
|
|
let {
|
|
value: prevValue,
|
|
name: prevName,
|
|
priority: prevPriority,
|
|
commentOffsets,
|
|
} = oldDeclarations[index] || {};
|
|
|
|
const { value: currentValue, name: currentName } =
|
|
newDeclarations[index] || {};
|
|
// A declaration is disabled if it has a `commentOffsets` array.
|
|
// Here we type coerce the value to a boolean with double-bang (!!)
|
|
const prevDisabled = !!commentOffsets;
|
|
// Append the "!important" string if defined in the previous priority flag.
|
|
prevValue =
|
|
prevValue && prevPriority ? `${prevValue} !important` : prevValue;
|
|
|
|
const data = this.metadata;
|
|
|
|
switch (change.type) {
|
|
case "set":
|
|
data.type = prevValue ? "declaration-add" : "declaration-update";
|
|
// If `change.newName` is defined, use it because the property is being renamed.
|
|
// Otherwise, a new declaration is being created or the value of an existing
|
|
// declaration is being updated. In that case, use the currentName computed
|
|
// by the engine.
|
|
const changeName = currentName || change.name;
|
|
const name = change.newName ? change.newName : changeName;
|
|
// Append the "!important" string if defined in the incoming priority flag.
|
|
|
|
const changeValue = currentValue || change.value;
|
|
const newValue = change.priority
|
|
? `${changeValue} !important`
|
|
: changeValue;
|
|
|
|
// Reuse the previous value string, when the property is renamed.
|
|
// Otherwise, use the incoming value string.
|
|
const value = change.newName ? prevValue : newValue;
|
|
|
|
data.add = [{ property: name, value, index }];
|
|
// If there is a previous value, log its removal together with the previous
|
|
// property name. Using the previous name handles the case for renaming a property
|
|
// and is harmless when updating an existing value (the name stays the same).
|
|
if (prevValue) {
|
|
data.remove = [{ property: prevName, value: prevValue, index }];
|
|
} else {
|
|
data.remove = null;
|
|
}
|
|
|
|
// When toggling a declaration from OFF to ON, if not renaming the property,
|
|
// do not mark the previous declaration for removal, otherwise the add and
|
|
// remove operations will cancel each other out when tracked. Tracked changes
|
|
// have no context of "disabled", only "add" or remove, like diffs.
|
|
if (prevDisabled && !change.newName && prevValue === newValue) {
|
|
data.remove = null;
|
|
}
|
|
|
|
break;
|
|
|
|
case "remove":
|
|
data.type = "declaration-remove";
|
|
data.add = null;
|
|
data.remove = [{ property: change.name, value: prevValue, index }];
|
|
break;
|
|
|
|
case "disable":
|
|
data.type = "declaration-disable";
|
|
data.add = null;
|
|
data.remove = [{ property: change.name, value: prevValue, index }];
|
|
break;
|
|
}
|
|
|
|
TrackChangeEmitter.trackChange(data);
|
|
}
|
|
|
|
/**
|
|
* Helper method for tracking CSS changes. Logs the change of this rule's selector as
|
|
* two operations: a removal using the old selector and an addition using the new one.
|
|
*
|
|
* @param {String} oldSelector
|
|
* This rule's previous selector.
|
|
* @param {String} newSelector
|
|
* This rule's new selector.
|
|
*/
|
|
logSelectorChange(oldSelector, newSelector) {
|
|
TrackChangeEmitter.trackChange({
|
|
...this.metadata,
|
|
type: "selector-remove",
|
|
add: null,
|
|
remove: null,
|
|
selector: oldSelector,
|
|
});
|
|
|
|
TrackChangeEmitter.trackChange({
|
|
...this.metadata,
|
|
type: "selector-add",
|
|
add: null,
|
|
remove: null,
|
|
selector: newSelector,
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Modify the current rule's selector by inserting a new rule with the new
|
|
* selector value and removing the current rule.
|
|
*
|
|
* Returns information about the new rule and applied style
|
|
* so that consumers can immediately display the new rule, whether or not the
|
|
* selector matches the current element without having to refresh the whole
|
|
* list.
|
|
*
|
|
* @param {DOMNode} node
|
|
* The current selected element
|
|
* @param {String} value
|
|
* The new selector value
|
|
* @param {Boolean} editAuthored
|
|
* True if the selector should be updated by editing the
|
|
* authored text; false if the selector should be updated via
|
|
* CSSOM.
|
|
* @returns {Object}
|
|
* Returns an object that contains the applied style properties of the
|
|
* new rule and a boolean indicating whether or not the new selector
|
|
* matches the current selected element
|
|
*/
|
|
modifySelector(node, value, editAuthored = false) {
|
|
if (this.type === ELEMENT_STYLE || this.rawRule.selectorText === value) {
|
|
return { ruleProps: null, isMatching: true };
|
|
}
|
|
|
|
// Nullify cached desugared selectors as it might be outdated
|
|
this._desugaredSelectors = null;
|
|
|
|
// The rule's previous selector is lost after calling _addNewSelector(). Save it now.
|
|
const oldValue = this.rawRule.selectorText;
|
|
let selectorPromise = this._addNewSelector(value, editAuthored);
|
|
|
|
if (editAuthored) {
|
|
selectorPromise = selectorPromise.then(newCssRule => {
|
|
if (newCssRule) {
|
|
this.logSelectorChange(oldValue, value);
|
|
const style = this.pageStyle._styleRef(newCssRule);
|
|
// See the comment in |form| to understand this.
|
|
return style.getAuthoredCssText().then(() => newCssRule);
|
|
}
|
|
return newCssRule;
|
|
});
|
|
}
|
|
|
|
return selectorPromise.then(newCssRule => {
|
|
let entries = null;
|
|
let isMatching = false;
|
|
|
|
if (newCssRule) {
|
|
const ruleEntry = this.pageStyle.findEntryMatchingRule(
|
|
node,
|
|
newCssRule
|
|
);
|
|
if (ruleEntry.length === 1) {
|
|
entries = this.pageStyle.getAppliedProps(node, ruleEntry, {
|
|
matchedSelectors: true,
|
|
});
|
|
} else {
|
|
entries = this.pageStyle.getNewAppliedProps(node, newCssRule);
|
|
}
|
|
|
|
isMatching = entries.some(
|
|
ruleProp => !!ruleProp.matchedDesugaredSelectors.length
|
|
);
|
|
}
|
|
|
|
const result = { isMatching };
|
|
if (entries) {
|
|
result.ruleProps = { entries };
|
|
}
|
|
|
|
return result;
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Get the eligible query container for a given @container rule and a given node
|
|
*
|
|
* @param {Number} ancestorRuleIndex: The index of the @container rule in this.ancestorRules
|
|
* @param {NodeActor} nodeActor: The nodeActor for which we want to retrieve the query container
|
|
* @returns {Object} An object with the following properties:
|
|
* - node: {NodeActor|null} The nodeActor representing the query container,
|
|
* null if none were found
|
|
* - containerType: {string} The computed `containerType` value of the query container
|
|
* - inlineSize: {string} The computed `inlineSize` value of the query container (e.g. `120px`)
|
|
* - blockSize: {string} The computed `blockSize` value of the query container (e.g. `812px`)
|
|
*/
|
|
getQueryContainerForNode(ancestorRuleIndex, nodeActor) {
|
|
const ancestorRule = this.ancestorRules[ancestorRuleIndex];
|
|
if (!ancestorRule) {
|
|
console.error(
|
|
`Couldn't not find an ancestor rule at index ${ancestorRuleIndex}`
|
|
);
|
|
return { node: null };
|
|
}
|
|
|
|
const containerEl = ancestorRule.rawRule.queryContainerFor(
|
|
nodeActor.rawNode
|
|
);
|
|
|
|
// queryContainerFor returns null when the container name wasn't find in any ancestor.
|
|
// In practice this shouldn't happen, as if the rule is applied, it means that an
|
|
// elligible container was found.
|
|
if (!containerEl) {
|
|
return { node: null };
|
|
}
|
|
|
|
const computedStyle = CssLogic.getComputedStyle(containerEl);
|
|
return {
|
|
node: this.pageStyle.walker.getNode(containerEl),
|
|
containerType: computedStyle.containerType,
|
|
inlineSize: computedStyle.inlineSize,
|
|
blockSize: computedStyle.blockSize,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Using the latest computed style applicable to the selected element,
|
|
* check the states of declarations in this CSS rule.
|
|
*
|
|
* If any have changed their used/unused state, potentially as a result of changes in
|
|
* another rule, fire a "rule-updated" event with this rule actor in its latest state.
|
|
*
|
|
* @param {Boolean} forceRefresh: Set to true to emit "rule-updated", even if the state
|
|
* of the declarations didn't change.
|
|
*/
|
|
maybeRefresh(forceRefresh) {
|
|
let hasChanged = false;
|
|
|
|
const el = this.pageStyle.selectedElement;
|
|
const style = CssLogic.getComputedStyle(el);
|
|
|
|
for (const decl of this._declarations) {
|
|
// TODO: convert from Object to Boolean. See Bug 1574471
|
|
const isUsed = isPropertyUsed(el, style, this.rawRule, decl.name);
|
|
|
|
if (decl.isUsed.used !== isUsed.used) {
|
|
decl.isUsed = isUsed;
|
|
hasChanged = true;
|
|
}
|
|
}
|
|
|
|
if (hasChanged || forceRefresh) {
|
|
// ⚠️ IMPORTANT ⚠️
|
|
// When an event is emitted via the protocol with the StyleRuleActor as payload, the
|
|
// corresponding StyleRuleFront will be automatically updated under the hood.
|
|
// Therefore, when the client looks up properties on the front reference it already
|
|
// has, it will get the latest values set on the actor, not the ones it originally
|
|
// had when the front was created. The client is not required to explicitly replace
|
|
// its previous front reference to the one it receives as this event's payload.
|
|
// The client doesn't even need to explicitly listen for this event.
|
|
// The update of the front happens automatically.
|
|
this.emit("rule-updated", this);
|
|
}
|
|
}
|
|
}
|
|
exports.StyleRuleActor = StyleRuleActor;
|
|
|
|
/**
|
|
* Compute the start and end offsets of a rule's selector text, given
|
|
* the CSS text and the line and column at which the rule begins.
|
|
* @param {String} initialText
|
|
* @param {Number} line (1-indexed)
|
|
* @param {Number} column (1-indexed)
|
|
* @return {array} An array with two elements: [startOffset, endOffset].
|
|
* The elements mark the bounds in |initialText| of
|
|
* the CSS rule's selector.
|
|
*/
|
|
function getSelectorOffsets(initialText, line, column) {
|
|
if (typeof line === "undefined" || typeof column === "undefined") {
|
|
throw new Error("Location information is missing");
|
|
}
|
|
|
|
const { offset: textOffset, text } = getTextAtLineColumn(
|
|
initialText,
|
|
line,
|
|
column
|
|
);
|
|
const lexer = getCSSLexer(text);
|
|
|
|
// Search forward for the opening brace.
|
|
let endOffset;
|
|
while (true) {
|
|
const token = lexer.nextToken();
|
|
if (!token) {
|
|
break;
|
|
}
|
|
if (token.tokenType === "symbol" && token.text === "{") {
|
|
if (endOffset === undefined) {
|
|
break;
|
|
}
|
|
return [textOffset, textOffset + endOffset];
|
|
}
|
|
// Preserve comments and whitespace just before the "{".
|
|
if (token.tokenType !== "comment" && token.tokenType !== "whitespace") {
|
|
endOffset = token.endOffset;
|
|
}
|
|
}
|
|
|
|
throw new Error("could not find bounds of rule");
|
|
}
|