| 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);
|
| + }
|
| + }
|
| + }
|
| +}
|
|
|