Bug 1195361 - Add getRuleText. r=pbrosset
authorTom Tromey <tromey@mozilla.com>
Mon, 24 Aug 2015 10:36:00 -0400
changeset 260036 b2b0c1f6e8f8e51e462d21c18ce1e3d606017334
parent 260035 5eda6b9070bc4ef245009926fb0e8b4dfdfb02dc
child 260037 1d2f59732f03e6538e56e8e65242c4a5b83ac4db
push id29298
push userryanvm@gmail.com
push dateMon, 31 Aug 2015 02:09:10 +0000
treeherdermozilla-central@f2518b8a7b97 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerspbrosset
bugs1195361
milestone43.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1195361 - Add getRuleText. r=pbrosset
toolkit/devtools/server/actors/styles.js
toolkit/devtools/server/tests/unit/test_getRuleText.js
toolkit/devtools/server/tests/unit/test_getTextAtLineColumn.js
toolkit/devtools/server/tests/unit/xpcshell.ini
--- a/toolkit/devtools/server/actors/styles.js
+++ b/toolkit/devtools/server/actors/styles.js
@@ -1550,8 +1550,109 @@ function getFontPreviewData(font, doc, o
 
   return {
     dataURL: dataURL,
     size: textWidth + FONT_PREVIEW_OFFSET * 2
   };
 }
 
 exports.getFontPreviewData = getFontPreviewData;
+
+/**
+ * Get the text content of a rule given some CSS text, a line and a column
+ * Consider the following example:
+ * body {
+ *  color: red;
+ * }
+ * p {
+ *  line-height: 2em;
+ *  color: blue;
+ * }
+ * Calling the function with the whole text above and line=4 and column=1 would
+ * return "line-height: 2em; color: blue;"
+ * @param {String} initialText
+ * @param {Number} line (1-indexed)
+ * @param {Number} column (1-indexed)
+ * @return {object} An object of the form {offset: number, text: string}
+ *                  The offset is the index into the input string where
+ *                  the rule text started.  The text is the content of
+ *                  the rule.
+ */
+function getRuleText(initialText, line, column) {
+  if (typeof line === "undefined" || typeof column === "undefined") {
+    throw new Error("Location information is missing");
+  }
+
+  let {offset: textOffset, text} =
+      getTextAtLineColumn(initialText, line, column);
+  let lexer = DOMUtils.getCSSLexer(text);
+
+  // Search forward for the opening brace.
+  while (true) {
+    let token = lexer.nextToken();
+    if (!token) {
+      throw new Error("couldn't find start of the rule");
+    }
+    if (token.tokenType === "symbol" && token.text === "{") {
+      break;
+    }
+  }
+
+  // Now collect text until we see the matching close brace.
+  let braceDepth = 1;
+  let startOffset, endOffset;
+  while (true) {
+    let token = lexer.nextToken();
+    if (!token) {
+      break;
+    }
+    if (startOffset === undefined) {
+      startOffset = token.startOffset;
+    }
+    if (token.tokenType === "symbol") {
+      if (token.text === "{") {
+        ++braceDepth;
+      } else if (token.text === "}") {
+        --braceDepth;
+        if (braceDepth == 0) {
+          break;
+        }
+      }
+    }
+    endOffset = token.endOffset;
+  }
+
+  // If the rule was of the form "selector {" with no closing brace
+  // and no properties, just return an empty string.
+  if (startOffset === undefined) {
+    return {offset: 0, text: ""};
+  }
+  // Note that this approach will preserve comments, despite the fact
+  // that cssTokenizer skips them.
+  return {offset: textOffset + startOffset,
+          text: text.substring(startOffset, endOffset)};
+}
+
+exports.getRuleText = getRuleText;
+
+/**
+ * Return the offset and substring of |text| that starts at the given
+ * line and column.
+ * @param {String} text
+ * @param {Number} line (1-indexed)
+ * @param {Number} column (1-indexed)
+ * @return {object} An object of the form {offset: number, text: string},
+ *                  where the offset is the offset into the input string
+ *                  where the text starts, and where text is the text.
+ */
+function getTextAtLineColumn(text, line, column) {
+  let offset;
+  if (line > 1) {
+    let rx = new RegExp("(?:.*(?:\\r\\n|\\n|\\r|\\f)){" + (line - 1) + "}");
+    offset = rx.exec(text)[0].length;
+  } else {
+    offset = 0;
+  }
+  offset += column - 1;
+  return {offset: offset, text: text.substr(offset) };
+}
+
+exports.getTextAtLineColumn = getTextAtLineColumn;
new file mode 100644
--- /dev/null
+++ b/toolkit/devtools/server/tests/unit/test_getRuleText.js
@@ -0,0 +1,130 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const {getRuleText} = devtools.require("devtools/server/actors/styles");
+
+const TEST_DATA = [
+  {
+    desc: "Empty input",
+    input: "",
+    line: 1,
+    column: 1,
+    throws: true
+  },
+  {
+    desc: "Simplest test case",
+    input: "#id{color:red;background:yellow;}",
+    line: 1,
+    column: 1,
+    expected: {offset: 4, text: "color:red;background:yellow;"}
+  },
+  {
+    desc: "Multiple rules test case",
+    input: "#id{color:red;background:yellow;}.class-one .class-two { position:absolute; line-height: 45px}",
+    line: 1,
+    column: 34,
+    expected: {offset: 56, text: " position:absolute; line-height: 45px"}
+  },
+  {
+    desc: "Unclosed rule",
+    input: "#id{color:red;background:yellow;",
+    line: 1,
+    column: 1,
+    expected: {offset: 4, text: "color:red;background:yellow;"}
+  },
+  {
+    desc: "Null input",
+    input: null,
+    line: 1,
+    column: 1,
+    throws: true
+  },
+  {
+    desc: "Missing loc",
+    input: "#id{color:red;background:yellow;}",
+    throws: true
+  },
+  {
+    desc: "Multi-lines CSS",
+    input: [
+      "/* this is a multi line css */",
+      "body {",
+      "  color: green;",
+      "  background-repeat: no-repeat",
+      "}",
+      " /*something else here */",
+      "* {",
+      "  color: purple;",
+      "}"
+    ].join("\n"),
+    line: 7,
+    column: 1,
+    expected: {offset: 116, text: "\n  color: purple;\n"}
+  },
+  {
+    desc: "Multi-lines CSS and multi-line rule",
+    input: [
+      "/* ",
+       "* some comments",
+       "*/",
+      "",
+      "body {",
+      "    margin: 0;",
+      "    padding: 15px 15px 2px 15px;",
+      "    color: red;",
+      "}",
+      "",
+      "#header .btn, #header .txt {",
+      "    font-size: 100%;",
+      "}",
+      "",
+      "#header #information {",
+      "    color: #dddddd;",
+      "    font-size: small;",
+      "}",
+    ].join("\n"),
+    line: 5,
+    column: 1,
+    expected: {
+      offset: 30,
+      text: "\n    margin: 0;\n    padding: 15px 15px 2px 15px;\n    color: red;\n"}
+  },
+  {
+    desc: "Content string containing a } character",
+    input: "   #id{border:1px solid red;content: '}';color:red;}",
+    line: 1,
+    column: 4,
+    expected: {offset: 7, text: "border:1px solid red;content: '}';color:red;"}
+  },
+];
+
+function run_test() {
+  for (let test of TEST_DATA) {
+    do_print("Starting test: " + test.desc);
+    do_print("Input string " + test.input);
+    let output;
+    try {
+      output = getRuleText(test.input, test.line, test.column);
+      if (test.throws) {
+        do_print("Test should have thrown");
+        do_check_true(false);
+      }
+    } catch (e) {
+      do_print("getRuleText threw an exception with the given input string");
+      if (test.throws) {
+        do_print("Exception expected");
+        do_check_true(true);
+      } else {
+        do_print("Exception unexpected\n" + e);
+        do_check_true(false);
+      }
+    }
+    if (output) {
+      deepEqual(output, test.expected);
+    }
+  }
+}
new file mode 100644
--- /dev/null
+++ b/toolkit/devtools/server/tests/unit/test_getTextAtLineColumn.js
@@ -0,0 +1,35 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const {getTextAtLineColumn} = devtools.require("devtools/server/actors/styles");
+
+const TEST_DATA = [
+  {
+    desc: "simplest",
+    input: "#id{color:red;background:yellow;}",
+    line: 1,
+    column: 5,
+    expected: {offset: 4, text: "color:red;background:yellow;}"}
+  },
+  {
+    desc: "multiple lines",
+    input: "one\n two\n  three",
+    line: 3,
+    column: 3,
+    expected: {offset: 11, text: "three"}
+  },
+];
+
+function run_test() {
+  for (let test of TEST_DATA) {
+    do_print("Starting test: " + test.desc);
+    do_print("Input string " + test.input);
+
+    let output = getTextAtLineColumn(test.input, test.line, test.column);
+    deepEqual(output, test.expected);
+  }
+}
--- a/toolkit/devtools/server/tests/unit/xpcshell.ini
+++ b/toolkit/devtools/server/tests/unit/xpcshell.ini
@@ -73,16 +73,18 @@ skip-if = os == 'linux' # Bug 1176173
 [test_blackboxing-06.js]
 [test_blackboxing-07.js]
 [test_frameactor-01.js]
 [test_frameactor-02.js]
 [test_frameactor-03.js]
 [test_frameactor-04.js]
 [test_frameactor-05.js]
 [test_framearguments-01.js]
+[test_getRuleText.js]
+[test_getTextAtLineColumn.js]
 [test_pauselifetime-01.js]
 [test_pauselifetime-02.js]
 [test_pauselifetime-03.js]
 [test_pauselifetime-04.js]
 [test_threadlifetime-01.js]
 [test_threadlifetime-02.js]
 [test_threadlifetime-03.js]
 [test_threadlifetime-04.js]