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