From 8095d386b972948911269ea0236c0fac15f2e703 Mon Sep 17 00:00:00 2001 From: aceman Date: Wed, 13 May 2015 11:36:47 -0700 Subject: [PATCH 01/54] Bug 596778 - Stop setting cache size with each user keystroke in size input field, by only setting the cache size pref when the field is loses focus. r=mak77 --- browser/components/preferences/advanced.js | 30 ++++++++++++------- browser/components/preferences/advanced.xul | 4 +-- .../preferences/in-content/advanced.js | 30 ++++++++++++------- .../preferences/in-content/advanced.xul | 6 ++-- 4 files changed, 41 insertions(+), 29 deletions(-) diff --git a/browser/components/preferences/advanced.js b/browser/components/preferences/advanced.js index ee264ddc4a73..e0121668397f 100644 --- a/browser/components/preferences/advanced.js +++ b/browser/components/preferences/advanced.js @@ -48,6 +48,7 @@ var gAdvancedPane = { this.initSubmitHealthReport(); #endif + this.updateCacheSizeInputField(); this.updateActualCacheSize(); this.updateActualAppCacheSize(); @@ -378,24 +379,31 @@ var gAdvancedPane = { }, /** - * Converts the cache size from units of KB to units of MB and returns that - * value. + * Converts the cache size from units of KB to units of MB and stores it in + * the textbox element. */ - readCacheSize: function () + updateCacheSizeInputField() { - var preference = document.getElementById("browser.cache.disk.capacity"); - return preference.value / 1024; + let cacheSizeElem = document.getElementById("cacheSize"); + let cachePref = document.getElementById("browser.cache.disk.capacity"); + cacheSizeElem.value = cachePref.value / 1024; + if (cachePref.locked) + cacheSizeElem.disabled = true; }, /** - * Converts the cache size as specified in UI (in MB) to KB and returns that - * value. + * Updates the cache size preference once user enters a new value. + * We intentionally do not set preference="browser.cache.disk.capacity" + * onto the textbox directly, as that would update the pref at each keypress + * not only after the final value is entered. */ - writeCacheSize: function () + updateCacheSizePref() { - var cacheSize = document.getElementById("cacheSize"); - var intValue = parseInt(cacheSize.value, 10); - return isNaN(intValue) ? 0 : intValue * 1024; + let cacheSizeElem = document.getElementById("cacheSize"); + let cachePref = document.getElementById("browser.cache.disk.capacity"); + // Converts the cache size as specified in UI (in MB) to KB. + let intValue = parseInt(cacheSizeElem.value, 10); + cachePref.value = isNaN(intValue) ? 0 : intValue * 1024; }, /** diff --git a/browser/components/preferences/advanced.xul b/browser/components/preferences/advanced.xul index 64a29f312f85..c375cd33b8f7 100644 --- a/browser/components/preferences/advanced.xul +++ b/browser/components/preferences/advanced.xul @@ -250,9 +250,7 @@ accesskey="&limitCacheSizeBefore.accesskey;" value="&limitCacheSizeBefore.label;"/> diff --git a/browser/components/preferences/in-content/advanced.js b/browser/components/preferences/in-content/advanced.js index 62abddfdb519..89e5fe037c89 100644 --- a/browser/components/preferences/in-content/advanced.js +++ b/browser/components/preferences/in-content/advanced.js @@ -45,6 +45,7 @@ var gAdvancedPane = { #ifdef MOZ_SERVICES_HEALTHREPORT this.initSubmitHealthReport(); #endif + this.updateCacheSizeInputField(); this.updateActualCacheSize(); this.updateActualAppCacheSize(); @@ -413,24 +414,31 @@ var gAdvancedPane = { }, /** - * Converts the cache size from units of KB to units of MB and returns that - * value. + * Converts the cache size from units of KB to units of MB and stores it in + * the textbox element. */ - readCacheSize: function () + updateCacheSizeInputField() { - var preference = document.getElementById("browser.cache.disk.capacity"); - return preference.value / 1024; + let cacheSizeElem = document.getElementById("cacheSize"); + let cachePref = document.getElementById("browser.cache.disk.capacity"); + cacheSizeElem.value = cachePref.value / 1024; + if (cachePref.locked) + cacheSizeElem.disabled = true; }, /** - * Converts the cache size as specified in UI (in MB) to KB and returns that - * value. + * Updates the cache size preference once user enters a new value. + * We intentionally do not set preference="browser.cache.disk.capacity" + * onto the textbox directly, as that would update the pref at each keypress + * not only after the final value is entered. */ - writeCacheSize: function () + updateCacheSizePref() { - var cacheSize = document.getElementById("cacheSize"); - var intValue = parseInt(cacheSize.value, 10); - return isNaN(intValue) ? 0 : intValue * 1024; + let cacheSizeElem = document.getElementById("cacheSize"); + let cachePref = document.getElementById("browser.cache.disk.capacity"); + // Converts the cache size as specified in UI (in MB) to KB. + let intValue = parseInt(cacheSizeElem.value, 10); + cachePref.value = isNaN(intValue) ? 0 : intValue * 1024; }, /** diff --git a/browser/components/preferences/in-content/advanced.xul b/browser/components/preferences/in-content/advanced.xul index 0a18f5cfb6f9..ecb31a30dc32 100644 --- a/browser/components/preferences/in-content/advanced.xul +++ b/browser/components/preferences/in-content/advanced.xul @@ -271,10 +271,8 @@ &limitCacheSizeBefore.label; + onchange="gAdvancedPane.updateCacheSizePref();" + aria-labelledby="useCacheBefore cacheSize useCacheAfter"/> From beb546cc3ea94424c4750f994649e8b77d9962a3 Mon Sep 17 00:00:00 2001 From: Tom Tromey Date: Wed, 13 May 2015 11:37:18 -0700 Subject: [PATCH 02/54] Bug 930680 - allow inserting ";" in values in style inspector; r=pbrosset --- browser/devtools/shared/inplace-editor.js | 28 ++++++++---- .../shared/test/browser_inplace-editor-01.js | 32 ++++++++++++++ .../shared/test/unit/test_advanceValidate.js | 33 ++++++++++++++ .../devtools/shared/test/unit/xpcshell.ini | 1 + browser/devtools/styleinspector/rule-view.js | 44 ++++++++++++++++++- 5 files changed, 129 insertions(+), 9 deletions(-) create mode 100644 browser/devtools/shared/test/unit/test_advanceValidate.js diff --git a/browser/devtools/shared/inplace-editor.js b/browser/devtools/shared/inplace-editor.js index 56a061dcf633..034b32e18d19 100644 --- a/browser/devtools/shared/inplace-editor.js +++ b/browser/devtools/shared/inplace-editor.js @@ -69,9 +69,15 @@ Cu.import("resource://gre/modules/devtools/event-emitter.js"); * This function is called before the editor has been torn down. * {function} destroy: * Called when the editor is destroyed and has been torn down. - * {string} advanceChars: - * If any characters in advanceChars are typed, focus will advance - * to the next element. + * {object} advanceChars: + * This can be either a string or a function. + * If it is a string, then if any characters in it are typed, + * focus will advance to the next element. + * Otherwise, if it is a function, then the function will + * be called with three arguments: a key code, the current text, + * and the insertion point. If the function returns true, + * then the focus advance takes place. If it returns false, + * then the character is inserted instead. * {boolean} stopOnReturn: * If true, the return key will not advance the editor to the next * focusable element. @@ -212,10 +218,15 @@ function InplaceEditor(aOptions, aEvent) // Pull out character codes for advanceChars, listing the // characters that should trigger a blur. - this._advanceCharCodes = {}; - let advanceChars = aOptions.advanceChars || ''; - for (let i = 0; i < advanceChars.length; i++) { - this._advanceCharCodes[advanceChars.charCodeAt(i)] = true; + if (typeof(aOptions.advanceChars) === "function") { + this._advanceChars = aOptions.advanceChars; + } else { + let advanceCharcodes = {}; + let advanceChars = aOptions.advanceChars || ''; + for (let i = 0; i < advanceChars.length; i++) { + advanceCharcodes[advanceChars.charCodeAt(i)] = true; + } + this._advanceChars = aCharCode => aCharCode in advanceCharcodes; } // Hide the provided element and add our editor. @@ -931,7 +942,8 @@ InplaceEditor.prototype = { aEvent.keyCode === Ci.nsIDOMKeyEvent.DOM_VK_RETURN && aEvent.shiftKey) { prevent = false; - } else if (aEvent.charCode in this._advanceCharCodes + } else if (this._advanceChars(aEvent.charCode, this.input.value, + this.input.selectionStart) || aEvent.keyCode === Ci.nsIDOMKeyEvent.DOM_VK_RETURN || aEvent.keyCode === Ci.nsIDOMKeyEvent.DOM_VK_TAB) { prevent = true; diff --git a/browser/devtools/shared/test/browser_inplace-editor-01.js b/browser/devtools/shared/test/browser_inplace-editor-01.js index 8b781601b64f..99e22f6b898f 100644 --- a/browser/devtools/shared/test/browser_inplace-editor-01.js +++ b/browser/devtools/shared/test/browser_inplace-editor-01.js @@ -17,6 +17,7 @@ add_task(function*() { yield testReturnCommit(doc); yield testBlurCommit(doc); yield testAdvanceCharCommit(doc); + yield testAdvanceCharsFunction(doc); host.destroy(); gBrowser.removeCurrentTab(); @@ -91,6 +92,37 @@ function testAdvanceCharCommit(doc) { return def.promise; } +function testAdvanceCharsFunction(doc) { + info("Testing advanceChars as a function"); + let def = promise.defer(); + + let firstTime = true; + + createInplaceEditorAndClick({ + initial: "", + advanceChars: function(aCharCode, aText, aInsertionPoint) { + if (aCharCode !== Components.interfaces.nsIDOMKeyEvent.DOM_VK_COLON) { + return false; + } + if (firstTime) { + firstTime = false; + return false; + } + + // Just to make sure we check it somehow. + return aText.length > 0; + }, + start: function(editor) { + for each (let ch in ":Test:") { + EventUtils.sendChar(ch); + } + }, + done: onDone(":Test", true, def) + }, doc); + + return def.promise; +} + function testEscapeCancel(doc) { info("Testing that escape cancels the new value"); let def = promise.defer(); diff --git a/browser/devtools/shared/test/unit/test_advanceValidate.js b/browser/devtools/shared/test/unit/test_advanceValidate.js new file mode 100644 index 000000000000..87194376aacf --- /dev/null +++ b/browser/devtools/shared/test/unit/test_advanceValidate.js @@ -0,0 +1,33 @@ +/* -*- Mode: Javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 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"; + +// Tests the advanceValidate function from rule-view.js. + +const Cu = Components.utils; +const Ci = Components.interfaces; +let {devtools} = Cu.import("resource://gre/modules/devtools/Loader.jsm", {}); +let require = devtools.require; +let {_advanceValidate} = require("devtools/styleinspector/rule-view"); + +// 1 2 3 +// 0123456789012345678901234567890 +let sampleInput = '\\symbol "string" url(somewhere)'; + +function testInsertion(where, result, testName) { + do_print(testName); + equal(_advanceValidate(Ci.nsIDOMKeyEvent.DOM_VK_SEMICOLON, sampleInput, where), + result, "testing _advanceValidate at " + where); +} + +function run_test() { + testInsertion(4, true, "inside a symbol"); + testInsertion(1, false, "after a backslash"); + testInsertion(8, true, "after whitespace"); + testInsertion(11, false, "inside a string"); + testInsertion(24, false, "inside a URL"); + testInsertion(31, true, "at the end"); +} diff --git a/browser/devtools/shared/test/unit/xpcshell.ini b/browser/devtools/shared/test/unit/xpcshell.ini index 35f8aa177302..71bad0b6bd75 100644 --- a/browser/devtools/shared/test/unit/xpcshell.ini +++ b/browser/devtools/shared/test/unit/xpcshell.ini @@ -5,6 +5,7 @@ tail = firefox-appdir = browser skip-if = toolkit == 'android' || toolkit == 'gonk' +[test_advanceValidate.js] [test_attribute-parsing-01.js] [test_attribute-parsing-02.js] [test_bezierCanvas.js] diff --git a/browser/devtools/styleinspector/rule-view.js b/browser/devtools/styleinspector/rule-view.js index a20c93a59855..1e26110f121b 100644 --- a/browser/devtools/styleinspector/rule-view.js +++ b/browser/devtools/styleinspector/rule-view.js @@ -2796,7 +2796,7 @@ TextPropertyEditor.prototype = { done: this._onValueDone, destroy: this.update, validate: this._onValidate, - advanceChars: ';', + advanceChars: advanceValidate, contentType: InplaceEditor.CONTENT_TYPES.CSS_VALUE, property: this.prop, popup: this.popup @@ -3525,6 +3525,48 @@ function getPropertyNameAndValue(node) { } } +/** + * Called when a character is typed in a value editor. This decides + * whether to advance or not, first by checking to see if ";" was + * typed, and then by lexing the input and seeing whether the ";" + * would be a terminator at this point. + * + * @param {number} aKeyCode Key code to be checked. + * @param {String} aValue Current text editor value. + * @param {number} aInsertionPoint The index of the insertion point. + * @return {Boolean} True if the focus should advance; false if + * the character should be inserted. + */ +function advanceValidate(aKeyCode, aValue, aInsertionPoint) { + // Only ";" has special handling here. + if (aKeyCode !== Ci.nsIDOMKeyEvent.DOM_VK_SEMICOLON) { + return false; + } + + // Insert the character provisionally and see what happens. If we + // end up with a ";" symbol token, then the semicolon terminates the + // value. Otherwise it's been inserted in some spot where it has a + // valid meaning, like a comment or string. + aValue = aValue.slice(0, aInsertionPoint) + ';' + aValue.slice(aInsertionPoint); + let lexer = domUtils.getCSSLexer(aValue); + while (true) { + let token = lexer.nextToken(); + if (token.endOffset > aInsertionPoint) { + if (token.tokenType === "symbol" && token.text === ";") { + // The ";" is a terminator. + return true; + } else { + // The ";" is not a terminator in this context. + break; + } + } + } + return false; +} + +// We're exporting _advanceValidate for unit tests. +exports._advanceValidate = advanceValidate; + XPCOMUtils.defineLazyGetter(this, "clipboardHelper", function() { return Cc["@mozilla.org/widget/clipboardhelper;1"]. getService(Ci.nsIClipboardHelper); From 58078b80e7b5ee215caa4954f2de6448609682b4 Mon Sep 17 00:00:00 2001 From: Tom Tromey Date: Wed, 13 May 2015 11:37:51 -0700 Subject: [PATCH 03/54] Bug 1153305 - change css-tokenizer.js to use CSSLexer r=pbrosset --HG-- extra : amend_source : 076c3f65cbbd635d39000f05e1ed6c34fc5ccd52 --- .../sourceeditor/css-autocompleter.js | 536 ++++++------ .../devtools/sourceeditor/css-tokenizer.js | 782 ++---------------- .../styleinspector/css-parsing-utils.js | 107 +-- .../test/unit/test_parseDeclarations.js | 14 +- 4 files changed, 376 insertions(+), 1063 deletions(-) diff --git a/browser/devtools/sourceeditor/css-autocompleter.js b/browser/devtools/sourceeditor/css-autocompleter.js index 3cc23549458d..b284b7d7695e 100644 --- a/browser/devtools/sourceeditor/css-autocompleter.js +++ b/browser/devtools/sourceeditor/css-autocompleter.js @@ -3,7 +3,8 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ const { Cc, Ci, Cu } = require('chrome'); -const cssTokenizer = require("devtools/sourceeditor/css-tokenizer"); +const {cssTokenizer, cssTokenizerWithLineColumn} = + require("devtools/sourceeditor/css-tokenizer"); const promise = Cu.import("resource://gre/modules/Promise.jsm"); /** @@ -180,18 +181,17 @@ CSSCompleter.prototype = { else { this.nullStates = []; } - let tokens = cssTokenizer(source, {loc:true}); + let tokens = cssTokenizerWithLineColumn(source); let tokIndex = tokens.length - 1; - if (tokens[tokIndex].loc.end.line < line || - (tokens[tokIndex].loc.end.line === line && - tokens[tokIndex].loc.end.column < ch)) { - // If the last token is not an EOF, we didn't tokenize it correctly. - // This special case is handled in case we couldn't tokenize, but the last - // token that *could be tokenized* was an identifier. + if (tokIndex >=0 && + (tokens[tokIndex].loc.end.line < line || + (tokens[tokIndex].loc.end.line === line && + tokens[tokIndex].loc.end.column < ch))) { + // If the last token ends before the cursor location, we didn't + // tokenize it correctly. This special case can happen if the + // final token is a comment. return null; } - // Since last token is EOF, the cursor token is last - 1 - tokIndex--; let cursor = 0; // This will maintain a stack of paired elements like { & }, @m & }, : & ; etc @@ -202,13 +202,14 @@ CSSCompleter.prototype = { case CSS_STATES.property: // From CSS_STATES.property, we can either go to CSS_STATES.value state // when we hit the first ':' or CSS_STATES.selector if "}" is reached. - switch(token.tokenType) { + if (token.tokenType === "symbol") { + switch(token.text) { case ":": scopeStack.push(":"); - if (tokens[cursor - 2].tokenType != "WHITESPACE") - propertyName = tokens[cursor - 2].value; + if (tokens[cursor - 2].tokenType != "whitespace") + propertyName = tokens[cursor - 2].text; else - propertyName = tokens[cursor - 3].value; + propertyName = tokens[cursor - 3].text; _state = CSS_STATES.value; break; @@ -224,13 +225,15 @@ CSSCompleter.prototype = { } } break; + } } break; case CSS_STATES.value: // From CSS_STATES.value, we can go to one of CSS_STATES.property, // CSS_STATES.frame, CSS_STATES.selector and CSS_STATES.null - switch(token.tokenType) { + if (token.tokenType === "symbol") { + switch(token.text) { case ";": if (/[:]/.test(peek(scopeStack))) { scopeStack.pop(); @@ -253,13 +256,14 @@ CSSCompleter.prototype = { } } break; + } } break; case CSS_STATES.selector: // From CSS_STATES.selector, we can only go to CSS_STATES.property when // we hit "{" - if (token.tokenType == "{") { + if (token.tokenType === "symbol" && token.text == "{") { scopeStack.push("{"); _state = CSS_STATES.property; selectors.push(selector); @@ -271,76 +275,71 @@ CSSCompleter.prototype = { case SELECTOR_STATES.class: case SELECTOR_STATES.tag: switch(token.tokenType) { - case "HASH": + case "hash": + case "id": selectorState = SELECTOR_STATES.id; - selector += "#" + token.value; + selector += "#" + token.text; break; - case "DELIM": - if (token.value == ".") { + case "symbol": + if (token.text == ".") { selectorState = SELECTOR_STATES.class; selector += "."; if (cursor <= tokIndex && - tokens[cursor].tokenType == "IDENT") { + tokens[cursor].tokenType == "ident") { token = tokens[cursor++]; - selector += token.value; + selector += token.text; } - } else if (token.value == "#") { + } else if (token.text == "#") { selectorState = SELECTOR_STATES.id; selector += "#"; - } else if (/[>~+]/.test(token.value)) { + } else if (/[>~+]/.test(token.text)) { selectorState = SELECTOR_STATES.null; - selector += token.value; - } else if (token.value == ",") { + selector += token.text; + } else if (token.text == ",") { selectorState = SELECTOR_STATES.null; selectors.push(selector); selector = ""; - } - break; - - case ":": - selectorState = SELECTOR_STATES.pseudo; - selector += ":"; - if (cursor > tokIndex) - break; - - token = tokens[cursor++]; - switch(token.tokenType) { - case "FUNCTION": - if (token.value == "not") { - selectorBeforeNot = selector; - selector = ""; - scopeStack.push("("); - } else { - selector += token.value + "("; - } - selectorState = SELECTOR_STATES.null; + } else if (token.text == ":") { + selectorState = SELECTOR_STATES.pseudo; + selector += ":"; + if (cursor > tokIndex) break; - case "IDENT": - selector += token.value; - break; + token = tokens[cursor++]; + switch(token.tokenType) { + case "function": + if (token.text == "not") { + selectorBeforeNot = selector; + selector = ""; + scopeStack.push("("); + } else { + selector += token.text + "("; + } + selectorState = SELECTOR_STATES.null; + break; + + case "ident": + selector += token.text; + break; + } + } else if (token.text == "[") { + selectorState = SELECTOR_STATES.attribute; + scopeStack.push("["); + selector += "["; + } else if (token.text == ")") { + if (peek(scopeStack) == "(") { + scopeStack.pop(); + selector = selectorBeforeNot + "not(" + selector + ")"; + selectorBeforeNot = null; + } else { + selector += ")"; + } + selectorState = SELECTOR_STATES.null; } break; - case "[": - selectorState = SELECTOR_STATES.attribute; - scopeStack.push("["); - selector += "["; - break; - - case ")": - if (peek(scopeStack) == "(") { - scopeStack.pop(); - selector = selectorBeforeNot + "not(" + selector + ")"; - selectorBeforeNot = null; - } else { - selector += ")"; - } - selectorState = SELECTOR_STATES.null; - break; - - case "WHITESPACE": + case "whitespace": selectorState = SELECTOR_STATES.null; selector && (selector += " "); break; @@ -351,83 +350,78 @@ CSSCompleter.prototype = { // From SELECTOR_STATES.null state, we can go to one of // SELECTOR_STATES.id, SELECTOR_STATES.class or SELECTOR_STATES.tag switch(token.tokenType) { - case "HASH": + case "hash": + case "id": selectorState = SELECTOR_STATES.id; - selector += "#" + token.value; + selector += "#" + token.text; break; - case "IDENT": + case "ident": selectorState = SELECTOR_STATES.tag; - selector += token.value; + selector += token.text; break; - case "DELIM": - if (token.value == ".") { + case "symbol": + if (token.text == ".") { selectorState = SELECTOR_STATES.class; selector += "."; if (cursor <= tokIndex && - tokens[cursor].tokenType == "IDENT") { + tokens[cursor].tokenType == "ident") { token = tokens[cursor++]; - selector += token.value; + selector += token.text; } - } else if (token.value == "#") { + } else if (token.text == "#") { selectorState = SELECTOR_STATES.id; selector += "#"; - } else if (token.value == "*") { + } else if (token.text == "*") { selectorState = SELECTOR_STATES.tag; selector += "*"; - } else if (/[>~+]/.test(token.value)) { - selector += token.value; - } else if (token.value == ",") { + } else if (/[>~+]/.test(token.text)) { + selector += token.text; + } else if (token.text == ",") { selectorState = SELECTOR_STATES.null; selectors.push(selector); selector = ""; - } - break; - - case ":": - selectorState = SELECTOR_STATES.pseudo; - selector += ":"; - if (cursor > tokIndex) - break; - - token = tokens[cursor++]; - switch(token.tokenType) { - case "FUNCTION": - if (token.value == "not") { - selectorBeforeNot = selector; - selector = ""; - scopeStack.push("("); - } else { - selector += token.value + "("; - } - selectorState = SELECTOR_STATES.null; + } else if (token.text == ":") { + selectorState = SELECTOR_STATES.pseudo; + selector += ":"; + if (cursor > tokIndex) break; - case "IDENT": - selector += token.value; - break; + token = tokens[cursor++]; + switch(token.tokenType) { + case "function": + if (token.text == "not") { + selectorBeforeNot = selector; + selector = ""; + scopeStack.push("("); + } else { + selector += token.text + "("; + } + selectorState = SELECTOR_STATES.null; + break; + + case "ident": + selector += token.text; + break; + } + } else if (token.text == "[") { + selectorState = SELECTOR_STATES.attribute; + scopeStack.push("["); + selector += "["; + } else if (token.text == ")") { + if (peek(scopeStack) == "(") { + scopeStack.pop(); + selector = selectorBeforeNot + "not(" + selector + ")"; + selectorBeforeNot = null; + } else { + selector += ")"; + } + selectorState = SELECTOR_STATES.null; } break; - case "[": - selectorState = SELECTOR_STATES.attribute; - scopeStack.push("["); - selector += "["; - break; - - case ")": - if (peek(scopeStack) == "(") { - scopeStack.pop(); - selector = selectorBeforeNot + "not(" + selector + ")"; - selectorBeforeNot = null; - } else { - selector += ")"; - } - selectorState = SELECTOR_STATES.null; - break; - - case "WHITESPACE": + case "whitespace": selector && (selector += " "); break; } @@ -435,49 +429,45 @@ CSSCompleter.prototype = { case SELECTOR_STATES.pseudo: switch(token.tokenType) { - case "DELIM": - if (/[>~+]/.test(token.value)) { + case "symbol": + if (/[>~+]/.test(token.text)) { selectorState = SELECTOR_STATES.null; - selector += token.value; - } else if (token.value == ",") { + selector += token.text; + } else if (token.text == ",") { selectorState = SELECTOR_STATES.null; selectors.push(selector); selector = ""; + } else if (token.text == ":") { + selectorState = SELECTOR_STATES.pseudo; + selector += ":"; + if (cursor > tokIndex) + break; + + token = tokens[cursor++]; + switch(token.tokenType) { + case "function": + if (token.text == "not") { + selectorBeforeNot = selector; + selector = ""; + scopeStack.push("("); + } else { + selector += token.text + "("; + } + selectorState = SELECTOR_STATES.null; + break; + + case "ident": + selector += token.text; + break; + } + } else if (token.text == "[") { + selectorState = SELECTOR_STATES.attribute; + scopeStack.push("["); + selector += "["; } break; - case ":": - selectorState = SELECTOR_STATES.pseudo; - selector += ":"; - if (cursor > tokIndex) - break; - - token = tokens[cursor++]; - switch(token.tokenType) { - case "FUNCTION": - if (token.value == "not") { - selectorBeforeNot = selector; - selector = ""; - scopeStack.push("("); - } else { - selector += token.value + "("; - } - selectorState = SELECTOR_STATES.null; - break; - - case "IDENT": - selector += token.value; - break; - } - break; - - case "[": - selectorState = SELECTOR_STATES.attribute; - scopeStack.push("["); - selector += "["; - break; - - case "WHITESPACE": + case "whitespace": selectorState = SELECTOR_STATES.null; selector && (selector += " "); break; @@ -486,31 +476,28 @@ CSSCompleter.prototype = { case SELECTOR_STATES.attribute: switch(token.tokenType) { - case "DELIM": - if (/[~|^$*]/.test(token.value)) { - selector += token.value; + case "symbol": + if (/[~|^$*]/.test(token.text)) { + selector += token.text; token = tokens[cursor++]; - } - if(token.value == "=") { + } else if (token.text == "=") { selectorState = SELECTOR_STATES.value; - selector += token.value; + selector += token.text; + } else if (token.text == "]") { + if (peek(scopeStack) == "[") + scopeStack.pop(); + + selectorState = SELECTOR_STATES.null; + selector += "]"; } break; - case "IDENT": - case "STRING": - selector += token.value; + case "ident": + case "string": + selector += token.text; break; - case "]": - if (peek(scopeStack) == "[") - scopeStack.pop(); - - selectorState = SELECTOR_STATES.null; - selector += "]"; - break; - - case "WHITESPACE": + case "whitespace": selector && (selector += " "); break; } @@ -518,20 +505,22 @@ CSSCompleter.prototype = { case SELECTOR_STATES.value: switch(token.tokenType) { - case "STRING": - case "IDENT": - selector += token.value; + case "string": + case "ident": + selector += token.text; break; - case "]": - if (peek(scopeStack) == "[") - scopeStack.pop(); + case "symbol": + if (token.text == "]") { + if (peek(scopeStack) == "[") + scopeStack.pop(); - selectorState = SELECTOR_STATES.null; - selector += "]"; + selectorState = SELECTOR_STATES.null; + selector += "]"; + } break; - case "WHITESPACE": + case "whitespace": selector && (selector += " "); break; } @@ -543,89 +532,83 @@ CSSCompleter.prototype = { // From CSS_STATES.null state, we can go to either CSS_STATES.media or // CSS_STATES.selector. switch(token.tokenType) { - case "HASH": + case "hash": + case "id": selectorState = SELECTOR_STATES.id; - selector = "#" + token.value; + selector = "#" + token.text; _state = CSS_STATES.selector; break; - case "IDENT": + case "ident": selectorState = SELECTOR_STATES.tag; - selector = token.value; + selector = token.text; _state = CSS_STATES.selector; break; - case "DELIM": - if (token.value == ".") { + case "symbol": + if (token.text == ".") { selectorState = SELECTOR_STATES.class; selector = "."; _state = CSS_STATES.selector; if (cursor <= tokIndex && - tokens[cursor].tokenType == "IDENT") { + tokens[cursor].tokenType == "ident") { token = tokens[cursor++]; - selector += token.value; + selector += token.text; } - } else if (token.value == "#") { + } else if (token.text == "#") { selectorState = SELECTOR_STATES.id; selector = "#"; _state = CSS_STATES.selector; - } else if (token.value == "*") { + } else if (token.text == "*") { selectorState = SELECTOR_STATES.tag; selector = "*"; _state = CSS_STATES.selector; + } else if (token.text == ":") { + _state = CSS_STATES.selector; + selectorState = SELECTOR_STATES.pseudo; + selector += ":"; + if (cursor > tokIndex) + break; + + token = tokens[cursor++]; + switch(token.tokenType) { + case "function": + if (token.text == "not") { + selectorBeforeNot = selector; + selector = ""; + scopeStack.push("("); + } else { + selector += token.text + "("; + } + selectorState = SELECTOR_STATES.null; + break; + + case "ident": + selector += token.text; + break; + } + } else if (token.text == "[") { + _state = CSS_STATES.selector; + selectorState = SELECTOR_STATES.attribute; + scopeStack.push("["); + selector += "["; + } else if (token.text == "}") { + if (peek(scopeStack) == "@m") + scopeStack.pop(); } break; - case ":": - _state = CSS_STATES.selector; - selectorState = SELECTOR_STATES.pseudo; - selector += ":"; - if (cursor > tokIndex) - break; - - token = tokens[cursor++]; - switch(token.tokenType) { - case "FUNCTION": - if (token.value == "not") { - selectorBeforeNot = selector; - selector = ""; - scopeStack.push("("); - } else { - selector += token.value + "("; - } - selectorState = SELECTOR_STATES.null; - break; - - case "IDENT": - selector += token.value; - break; - } - break; - - case "[": - _state = CSS_STATES.selector; - selectorState = SELECTOR_STATES.attribute; - scopeStack.push("["); - selector += "["; - break; - - case "AT-KEYWORD": - _state = token.value.startsWith("m") ? CSS_STATES.media + case "at": + _state = token.text.startsWith("m") ? CSS_STATES.media : CSS_STATES.keyframes; break; - - case "}": - if (peek(scopeStack) == "@m") - scopeStack.pop(); - - break; } break; case CSS_STATES.media: // From CSS_STATES.media, we can only go to CSS_STATES.null state when // we hit the first '{' - if (token.tokenType == "{") { + if (token.tokenType == "symbol" && token.text == "{") { scopeStack.push("@m"); _state = CSS_STATES.null; } @@ -634,7 +617,7 @@ CSSCompleter.prototype = { case CSS_STATES.keyframes: // From CSS_STATES.keyframes, we can only go to CSS_STATES.frame state // when we hit the first '{' - if (token.tokenType == "{") { + if (token.tokenType == "symbol" && token.text == "{") { scopeStack.push("@k"); _state = CSS_STATES.frame; } @@ -643,14 +626,16 @@ CSSCompleter.prototype = { case CSS_STATES.frame: // From CSS_STATES.frame, we can either go to CSS_STATES.property state // when we hit the first '{' or to CSS_STATES.selector when we hit '}' - if (token.tokenType == "{") { - scopeStack.push("f"); - _state = CSS_STATES.property; - } else if (token.tokenType == "}") { - if (peek(scopeStack) == "@k") - scopeStack.pop(); + if (token.tokenType == "symbol") { + if (token.text == "{") { + scopeStack.push("f"); + _state = CSS_STATES.property; + } else if (token.text == "}") { + if (peek(scopeStack) == "@k") + scopeStack.pop(); - _state = CSS_STATES.null; + _state = CSS_STATES.null; + } } break; } @@ -682,10 +667,14 @@ CSSCompleter.prototype = { } this.selectors = selectors; - if (token && token.tokenType != "WHITESPACE") { - this.completing = ((token.value || token.repr || token.tokenType) + "") - .slice(0, ch - token.loc.start.column) - .replace(/^[.#]$/, ""); + if (token && token.tokenType != "whitespace") { + let text; + if (token.tokenType == "dimension" || !token.text) + text = source.substring(token.startOffset, token.endOffset); + else + text = token.text; + this.completing = (text.slice(0, ch - token.loc.start.column) + .replace(/^[.#]$/, "")); } else { this.completing = ""; } @@ -695,7 +684,7 @@ CSSCompleter.prototype = { this.completing = ""; // Special check for !important; case. - if (token && tokens[cursor - 2] && tokens[cursor - 2].value == "!" && + if (token && tokens[cursor - 2] && tokens[cursor - 2].text == "!" && this.completing == "important".slice(0, this.completing.length)) { this.completing = "!" + this.completing; } @@ -989,38 +978,41 @@ CSSCompleter.prototype = { if (line == caret.line) lineText = lineText.substring(caret.ch); - let tokens = cssTokenizer(lineText, {loc: true}); + let prevToken = undefined; + let tokens = cssTokenizer(lineText); let found = false; let ech = line == caret.line ? caret.ch : 0; - for (let i = 0; i < tokens.length; i++) { - let token = tokens[i]; + for (let token of tokens) { // If the line is completely spaces, handle it differently if (lineText.trim() == "") { limitedSource += lineText; } else { limitedSource += sourceArray[line] - .substring(ech + token.loc.start.column, - ech + token.loc.end.column); + .substring(ech + token.startOffset, + ech + token.endOffset); } // Whitespace cannot change state. - if (token.tokenType == "WHITESPACE") + if (token.tokenType == "whitespace") { + prevToken = token; continue; + } let state = this.resolveState(limitedSource, { line: line, - ch: token.loc.end.column + ech + ch: token.endOffset + ech }); if (check(state)) { - if (tokens[i - 1] && tokens[i - 1].tokenType == "WHITESPACE") - token = tokens[i - 1]; + if (prevToken && prevToken.tokenType == "whitespace") + token = prevToken; location = { line: line, - ch: token.loc.start.column + ech + ch: token.startOffset + ech }; found = true; break; } + prevToken = token; } limitedSource += "\n"; if (found) @@ -1047,33 +1039,33 @@ CSSCompleter.prototype = { if (line == caret.line) lineText = lineText.substring(0, caret.ch); - let tokens = cssTokenizer(lineText, {loc: true}); + let tokens = Array.from(cssTokenizer(lineText)); let found = false; let ech = 0; - for (let i = tokens.length - 2; i >= 0; i--) { + for (let i = tokens.length - 1; i >= 0; i--) { let token = tokens[i]; // If the line is completely spaces, handle it differently if (lineText.trim() == "") { limitedSource = limitedSource.slice(0, -1 * lineText.length); } else { - let length = token.loc.end.column - token.loc.start.column; + let length = token.endOffset - token.startOffset; limitedSource = limitedSource.slice(0, -1 * length); } // Whitespace cannot change state. - if (token.tokenType == "WHITESPACE") + if (token.tokenType == "whitespace") continue; let state = this.resolveState(limitedSource, { line: line, - ch: token.loc.start.column + ch: token.startOffset }); if (check(state)) { - if (tokens[i + 1] && tokens[i + 1].tokenType == "WHITESPACE") + if (tokens[i + 1] && tokens[i + 1].tokenType == "whitespace") token = tokens[i + 1]; location = { line: line, - ch: isValue ? token.loc.end.column: token.loc.start.column + ch: isValue ? token.endOffset: token.startOffset }; found = true; break; @@ -1121,21 +1113,23 @@ CSSCompleter.prototype = { } else if (state == CSS_STATES.property) { // A property can only be a single word and thus very easy to calculate. - let tokens = cssTokenizer(sourceArray[line], {loc: true}); + let tokens = cssTokenizer(sourceArray[line]); for (let token of tokens) { - if (token.loc.start.column <= ch && token.loc.end.column >= ch) { + // Note that, because we're tokenizing a single line, the + // token's offset is also the column number. + if (token.startOffset <= ch && token.endOffset >= ch) { return { state: state, - propertyName: token.value, + propertyName: token.text, selectors: this.selectors, loc: { start: { line: line, - ch: token.loc.start.column + ch: token.startOffset }, end: { line: line, - ch: token.loc.end.column + ch: token.endOffset } } }; diff --git a/browser/devtools/sourceeditor/css-tokenizer.js b/browser/devtools/sourceeditor/css-tokenizer.js index f7d596a806b2..7bbfdcc71618 100644 --- a/browser/devtools/sourceeditor/css-tokenizer.js +++ b/browser/devtools/sourceeditor/css-tokenizer.js @@ -1,717 +1,95 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set ts=2 et sw=2 tw=80: */ +/* 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 {Cc, Ci} = require("chrome"); +loader.lazyGetter(this, "DOMUtils", () => { + return Cc["@mozilla.org/inspector/dom-utils;1"].getService(Ci.inIDOMUtils); +}); + /** - * This file is taken from the below mentioned url and is under CC0 license. - * https://github.com/tabatkins/css-parser/blob/master/tokenizer.js - * Please retain this comment while updating this file from upstream. + * A generator function that lexes a CSS source string, yielding the + * CSS tokens. Comment tokens are dropped. + * + * @param {String} CSS source string + * @yield {CSSToken} The next CSSToken that is lexed + * @see CSSToken for details about the returned tokens */ - -(function (root, factory) { - // Universal Module Definition (UMD) to support AMD, CommonJS/Node.js, - // Rhino, and plain browser loading. - if (typeof define === 'function' && define.amd) { - define(['exports'], factory); - } else if (typeof exports !== 'undefined') { - factory(exports); - } else { - factory(root); +function* cssTokenizer(string) { + let lexer = DOMUtils.getCSSLexer(string); + while (true) { + let token = lexer.nextToken(); + if (!token) { + break; } -}(this, function (exports) { - -var between = function (num, first, last) { return num >= first && num <= last; } -function digit(code) { return between(code, 0x30,0x39); } -function hexdigit(code) { return digit(code) || between(code, 0x41,0x46) || between(code, 0x61,0x66); } -function uppercaseletter(code) { return between(code, 0x41,0x5a); } -function lowercaseletter(code) { return between(code, 0x61,0x7a); } -function letter(code) { return uppercaseletter(code) || lowercaseletter(code); } -function nonascii(code) { return code >= 0xa0; } -function namestartchar(code) { return letter(code) || nonascii(code) || code == 0x5f; } -function namechar(code) { return namestartchar(code) || digit(code) || code == 0x2d; } -function nonprintable(code) { return between(code, 0,8) || between(code, 0xe,0x1f) || between(code, 0x7f,0x9f); } -function newline(code) { return code == 0xa || code == 0xc; } -function whitespace(code) { return newline(code) || code == 9 || code == 0x20; } -function badescape(code) { return newline(code) || isNaN(code); } - -// Note: I'm not yet acting smart enough to actually handle astral characters. -var maximumallowedcodepoint = 0x10ffff; - -function tokenize(str, options) { - if(options == undefined) options = {transformFunctionWhitespace:false, scientificNotation:false}; - var i = -1; - var tokens = []; - var state = "data"; - var code; - var currtoken; - - // Line number information. - var line = 0; - var column = 0; - // The only use of lastLineLength is in reconsume(). - var lastLineLength = 0; - var incrLineno = function() { - line += 1; - lastLineLength = column; - column = 0; - }; - var locStart = {line:line, column:column}; - - var next = function(num) { if(num === undefined) num = 1; return str.charCodeAt(i+num); }; - var consume = function(num) { - if(num === undefined) - num = 1; - i += num; - code = str.charCodeAt(i); - if (newline(code)) incrLineno(); - else column += num; - //console.log('Consume '+i+' '+String.fromCharCode(code) + ' 0x' + code.toString(16)); - return true; - }; - var reconsume = function() { - i -= 1; - if (newline(code)) { - line -= 1; - column = lastLineLength; - } else { - column -= 1; - } - locStart.line = line; - locStart.column = column; - return true; - }; - var eof = function() { return i >= str.length; }; - var donothing = function() {}; - var emit = function(token) { - if(token) { - token.finish(); - } else { - token = currtoken.finish(); - } - if (options.loc === true) { - token.loc = {}; - token.loc.start = {line:locStart.line, column:locStart.column}; - locStart = {line: line, column: column}; - token.loc.end = locStart; - } - tokens.push(token); - //console.log('Emitting ' + token); - currtoken = undefined; - return true; - }; - var create = function(token) { currtoken = token; return true; }; - var parseerror = function() { console.log("Parse error at index " + i + ", processing codepoint 0x" + code.toString(16) + " in state " + state + ".");return true; }; - var switchto = function(newstate) { - state = newstate; - //console.log('Switching to ' + state); - return true; - }; - var consumeEscape = function() { - // Assume the the current character is the \ - consume(); - if(hexdigit(code)) { - // Consume 1-6 hex digits - var digits = []; - for(var total = 0; total < 6; total++) { - if(hexdigit(code)) { - digits.push(code); - consume(); - } else { break; } - } - var value = parseInt(digits.map(String.fromCharCode).join(''), 16); - if( value > maximumallowedcodepoint ) value = 0xfffd; - // If the current char is whitespace, cool, we'll just eat it. - // Otherwise, put it back. - if(!whitespace(code)) reconsume(); - return value; - } else { - return code; - } - }; - - for(;;) { - if(i > str.length*2) return "I'm infinite-looping!"; - consume(); - switch(state) { - case "data": - if(whitespace(code)) { - emit(new WhitespaceToken); - while(whitespace(next())) consume(); - } - else if(code == 0x22) switchto("double-quote-string"); - else if(code == 0x23) switchto("hash"); - else if(code == 0x27) switchto("single-quote-string"); - else if(code == 0x28) emit(new OpenParenToken); - else if(code == 0x29) emit(new CloseParenToken); - else if(code == 0x2b) { - if(digit(next()) || (next() == 0x2e && digit(next(2)))) switchto("number") && reconsume(); - else emit(new DelimToken(code)); - } - else if(code == 0x2d) { - if(next(1) == 0x2d && next(2) == 0x3e) consume(2) && emit(new CDCToken); - else if(digit(next()) || (next(1) == 0x2e && digit(next(2)))) switchto("number") && reconsume(); - else if(namestartchar(next())) switchto("identifier") && reconsume(); - else emit(new DelimToken(code)); - } - else if(code == 0x2e) { - if(digit(next())) switchto("number") && reconsume(); - else emit(new DelimToken(code)); - } - else if(code == 0x2f) { - if(next() == 0x2a) switchto("comment"); - else emit(new DelimToken(code)); - } - else if(code == 0x3a) emit(new ColonToken); - else if(code == 0x3b) emit(new SemicolonToken); - else if(code == 0x3c) { - if(next(1) == 0x21 && next(2) == 0x2d && next(3) == 0x2d) consume(3) && emit(new CDOToken); - else emit(new DelimToken(code)); - } - else if(code == 0x40) switchto("at-keyword"); - else if(code == 0x5b) emit(new OpenSquareToken); - else if(code == 0x5c) { - if(badescape(next())) parseerror() && emit(new DelimToken(code)); - else switchto("identifier") && reconsume(); - } - else if(code == 0x5d) emit(new CloseSquareToken); - else if(code == 0x7b) emit(new OpenCurlyToken); - else if(code == 0x7d) emit(new CloseCurlyToken); - else if(digit(code)) switchto("number") && reconsume(); - else if(code == 0x55 || code == 0x75) { - if(next(1) == 0x2b && hexdigit(next(2))) consume() && switchto("unicode-range"); - else if((next(1) == 0x52 || next(1) == 0x72) && (next(2) == 0x4c || next(2) == 0x6c) && (next(3) == 0x28)) consume(3) && switchto("url"); - else switchto("identifier") && reconsume(); - } - else if(namestartchar(code)) switchto("identifier") && reconsume(); - else if(eof()) { emit(new EOFToken); return tokens; } - else emit(new DelimToken(code)); - break; - - case "double-quote-string": - if(currtoken == undefined) create(new StringToken); - - if(code == 0x22) emit() && switchto("data"); - else if(eof()) parseerror() && emit() && switchto("data"); - else if(newline(code)) parseerror() && emit(new BadStringToken) && switchto("data") && reconsume(); - else if(code == 0x5c) { - if(badescape(next())) parseerror() && emit(new BadStringToken) && switchto("data"); - else if(newline(next())) consume(); - else currtoken.append(consumeEscape()); - } - else currtoken.append(code); - break; - - case "single-quote-string": - if(currtoken == undefined) create(new StringToken); - - if(code == 0x27) emit() && switchto("data"); - else if(eof()) parseerror() && emit() && switchto("data"); - else if(newline(code)) parseerror() && emit(new BadStringToken) && switchto("data") && reconsume(); - else if(code == 0x5c) { - if(badescape(next())) parseerror() && emit(new BadStringToken) && switchto("data"); - else if(newline(next())) consume(); - else currtoken.append(consumeEscape()); - } - else currtoken.append(code); - break; - - case "hash": - if(namechar(code)) create(new HashToken(code)) && switchto("hash-rest"); - else if(code == 0x5c) { - if(badescape(next())) parseerror() && emit(new DelimToken(0x23)) && switchto("data") && reconsume(); - else create(new HashToken(consumeEscape())) && switchto('hash-rest'); - } - else emit(new DelimToken(0x23)) && switchto('data') && reconsume(); - break; - - case "hash-rest": - if(namechar(code)) currtoken.append(code); - else if(code == 0x5c) { - if(badescape(next())) parseerror() && emit(new DelimToken(0x23)) && switchto("data") && reconsume(); - else currtoken.append(consumeEscape()); - } - else emit() && switchto('data') && reconsume(); - break; - - case "comment": - if(code == 0x2a) { - if(next() == 0x2f) consume() && switchto('data'); - else donothing(); - } - else if(eof()) parseerror() && switchto('data') && reconsume(); - else donothing(); - break; - - case "at-keyword": - if(code == 0x2d) { - if(namestartchar(next())) consume() && create(new AtKeywordToken([0x40,code])) && switchto('at-keyword-rest'); - else emit(new DelimToken(0x40)) && switchto('data') && reconsume(); - } - else if(namestartchar(code)) create(new AtKeywordToken(code)) && switchto('at-keyword-rest'); - else if(code == 0x5c) { - if(badescape(next())) parseerror() && emit(new DelimToken(0x23)) && switchto("data") && reconsume(); - else create(new AtKeywordToken(consumeEscape())) && switchto('at-keyword-rest'); - } - else emit(new DelimToken(0x40)) && switchto('data') && reconsume(); - break; - - case "at-keyword-rest": - if(namechar(code)) currtoken.append(code); - else if(code == 0x5c) { - if(badescape(next())) parseerror() && emit() && switchto("data") && reconsume(); - else currtoken.append(consumeEscape()); - } - else emit() && switchto('data') && reconsume(); - break; - - case "identifier": - if(code == 0x2d) { - if(namestartchar(next())) create(new IdentifierToken(code)) && switchto('identifier-rest'); - else switchto('data') && reconsume(); - } - else if(namestartchar(code)) create(new IdentifierToken(code)) && switchto('identifier-rest'); - else if(code == 0x5c) { - if(badescape(next())) parseerror() && switchto("data") && reconsume(); - else create(new IdentifierToken(consumeEscape())) && switchto('identifier-rest'); - } - else switchto('data') && reconsume(); - break; - - case "identifier-rest": - if(namechar(code)) currtoken.append(code); - else if(code == 0x5c) { - if(badescape(next())) parseerror() && emit() && switchto("data") && reconsume(); - else currtoken.append(consumeEscape()); - } - else if(code == 0x28) emit(new FunctionToken(currtoken)) && switchto('data'); - else if(whitespace(code) && options.transformFunctionWhitespace) switchto('transform-function-whitespace'); - else emit() && switchto('data') && reconsume(); - break; - - case "transform-function-whitespace": - if(whitespace(code)) donothing(); - else if(code == 0x28) emit(new FunctionToken(currtoken)) && switchto('data'); - else emit() && switchto('data') && reconsume(); - break; - - case "number": - create(new NumberToken()); - - if(code == 0x2d) { - if(digit(next())) consume() && currtoken.append([0x2d,code]) && switchto('number-rest'); - else if(next(1) == 0x2e && digit(next(2))) consume(2) && currtoken.append([0x2d,0x2e,code]) && switchto('number-fraction'); - else switchto('data') && reconsume(); - } - else if(code == 0x2b) { - if(digit(next())) consume() && currtoken.append([0x2b,code]) && switchto('number-rest'); - else if(next(1) == 0x2e && digit(next(2))) consume(2) && currtoken.append([0x2b,0x2e,code]) && switchto('number-fraction'); - else switchto('data') && reconsume(); - } - else if(digit(code)) currtoken.append(code) && switchto('number-rest'); - else if(code == 0x2e) { - if(digit(next())) consume() && currtoken.append([0x2e,code]) && switchto('number-fraction'); - else switchto('data') && reconsume(); - } - else switchto('data') && reconsume(); - break; - - case "number-rest": - if(digit(code)) currtoken.append(code); - else if(code == 0x2e) { - if(digit(next())) consume() && currtoken.append([0x2e,code]) && switchto('number-fraction'); - else emit() && switchto('data') && reconsume(); - } - else if(code == 0x25) emit(new PercentageToken(currtoken)) && switchto('data') && reconsume(); - else if(code == 0x45 || code == 0x65) { - if(!options.scientificNotation) create(new DimensionToken(currtoken,code)) && switchto('dimension'); - else if(digit(next())) consume() && currtoken.append([0x25,code]) && switchto('sci-notation'); - else if((next(1) == 0x2b || next(1) == 0x2d) && digit(next(2))) currtoken.append([0x25,next(1),next(2)]) && consume(2) && switchto('sci-notation'); - else create(new DimensionToken(currtoken,code)) && switchto('dimension'); - } - else if(code == 0x2d) { - if(namestartchar(next())) consume() && create(new DimensionToken(currtoken,[0x2d,code])) && switchto('dimension'); - else if(next(1) == 0x5c && badescape(next(2))) parseerror() && emit() && switchto('data') && reconsume(); - else if(next(1) == 0x5c) consume() && create(new DimensionToken(currtoken, [0x2d,consumeEscape()])) && switchto('dimension'); - else emit() && switchto('data') && reconsume(); - } - else if(namestartchar(code)) create(new DimensionToken(currtoken, code)) && switchto('dimension'); - else if(code == 0x5c) { - if(badescape(next)) emit() && switchto('data') && reconsume(); - else create(new DimensionToken(currtoken,consumeEscape)) && switchto('dimension'); - } - else emit() && switchto('data') && reconsume(); - break; - - case "number-fraction": - currtoken.type = "number"; - - if(digit(code)) currtoken.append(code); - else if(code == 0x2e) emit() && switchto('data') && reconsume(); - else if(code == 0x25) emit(new PercentageToken(currtoken)) && switchto('data') && reconsume(); - else if(code == 0x45 || code == 0x65) { - if(!options.scientificNotation) create(new DimensionToken(currtoken,code)) && switchto('dimension'); - else if(digit(next())) consume() && currtoken.append([0x25,code]) && switchto('sci-notation'); - else if((next(1) == 0x2b || next(1) == 0x2d) && digit(next(2))) currtoken.append([0x25,next(1),next(2)]) && consume(2) && switchto('sci-notation'); - else create(new DimensionToken(currtoken,code)) && switchto('dimension'); - } - else if(code == 0x2d) { - if(namestartchar(next())) consume() && create(new DimensionToken(currtoken,[0x2d,code])) && switchto('dimension'); - else if(next(1) == 0x5c && badescape(next(2))) parseerror() && emit() && switchto('data') && reconsume(); - else if(next(1) == 0x5c) consume() && create(new DimensionToken(currtoken, [0x2d,consumeEscape()])) && switchto('dimension'); - else emit() && switchto('data') && reconsume(); - } - else if(namestartchar(code)) create(new DimensionToken(currtoken, code)) && switchto('dimension'); - else if(code == 0x5c) { - if(badescape(next)) emit() && switchto('data') && reconsume(); - else create(new DimensionToken(currtoken,consumeEscape)) && switchto('dimension'); - } - else emit() && switchto('data') && reconsume(); - break; - - case "dimension": - if(namechar(code)) currtoken.append(code); - else if(code == 0x5c) { - if(badescape(next())) parseerror() && emit() && switchto('data') && reconsume(); - else currtoken.append(consumeEscape()); - } - else emit() && switchto('data') && reconsume(); - break; - - case "sci-notation": - if(digit(code)) currtoken.append(code); - else emit() && switchto('data') && reconsume(); - break; - - case "url": - if(code == 0x22) switchto('url-double-quote'); - else if(code == 0x27) switchto('url-single-quote'); - else if(code == 0x29) emit(new URLToken) && switchto('data'); - else if(whitespace(code)) donothing(); - else switchto('url-unquoted') && reconsume(); - break; - - case "url-double-quote": - if(currtoken == undefined) create(new URLToken); - - if(code == 0x22) switchto('url-end'); - else if(newline(code)) parseerror() && switchto('bad-url'); - else if(code == 0x5c) { - if(newline(next())) consume(); - else if(badescape(next())) parseerror() && emit(new BadURLToken) && switchto('data') && reconsume(); - else currtoken.append(consumeEscape()); - } - else currtoken.append(code); - break; - - case "url-single-quote": - if(currtoken == undefined) create(new URLToken); - - if(code == 0x27) switchto('url-end'); - else if(newline(code)) parseerror() && switchto('bad-url'); - else if(code == 0x5c) { - if(newline(next())) consume(); - else if(badescape(next())) parseerror() && emit(new BadURLToken) && switchto('data') && reconsume(); - else currtoken.append(consumeEscape()); - } - else currtoken.append(code); - break; - - case "url-end": - if(whitespace(code)) donothing(); - else if(code == 0x29) emit() && switchto('data'); - else parseerror() && switchto('bad-url') && reconsume(); - break; - - case "url-unquoted": - if(currtoken == undefined) create(new URLToken); - - if(whitespace(code)) switchto('url-end'); - else if(code == 0x29) emit() && switchto('data'); - else if(code == 0x22 || code == 0x27 || code == 0x28 || nonprintable(code)) parseerror() && switchto('bad-url'); - else if(code == 0x5c) { - if(badescape(next())) parseerror() && switchto('bad-url'); - else currtoken.append(consumeEscape()); - } - else currtoken.append(code); - break; - - case "bad-url": - if(code == 0x29) emit(new BadURLToken) && switchto('data'); - else if(code == 0x5c) { - if(badescape(next())) donothing(); - else consumeEscape() - } - else donothing(); - break; - - case "unicode-range": - // We already know that the current code is a hexdigit. - - var start = [code], end = [code]; - - for(var total = 1; total < 6; total++) { - if(hexdigit(next())) { - consume(); - start.push(code); - end.push(code); - } - else break; - } - - if(next() == 0x3f) { - for(;total < 6; total++) { - if(next() == 0x3f) { - consume(); - start.push("0".charCodeAt(0)); - end.push("f".charCodeAt(0)); - } - else break; - } - emit(new UnicodeRangeToken(start,end)) && switchto('data'); - } - else if(next(1) == 0x2d && hexdigit(next(2))) { - consume(); - consume(); - end = [code]; - for(var total = 1; total < 6; total++) { - if(hexdigit(next())) { - consume(); - end.push(code); - } - else break; - } - emit(new UnicodeRangeToken(start,end)) && switchto('data'); - } - else emit(new UnicodeRangeToken(start)) && switchto('data'); - break; - - default: - console.log("Unknown state '" + state + "'"); + // None of the existing consumers want comments. + if (token.tokenType !== "comment") { + yield token; } } } -function stringFromCodeArray(arr) { - return String.fromCharCode.apply(null,arr.filter(function(e){return e;})); -} +exports.cssTokenizer = cssTokenizer; -function CSSParserToken(options) { return this; } -CSSParserToken.prototype.finish = function() { return this; } -CSSParserToken.prototype.toString = function() { return this.tokenType; } -CSSParserToken.prototype.toJSON = function() { return this.toString(); } +/** + * Pass |string| to the CSS lexer and return an array of all the + * returned tokens. Comment tokens are not included. In addition to + * the usual information, each token will have starting and ending + * line and column information attached. Specifically, each token + * has an additional "loc" attribute. This attribute is an object + * of the form {line: L, column: C}. Lines and columns are both zero + * based. + * + * It's best not to add new uses of this function. In general it is + * simpler and better to use the CSSToken offsets, rather than line + * and column. Also, this function lexes the entire input string at + * once, rather than lazily yielding a token stream. Use + * |cssTokenizer| or |DOMUtils.getCSSLexer| instead. + * + * @param{String} string The input string. + * @return {Array} An array of tokens (@see CSSToken) that have + * line and column information. + */ +function cssTokenizerWithLineColumn(string) { + let lexer = DOMUtils.getCSSLexer(string); + let result = []; + let prevToken = undefined; + while (true) { + let token = lexer.nextToken(); + let lineNumber = lexer.lineNumber; + let columnNumber = lexer.columnNumber; -function BadStringToken() { return this; } -BadStringToken.prototype = new CSSParserToken; -BadStringToken.prototype.tokenType = "BADSTRING"; - -function BadURLToken() { return this; } -BadURLToken.prototype = new CSSParserToken; -BadURLToken.prototype.tokenType = "BADURL"; - -function WhitespaceToken() { return this; } -WhitespaceToken.prototype = new CSSParserToken; -WhitespaceToken.prototype.tokenType = "WHITESPACE"; -WhitespaceToken.prototype.toString = function() { return "WS"; } - -function CDOToken() { return this; } -CDOToken.prototype = new CSSParserToken; -CDOToken.prototype.tokenType = "CDO"; - -function CDCToken() { return this; } -CDCToken.prototype = new CSSParserToken; -CDCToken.prototype.tokenType = "CDC"; - -function ColonToken() { return this; } -ColonToken.prototype = new CSSParserToken; -ColonToken.prototype.tokenType = ":"; - -function SemicolonToken() { return this; } -SemicolonToken.prototype = new CSSParserToken; -SemicolonToken.prototype.tokenType = ";"; - -function OpenCurlyToken() { return this; } -OpenCurlyToken.prototype = new CSSParserToken; -OpenCurlyToken.prototype.tokenType = "{"; - -function CloseCurlyToken() { return this; } -CloseCurlyToken.prototype = new CSSParserToken; -CloseCurlyToken.prototype.tokenType = "}"; - -function OpenSquareToken() { return this; } -OpenSquareToken.prototype = new CSSParserToken; -OpenSquareToken.prototype.tokenType = "["; - -function CloseSquareToken() { return this; } -CloseSquareToken.prototype = new CSSParserToken; -CloseSquareToken.prototype.tokenType = "]"; - -function OpenParenToken() { return this; } -OpenParenToken.prototype = new CSSParserToken; -OpenParenToken.prototype.tokenType = "("; - -function CloseParenToken() { return this; } -CloseParenToken.prototype = new CSSParserToken; -CloseParenToken.prototype.tokenType = ")"; - -function EOFToken() { return this; } -EOFToken.prototype = new CSSParserToken; -EOFToken.prototype.tokenType = "EOF"; - -function DelimToken(code) { - this.value = String.fromCharCode(code); - return this; -} -DelimToken.prototype = new CSSParserToken; -DelimToken.prototype.tokenType = "DELIM"; -DelimToken.prototype.toString = function() { return "DELIM("+this.value+")"; } - -function StringValuedToken() { return this; } -StringValuedToken.prototype = new CSSParserToken; -StringValuedToken.prototype.append = function(val) { - if(val instanceof Array) { - for(var i = 0; i < val.length; i++) { - this.value.push(val[i]); + if (prevToken) { + prevToken.loc.end = { + line: lineNumber, + column: columnNumber + }; } - } else { - this.value.push(val); - } - return true; -} -StringValuedToken.prototype.finish = function() { - this.value = stringFromCodeArray(this.value); - return this; -} -function IdentifierToken(val) { - this.value = []; - this.append(val); -} -IdentifierToken.prototype = new StringValuedToken; -IdentifierToken.prototype.tokenType = "IDENT"; -IdentifierToken.prototype.toString = function() { return "IDENT("+this.value+")"; } - -function FunctionToken(val) { - // These are always constructed by passing an IdentifierToken - this.value = val.finish().value; -} -FunctionToken.prototype = new CSSParserToken; -FunctionToken.prototype.tokenType = "FUNCTION"; -FunctionToken.prototype.toString = function() { return "FUNCTION("+this.value+")"; } - -function AtKeywordToken(val) { - this.value = []; - this.append(val); -} -AtKeywordToken.prototype = new StringValuedToken; -AtKeywordToken.prototype.tokenType = "AT-KEYWORD"; -AtKeywordToken.prototype.toString = function() { return "AT("+this.value+")"; } - -function HashToken(val) { - this.value = []; - this.append(val); -} -HashToken.prototype = new StringValuedToken; -HashToken.prototype.tokenType = "HASH"; -HashToken.prototype.toString = function() { return "HASH("+this.value+")"; } - -function StringToken(val) { - this.value = []; - this.append(val); -} -StringToken.prototype = new StringValuedToken; -StringToken.prototype.tokenType = "STRING"; -StringToken.prototype.toString = function() { return "\""+this.value+"\""; } - -function URLToken(val) { - this.value = []; - this.append(val); -} -URLToken.prototype = new StringValuedToken; -URLToken.prototype.tokenType = "URL"; -URLToken.prototype.toString = function() { return "URL("+this.value+")"; } - -function NumberToken(val) { - this.value = []; - this.append(val); - this.type = "integer"; -} -NumberToken.prototype = new StringValuedToken; -NumberToken.prototype.tokenType = "NUMBER"; -NumberToken.prototype.toString = function() { - if(this.type == "integer") - return "INT("+this.value+")"; - return "NUMBER("+this.value+")"; -} -NumberToken.prototype.finish = function() { - this.repr = stringFromCodeArray(this.value); - this.value = this.repr * 1; - if(Math.abs(this.value) % 1 != 0) this.type = "number"; - return this; -} - -function PercentageToken(val) { - // These are always created by passing a NumberToken as val - val.finish(); - this.value = val.value; - this.repr = val.repr; -} -PercentageToken.prototype = new CSSParserToken; -PercentageToken.prototype.tokenType = "PERCENTAGE"; -PercentageToken.prototype.toString = function() { return "PERCENTAGE("+this.value+")"; } - -function DimensionToken(val,unit) { - // These are always created by passing a NumberToken as the val - val.finish(); - this.num = val.value; - this.unit = []; - this.repr = val.repr; - this.append(unit); -} -DimensionToken.prototype = new CSSParserToken; -DimensionToken.prototype.tokenType = "DIMENSION"; -DimensionToken.prototype.toString = function() { return "DIM("+this.num+","+this.unit+")"; } -DimensionToken.prototype.append = function(val) { - if(val instanceof Array) { - for(var i = 0; i < val.length; i++) { - this.unit.push(val[i]); + if (!token) { + break; + } + + if (token.tokenType === "comment") { + // We've already dealt with the previous token's location. + prevToken = undefined; + } else { + let startLoc = { + line: lineNumber, + column: columnNumber + }; + token.loc = {start: startLoc}; + + result.push(token); + prevToken = token; } - } else { - this.unit.push(val); } - return true; -} -DimensionToken.prototype.finish = function() { - this.unit = stringFromCodeArray(this.unit); - this.repr += this.unit; - return this; + + return result; } -function UnicodeRangeToken(start,end) { - // start and end are array of char codes, completely finished - start = parseInt(stringFromCodeArray(start),16); - if(end === undefined) end = start + 1; - else end = parseInt(stringFromCodeArray(end),16); - - if(start > maximumallowedcodepoint) end = start; - if(end < start) end = start; - if(end > maximumallowedcodepoint) end = maximumallowedcodepoint; - - this.start = start; - this.end = end; - return this; -} -UnicodeRangeToken.prototype = new CSSParserToken; -UnicodeRangeToken.prototype.tokenType = "UNICODE-RANGE"; -UnicodeRangeToken.prototype.toString = function() { - if(this.start+1 == this.end) - return "UNICODE-RANGE("+this.start.toString(16).toUpperCase()+")"; - if(this.start < this.end) - return "UNICODE-RANGE("+this.start.toString(16).toUpperCase()+"-"+this.end.toString(16).toUpperCase()+")"; - return "UNICODE-RANGE()"; -} -UnicodeRangeToken.prototype.contains = function(code) { - return code >= this.start && code < this.end; -} - - -// Exportation. -// TODO: also export the various tokens objects? -module.exports = tokenize; - -})); +exports.cssTokenizerWithLineColumn = cssTokenizerWithLineColumn; diff --git a/browser/devtools/styleinspector/css-parsing-utils.js b/browser/devtools/styleinspector/css-parsing-utils.js index f287cf640170..fc82a8a197a1 100644 --- a/browser/devtools/styleinspector/css-parsing-utils.js +++ b/browser/devtools/styleinspector/css-parsing-utils.js @@ -6,36 +6,7 @@ "use strict"; -const cssTokenizer = require("devtools/sourceeditor/css-tokenizer"); - -/** - * Returns the string enclosed in quotes - */ -function quoteString(string) { - let hasDoubleQuotes = string.includes('"'); - let hasSingleQuotes = string.includes("'"); - - let quote = '"'; - if (hasDoubleQuotes && !hasSingleQuotes) { - quote = "'"; - } - - // Quote special characters as specified by the CSS grammar. - // See http://www.w3.org/TR/CSS2/syndata.html#tokenization - // and http://www.w3.org/TR/CSS2/syndata.html#strings - return quote + - string.replace(/[\\"]/g, match => { - switch (match) { - case '\\': - return '\\\\'; - case '"': - if (quote == '"') - return '\\"'; - return match; - } - }) + - quote; -} +const {cssTokenizer} = require("devtools/sourceeditor/css-tokenizer"); /** * Returns an array of CSS declarations given an string. @@ -52,6 +23,10 @@ function quoteString(string) { * [{"name": string, "value": string, "priority": string}, ...] */ function parseDeclarations(inputString) { + if (inputString === null || inputString === undefined) { + throw new Error("empty input string"); + } + let tokens = cssTokenizer(inputString); let declarations = [{name: "", value: "", priority: ""}]; @@ -60,7 +35,7 @@ function parseDeclarations(inputString) { for (let token of tokens) { lastProp = declarations[declarations.length - 1]; - if (token.tokenType === ":") { + if (token.tokenType === "symbol" && token.text === ":") { if (!lastProp.name) { // Set the current declaration name if there's no name yet lastProp.name = current.trim(); @@ -71,63 +46,29 @@ function parseDeclarations(inputString) { // with colons) current += ":"; } - } else if (token.tokenType === ";") { + } else if (token.tokenType === "symbol" && token.text === ";") { lastProp.value = current.trim(); current = ""; hasBang = false; declarations.push({name: "", value: "", priority: ""}); - } else { - switch(token.tokenType) { - case "IDENT": - if (token.value === "important" && hasBang) { - lastProp.priority = "important"; - hasBang = false; - } else { - if (hasBang) { - current += "!"; - } - current += token.value; - } - break; - case "WHITESPACE": - current += " "; - break; - case "DIMENSION": - current += token.repr; - break; - case "HASH": - current += "#" + token.value; - break; - case "URL": - current += "url(" + quoteString(token.value) + ")"; - break; - case "FUNCTION": - current += token.value + "("; - break; - case "(": - case ")": - current += token.tokenType; - break; - case "EOF": - break; - case "DELIM": - if (token.value === "!") { - hasBang = true; - } else { - current += token.value; - } - break; - case "STRING": - current += quoteString(token.value); - break; - case "{": - case "}": - current += token.tokenType; - break; - default: - current += token.value; - break; + } else if (token.tokenType === "ident") { + if (token.text === "important" && hasBang) { + lastProp.priority = "important"; + hasBang = false; + } else { + if (hasBang) { + current += "!"; + } + current += token.text; } + } else if (token.tokenType === "symbol" && token.text === "!") { + hasBang = true; + } else if (token.tokenType === "whitespace") { + current += " "; + } else if (token.tokenType === "comment") { + // For now, just ignore. + } else { + current += inputString.substring(token.startOffset, token.endOffset); } } diff --git a/browser/devtools/styleinspector/test/unit/test_parseDeclarations.js b/browser/devtools/styleinspector/test/unit/test_parseDeclarations.js index 547d10a7b253..7104629c217d 100644 --- a/browser/devtools/styleinspector/test/unit/test_parseDeclarations.js +++ b/browser/devtools/styleinspector/test/unit/test_parseDeclarations.js @@ -74,15 +74,15 @@ const TEST_DATA = [ // Test various types of background-image urls { input: "background-image: url(../../relative/image.png)", - expected: [{name: "background-image", value: "url(\"../../relative/image.png\")", priority: ""}] + expected: [{name: "background-image", value: "url(../../relative/image.png)", priority: ""}] }, { input: "background-image: url(http://site.com/test.png)", - expected: [{name: "background-image", value: "url(\"http://site.com/test.png\")", priority: ""}] + expected: [{name: "background-image", value: "url(http://site.com/test.png)", priority: ""}] }, { input: "background-image: url(wow.gif)", - expected: [{name: "background-image", value: "url(\"wow.gif\")", priority: ""}] + expected: [{name: "background-image", value: "url(wow.gif)", priority: ""}] }, // Test that urls with :;{} characters in them are parsed correctly { @@ -152,15 +152,15 @@ const TEST_DATA = [ {input: "wat: #XYZ", expected: [{name: "wat", value: "#XYZ", priority: ""}]}, // Test string/url quotes escaping {input: "content: \"this is a 'string'\"", expected: [{name: "content", value: "\"this is a 'string'\"", priority: ""}]}, - {input: 'content: "this is a \\"string\\""', expected: [{name: "content", value: '\'this is a "string"\'', priority: ""}]}, + {input: 'content: "this is a \\"string\\""', expected: [{name: "content", value: '"this is a \\"string\\""', priority: ""}]}, {input: "content: 'this is a \"string\"'", expected: [{name: "content", value: '\'this is a "string"\'', priority: ""}]}, - {input: "content: 'this is a \\'string\\'", expected: [{name: "content", value: '"this is a \'string\'"', priority: ""}]}, - {input: "content: 'this \\' is a \" really strange string'", expected: [{name: "content", value: '"this \' is a \\\" really strange string"', priority: ""}]}, + {input: "content: 'this is a \\'string\\''", expected: [{name: "content", value: "'this is a \\'string\\''", priority: ""}]}, + {input: "content: 'this \\' is a \" really strange string'", expected: [{name: "content", value: "'this \\' is a \" really strange string'", priority: ""}]}, { input: "content: \"a not s\\\ o very long title\"", expected: [ - {name: "content", value: '"a not s\ + {name: "content", value: '"a not s\\\ o very long title"', priority: ""} ] }, From f267022f4adabfc5b6c255d315e590e4c068e438 Mon Sep 17 00:00:00 2001 From: Amila Amunugama Date: Wed, 13 May 2015 11:39:12 -0700 Subject: [PATCH 04/54] Bug 1161495 - Remove line-height from the in-content preferences main pane r=dao --HG-- extra : amend_source : a6af8ad154d231938cd58c8c4b3c7f2414afaeb3 --- browser/themes/shared/incontentprefs/preferences.inc.css | 1 - 1 file changed, 1 deletion(-) diff --git a/browser/themes/shared/incontentprefs/preferences.inc.css b/browser/themes/shared/incontentprefs/preferences.inc.css index 7d46d5ea71a4..715685ea5cf7 100644 --- a/browser/themes/shared/incontentprefs/preferences.inc.css +++ b/browser/themes/shared/incontentprefs/preferences.inc.css @@ -10,7 +10,6 @@ padding: 0; font: message-box; font-size: 1.25rem; - line-height: 22px; } * { From 24944a62ae06d8530555447b7fc2747ec227032a Mon Sep 17 00:00:00 2001 From: Alexander Ploner Date: Sun, 10 May 2015 14:01:20 +0200 Subject: [PATCH 05/54] Bug 1148549 - Change menu items divider color to #D7D9DB. r=mcomella --- mobile/android/base/resources/values/styles.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mobile/android/base/resources/values/styles.xml b/mobile/android/base/resources/values/styles.xml index 6e90491a9bbd..cdaf19d9b99f 100644 --- a/mobile/android/base/resources/values/styles.xml +++ b/mobile/android/base/resources/values/styles.xml @@ -81,7 +81,7 @@ @@ -33,4 +36,5 @@ + diff --git a/browser/components/loop/test/desktop-local/panel_test.js b/browser/components/loop/test/desktop-local/panel_test.js index cef39568cd4e..bf839874e3ee 100644 --- a/browser/components/loop/test/desktop-local/panel_test.js +++ b/browser/components/loop/test/desktop-local/panel_test.js @@ -812,8 +812,7 @@ describe("loop.panel", function() { var node = view.getDOMNode(); // Select the checkbox - TestUtils.Simulate.change(node.querySelector(".context-checkbox"), - {"target": {"checked": true}}); + TestUtils.Simulate.click(node.querySelector(".checkbox-wrapper")); TestUtils.Simulate.click(node.querySelector(".new-room-button")); @@ -850,8 +849,8 @@ describe("loop.panel", function() { // Simulate being visible view.onDocumentVisible(); - var contextEnabledCheckbox = view.getDOMNode().querySelector(".context-enabled"); - expect(contextEnabledCheckbox).to.not.equal(null); + var contextContent = view.getDOMNode().querySelector(".context-content"); + expect(contextContent).to.not.equal(null); }); it("should not show context information when a URL is unavailable", function() { diff --git a/browser/locales/en-US/chrome/browser/loop/loop.properties b/browser/locales/en-US/chrome/browser/loop/loop.properties index 6c04e17c46eb..a29c87e02f3c 100644 --- a/browser/locales/en-US/chrome/browser/loop/loop.properties +++ b/browser/locales/en-US/chrome/browser/loop/loop.properties @@ -339,7 +339,7 @@ infobar_menuitem_dontshowagain_label=Don't show this again infobar_menuitem_dontshowagain_accesskey=D # Context in conversation strings -context_offer_label=Let's talk about this page + # LOCALIZATION NOTE (context_inroom_label): this string is followed by the # title/URL of the website you are having a conversation about, displayed on a # separate line. If this structure doesn't work for your locale, you might want From a065d4535dc06d07eb2a6337cd6b2933610f3397 Mon Sep 17 00:00:00 2001 From: Martyn Haigh Date: Thu, 14 May 2015 14:04:04 +0100 Subject: [PATCH 51/54] Bug 1159752 - Visible and selected tab incorrect when tab queue loaded and Fx isn't loaded (r=mcomella) --HG-- extra : rebase_source : 6273a8ddeda764f3fb5884a5d97f1e2ab30cdcba --- mobile/android/base/BrowserApp.java | 8 +- mobile/android/base/GeckoApp.java | 118 ++++++++++++++++------------ 2 files changed, 72 insertions(+), 54 deletions(-) diff --git a/mobile/android/base/BrowserApp.java b/mobile/android/base/BrowserApp.java index e78a5bab60e7..f5436ac9a34b 100644 --- a/mobile/android/base/BrowserApp.java +++ b/mobile/android/base/BrowserApp.java @@ -914,8 +914,9 @@ public class BrowserApp extends GeckoApp checkFirstrun(this, new SafeIntent(getIntent())); } - private void processTabQueue() { - if (AppConstants.NIGHTLY_BUILD && AppConstants.MOZ_ANDROID_TAB_QUEUE) { + @Override + protected void processTabQueue() { + if (AppConstants.NIGHTLY_BUILD && AppConstants.MOZ_ANDROID_TAB_QUEUE && mInitialized) { ThreadUtils.postToBackgroundThread(new Runnable() { @Override public void run() { @@ -927,7 +928,8 @@ public class BrowserApp extends GeckoApp } } - private void openQueuedTabs() { + @Override + protected void openQueuedTabs() { ThreadUtils.assertNotOnUiThread(); int queuedTabCount = TabQueueHelper.getTabQueueLength(BrowserApp.this); diff --git a/mobile/android/base/GeckoApp.java b/mobile/android/base/GeckoApp.java index 931e05fb67b9..46119c478ece 100644 --- a/mobile/android/base/GeckoApp.java +++ b/mobile/android/base/GeckoApp.java @@ -5,24 +5,6 @@ package org.mozilla.gecko; -import java.io.ByteArrayOutputStream; -import java.io.File; -import java.io.IOException; -import java.io.InputStream; -import java.net.URL; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.HashSet; -import java.util.Iterator; -import java.util.LinkedList; -import java.util.List; -import java.util.Locale; -import java.util.Map; -import java.util.Set; - -import org.json.JSONArray; -import org.json.JSONException; -import org.json.JSONObject; import org.mozilla.gecko.AppConstants.Versions; import org.mozilla.gecko.GeckoProfileDirectories.NoMozillaDirectoryException; import org.mozilla.gecko.db.BrowserDB; @@ -112,6 +94,24 @@ import android.widget.RelativeLayout; import android.widget.SimpleAdapter; import android.widget.TextView; import android.widget.Toast; +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.net.URL; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Set; public abstract class GeckoApp extends GeckoActivity @@ -204,6 +204,10 @@ public abstract class GeckoApp abstract protected String getDefaultProfileName() throws NoMozillaDirectoryException; + protected void processTabQueue() {}; + + protected void openQueuedTabs() {}; + @SuppressWarnings("serial") class SessionRestoreException extends Exception { public SessionRestoreException(Exception e) { @@ -1509,17 +1513,24 @@ public abstract class GeckoApp // Restore tabs before opening an external URL so that the new tab // is animated properly. Tabs.getInstance().notifyListeners(null, Tabs.TabEvents.RESTORED); - int flags = Tabs.LOADURL_NEW_TAB | Tabs.LOADURL_USER_ENTERED | Tabs.LOADURL_EXTERNAL; - if (ACTION_HOMESCREEN_SHORTCUT.equals(action)) { - flags |= Tabs.LOADURL_PINNED; - } - loadStartupTab(passedUri, intent, flags); + processActionViewIntent(new Runnable() { + @Override + public void run() { + int flags = Tabs.LOADURL_NEW_TAB | Tabs.LOADURL_USER_ENTERED | Tabs.LOADURL_EXTERNAL; + if (ACTION_HOMESCREEN_SHORTCUT.equals(action)) { + flags |= Tabs.LOADURL_PINNED; + } + loadStartupTab(passedUri, intent, flags); + } + }); } else { if (!mIsRestoringActivity) { loadStartupTabWithAboutHome(Tabs.LOADURL_NEW_TAB); } Tabs.getInstance().notifyListeners(null, Tabs.TabEvents.RESTORED); + + processTabQueue(); } // If we're not restoring, move the session file so it can be read for @@ -1598,6 +1609,34 @@ public abstract class GeckoApp } } + protected void processActionViewIntent(final Runnable openTabsRunnable) { + // We need to ensure that if we receive a VIEW action and there are tabs queued then the + // site loaded from the intent is on top (last loaded) and selected with all other tabs + // being opened behind it. We process the tab queue first and request a callback from the JS - the + // listener will open the url from the intent as normal when the tab queue has been processed. + ThreadUtils.postToBackgroundThread(new Runnable() { + @Override + public void run() { + if (AppConstants.NIGHTLY_BUILD && AppConstants.MOZ_ANDROID_TAB_QUEUE + && TabQueueHelper.shouldOpenTabQueueUrls(GeckoApp.this)) { + + EventDispatcher.getInstance().registerGeckoThreadListener(new NativeEventListener() { + @Override + public void handleMessage(String event, NativeJSObject message, EventCallback callback) { + if ("Tabs:TabsOpened".equals(event)) { + EventDispatcher.getInstance().unregisterGeckoThreadListener(this, "Tabs:TabsOpened"); + openTabsRunnable.run(); + } + } + }, "Tabs:TabsOpened"); + TabQueueHelper.openQueuedUrls(GeckoApp.this, mProfile, TabQueueHelper.FILE_NAME, true); + } else { + openTabsRunnable.run(); + } + } + }); + } + private String restoreSessionTabs(final boolean isExternalURL) throws SessionRestoreException { try { String sessionString = getProfile().readSessionFile(false); @@ -1802,36 +1841,13 @@ public abstract class GeckoApp String uri = intent.getDataString(); Tabs.getInstance().loadUrl(uri); } else if (Intent.ACTION_VIEW.equals(action)) { - // We need to ensure that if we receive a VIEW action and there are tabs queued then the - // site loaded from the intent is op top (last loaded) and selected with all other tabs - // being opened behind it. We process the tab queue first and request a callback from the JS - the - // listener will open the url from the intent as normal when the tab queue has been processed. - ThreadUtils.postToBackgroundThread(new Runnable() { + processActionViewIntent(new Runnable() { @Override public void run() { - if (AppConstants.NIGHTLY_BUILD && AppConstants.MOZ_ANDROID_TAB_QUEUE - && TabQueueHelper.shouldOpenTabQueueUrls(GeckoApp.this)) { - - EventDispatcher.getInstance().registerGeckoThreadListener(new NativeEventListener() { - @Override - public void handleMessage(String event, NativeJSObject message, EventCallback callback) { - if ("Tabs:TabsOpened".equals(event)) { - EventDispatcher.getInstance().unregisterGeckoThreadListener(this, "Tabs:TabsOpened"); - String uri = intent.getDataString(); - Tabs.getInstance().loadUrl(uri, Tabs.LOADURL_NEW_TAB | - Tabs.LOADURL_USER_ENTERED | - Tabs.LOADURL_EXTERNAL); - } - } - }, "Tabs:TabsOpened"); - - TabQueueHelper.openQueuedUrls(GeckoApp.this, mProfile, TabQueueHelper.FILE_NAME, true); - } else { - final String url = intent.getDataString(); - Tabs.getInstance().loadUrlWithIntentExtras(url, intent, Tabs.LOADURL_NEW_TAB | - Tabs.LOADURL_USER_ENTERED | - Tabs.LOADURL_EXTERNAL); - } + final String url = intent.getDataString(); + Tabs.getInstance().loadUrlWithIntentExtras(url, intent, Tabs.LOADURL_NEW_TAB | + Tabs.LOADURL_USER_ENTERED | + Tabs.LOADURL_EXTERNAL); } }); } else if (ACTION_HOMESCREEN_SHORTCUT.equals(action)) { From f66879ad0482a8021d0d6f00987a0b8a183eab3e Mon Sep 17 00:00:00 2001 From: Martyn Haigh Date: Fri, 8 May 2015 17:12:09 +0100 Subject: [PATCH 52/54] Bug 1155911 - [tab queue] Opening external links can cause two nightly apps in task switcher (r=rnewman) --HG-- extra : rebase_source : 2e18c4d16c7896d3d14dab19eefb2306146aae66 extra : amend_source : 2b5700ff7c00b391f7f556711839e29875c01dcb --- mobile/android/base/AndroidManifest.xml.in | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/mobile/android/base/AndroidManifest.xml.in b/mobile/android/base/AndroidManifest.xml.in index 826e695d043d..36124f2e5460 100644 --- a/mobile/android/base/AndroidManifest.xml.in +++ b/mobile/android/base/AndroidManifest.xml.in @@ -254,7 +254,8 @@ - + From 80dddf5864131316c46287f0a561145e3e8c2e88 Mon Sep 17 00:00:00 2001 From: Paolo Amadini Date: Wed, 13 May 2015 15:34:14 +0100 Subject: [PATCH 53/54] Bug 1149975 - Part 1 of 2 - Handle visibility of the login fill doorhanger anchor. r=MattN --HG-- extra : rebase_source : fdc290d5179bf79af1198bf62b5aac98e30aa894 --- browser/base/content/browser.css | 4 +- browser/base/content/browser.xul | 1 + browser/base/content/content.js | 8 +- browser/base/content/urlbarBindings.xml | 2 +- .../themes/shared/login-doorhanger.inc.css | 14 ++ .../themes/shared/notification-icons.inc.css | 7 + mobile/android/chrome/content/browser.js | 7 +- mobile/android/chrome/content/content.js | 5 + modules/libpref/init/all.js | 1 + .../passwordmgr/LoginDoorhangers.jsm | 65 ++++++- .../passwordmgr/LoginManagerContent.jsm | 158 ++++++++++++++++-- .../passwordmgr/LoginManagerParent.jsm | 139 ++++++++++++++- .../components/passwordmgr/nsLoginManager.js | 17 -- 13 files changed, 376 insertions(+), 52 deletions(-) diff --git a/browser/base/content/browser.css b/browser/base/content/browser.css index 106cae155a22..d64b9ac05950 100644 --- a/browser/base/content/browser.css +++ b/browser/base/content/browser.css @@ -773,8 +773,8 @@ window[chromehidden~="toolbar"] toolbar:not(#nav-bar):not(#TabsToolbar):not(#pri -moz-binding: url("chrome://browser/content/urlbarBindings.xml#click-to-play-plugins-notification"); } -#password-fill-notification { - -moz-binding: url("chrome://browser/content/urlbarBindings.xml#password-fill-notification"); +#login-fill-notification { + -moz-binding: url("chrome://browser/content/urlbarBindings.xml#login-fill-notification"); } .login-fill-item { diff --git a/browser/base/content/browser.xul b/browser/base/content/browser.xul index 3b4e715c5baf..e62e99ca8c5e 100644 --- a/browser/base/content/browser.xul +++ b/browser/base/content/browser.xul @@ -783,6 +783,7 @@ + diff --git a/browser/base/content/content.js b/browser/base/content/content.js index 0d6b73b841c7..d42d5636c813 100644 --- a/browser/base/content/content.js +++ b/browser/base/content/content.js @@ -49,9 +49,15 @@ addMessageListener("ContextMenu:DoCustomCommand", function(message) { PageMenuChild.executeMenu(message.data); }); +addMessageListener("RemoteLogins:fillForm", function(message) { + LoginManagerContent.receiveMessage(message, content); +}); addEventListener("DOMFormHasPassword", function(event) { + LoginManagerContent.onDOMFormHasPassword(event, content); InsecurePasswordUtils.checkForInsecurePasswords(event.target); - LoginManagerContent.onFormPassword(event); +}); +addEventListener("pageshow", function(event) { + LoginManagerContent.onPageShow(event, content); }); addEventListener("DOMAutoComplete", function(event) { LoginManagerContent.onUsernameInput(event); diff --git a/browser/base/content/urlbarBindings.xml b/browser/base/content/urlbarBindings.xml index 0a6cb2ad71a5..3e777a7fb8e5 100644 --- a/browser/base/content/urlbarBindings.xml +++ b/browser/base/content/urlbarBindings.xml @@ -2781,7 +2781,7 @@ file, You can obtain one at http://mozilla.org/MPL/2.0/. which is empty because the actual panel is not implemented inside an XBL binding, but made of elements added to the notification panel. This allows accessing the full structure while the panel is hidden. --> - + diff --git a/browser/themes/shared/login-doorhanger.inc.css b/browser/themes/shared/login-doorhanger.inc.css index c153d67365e6..1b32443a0027 100644 --- a/browser/themes/shared/login-doorhanger.inc.css +++ b/browser/themes/shared/login-doorhanger.inc.css @@ -8,11 +8,25 @@ max-height: 20em; } +.login-fill-item[disabled] { + color: #888; + background-color: #fff; +} + +.login-fill-item[disabled][selected] { + background-color: #eef; +} + .login-hostname { margin: 4px; font-weight: bold; } +.login-fill-item.different-hostname > .login-hostname { + color: #888; + font-style: italic; +} + .login-username { margin: 4px; color: #888; diff --git a/browser/themes/shared/notification-icons.inc.css b/browser/themes/shared/notification-icons.inc.css index dffdae3115f7..63361586a779 100644 --- a/browser/themes/shared/notification-icons.inc.css +++ b/browser/themes/shared/notification-icons.inc.css @@ -140,6 +140,12 @@ list-style-image: url(chrome://mozapps/skin/passwordmgr/key-16.png); } +#login-fill-notification-icon { + /* Temporary icon until the capture and fill doorhangers are unified. */ + list-style-image: url(chrome://mozapps/skin/passwordmgr/key-16.png); + transform: scaleX(-1); +} + .webapps-notification-icon, #webapps-notification-icon { list-style-image: url(chrome://global/skin/icons/webapps-16.png); @@ -311,6 +317,7 @@ list-style-image: url(chrome://mozapps/skin/extensions/extensionGeneric.png); } + #login-fill-notification-icon, #password-notification-icon { list-style-image: url(chrome://mozapps/skin/passwordmgr/key-16@2x.png); } diff --git a/mobile/android/chrome/content/browser.js b/mobile/android/chrome/content/browser.js index 872db84b2540..317aa2cd186f 100644 --- a/mobile/android/chrome/content/browser.js +++ b/mobile/android/chrome/content/browser.js @@ -4223,7 +4223,8 @@ Tab.prototype = { } case "DOMFormHasPassword": { - LoginManagerContent.onFormPassword(aEvent); + LoginManagerContent.onDOMFormHasPassword(aEvent, + this.browser.contentWindow); break; } @@ -4365,7 +4366,9 @@ Tab.prototype = { } case "pageshow": { - // only send pageshow for the top-level document + LoginManagerContent.onPageShow(aEvent, this.browser.contentWindow); + + // The rest of this only handles pageshow for the top-level document. if (aEvent.originalTarget.defaultView != this.browser.contentWindow) return; diff --git a/mobile/android/chrome/content/content.js b/mobile/android/chrome/content/content.js index 125952a06721..5a345566d812 100644 --- a/mobile/android/chrome/content/content.js +++ b/mobile/android/chrome/content/content.js @@ -10,6 +10,7 @@ Cu.import("resource://gre/modules/XPCOMUtils.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "AboutReader", "resource://gre/modules/AboutReader.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "ReaderMode", "resource://gre/modules/ReaderMode.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "LoginManagerContent", "resource://gre/modules/LoginManagerContent.jsm"); let dump = Cu.import("resource://gre/modules/AndroidLog.jsm", {}).AndroidLog.d.bind(null, "Content"); @@ -87,3 +88,7 @@ let AboutReaderListener = { } }; AboutReaderListener.init(); + +addMessageListener("RemoteLogins:fillForm", function(message) { + LoginManagerContent.receiveMessage(message, content); +}); diff --git a/modules/libpref/init/all.js b/modules/libpref/init/all.js index f1f8ff04de8e..bde9fc2492d7 100644 --- a/modules/libpref/init/all.js +++ b/modules/libpref/init/all.js @@ -3812,6 +3812,7 @@ pref("signon.rememberSignons", true); pref("signon.autofillForms", true); pref("signon.autologin.proxy", false); pref("signon.storeWhenAutocompleteOff", true); +pref("signon.ui.experimental", false); pref("signon.debug", false); // Satchel (Form Manager) prefs diff --git a/toolkit/components/passwordmgr/LoginDoorhangers.jsm b/toolkit/components/passwordmgr/LoginDoorhangers.jsm index 1aaf2816ce21..58b420a8619f 100644 --- a/toolkit/components/passwordmgr/LoginDoorhangers.jsm +++ b/toolkit/components/passwordmgr/LoginDoorhangers.jsm @@ -11,6 +11,7 @@ this.EXPORTED_SYMBOLS = [ const { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components; Cu.import("resource://gre/modules/Services.jsm"); +Cu.import("resource://gre/modules/LoginManagerParent.jsm"); const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"; @@ -27,10 +28,8 @@ this.LoginDoorhangers.FillDoorhanger = function (properties) { this.onListDblClick = this.onListDblClick.bind(this); this.onListKeyPress = this.onListKeyPress.bind(this); - this.filterString = properties.filterString; - - if (properties.browser) { - this.browser = properties.browser; + for (let name of Object.getOwnPropertyNames(properties)) { + this[name] = properties[name]; } }; @@ -48,20 +47,25 @@ this.LoginDoorhangers.FillDoorhanger.prototype = { * web page is moved to a different chrome window by the swapDocShells method. */ set browser(browser) { + const MAX_DATE_VALUE = new Date(8640000000000000); + this._browser = browser; let doorhanger = this; let PopupNotifications = this.chomeDocument.defaultView.PopupNotifications; let notification = PopupNotifications.show( browser, - "password-fill", + "login-fill", "", - "password-notification-icon", + "login-fill-notification-icon", null, null, { dismissed: true, - persistWhileVisible: true, + // This will make the anchor persist forever even if the popup is not + // visible. We'll remove the notification manually when the page + // changes, after we had time to check its final state asynchronously. + timeout: MAX_DATE_VALUE, eventCallback: function (topic, otherBrowser) { switch (topic) { case "shown": @@ -69,6 +73,8 @@ this.LoginDoorhangers.FillDoorhanger.prototype = { // be called after the "show" method returns, so the reference to // "this.notification" will be available at this point. doorhanger.bound = true; + doorhanger.promiseHidden = + new Promise(resolve => doorhanger.onUnbind = resolve); doorhanger.bind(); break; @@ -76,11 +82,12 @@ this.LoginDoorhangers.FillDoorhanger.prototype = { case "removed": if (doorhanger.bound) { doorhanger.unbind(); + doorhanger.onUnbind(); } break; case "swapping": - this._browser = otherBrowser; + doorhanger._browser = otherBrowser; return true; } return false; @@ -116,6 +123,11 @@ this.LoginDoorhangers.FillDoorhanger.prototype = { } }, + /** + * Promise resolved as soon as the notification is hidden. + */ + promiseHidden: Promise.resolve(), + /** * Removes the doorhanger from the browser. */ @@ -159,6 +171,18 @@ this.LoginDoorhangers.FillDoorhanger.prototype = { this.chomeDocument.getElementById("mainPopupSet").appendChild(this.element); }, + /** + * Origin for which the manual fill UI should be displayed, for example + * "http://www.example.com". + */ + loginFormOrigin: "", + + /** + * When no login form is present on the page, we may still display a list of + * logins, but we cannot offer manual filling. + */ + loginFormPresent: false, + /** * User-editable string used to filter the list of all logins. */ @@ -192,6 +216,12 @@ this.LoginDoorhangers.FillDoorhanger.prototype = { item.classList.add("login-fill-item"); item.setAttribute("hostname", hostname); item.setAttribute("username", username); + if (hostname != this.loginFormOrigin) { + item.classList.add("different-hostname"); + } + if (!this.loginFormPresent) { + item.setAttribute("disabled", "true"); + } this.list.appendChild(item); } }, @@ -222,6 +252,23 @@ this.LoginDoorhangers.FillDoorhanger.prototype = { this.fillLogin(); }, fillLogin() { + if (this.list.selectedItem.hasAttribute("disabled")) { + return; + } + let formLogins = Services.logins.findLogins({}, "", "", null); + let login = formLogins.find(login => { + return login.hostname == this.list.selectedItem.getAttribute("hostname") && + login.username == this.list.selectedItem.getAttribute("username"); + }); + if (login) { + LoginManagerParent.fillForm({ + browser: this.browser, + loginFormOrigin: this.loginFormOrigin, + login, + }).catch(Cu.reportError); + } else { + Cu.reportError("The selected login has been removed in the meantime."); + } this.hide(); }, }; @@ -238,7 +285,7 @@ this.LoginDoorhangers.FillDoorhanger.prototype = { */ this.LoginDoorhangers.FillDoorhanger.find = function ({ browser }) { let PopupNotifications = browser.ownerDocument.defaultView.PopupNotifications; - let notification = PopupNotifications.getNotification("password-fill", + let notification = PopupNotifications.getNotification("login-fill", browser); return notification && notification.doorhanger; }; diff --git a/toolkit/components/passwordmgr/LoginManagerContent.jsm b/toolkit/components/passwordmgr/LoginManagerContent.jsm index a108fcf91b19..b26891d4cc34 100644 --- a/toolkit/components/passwordmgr/LoginManagerContent.jsm +++ b/toolkit/components/passwordmgr/LoginManagerContent.jsm @@ -164,7 +164,7 @@ var LoginManagerContent = { return deferred.promise; }, - receiveMessage: function (msg) { + receiveMessage: function (msg, window) { // Convert an array of logins in simple JS-object form to an array of // nsILoginInfo objects. function jsLoginsToXPCOM(logins) { @@ -179,6 +179,16 @@ var LoginManagerContent = { }); } + if (msg.name == "RemoteLogins:fillForm") { + this.fillForm({ + topDocument: window.document, + loginFormOrigin: msg.data.loginFormOrigin, + loginsFound: jsLoginsToXPCOM(msg.data.logins), + recipes: msg.data.recipes, + }); + return; + } + let request = this._takeRequest(msg); switch (msg.name) { case "RemoteLogins:loginsFound": { @@ -253,35 +263,145 @@ var LoginManagerContent = { messageData); }, - /* - * onFormPassword - * - * Called when an element is added to the page - */ - onFormPassword: function (event) { - if (!event.isTrusted) + onDOMFormHasPassword(event, window) { + if (!event.isTrusted) { return; + } + let form = event.target; - let doc = form.ownerDocument; - let win = doc.defaultView; - let messageManager = messageManagerFromWindow(win); + // Always record the most recently added form with a password field. + this.stateForDocument(form.ownerDocument).loginForm = form; + + this._updateLoginFormPresence(window); + + let messageManager = messageManagerFromWindow(window); messageManager.sendAsyncMessage("LoginStats:LoginEncountered"); - if (!gEnabled) + if (!gEnabled) { return; + } - log("onFormPassword for", form.ownerDocument.documentURI); + log("onDOMFormHasPassword for", form.ownerDocument.documentURI); this._getLoginDataFromParent(form, { showMasterPassword: true }) .then(this.loginsFound.bind(this)) .then(null, Cu.reportError); }, + onPageShow(event, window) { + this._updateLoginFormPresence(window); + }, + + /** + * Maps all DOM content documents in this content process, including those in + * frames, to the current state used by the Login Manager. + */ + loginFormStateByDocument: new WeakMap(), + + /** + * Retrieves a reference to the state object associated with the given + * document. This is initialized to an empty object. + */ + stateForDocument(document) { + let loginFormState = this.loginFormStateByDocument.get(document); + if (!loginFormState) { + loginFormState = {}; + this.loginFormStateByDocument.set(document, loginFormState); + } + return loginFormState; + }, + + /** + * Compute whether there is a login form on any frame of the current page, and + * notify the parent process. This is one of the factors used to control the + * visibility of the password fill doorhanger anchor. + */ + _updateLoginFormPresence(topWindow) { + // For the login form presence notification, we currently support only one + // origin for each browser, so the form origin will always match the origin + // of the top level document. + let loginFormOrigin = + LoginUtils._getPasswordOrigin(topWindow.document.documentURI); + + // Returns the first known loginForm present in this window or in any + // same-origin subframes. Returns null if no loginForm is currently present. + let getFirstLoginForm = thisWindow => { + let loginForm = this.stateForDocument(thisWindow.document).loginForm; + if (loginForm) { + return loginForm; + } + for (let i = 0; i < thisWindow.frames.length; i++) { + let frame = thisWindow.frames[i]; + if (LoginUtils._getPasswordOrigin(frame.document.documentURI) != + loginFormOrigin) { + continue; + } + let loginForm = getFirstLoginForm(frame); + if (loginForm) { + return loginForm; + } + } + return null; + }; + + // Store the actual form to use on the state for the top-level document. + let topState = this.stateForDocument(topWindow.document); + topState.loginFormForFill = getFirstLoginForm(topWindow); + + // Determine whether to show the anchor icon for the current tab. + let messageManager = messageManagerFromWindow(topWindow); + messageManager.sendAsyncMessage("RemoteLogins:updateLoginFormPresence", { + loginFormOrigin, + loginFormPresent: !!topState.loginFormForFill, + }); + }, + + /** + * Perform a password fill upon user request coming from the parent process. + * The fill will be in the form previously identified during page navigation. + * + * @param An object with the following properties: + * { + * topDocument: + * DOM document currently associated to the the top-level window + * for which the fill is requested. This may be different from the + * document that originally caused the login UI to be displayed. + * loginFormOrigin: + * String with the origin for which the login UI was displayed. + * This must match the origin of the form used for the fill. + * loginsFound: + * Array containing the login to fill. While other messages may + * have more logins, for this use case this is expected to have + * exactly one element. The origin of the login may be different + * from the origin of the form used for the fill. + * recipes: + * Fill recipes transmitted together with the original message. + * } + */ + fillForm({ topDocument, loginFormOrigin, loginsFound, recipes }) { + let topState = this.stateForDocument(topDocument); + if (!topState.loginFormForFill) { + log("fillForm: There is no login form anymore. The form may have been", + "removed or the document may have changed."); + return; + } + if (LoginUtils._getPasswordOrigin(topDocument.documentURI) != + loginFormOrigin) { + log("fillForm: The requested origin doesn't match the one form the", + "document. This may mean we navigated to a document from a different", + "site before we had a chance to indicate this change in the user", + "interface."); + return; + } + this._fillForm(topState.loginFormForFill, true, true, true, true, + loginsFound, recipes); + }, + loginsFound: function({ form, loginsFound, recipes }) { let doc = form.ownerDocument; let autofillForm = gAutofillForms && !PrivateBrowsingUtils.isContentWindowPrivate(doc.defaultView); - this._fillForm(form, autofillForm, false, false, loginsFound, recipes); + this._fillForm(form, autofillForm, false, false, false, loginsFound, recipes); }, /* @@ -324,7 +444,7 @@ var LoginManagerContent = { if (usernameField == acInputField && passwordField) { this._getLoginDataFromParent(acForm, { showMasterPassword: false }) .then(({ form, loginsFound, recipes }) => { - this._fillForm(form, true, true, true, loginsFound, recipes); + this._fillForm(form, true, false, true, true, loginsFound, recipes); }) .then(null, Cu.reportError); } else { @@ -626,6 +746,8 @@ var LoginManagerContent = { * * @param {HTMLFormElement} form * @param {bool} autofillForm denotes if we should fill the form in automatically + * @param {bool} clobberUsername controls if an existing username can be + * overwritten * @param {bool} clobberPassword controls if an existing password value can be * overwritten * @param {bool} userTriggered is an indication of whether this filling was triggered by @@ -633,7 +755,7 @@ var LoginManagerContent = { * @param {nsILoginInfo[]} foundLogins is an array of nsILoginInfo that could be used for the form * @param {Set} recipes that could be used to affect how the form is filled */ - _fillForm : function (form, autofillForm, clobberPassword, + _fillForm : function (form, autofillForm, clobberUsername, clobberPassword, userTriggered, foundLogins, recipes) { let ignoreAutocomplete = true; const AUTOFILL_RESULT = { @@ -737,7 +859,9 @@ var LoginManagerContent = { // Select a login to use for filling in the form. var selectedLogin; - if (usernameField && (usernameField.value || usernameField.disabled || usernameField.readOnly)) { + if (!clobberUsername && usernameField && (usernameField.value || + usernameField.disabled || + usernameField.readOnly)) { // If username was specified in the field, it's disabled or it's readOnly, only fill in the // password if we find a matching login. var username = usernameField.value.toLowerCase(); diff --git a/toolkit/components/passwordmgr/LoginManagerParent.jsm b/toolkit/components/passwordmgr/LoginManagerParent.jsm index 0460a833a6fb..e16de58131a6 100644 --- a/toolkit/components/passwordmgr/LoginManagerParent.jsm +++ b/toolkit/components/passwordmgr/LoginManagerParent.jsm @@ -15,6 +15,10 @@ XPCOMUtils.defineLazyModuleGetter(this, "UserAutoCompleteResult", "resource://gre/modules/LoginManagerContent.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "AutoCompleteE10S", "resource://gre/modules/AutoCompleteE10S.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "DeferredTask", + "resource://gre/modules/DeferredTask.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "LoginDoorhangers", + "resource://gre/modules/LoginDoorhangers.jsm"); this.EXPORTED_SYMBOLS = [ "LoginManagerParent", "PasswordsMetricsProvider" ]; @@ -168,6 +172,7 @@ var LoginManagerParent = { mm.addMessageListener("RemoteLogins:findLogins", this); mm.addMessageListener("RemoteLogins:onFormSubmit", this); mm.addMessageListener("RemoteLogins:autoCompleteLogins", this); + mm.addMessageListener("RemoteLogins:updateLoginFormPresence", this); mm.addMessageListener("LoginStats:LoginEncountered", this); mm.addMessageListener("LoginStats:LoginFillSuccessful", this); Services.obs.addObserver(this, "LoginStats:NewSavedPassword", false); @@ -216,6 +221,11 @@ var LoginManagerParent = { break; } + case "RemoteLogins:updateLoginFormPresence": { + this.updateLoginFormPresence(msg.target, data); + break; + } + case "RemoteLogins:autoCompleteLogins": { this.doAutocompleteSearch(data, msg.target); break; @@ -241,6 +251,33 @@ var LoginManagerParent = { } }, + /** + * Trigger a login form fill and send relevant data (e.g. logins and recipes) + * to the child process (LoginManagerContent). + */ + fillForm: Task.async(function* ({ browser, loginFormOrigin, login }) { + let recipes = []; + if (loginFormOrigin) { + let formHost; + try { + formHost = (new URL(loginFormOrigin)).host; + let recipeManager = yield this.recipeParentPromise; + recipes = recipeManager.getRecipesForHost(formHost); + } catch (ex) { + // Some schemes e.g. chrome aren't supported by URL + } + } + + // Convert the array of nsILoginInfo to vanilla JS objects since nsILoginInfo + // doesn't support structured cloning. + let jsLogins = JSON.parse(JSON.stringify([login])); + browser.messageManager.sendAsyncMessage("RemoteLogins:fillForm", { + loginFormOrigin, + logins: jsLogins, + recipes, + }); + }), + /** * Send relevant data (e.g. logins and recipes) to the child process (LoginManagerContent). */ @@ -281,14 +318,14 @@ var LoginManagerParent = { // If we're currently displaying a master password prompt, defer // processing this form until the user handles the prompt. if (Services.logins.uiBusy) { - log("deferring onFormPassword for", formOrigin); + log("deferring sendLoginDataToChild for", formOrigin); let self = this; let observer = { QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver, Ci.nsISupportsWeakReference]), observe: function (subject, topic, data) { - log("Got deferred onFormPassword notification:", topic); + log("Got deferred sendLoginDataToChild notification:", topic); // Only run observer once. Services.obs.removeObserver(this, "passwordmgr-crypto-login"); Services.obs.removeObserver(this, "passwordmgr-crypto-loginCanceled"); @@ -507,5 +544,101 @@ var LoginManagerParent = { // Prompt user to save login (via dialog or notification bar) prompter = getPrompter(); prompter.promptToSavePassword(formLogin); - } + }, + + /** + * Maps all the elements for tabs in the parent process to the + * current state used to display tab-specific UI. + * + * This mapping is not updated in case a web page is moved to a different + * chrome window by the swapDocShells method. In this case, it is possible + * that a UI update just requested for the login fill doorhanger and then + * delayed by a few hundred milliseconds will be lost. Later requests would + * use the new browser reference instead. + * + * Given that the case above is rare, and it would not cause any origin + * mismatch at the time of filling because the origin is checked later in the + * content process, this case is left unhandled. + */ + loginFormStateByBrowser: new WeakMap(), + + /** + * Retrieves a reference to the state object associated with the given + * browser. This is initialized to an empty object. + */ + stateForBrowser(browser) { + let loginFormState = this.loginFormStateByBrowser.get(browser); + if (!loginFormState) { + loginFormState = {}; + this.loginFormStateByBrowser.set(browser, loginFormState); + } + return loginFormState; + }, + + /** + * Called to indicate whether a login form on the currently loaded page is + * present or not. This is one of the factors used to control the visibility + * of the password fill doorhanger. + */ + updateLoginFormPresence(browser, { loginFormOrigin, loginFormPresent }) { + const ANCHOR_DELAY_MS = 200; + + let state = this.stateForBrowser(browser); + + // Update the data to use to the latest known values. Since messages are + // processed in order, this will always be the latest version to use. + state.loginFormOrigin = loginFormOrigin; + state.loginFormPresent = loginFormPresent; + + // Apply the data to the currently displayed icon later. + if (!state.anchorDeferredTask) { + state.anchorDeferredTask = new DeferredTask( + () => this.updateLoginAnchor(browser), + ANCHOR_DELAY_MS + ); + } + state.anchorDeferredTask.arm(); + }, + updateLoginAnchor: Task.async(function* (browser) { + // Copy the state to use for this execution of the task. These will not + // change during this execution of the asynchronous function, but in case a + // change happens in the state, the function will be retriggered. + let { loginFormOrigin, loginFormPresent } = this.stateForBrowser(browser); + + yield Services.logins.initializationPromise; + + // Check if there are form logins for the site, ignoring formSubmitURL. + let hasLogins = loginFormOrigin && + Services.logins.countLogins(loginFormOrigin, "", null) > 0; + + // Once this preference is removed, this version of the fill doorhanger + // should be enabled for Desktop only, and not for Android or B2G. + if (!Services.prefs.getBoolPref("signon.ui.experimental")) { + return; + } + + let showLoginAnchor = loginFormPresent || hasLogins; + + let fillDoorhanger = LoginDoorhangers.FillDoorhanger.find({ browser }); + if (fillDoorhanger) { + if (!showLoginAnchor) { + fillDoorhanger.remove(); + return; + } + // We should only update the state of the doorhanger while it is hidden. + yield fillDoorhanger.promiseHidden; + fillDoorhanger.loginFormPresent = loginFormPresent; + fillDoorhanger.loginFormOrigin = loginFormOrigin; + fillDoorhanger.filterString = loginFormOrigin; + return; + } + if (showLoginAnchor) { + fillDoorhanger = new LoginDoorhangers.FillDoorhanger({ + browser, + loginFormPresent, + loginFormOrigin, + filterString: loginFormOrigin, + }); + } + }), }; diff --git a/toolkit/components/passwordmgr/nsLoginManager.js b/toolkit/components/passwordmgr/nsLoginManager.js index f50ac499bd57..c23c211cc088 100644 --- a/toolkit/components/passwordmgr/nsLoginManager.js +++ b/toolkit/components/passwordmgr/nsLoginManager.js @@ -120,8 +120,6 @@ LoginManager.prototype = { // Form submit observer checks forms for new logins and pw changes. Services.obs.addObserver(this._observer, "xpcom-shutdown", false); - // TODO: Make this class useful in the child process (in addition to - // autoCompleteSearchAsync and fillForm). if (Services.appinfo.processType === Services.appinfo.PROCESS_TYPE_DEFAULT) { Services.obs.addObserver(this._observer, "passwordmgr-storage-replace", @@ -577,21 +575,6 @@ LoginManager.prototype = { return this._getPasswordOrigin(uriString, true); }, - - - /* - * fillForm - * - * Fill the form with login information if we can find it. - */ - fillForm : function (form) { - log("fillForm processing form[ id:", form.id, "]"); - return LoginManagerContent._asyncFindLogins(form, { showMasterPassword: true }) - .then(function({ form, loginsFound }) { - return LoginManagerContent._fillForm(form, true, false, false, loginsFound)[0]; - }); - }, - }; // end of LoginManager implementation this.NSGetFactory = XPCOMUtils.generateNSGetFactory([LoginManager]); From 0b487930a14763beb0bb031fb90a899d8fc844ae Mon Sep 17 00:00:00 2001 From: Paolo Amadini Date: Wed, 13 May 2015 15:39:02 +0100 Subject: [PATCH 54/54] Bug 1149975 - Part 2 of 2 - Test manual login fill. r=MattN --HG-- extra : rebase_source : 78b0272752273a543aca66de66de6b9196d1a7d7 --- .../passwordmgr/test/browser/browser.ini | 1 + .../test/browser/browser_filldoorhanger.js | 75 +++++++++++++++++++ 2 files changed, 76 insertions(+) create mode 100644 toolkit/components/passwordmgr/test/browser/browser_filldoorhanger.js diff --git a/toolkit/components/passwordmgr/test/browser/browser.ini b/toolkit/components/passwordmgr/test/browser/browser.ini index 94ef96c8c683..b64dcecc98e8 100644 --- a/toolkit/components/passwordmgr/test/browser/browser.ini +++ b/toolkit/components/passwordmgr/test/browser/browser.ini @@ -3,6 +3,7 @@ support-files = authenticate.sjs form_basic.html +[browser_filldoorhanger.js] [browser_notifications.js] [browser_passwordmgr_fields.js] [browser_passwordmgr_observers.js] diff --git a/toolkit/components/passwordmgr/test/browser/browser_filldoorhanger.js b/toolkit/components/passwordmgr/test/browser/browser_filldoorhanger.js new file mode 100644 index 000000000000..4f183f53e6d5 --- /dev/null +++ b/toolkit/components/passwordmgr/test/browser/browser_filldoorhanger.js @@ -0,0 +1,75 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +Cu.import("resource://testing-common/LoginTestUtils.jsm", this); + +/** + * All these tests require the experimental login fill UI to be enabled. We also + * disable autofill for login forms for easier testing of manual fill. + */ +add_task(function* test_initialize() { + Services.prefs.setBoolPref("signon.ui.experimental", true); + Services.prefs.setBoolPref("signon.autofillForms", false); + registerCleanupFunction(function () { + Services.prefs.clearUserPref("signon.ui.experimental"); + Services.prefs.clearUserPref("signon.autofillForms"); + }); +}); + +/** + * Tests manual fill when the page has a login form. + */ +add_task(function* test_fill() { + Services.logins.addLogin(LoginTestUtils.testData.formLogin({ + hostname: "https://example.com", + formSubmitURL: "https://example.com", + username: "username", + password: "password", + })); + + // The anchor icon may be shown during the initial page load in the new tab, + // so we have to set up the observers first. When we receive the notification + // from PopupNotifications.jsm, we check it is the one for the right anchor. + let anchor = document.getElementById("login-fill-notification-icon"); + let promiseAnchorShown = + TestUtils.topicObserved("PopupNotifications-updateNotShowing", + () => anchor.hasAttribute("showing")); + + yield BrowserTestUtils.withNewTab({ + gBrowser, + url: "https://example.com/browser/toolkit/components/" + + "passwordmgr/test/browser/form_basic.html", + }, function* (browser) { + yield promiseAnchorShown; + + let promiseShown = BrowserTestUtils.waitForEvent(PopupNotifications.panel, + "Shown"); + anchor.click(); + yield promiseShown; + + let list = document.getElementById("login-fill-list"); + Assert.equal(list.childNodes.length, 1, + "list.childNodes.length === 1"); + + let promiseHidden = BrowserTestUtils.waitForEvent(PopupNotifications.panel, + "popuphidden"); + list.focus(); + EventUtils.sendMouseEvent({ type: "dblclick" }, list.childNodes[0]); + yield promiseHidden; + + let result = yield ContentTask.spawn(browser, null, function* () { + let doc = content.document; + return { + username: doc.getElementById("form-basic-username").value, + password: doc.getElementById("form-basic-password").value, + } + }); + + Assert.equal(result.username, "username", + 'result.username === "username"'); + Assert.equal(result.password, "password", + 'result.password === "password"'); + }); + + Services.logins.removeAllLogins(); +});