Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(1489)

Unified Diff: Source/devtools/front_end/bindings/SASSSupport.js

Issue 1331083002: DevTools: [STRUCT] edit SASS through SourceMaps (Closed) Base URL: https://chromium.googlesource.com/chromium/blink.git@master
Patch Set: rebaseline atop master Created 5 years, 3 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View side-by-side diff with in-line comments
Download patch
Index: Source/devtools/front_end/bindings/SASSSupport.js
diff --git a/Source/devtools/front_end/bindings/SASSSupport.js b/Source/devtools/front_end/bindings/SASSSupport.js
new file mode 100644
index 0000000000000000000000000000000000000000..c1aa838fe462a5a5b87029b895ae27619cc53b86
--- /dev/null
+++ b/Source/devtools/front_end/bindings/SASSSupport.js
@@ -0,0 +1,494 @@
+// Copyright 2015 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+WebInspector.SASSSupport = {
+ insertPropertyAfter: function(property, after)
+ {
+ var rule = after.parent;
+ rule.insertPropertyAfter(property, after);
+
+ var ast = after.root();
+ var oldRange = WebInspector.TextRange.createFromLocation(after.range.endLine, after.range.endColumn);
+ var indent = (new WebInspector.TextRange(after.range.startLine, 0, after.range.startLine, after.range.startColumn)).extract(ast.text);
+ if (!/^\s+$/.test(indent)) indent = "";
+
+ // Split property addition into chunks to preserve mappings.
+ var newText = String.sprintf("\n%s%s: %s;", indent, property.name.text, property.value.text);
+ return new WebInspector.SourceEdit(ast.url, oldRange, "", newText);
+ },
+
+ insertPropertyBefore: function(property, before)
+ {
+ var rule = before.parent;
+ rule.insertPropertyAfter(property, null);
+
+ var ast = before.root();
+ var oldRange = WebInspector.TextRange.createFromLocation(before.range.startLine, before.range.startColumn);
+ var indent = (new WebInspector.TextRange(before.range.startLine, 0, before.range.startLine, before.range.startColumn)).extract(ast.text);
+ if (!/^\s+$/.test(indent)) indent = "";
+
+ // Split property addition into chunks to preserve mappings.
+ var newText = String.sprintf("%s: %s;\n%s", property.name.text, property.value.text, indent);
+ return new WebInspector.SourceEdit(ast.url, oldRange, "", newText);
+ },
+
+ removeProperty: function(property)
+ {
+ var rule = property.parent;
+ var ast = property.root();
+ rule.removeProperty(property);
+
+ var lineRemoveRange = new WebInspector.TextRange(property.range.startLine, 0, property.range.endLine + 1, 0);
+ var oldRange = (lineRemoveRange.extract(ast.text).trim() === property.range.extract(ast.text).trim()) ? lineRemoveRange : property.range;
+ return new WebInspector.SourceEdit(ast.url, oldRange, oldRange.extract(ast.text), "");
+ },
+
+ setText: function(node, newText)
+ {
+ console.assert(node.type === "TextNode", "Cannot set text to node of type " + node.type);
+ node.text = newText;
+ var ast = node.root();
+ return new WebInspector.SourceEdit(ast.url, node.range, node.range.extract(ast.text), newText);
+ },
+
+ toggleDisabled: function(property, disabled)
+ {
+ property.disabled = disabled;
+ var ast = property.root();
+ if (disabled) {
+ var oldRange1 = WebInspector.TextRange.createFromLocation(property.range.startLine, property.range.startColumn);
+ var edit1 = new WebInspector.SourceEdit(ast.url, oldRange1, "", "/* ");
+ var oldRange2 = WebInspector.TextRange.createFromLocation(property.range.endLine, property.range.endColumn);
+ var edit2 = new WebInspector.SourceEdit(ast.url, oldRange2, "", " */");
+ return [edit1, edit2];
+ }
+ var oldRange1 = new WebInspector.TextRange(property.range.startLine, property.range.startColumn, property.range.startLine, property.name.range.startColumn);
+ var text = ast.text;
+ var edit1 = new WebInspector.SourceEdit(ast.url, oldRange1, oldRange1.extract(text), "");
+ var oldRange2 = new WebInspector.TextRange(property.range.endLine, property.range.endColumn - 2, property.range.endLine, property.range.endColumn);
+ var edit2 = new WebInspector.SourceEdit(ast.url, oldRange2, "*/", "");
+ return [edit1, edit2];
+ },
+}
+
+WebInspector.SASSSupport.parseCSS = function(url, text)
+{
+ var parser = new WebInspector.CSSParser();
+ return parsePromise = parser.parsePromise(text)
+ .then(onParsed.bind(this));
+
+ function onParsed(parsedCSS)
+ {
+ parser.dispose();
+ var rules = [];
+ for (var i = 0; i < parsedCSS.length; ++i) {
+ var rule = parsedCSS[i];
+ if (!rule.properties)
+ continue;
+ var properties = [];
+ for (var j = 0; j < rule.properties.length; ++j) {
+ var cssProperty = rule.properties[j];
+ var name = new WebInspector.SASSSupport.TextNode(cssProperty.name, WebInspector.TextRange.fromObject(cssProperty.nameRange));
+ var value = new WebInspector.SASSSupport.TextNode(cssProperty.value, WebInspector.TextRange.fromObject(cssProperty.valueRange));
+ var property = new WebInspector.SASSSupport.Property(name, value, WebInspector.TextRange.fromObject(cssProperty.range));
+ property.disabled = cssProperty.disabled;
+ properties.push(property);
+ }
+ rules.push(new WebInspector.SASSSupport.Rule(rule.selectorText, properties));
+ }
+ return new WebInspector.SASSSupport.AST(url, text, rules);
+ }
+},
+
+WebInspector.SASSSupport.parseSASS = function(url, text, tokenizerFactory)
+{
+ var result = WebInspector.SASSSupport._innerParseSASS(text, tokenizerFactory);
+
+ var rules = [
+ new WebInspector.SASSSupport.Rule("variables", result.variables),
+ new WebInspector.SASSSupport.Rule("properties", result.properties),
+ new WebInspector.SASSSupport.Rule("mixins", result.mixins)
+ ];
+
+ return new WebInspector.SASSSupport.AST(url, text, rules);
+}
+
+WebInspector.SASSSupport.SCSSParserStates = {
+ Initial: "Initial",
+ PropertyName: "PropertyName",
+ PropertyValue: "PropertyValue",
+ VariableName: "VariableName",
+ VariableValue: "VariableValue",
+ MixinInclude: "MixinInclude",
+ MixinValue: "MixinValue"
+}
+
+WebInspector.SASSSupport._innerParseSASS = function(text, tokenizerFactory)
+{
+ var lines = text.split("\n");
+ var properties = [];
+ var variables = [];
+ var mixins = [];
+
+ var States = WebInspector.SASSSupport.SCSSParserStates;
+ var state = States.Initial;
+ var propertyName, propertyValue;
+ var variableName, variableValue;
+ var mixinName, mixinValue;
+ var UndefTokenType = {};
+
+ /**
+ * @param {string} tokenValue
+ * @param {?string} tokenTypes
+ * @param {number} column
+ * @param {number} newColumn
+ */
+ function processToken(tokenValue, tokenTypes, column, newColumn)
+ {
+ var tokenType = tokenTypes ? tokenTypes.split(" ").keySet() : UndefTokenType;
+ switch (state) {
+ case States.Initial:
+ if (tokenType["css-variable-2"]) {
+ variableName = new WebInspector.SASSSupport.TextNode(tokenValue, new WebInspector.TextRange(lineNumber, column, lineNumber, newColumn));
+ state = States.VariableName;
+ } else if (tokenType["css-property"] || tokenType["css-meta"]) {
+ propertyName = new WebInspector.SASSSupport.TextNode(tokenValue, new WebInspector.TextRange(lineNumber, column, lineNumber, newColumn));
+ state = States.PropertyName;
+ } else if (tokenType["css-def"] && tokenValue === "@include") {
+ mixinName = new WebInspector.SASSSupport.TextNode(tokenValue, new WebInspector.TextRange(lineNumber, column, lineNumber, newColumn));
+ state = States.MixinInclude;
+ } else if (tokenType["css-comment"]) {
+ // The |processToken| is called per-line, so no token spans more then one line.
+ // Support only a one-line comments.
+ if (tokenValue.substring(0, 2) !== "/*" || tokenValue.substring(tokenValue.length - 2) !== "*/")
+ break;
+ var uncommentedText = tokenValue.substring(2, tokenValue.length - 2);
+ var fakeRule = "a{\n" + uncommentedText + "}";
+ disabledRules = [];
+ var result = WebInspector.SASSSupport._innerParseSASS(fakeRule, tokenizerFactory);
+ if (result.properties.length === 1 && result.variables.length === 0 && result.mixins.length === 0) {
+ var disabledProperty = result.properties[0];
+ // We should offset property to current coordinates.
+ var offset = column + 2;
+ disabledProperty.disabled = true;
+ disabledProperty.range = WebInspector.TextRange.createFromLocation(lineNumber, column);
+ disabledProperty.range.endColumn = newColumn;
+ disabledProperty.name.range.startColumn += offset;
+ disabledProperty.name.range.endColumn += offset;
+ disabledProperty.value.range.startColumn += offset;
+ disabledProperty.value.range.endColumn += offset;
+ properties.push(disabledProperty);
+ }
+ }
+ break;
+ case States.VariableName:
+ if (tokenValue === ")" && tokenType === UndefTokenType) {
+ state = States.Initial;
+ } else if (tokenValue === ":" && tokenType === UndefTokenType) {
+ state = States.VariableValue;
+ variableValue = new WebInspector.SASSSupport.TextNode("", WebInspector.TextRange.createFromLocation(lineNumber, newColumn));
+ } else if (tokenType !== UndefTokenType) {
+ state = States.Initial;
+ }
+ break;
+ case States.VariableValue:
+ if (tokenValue === ";" && tokenType === UndefTokenType) {
+ variableValue.range.endLine = lineNumber;
+ variableValue.range.endColumn = column;
+ var variable = new WebInspector.SASSSupport.Property(variableName, variableValue, variableName.range.clone());
+ variable.range.endLine = lineNumber;
+ variable.range.endColumn = newColumn;
+ variables.push(variable);
+ state = States.Initial;
+ } else {
+ variableValue.text += tokenValue;
+ }
+ break;
+ case States.PropertyName:
+ if (tokenValue === ":" && tokenType === UndefTokenType) {
+ state = States.PropertyValue;
+ propertyName.range.endLine = lineNumber;
+ propertyName.range.endColumn = column;
+ propertyValue = new WebInspector.SASSSupport.TextNode("", WebInspector.TextRange.createFromLocation(lineNumber, newColumn));
+ } else if (tokenType["css-property"]) {
+ propertyName.text += tokenValue;
+ }
+ break;
+ case States.PropertyValue:
+ if (tokenValue === ";" && tokenType === UndefTokenType) {
+ propertyValue.range.endLine = lineNumber;
+ propertyValue.range.endColumn = column;
+ var property = new WebInspector.SASSSupport.Property(propertyName, propertyValue, propertyName.range.clone());
+ property.range.endLine = lineNumber;
+ property.range.endColumn = newColumn;
+ properties.push(property);
+ state = States.Initial;
+ } else {
+ propertyValue.text += tokenValue;
+ }
+ break;
+ case States.MixinInclude:
+ if (tokenValue === "(" && tokenType === UndefTokenType) {
+ state = States.MixinValue;
+ mixinValue = new WebInspector.SASSSupport.TextNode("", WebInspector.TextRange.createFromLocation(lineNumber, newColumn));
+ } else if (tokenValue === ";" && tokenType === UndefTokenType) {
+ state = States.Initial;
+ if (mixinValue) {
+ var mixin = new WebInspector.SASSSupport.Property(mixinName, mixinValue);
+ mixins.push(mixin);
+ }
+ mixinValue = null;
+ } else {
+ mixinName.text += tokenValue;
+ }
+ break;
+ case States.MixinValue:
+ if (tokenValue === ")" && tokenType === UndefTokenType) {
+ state = States.MixinInclude;
+ mixinValue.range.endLine = lineNumber;
+ mixinValue.range.endColumn = column;
+ } else {
+ mixinValue.text += tokenValue;
+ }
+ break;
+ default:
+ console.assert(false, "Unknown SASS parser state.");
+ }
+ }
+ var tokenizer = tokenizerFactory.createTokenizer("text/x-scss");
+ var lineNumber;
+ for (lineNumber = 0; lineNumber < lines.length; ++lineNumber) {
+ var line = lines[lineNumber];
+ tokenizer(line, processToken);
+ }
+ return {
+ variables: variables,
+ properties: properties,
+ mixins: mixins
+ };
+}
+
+WebInspector.SASSSupport.Node = function(type)
+{
+ this.type = type;
+}
+
+WebInspector.SASSSupport.Node.prototype = {
+ root: function()
+ {
+ if (this._root)
+ return this._root;
+ this._root = this;
+ while (this._root.parent)
+ this._root = this._root.parent;
+ return this._root;
+ }
+}
+
+WebInspector.SASSSupport.TextNode = function(text, range)
+{
+ WebInspector.SASSSupport.Node.call(this, "TextNode");
+ this.text = text;
+ this.range = range;
+ this.parent = null;
+}
+
+WebInspector.SASSSupport.TextNode.prototype = {
+ clone: function()
+ {
+ return new WebInspector.SASSSupport.TextNode(this.text, this.range.clone());
+ },
+
+ __proto__: WebInspector.SASSSupport.Node.prototype
+}
+
+WebInspector.SASSSupport.Property = function(name, value, range)
+{
+ WebInspector.SASSSupport.Node.call(this, "Property");
+ this.name = name;
+ this.value = value;
+ this.range = range;
+ this.name.parent = this;
+ this.value.parent = this;
+ this.parent = null;
+ this.disabled = false;
+}
+
+WebInspector.SASSSupport.Property.prototype = {
+ clone: function()
+ {
+ return new WebInspector.SASSSupport.Property(this.name.clone(), this.value.clone(), this.range.clone());
+ },
+
+ __proto__: WebInspector.SASSSupport.Node.prototype
+}
+
+WebInspector.SASSSupport.Rule = function(selector, properties)
+{
+ WebInspector.SASSSupport.Node.call(this, "Rule");
+ this.selector = selector;
+ this.properties = properties;
+ this.parent = null;
+ for (var i = 0; i < this.properties.length; ++i)
+ this.properties[i].parent = this;
+}
+
+WebInspector.SASSSupport.Rule.prototype = {
+ insertPropertyAfter: function(property, after)
+ {
+ var index = this.properties.indexOf(after);
+ this.properties.splice(index + 1, 0, property);
+ property.parent = this;
+ },
+
+ removeProperty: function(property)
+ {
+ var index = this.properties.indexOf(property);
+ this.properties.splice(index, 1);
+ property.parent = null;
+ },
+
+ __proto__: WebInspector.SASSSupport.Node.prototype
+}
+
+WebInspector.SASSSupport.AST = function(url, text, rules)
+{
+ WebInspector.SASSSupport.Node.call(this, "AST");
+ this.url = url;
+ this.rules = rules;
+ for (var i = 0; i < rules.length; ++i) {
+ rules[i].before = i > 0 ? rules[i - 1] : null;
+ rules[i].after = i < rules.length - 1 ? rules[i + 1] : null;
+ rules[i].parent = this;
+ }
+ this.text = text;
+}
+
+WebInspector.SASSSupport.AST.prototype.__proto__ = WebInspector.SASSSupport.Node.prototype;
+
+WebInspector.SASSSourceMapping.diffModels = function(oldAST, newAST)
+{
+ if (oldAST.rules.length !== newAST.rules.length)
+ throw new Error("not implemented for rule diff.");
+ var structuralDiff = [];
+ var aToB = new Map();
+ var bToA = new Map();
+ for (var i = 0; i < oldAST.rules.length; ++i) {
+ var oldRule = oldAST.rules[i];
+ var newRule = newAST.rules[i];
+ var removedSet = new Set();
+ var addedSet = new Set();
+ if (oldRule.properties.length !== newRule.properties.length)
+ WebInspector.SASSSourceMapping.cssPropertiesDiff(oldRule.properties, newRule.properties, removedSet, addedSet);
+
+ // Compute PropertyRemoved diff entries.
+ for (var property of removedSet.values()) {
+ structuralDiff.push({
+ type: "PropertyRemoved",
+ property: property
+ });
+ }
+
+ // Map similar properties.
+ var p1 = 0;
+ var p2 = 0;
+ while (p1 < oldRule.properties.length && p2 < newRule.properties.length) {
+ if (removedSet.has(oldRule.properties[p1])) {
+ ++p1;
+ continue;
+ }
+ if (addedSet.has(newRule.properties[p2])) {
+ ++p2;
+ continue;
+ }
+ var oldProperty = oldRule.properties[p1++];
+ var newProperty = newRule.properties[p2++];
+ aToB.set(oldProperty, newProperty);
+ bToA.set(newProperty, oldProperty);
+ if (oldProperty.name.text !== newProperty.name.text) {
+ structuralDiff.push({
+ type: "NameChanged",
+ property: newProperty,
+ });
+ }
+ if (oldProperty.value.text !== newProperty.value.text) {
+ structuralDiff.push({
+ type: "ValueChanged",
+ property: newProperty,
+ });
+ }
+ if (oldProperty.disabled !== newProperty.disabled) {
+ structuralDiff.push({
+ type: "toggleDisabled",
+ property: newProperty,
+ });
+ }
+ }
+
+ // Compute PropertyAdded diff entries.
+ var firstValidProperty = null;
+ for (var j = 0; j < newRule.properties.length; ++j) {
+ var property = newRule.properties[j];
+ if (!addedSet.has(property)) {
+ firstValidProperty = property;
+ break;
+ }
+ }
+ var lastValidProperty = null;
+ for (var j = 0; j < newRule.properties.length; ++j) {
+ var property = newRule.properties[j];
+ if (!addedSet.has(property)) {
+ lastValidProperty = property;
+ continue;
+ }
+ var diff = {
+ type: "PropertyAdded",
+ property: property,
+ };
+ if (lastValidProperty)
+ diff.after = lastValidProperty;
+ else if (firstValidProperty)
+ diff.before = firstValidProperty;
+ structuralDiff.push(diff);
+ }
+ }
+ return {
+ aToB: aToB,
+ bToA: bToA,
+ structuralDiff
+ };
+}
+
+WebInspector.SASSSourceMapping.cssPropertiesDiff = function(properties1, properties2, removedSet, addedSet)
+{
+ var charCode = 33;
+ var encodedProperties = new Map();
+ var lines1 = [];
+ for (var i = 0; i < properties1.length; ++i)
+ lines1.push(properties1[i].name.text + ":" + properties1[i].value.text);
+
+ var lines2 = [];
+ for (var i = 0; i < properties2.length; ++i)
+ lines2.push(properties2[i].name.text + ":" + properties2[i].value.text);
+
+ var diff = WebInspector.Diff.lineDiff(lines1, lines2);
+ var p1 = 0, p2 = 0;
+ for (var i = 0; i < diff.length; ++i) {
+ var token = diff[i];
+ if (token[0] === 0) {
+ p1 += token[1].length;
+ p2 += token[1].length;
+ } else if (token[0] === -1) {
+ for (var j = 0; j < token[1].length; ++j) {
+ var property = properties1[p1++];
+ removedSet.add(property);
+ }
+ } else {
+ for (var j = 0; j < token[1].length; ++j) {
+ var property = properties2[p2++];
+ addedSet.add(property);
+ }
+ }
+ }
+}
« no previous file with comments | « Source/devtools/front_end/bindings/SASSSourceMapping.js ('k') | Source/devtools/front_end/bindings/module.json » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698