merge fx-team to mozilla-central a=merge
authorCarsten "Tomcat" Book <cbook@mozilla.com>
Mon, 04 May 2015 13:06:34 +0200
changeset 273526 843f6046ee00f73979424e662c13cb76273a36f3
parent 273514 08c8eb804d4bb10df9e146f5b4bfb3621616cd79 (current diff)
parent 273525 9d27ff2b5d6fbcbca43f9302ba9982880e193f4e (diff)
child 273550 34828fed163992d13fa46d5a087f2f4a1b3d09ae
push id863
push userraliiev@mozilla.com
push dateMon, 03 Aug 2015 13:22:43 +0000
treeherdermozilla-release@f6321b14228d [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmerge
milestone40.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
merge fx-team to mozilla-central a=merge
mobile/android/base/tests/AboutHomeTest.java
mobile/android/base/tests/BaseRobocopTest.java
mobile/android/base/tests/BaseTest.java
mobile/android/base/tests/ContentContextMenuTest.java
mobile/android/base/tests/ContentProviderTest.java
mobile/android/base/tests/DatabaseHelper.java
mobile/android/base/tests/Firefox.jpg
mobile/android/base/tests/JavascriptTest.java
mobile/android/base/tests/MotionEventHelper.java
mobile/android/base/tests/MotionEventReplayer.java
mobile/android/base/tests/PixelTest.java
mobile/android/base/tests/README.rst
mobile/android/base/tests/SelectionHandlerTest.java
mobile/android/base/tests/SessionTest.java
mobile/android/base/tests/StringHelper.java
mobile/android/base/tests/UITest.java
mobile/android/base/tests/UITestContext.java
mobile/android/base/tests/assets/README
mobile/android/base/tests/assets/mock-package.zip
mobile/android/base/tests/assets/testcheck2-motionevents
mobile/android/base/tests/components/AboutHomeComponent.java
mobile/android/base/tests/components/AppMenuComponent.java
mobile/android/base/tests/components/BaseComponent.java
mobile/android/base/tests/components/GeckoViewComponent.java
mobile/android/base/tests/components/TabStripComponent.java
mobile/android/base/tests/components/ToolbarComponent.java
mobile/android/base/tests/devicesearch.xml
mobile/android/base/tests/green.swf
mobile/android/base/tests/helpers/AssertionHelper.java
mobile/android/base/tests/helpers/DeviceHelper.java
mobile/android/base/tests/helpers/FrameworkHelper.java
mobile/android/base/tests/helpers/GeckoClickHelper.java
mobile/android/base/tests/helpers/GeckoHelper.java
mobile/android/base/tests/helpers/HelperInitializer.java
mobile/android/base/tests/helpers/JavascriptBridge.java
mobile/android/base/tests/helpers/JavascriptMessageParser.java
mobile/android/base/tests/helpers/NavigationHelper.java
mobile/android/base/tests/helpers/TextInputHelper.java
mobile/android/base/tests/helpers/WaitHelper.java
mobile/android/base/tests/javascript_redirect.sjs
mobile/android/base/tests/link_discovery.html
mobile/android/base/tests/reader_mode_pages/basic_article.html
mobile/android/base/tests/reader_mode_pages/developer.mozilla.org/en/XULRunner/Build_Instructions.html
mobile/android/base/tests/reader_mode_pages/not_an_article.html
mobile/android/base/tests/robocop.ini
mobile/android/base/tests/robocop_404.sjs
mobile/android/base/tests/robocop_adobe_flash.html
mobile/android/base/tests/robocop_autophone.ini
mobile/android/base/tests/robocop_big_link.html
mobile/android/base/tests/robocop_big_mailto.html
mobile/android/base/tests/robocop_blank_01.html
mobile/android/base/tests/robocop_blank_02.html
mobile/android/base/tests/robocop_blank_03.html
mobile/android/base/tests/robocop_blank_04.html
mobile/android/base/tests/robocop_blank_05.html
mobile/android/base/tests/robocop_boxes.html
mobile/android/base/tests/robocop_dynamic.sjs
mobile/android/base/tests/robocop_geolocation.html
mobile/android/base/tests/robocop_getusermedia.html
mobile/android/base/tests/robocop_getusermedia2.html
mobile/android/base/tests/robocop_head.js
mobile/android/base/tests/robocop_input.html
mobile/android/base/tests/robocop_javascript.html
mobile/android/base/tests/robocop_link_to_slow_loading.html
mobile/android/base/tests/robocop_login_01.html
mobile/android/base/tests/robocop_login_02.html
mobile/android/base/tests/robocop_offline_storage.html
mobile/android/base/tests/robocop_picture_link.html
mobile/android/base/tests/robocop_popup.html
mobile/android/base/tests/robocop_search.html
mobile/android/base/tests/robocop_slow_loading.html
mobile/android/base/tests/robocop_suggestions.sjs
mobile/android/base/tests/robocop_testharness.js
mobile/android/base/tests/robocop_text_page.html
mobile/android/base/tests/robocop_tiles.sjs
mobile/android/base/tests/roboextender/SelectionUtils.js
mobile/android/base/tests/roboextender/paymentsUI.html
mobile/android/base/tests/roboextender/robocop_home_banner.html
mobile/android/base/tests/roboextender/robocop_prompt_gridinput.html
mobile/android/base/tests/roboextender/testInputSelections.html
mobile/android/base/tests/roboextender/testSelectionHandler.html
mobile/android/base/tests/roboextender/testTextareaSelections.html
mobile/android/base/tests/session_formdata_sample.html
mobile/android/base/tests/simple_redirect.sjs
mobile/android/base/tests/simpleservice.xml
mobile/android/base/tests/testANRReporter.java
mobile/android/base/tests/testAboutHomePageNavigation.java
mobile/android/base/tests/testAboutHomeVisibility.java
mobile/android/base/tests/testAboutPage.java
mobile/android/base/tests/testAboutPasswords.java
mobile/android/base/tests/testAboutPasswords.js
mobile/android/base/tests/testAccounts.java
mobile/android/base/tests/testAccounts.js
mobile/android/base/tests/testAddSearchEngine.java
mobile/android/base/tests/testAddonManager.java
mobile/android/base/tests/testAdobeFlash.java
mobile/android/base/tests/testAndroidLog.java
mobile/android/base/tests/testAndroidLog.js
mobile/android/base/tests/testAppConstants.java
mobile/android/base/tests/testAppConstants.js
mobile/android/base/tests/testAppMenuPathways.java
mobile/android/base/tests/testAwesomebar.java
mobile/android/base/tests/testAxisLocking.java
mobile/android/base/tests/testBackButtonInEditMode.java
mobile/android/base/tests/testBookmark.java
mobile/android/base/tests/testBookmarkFolders.java
mobile/android/base/tests/testBookmarkKeyword.java
mobile/android/base/tests/testBookmarklets.java
mobile/android/base/tests/testBookmarksPanel.java
mobile/android/base/tests/testBrowserDiscovery.java
mobile/android/base/tests/testBrowserDiscovery.js
mobile/android/base/tests/testBrowserProvider.java
mobile/android/base/tests/testBrowserProviderPerf.java
mobile/android/base/tests/testBrowserSearchVisibility.java
mobile/android/base/tests/testCheck.java
mobile/android/base/tests/testCheck2.java
mobile/android/base/tests/testClearPrivateData.java
mobile/android/base/tests/testDBUtils.java
mobile/android/base/tests/testDebuggerServer.java
mobile/android/base/tests/testDebuggerServer.js
mobile/android/base/tests/testDeviceSearchEngine.java
mobile/android/base/tests/testDeviceSearchEngine.js
mobile/android/base/tests/testDistribution.java
mobile/android/base/tests/testDoorHanger.java
mobile/android/base/tests/testEventDispatcher.java
mobile/android/base/tests/testEventDispatcher.js
mobile/android/base/tests/testFilePicker.java
mobile/android/base/tests/testFilePicker.js
mobile/android/base/tests/testFilterOpenTab.java
mobile/android/base/tests/testFindInPage.java
mobile/android/base/tests/testFindInPage.js
mobile/android/base/tests/testFlingCorrectness.java
mobile/android/base/tests/testFormHistory.java
mobile/android/base/tests/testGeckoProfile.java
mobile/android/base/tests/testGeckoRequest.java
mobile/android/base/tests/testGeckoRequest.js
mobile/android/base/tests/testGetUserMedia.java
mobile/android/base/tests/testHistory.java
mobile/android/base/tests/testHistoryService.java
mobile/android/base/tests/testHistoryService.js
mobile/android/base/tests/testHomeBanner.java
mobile/android/base/tests/testHomeListsProvider.java
mobile/android/base/tests/testHomeProvider.java
mobile/android/base/tests/testHomeProvider.js
mobile/android/base/tests/testImportFromAndroid.java
mobile/android/base/tests/testInputConnection.java
mobile/android/base/tests/testInputSelections.java
mobile/android/base/tests/testInputUrlBar.java
mobile/android/base/tests/testJNI.java
mobile/android/base/tests/testJNI.js
mobile/android/base/tests/testJarReader.java
mobile/android/base/tests/testJavascriptBridge.java
mobile/android/base/tests/testJavascriptBridge.js
mobile/android/base/tests/testLinkContextMenu.java
mobile/android/base/tests/testLoad.java
mobile/android/base/tests/testMailToContextMenu.java
mobile/android/base/tests/testMasterPassword.java
mobile/android/base/tests/testMigrateUI.java
mobile/android/base/tests/testMigrateUI.js
mobile/android/base/tests/testMozPay.java
mobile/android/base/tests/testMozPay.js
mobile/android/base/tests/testNativeCrypto.java
mobile/android/base/tests/testNetworkManager.java
mobile/android/base/tests/testNetworkManager.js
mobile/android/base/tests/testNewTab.java
mobile/android/base/tests/testOSLocale.java
mobile/android/base/tests/testOfflinePage.java
mobile/android/base/tests/testOfflinePage.js
mobile/android/base/tests/testOrderedBroadcast.java
mobile/android/base/tests/testOrderedBroadcast.js
mobile/android/base/tests/testPan.java
mobile/android/base/tests/testPanCorrectness.java
mobile/android/base/tests/testPasswordEncrypt.java
mobile/android/base/tests/testPasswordProvider.java
mobile/android/base/tests/testPermissions.java
mobile/android/base/tests/testPictureLinkContextMenu.java
mobile/android/base/tests/testPrefsObserver.java
mobile/android/base/tests/testPrivateBrowsing.java
mobile/android/base/tests/testPromptGridInput.java
mobile/android/base/tests/testReaderModeTitle.java
mobile/android/base/tests/testReaderView.java
mobile/android/base/tests/testReaderView.js
mobile/android/base/tests/testReadingListCache.java
mobile/android/base/tests/testReadingListCache.js
mobile/android/base/tests/testReadingListProvider.java
mobile/android/base/tests/testResourceSubstitutions.java
mobile/android/base/tests/testResourceSubstitutions.js
mobile/android/base/tests/testRestrictedProfiles.java
mobile/android/base/tests/testRestrictedProfiles.js
mobile/android/base/tests/testSearchHistoryProvider.java
mobile/android/base/tests/testSearchSuggestions.java
mobile/android/base/tests/testSelectionHandler.java
mobile/android/base/tests/testSessionFormData.java
mobile/android/base/tests/testSessionFormData.js
mobile/android/base/tests/testSessionHistory.java
mobile/android/base/tests/testSessionOOMRestore.java
mobile/android/base/tests/testSessionOOMSave.java
mobile/android/base/tests/testSettingsMenuItems.java
mobile/android/base/tests/testShareLink.java
mobile/android/base/tests/testSharedPreferences.java
mobile/android/base/tests/testSharedPreferences.js
mobile/android/base/tests/testSimpleDiscovery.java
mobile/android/base/tests/testSimpleDiscovery.js
mobile/android/base/tests/testStateWhileLoading.java
mobile/android/base/tests/testStumblerSetting.java
mobile/android/base/tests/testSystemPages.java
mobile/android/base/tests/testTextareaSelections.java
mobile/android/base/tests/testThumbnails.java
mobile/android/base/tests/testTitleBar.java
mobile/android/base/tests/testTrackingProtection.java
mobile/android/base/tests/testTrackingProtection.js
mobile/android/base/tests/testUITelemetry.java
mobile/android/base/tests/testUITelemetry.js
mobile/android/base/tests/testVideoControls.java
mobile/android/base/tests/testVideoControls.js
mobile/android/base/tests/testVideoDiscovery.java
mobile/android/base/tests/testVideoDiscovery.js
mobile/android/base/tests/testVkbOverlap.java
mobile/android/base/tests/test_bug720538.html
mobile/android/base/tests/test_bug720538.java
mobile/android/base/tests/test_viewport.sjs
mobile/android/base/tests/tracking_bad.html
mobile/android/base/tests/tracking_good.html
mobile/android/base/tests/video-pattern.ogg
mobile/android/base/tests/video-pattern.webm
mobile/android/base/tests/video_controls.html
mobile/android/base/tests/video_discovery.html
--- a/browser/devtools/inspector/inspector-panel.js
+++ b/browser/devtools/inspector/inspector-panel.js
@@ -687,16 +687,19 @@ InspectorPanel.prototype = {
       copyInnerHTML.setAttribute("disabled", "true");
       copyOuterHTML.setAttribute("disabled", "true");
       scrollIntoView.setAttribute("disabled", "true");
     }
     if (!this.canGetUniqueSelector) {
       unique.hidden = true;
     }
 
+    // Enable/Disable the link open/copy items.
+    this._setupNodeLinkMenu();
+
     // Enable the "edit HTML" item if the selection is an element and the root
     // actor has the appropriate trait (isOuterHTMLEditable)
     let editHTML = this.panelDoc.getElementById("node-menu-edithtml");
     if (isEditableElement && this.isOuterHTMLEditable) {
       editHTML.removeAttribute("disabled");
     } else {
       editHTML.setAttribute("disabled", "true");
     }
@@ -745,16 +748,66 @@ InspectorPanel.prototype = {
   _resetNodeMenu: function InspectorPanel_resetNodeMenu() {
     // Remove any extra items
     while (this.lastNodemenuItem.nextSibling) {
       let toDelete = this.lastNodemenuItem.nextSibling;
       toDelete.parentNode.removeChild(toDelete);
     }
   },
 
+  /**
+   * Link menu items can be shown or hidden depending on the context and
+   * selected node, and their labels can vary.
+   */
+  _setupNodeLinkMenu: function InspectorPanel_setupNodeLinkMenu() {
+    let linkSeparator = this.panelDoc.getElementById("node-menu-link-separator");
+    let linkFollow = this.panelDoc.getElementById("node-menu-link-follow");
+    let linkCopy = this.panelDoc.getElementById("node-menu-link-copy");
+
+    // Hide all by default.
+    linkSeparator.setAttribute("hidden", "true");
+    linkFollow.setAttribute("hidden", "true");
+    linkCopy.setAttribute("hidden", "true");
+
+    // Get information about the right-clicked node.
+    let popupNode = this.panelDoc.popupNode;
+    if (!popupNode || !popupNode.classList.contains("link")) {
+      return;
+    }
+
+    let type = popupNode.dataset.type;
+    // Bug 1158822 will make "resource" type URLs open in devtools, but for now
+    // they're considered like "uri".
+    if (type === "uri" || type === "resource") {
+      // First make sure the target can resolve relative URLs.
+      this.target.actorHasMethod("inspector", "resolveRelativeURL").then(canResolve => {
+        if (!canResolve) {
+          return;
+        }
+
+        linkSeparator.removeAttribute("hidden");
+
+        // Links can't be opened in new tabs in the browser toolbox.
+        if (!this.target.chrome) {
+          linkFollow.removeAttribute("hidden");
+          linkFollow.setAttribute("label", this.strings.GetStringFromName(
+            "inspector.menu.openUrlInNewTab.label"));
+        }
+        linkCopy.removeAttribute("hidden");
+        linkCopy.setAttribute("label", this.strings.GetStringFromName(
+          "inspector.menu.copyUrlToClipboard.label"));
+      }, console.error);
+    } else if (type === "idref") {
+      linkSeparator.removeAttribute("hidden");
+      linkFollow.removeAttribute("hidden");
+      linkFollow.setAttribute("label", this.strings.formatStringFromName(
+        "inspector.menu.selectElement.label", [popupNode.dataset.link], 1));
+    }
+  },
+
   _initMarkup: function InspectorPanel_initMarkup() {
     let doc = this.panelDoc;
 
     this._markupBox = doc.getElementById("markup-box");
 
     // create tool iframe
     this._markupFrame = doc.createElement("iframe");
     this._markupFrame.setAttribute("flex", "1");
@@ -1033,16 +1086,62 @@ InspectorPanel.prototype = {
       this.markup.deleteNode(this.selection.nodeFront);
     } else {
       // remove the node from content
       this.walker.removeNode(this.selection.nodeFront);
     }
   },
 
   /**
+   * This method is here for the benefit of the node-menu-link-follow menu item
+   * in the inspector contextual-menu. It's behavior depends on which node was
+   * right-clicked when the menu was opened.
+   */
+  followAttributeLink: function InspectorPanel_followLink(e) {
+    let type = this.panelDoc.popupNode.dataset.type;
+    let link = this.panelDoc.popupNode.dataset.link;
+
+    // "resource" type links should open appropriate tool instead (bug 1158822).
+    if (type === "uri" || type === "resource") {
+      // Open link in a new tab.
+      // When the inspector menu was setup on click (see _setupNodeLinkMenu), we
+      // already checked that resolveRelativeURL existed.
+      this.inspector.resolveRelativeURL(link, this.selection.nodeFront).then(url => {
+        let browserWin = this.target.tab.ownerDocument.defaultView;
+        browserWin.openUILinkIn(url, "tab");
+      }, console.error);
+    } else if (type == "idref") {
+      // Select the node in the same document.
+      this.walker.document(this.selection.nodeFront).then(doc => {
+        this.walker.querySelector(doc, "#" + CSS.escape(link)).then(node => {
+          if (!node) {
+            this.emit("idref-attribute-link-failed");
+            return;
+          }
+          this.selection.setNodeFront(node);
+        }, console.error);
+      }, console.error);
+    }
+  },
+
+  /**
+   * This method is here for the benefit of the node-menu-link-copy menu item
+   * in the inspector contextual-menu. It's behavior depends on which node was
+   * right-clicked when the menu was opened.
+   */
+  copyAttributeLink: function InspectorPanel_copyLink(e) {
+    let link = this.panelDoc.popupNode.dataset.link;
+    // When the inspector menu was setup on click (see _setupNodeLinkMenu), we
+    // already checked that resolveRelativeURL existed.
+    this.inspector.resolveRelativeURL(link, this.selection.nodeFront).then(url => {
+      clipboardHelper.copyString(url);
+    }, console.error);
+  },
+
+  /**
   * Trigger a high-priority layout change for things that need to be
   * updated immediately
   */
   immediateLayoutChange: function Inspector_immediateLayoutChange()
   {
     this.emit("layout-change");
   },
 
--- a/browser/devtools/inspector/inspector.xul
+++ b/browser/devtools/inspector/inspector.xul
@@ -86,21 +86,25 @@
             oncommand="inspector.pasteAdjacentHTML('beforeEnd')"/>
         </menupopup>
       </menu>
       <menuseparator/>
       <menuitem id="node-menu-scrollnodeintoview"
         label="&inspectorScrollNodeIntoView.label;"
         accesskey="&inspectorScrollNodeIntoView.accesskey;"
         oncommand="inspector.scrollNodeIntoView()"/>
-      <menuseparator/>
       <menuitem id="node-menu-delete"
         label="&inspectorHTMLDelete.label;"
         accesskey="&inspectorHTMLDelete.accesskey;"
         oncommand="inspector.deleteNode()"/>
+      <menuseparator id="node-menu-link-separator"/>
+      <menuitem id="node-menu-link-follow"
+        oncommand="inspector.followAttributeLink()"/>
+      <menuitem id="node-menu-link-copy"
+        oncommand="inspector.copyAttributeLink()"/>
       <menuseparator/>
       <menuitem id="node-menu-pseudo-hover"
         label=":hover" type="checkbox"
         oncommand="inspector.togglePseudoClass(':hover')"/>
       <menuitem id="node-menu-pseudo-active"
         label=":active" type="checkbox"
         oncommand="inspector.togglePseudoClass(':active')"/>
       <menuitem id="node-menu-pseudo-focus"
--- a/browser/devtools/markupview/markup-view.css
+++ b/browser/devtools/markupview/markup-view.css
@@ -157,16 +157,20 @@ ul.children + .tag-line::before {
 .newattr {
   display: inline-block;
   width: 1em;
   height: 1ex;
   margin-right: -1em;
   padding: 1px 0;
 }
 
+.attr-value .link {
+  text-decoration: underline;
+}
+
 .newattr:focus {
   margin-right: 0;
 }
 
 .flash-out {
   transition: background .5s;
 }
 
--- a/browser/devtools/markupview/markup-view.js
+++ b/browser/devtools/markupview/markup-view.js
@@ -23,16 +23,17 @@ const {UndoStack} = require("devtools/sh
 const {editableField, InplaceEditor} = require("devtools/shared/inplace-editor");
 const {gDevTools} = Cu.import("resource:///modules/devtools/gDevTools.jsm", {});
 const {HTMLEditor} = require("devtools/markupview/html-editor");
 const promise = require("resource://gre/modules/Promise.jsm").Promise;
 const {Tooltip} = require("devtools/shared/widgets/Tooltip");
 const EventEmitter = require("devtools/toolkit/event-emitter");
 const Heritage = require("sdk/core/heritage");
 const {setTimeout, clearTimeout, setInterval, clearInterval} = require("sdk/timers");
+const {parseAttribute} = require("devtools/shared/node-attribute-parser");
 const ELLIPSIS = Services.prefs.getComplexValue("intl.ellipsis", Ci.nsIPrefLocalizedString).data;
 
 Cu.import("resource://gre/modules/devtools/LayoutHelpers.jsm");
 Cu.import("resource://gre/modules/devtools/Templater.jsm");
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 
 loader.lazyGetter(this, "DOMParser", function() {
@@ -1809,26 +1810,16 @@ MarkupContainer.prototype = {
   _onMouseDown: function(event) {
     let target = event.target;
 
     // The "show more nodes" button (already has its onclick).
     if (target.nodeName === "button") {
       return;
     }
 
-    // output-parser generated links handling.
-    if (target.nodeName === "a") {
-      event.stopPropagation();
-      event.preventDefault();
-      let browserWin = this.markup._inspector.target
-                           .tab.ownerDocument.defaultView;
-      browserWin.openUILinkIn(target.href, "tab");
-      return;
-    }
-
     // target is the MarkupContainer itself.
     this._isMouseDown = true;
     this.hovered = false;
     this.markup.navigate(this);
     event.stopPropagation();
 
     // Preventing the default behavior will avoid the body to gain focus on
     // mouseup (through bubbling) when clicking on a non focusable node in the
@@ -2134,18 +2125,19 @@ MarkupElementContainer.prototype = Herit
    * been retrieved
    */
   _prepareImagePreview: function() {
     if (this.isPreviewable()) {
       // Get the image data for later so that when the user actually hovers over
       // the element, the tooltip does contain the image
       let def = promise.defer();
 
+      let hasSrc = this.editor.getAttributeElement("src");
       this.tooltipData = {
-        target: this.editor.getAttributeElement("src") || this.editor.tag,
+        target: hasSrc ? hasSrc.querySelector(".link") : this.editor.tag,
         data: def.promise
       };
 
       let maxDim = Services.prefs.getIntPref("devtools.inspector.imagePreviewTooltipSize");
       this.node.getImageData(maxDim).then(data => {
         data.data.string().then(str => {
           let res = {data: str, size: data.size};
           // Resolving the data promise and, to always keep tooltipData.data
@@ -2434,17 +2426,17 @@ ElementEditor.prototype = {
         this.removeAttribute(name);
       }
     }
 
     // Only loop through the current attributes on the node.  Missing
     // attributes have already been removed at this point.
     for (let attr of nodeAttributes) {
       let el = this.attrElements.get(attr.name);
-      let valueChanged = el && el.querySelector(".attr-value").innerHTML !== attr.value;
+      let valueChanged = el && el.querySelector(".attr-value").textContent !== attr.value;
       let isEditing = el && el.querySelector(".editable").inplaceEditor;
       let canSimplyShowEditor = el && (!valueChanged || isEditing);
 
       if (canSimplyShowEditor) {
         // Element already exists and doesn't need to be recreated.
         // Just show it (it's hidden by default due to the template).
         el.style.removeProperty("display");
       } else {
@@ -2490,52 +2482,52 @@ ElementEditor.prototype = {
   },
 
   _createAttribute: function(aAttr, aBefore = null) {
     // Create the template editor, which will save some variables here.
     let data = {
       attrName: aAttr.name,
     };
     this.template("attribute", data);
-    var {attr, inner, name, val} = data;
+    let {attr, inner, name, val} = data;
 
     // Double quotes need to be handled specially to prevent DOMParser failing.
     // name="v"a"l"u"e" when editing -> name='v"a"l"u"e"'
     // name="v'a"l'u"e" when editing -> name="v'a&quot;l'u&quot;e"
     let editValueDisplayed = aAttr.value || "";
     let hasDoubleQuote = editValueDisplayed.includes('"');
     let hasSingleQuote = editValueDisplayed.includes("'");
     let initial = aAttr.name + '="' + editValueDisplayed + '"';
 
     // Can't just wrap value with ' since the value contains both " and '.
     if (hasDoubleQuote && hasSingleQuote) {
-        editValueDisplayed = editValueDisplayed.replace(/\"/g, "&quot;");
-        initial = aAttr.name + '="' + editValueDisplayed + '"';
+      editValueDisplayed = editValueDisplayed.replace(/\"/g, "&quot;");
+      initial = aAttr.name + '="' + editValueDisplayed + '"';
     }
 
     // Wrap with ' since there are no single quotes in the attribute value.
     if (hasDoubleQuote && !hasSingleQuote) {
-        initial = aAttr.name + "='" + editValueDisplayed + "'";
+      initial = aAttr.name + "='" + editValueDisplayed + "'";
     }
 
     // Make the attribute editable.
     attr.editMode = editableField({
       element: inner,
       trigger: "dblclick",
       stopOnReturn: true,
       selectAll: false,
       initial: initial,
       contentType: InplaceEditor.CONTENT_TYPES.CSS_MIXED,
       popup: this.markup.popup,
       start: (aEditor, aEvent) => {
         // If the editing was started inside the name or value areas,
         // select accordingly.
         if (aEvent && aEvent.target === name) {
           aEditor.input.setSelectionRange(0, name.textContent.length);
-        } else if (aEvent && aEvent.target === val) {
+        } else if (aEvent && aEvent.target.closest(".attr-value") === val) {
           let length = editValueDisplayed.length;
           let editorLength = aEditor.input.value.length;
           let start = editorLength - (length + 1);
           aEditor.input.setSelectionRange(start, start + length);
         } else {
           aEditor.input.select();
         }
       },
@@ -2574,25 +2566,50 @@ ElementEditor.prototype = {
       let idNode = this.attrElements.get("id");
       before = idNode ? idNode.nextSibling : this.attrList.firstChild;
     }
     this.attrList.insertBefore(attr, before);
 
     this.removeAttribute(aAttr.name);
     this.attrElements.set(aAttr.name, attr);
 
-    let collapsedValue;
-    if (aAttr.value.match(COLLAPSE_DATA_URL_REGEX)) {
-      collapsedValue = truncateString(aAttr.value, COLLAPSE_DATA_URL_LENGTH);
-    } else {
-      collapsedValue = truncateString(aAttr.value, COLLAPSE_ATTRIBUTE_LENGTH);
+    // Parse the attribute value to detect whether there are linkable parts in
+    // it (make sure to pass a complete list of existing attributes to the
+    // parseAttribute function, by concatenating aAttr, because this could be a
+    // newly added attribute not yet on this.node).
+    let attributes = this.node.attributes.filter(({name}) => name !== aAttr.name);
+    attributes.push(aAttr);
+    let parsedLinksData = parseAttribute(this.node.namespaceURI,
+      this.node.tagName, attributes, aAttr.name);
+
+    // Create links in the attribute value, and collapse long attributes if
+    // needed.
+    let collapse = value => {
+      if (value.match(COLLAPSE_DATA_URL_REGEX)) {
+        return truncateString(value, COLLAPSE_DATA_URL_LENGTH);
+      } else {
+        return truncateString(value, COLLAPSE_ATTRIBUTE_LENGTH);
+      }
+    };
+
+    val.innerHTML = "";
+    for (let token of parsedLinksData) {
+      if (token.type === "string") {
+        val.appendChild(this.doc.createTextNode(collapse(token.value)));
+      } else {
+        let link = this.doc.createElement("span");
+        link.classList.add("link");
+        link.setAttribute("data-type", token.type);
+        link.setAttribute("data-link", token.value);
+        link.textContent = collapse(token.value);
+        val.appendChild(link);
+      }
     }
 
     name.textContent = aAttr.name;
-    val.textContent = collapsedValue;
 
     return attr;
   },
 
   /**
    * Parse a user-entered attribute string and apply the resulting
    * attributes to the node.  This operation is undoable.
    *
--- a/browser/devtools/markupview/test/browser.ini
+++ b/browser/devtools/markupview/test/browser.ini
@@ -5,16 +5,17 @@ support-files =
   doc_markup_anonymous.html
   doc_markup_dragdrop.html
   doc_markup_dragdrop_autoscroll.html
   doc_markup_edit.html
   doc_markup_events.html
   doc_markup_events_jquery.html
   doc_markup_events-overflow.html
   doc_markup_flashing.html
+  doc_markup_links.html
   doc_markup_mutation.html
   doc_markup_navigation.html
   doc_markup_not_displayed.html
   doc_markup_pagesize_01.html
   doc_markup_pagesize_02.html
   doc_markup_search.html
   doc_markup_svg_attributes.html
   doc_markup_toggle.html
@@ -63,16 +64,21 @@ skip-if = e10s # Bug 1040751 - CodeMirro
 [browser_markupview_events_jquery_1.6.js]
 skip-if = e10s # Bug 1040751 - CodeMirror editor.destroy() isn't e10s compatible
 [browser_markupview_events_jquery_1.7.js]
 skip-if = e10s # Bug 1040751 - CodeMirror editor.destroy() isn't e10s compatible
 [browser_markupview_events_jquery_1.11.1.js]
 skip-if = e10s # Bug 1040751 - CodeMirror editor.destroy() isn't e10s compatible
 [browser_markupview_events_jquery_2.1.1.js]
 skip-if = e10s # Bug 1040751 - CodeMirror editor.destroy() isn't e10s compatible
+[browser_markupview_links_01.js]
+[browser_markupview_links_02.js]
+[browser_markupview_links_03.js]
+[browser_markupview_links_04.js]
+[browser_markupview_links_05.js]
 [browser_markupview_load_01.js]
 [browser_markupview_html_edit_01.js]
 [browser_markupview_html_edit_02.js]
 [browser_markupview_html_edit_03.js]
 [browser_markupview_image_tooltip.js]
 [browser_markupview_keybindings_01.js]
 [browser_markupview_keybindings_02.js]
 [browser_markupview_keybindings_03.js]
--- a/browser/devtools/markupview/test/browser_markupview_image_tooltip.js
+++ b/browser/devtools/markupview/test/browser_markupview_image_tooltip.js
@@ -56,20 +56,20 @@ function createPage() {
 }
 
 function* getImageTooltipTarget({selector}, inspector) {
   let nodeFront = yield getNodeFront(selector, inspector);
   let isImg = nodeFront.tagName.toLowerCase() === "img";
 
   let container = getContainerForNodeFront(nodeFront, inspector);
 
-   let target = container.editor.tag;
-   if (isImg) {
-     target = container.editor.getAttributeElement("src");
-   }
+  let target = container.editor.tag;
+  if (isImg) {
+    target = container.editor.getAttributeElement("src").querySelector(".link");
+  }
   return target;
 }
 
 function* assertTooltipShownOn(element, {markup}) {
   info("Is the element a valid hover target");
   let isValid = yield markup.tooltip.isValidHoverTarget(element);
   ok(isValid, "The element is a valid hover target for the image tooltip");
 }
new file mode 100644
--- /dev/null
+++ b/browser/devtools/markupview/test/browser_markupview_links_01.js
@@ -0,0 +1,122 @@
+/* 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 that links are shown in attributes when the values (or part of the
+// values) are URIs or pointers to IDs.
+
+const TEST_URL = TEST_URL_ROOT + "doc_markup_links.html";
+
+const TEST_DATA = [{
+  selector: "link",
+  attributes: [{
+    attributeName: "href",
+    links: [{type: "resource", value: "style.css"}]
+  }]
+}, {
+  selector: "link[rel=icon]",
+  attributes: [{
+    attributeName: "href",
+    links: [{type: "uri", value: "/media/img/firefox/favicon-196.223e1bcaf067.png"}]
+  }]
+}, {
+  selector: "form",
+  attributes: [{
+    attributeName: "action",
+    links: [{type: "uri", value: "/post_message"}]
+  }]
+}, {
+  selector: "label[for=name]",
+  attributes: [{
+    attributeName: "for",
+    links: [{type: "idref", value: "name"}]
+  }]
+}, {
+  selector: "label[for=message]",
+  attributes: [{
+    attributeName: "for",
+    links: [{type: "idref", value: "message"}]
+  }]
+}, {
+  selector: "output",
+  attributes: [{
+    attributeName: "form",
+    links: [{type: "idref", value: "message-form"}]
+  }, {
+    attributeName: "for",
+    links: [
+      {type: "idref", value: "name"},
+      {type: "idref", value: "message"},
+      {type: "idref", value: "invalid"}
+    ]
+  }]
+}, {
+  selector: "a",
+  attributes: [{
+    attributeName: "href",
+    links: [{type: "uri", value: "/go/somewhere/else"}]
+  }, {
+    attributeName: "ping",
+    links: [
+      {type: "uri", value: "/analytics?page=pageA"},
+      {type: "uri", value: "/analytics?user=test"}
+    ]
+  }]
+}, {
+  selector: "li[contextmenu=menu1]",
+  attributes: [{
+    attributeName: "contextmenu",
+    links: [{type: "idref", value: "menu1"}]
+  }]
+}, {
+  selector: "li[contextmenu=menu2]",
+  attributes: [{
+    attributeName: "contextmenu",
+    links: [{type: "idref", value: "menu2"}]
+  }]
+}, {
+  selector: "li[contextmenu=menu3]",
+  attributes: [{
+    attributeName: "contextmenu",
+    links: [{type: "idref", value: "menu3"}]
+  }]
+}, {
+  selector: "video",
+  attributes: [{
+    attributeName: "poster",
+    links: [{type: "uri", value: "doc_markup_tooltip.png"}]
+  }, {
+    attributeName: "src",
+    links: [{type: "uri", value: "code-rush.mp4"}]
+  }]
+}, {
+  selector: "script",
+  attributes: [{
+    attributeName: "src",
+    links: [{type: "resource", value: "lib_jquery_1.0.js"}]
+  }]
+}];
+
+add_task(function*() {
+  let {inspector} = yield addTab(TEST_URL).then(openInspector);
+
+  for (let {selector, attributes} of TEST_DATA) {
+    info("Testing attributes on node " + selector);
+    yield selectNode(selector, inspector);
+    let {editor} = yield getContainerForSelector(selector, inspector);
+
+    for (let {attributeName, links} of attributes) {
+      info("Testing attribute " + attributeName);
+      let linkEls = editor.attrElements.get(attributeName).querySelectorAll(".link");
+
+      is(linkEls.length, links.length, "The right number of links were found");
+
+      for (let i = 0; i < links.length; i ++) {
+        is(linkEls[i].dataset.type, links[i].type, "Link " + i + " has the right type");
+        is(linkEls[i].textContent, links[i].value, "Link " + i + " has the right value");
+      }
+    }
+  }
+});
new file mode 100644
--- /dev/null
+++ b/browser/devtools/markupview/test/browser_markupview_links_02.js
@@ -0,0 +1,37 @@
+/* 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 that attributes are linkified correctly when attributes are updated
+// and created.
+
+const TEST_URL = TEST_URL_ROOT + "doc_markup_links.html";
+
+add_task(function*() {
+  let {inspector} = yield addTab(TEST_URL).then(openInspector);
+
+  info("Adding a contextmenu attribute to the body node");
+  yield addNewAttributes("body", "contextmenu=\"menu1\"", inspector);
+
+  info("Checking for links in the new attribute");
+  let {editor} = yield getContainerForSelector("body", inspector);
+  let linkEls = editor.attrElements.get("contextmenu").querySelectorAll(".link");
+  is(linkEls.length, 1, "There is one link in the contextmenu attribute");
+  is(linkEls[0].dataset.type, "idref", "The link has the right type");
+  is(linkEls[0].textContent, "menu1", "The link has the right value");
+
+  info("Editing the contextmenu attribute on the body node");
+  let nodeMutated = inspector.once("markupmutation");
+  let attr = editor.attrElements.get("contextmenu").querySelector(".editable");
+  setEditableFieldValue(attr, "contextmenu=\"menu2\"", inspector);
+  yield nodeMutated;
+
+  info("Checking for links in the updated attribute");
+  ({editor}) = yield getContainerForSelector("body", inspector);
+  linkEls = editor.attrElements.get("contextmenu").querySelectorAll(".link");
+  is(linkEls.length, 1, "There is one link in the contextmenu attribute");
+  is(linkEls[0].dataset.type, "idref", "The link has the right type");
+  is(linkEls[0].textContent, "menu2", "The link has the right value");
+});
new file mode 100644
--- /dev/null
+++ b/browser/devtools/markupview/test/browser_markupview_links_03.js
@@ -0,0 +1,37 @@
+/* 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 that links appear correctly in attributes created in content.
+
+const TEST_URL = TEST_URL_ROOT + "doc_markup_links.html";
+
+add_task(function*() {
+  let {inspector} = yield addTab(TEST_URL).then(openInspector);
+
+  info("Adding a contextmenu attribute to the body node via the content");
+  let onMutated = inspector.once("markupmutation");
+  yield setNodeAttribute("body", "contextmenu", "menu1");
+  yield onMutated;
+
+  info("Checking for links in the new attribute");
+  let {editor} = yield getContainerForSelector("body", inspector);
+  let linkEls = editor.attrElements.get("contextmenu").querySelectorAll(".link");
+  is(linkEls.length, 1, "There is one link in the contextmenu attribute");
+  is(linkEls[0].dataset.type, "idref", "The link has the right type");
+  is(linkEls[0].textContent, "menu1", "The link has the right value");
+
+  info("Editing the contextmenu attribute on the body node");
+  onMutated = inspector.once("markupmutation");
+  yield setNodeAttribute("body", "contextmenu", "menu2");
+  yield onMutated;
+
+  info("Checking for links in the updated attribute");
+  ({editor}) = yield getContainerForSelector("body", inspector);
+  linkEls = editor.attrElements.get("contextmenu").querySelectorAll(".link");
+  is(linkEls.length, 1, "There is one link in the contextmenu attribute");
+  is(linkEls[0].dataset.type, "idref", "The link has the right type");
+  is(linkEls[0].textContent, "menu2", "The link has the right value");
+});
new file mode 100644
--- /dev/null
+++ b/browser/devtools/markupview/test/browser_markupview_links_04.js
@@ -0,0 +1,108 @@
+/* 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 that the contextual menu shows the right items when clicking on a link
+// in an attribute.
+
+const TEST_URL = TEST_URL_ROOT + "doc_markup_links.html";
+const STRINGS = Services.strings
+  .createBundle("chrome://browser/locale/devtools/inspector.properties");
+
+// The test case array contains objects with the following properties:
+// - selector: css selector for the node to select in the inspector
+// - attributeName: name of the attribute to test
+// - popupNodeSelector: css selector for the element inside the attribute
+//   element to use as the contextual menu anchor
+// - isLinkFollowItemVisible: is the follow-link item expected to be displayed
+// - isLinkCopyItemVisible: is the copy-link item expected to be displayed
+// - linkFollowItemLabel: the expected label of the follow-link item
+// - linkCopyItemLabel: the expected label of the copy-link item
+const TEST_DATA = [{
+  selector: "link",
+  attributeName: "href",
+  popupNodeSelector: ".link",
+  isLinkFollowItemVisible: true,
+  isLinkCopyItemVisible: true,
+  linkFollowItemLabel: STRINGS.GetStringFromName("inspector.menu.openUrlInNewTab.label"),
+  linkCopyItemLabel: STRINGS.GetStringFromName("inspector.menu.copyUrlToClipboard.label")
+}, {
+  selector: "link[rel=icon]",
+  attributeName: "href",
+  popupNodeSelector: ".link",
+  isLinkFollowItemVisible: true,
+  isLinkCopyItemVisible: true,
+  linkFollowItemLabel: STRINGS.GetStringFromName("inspector.menu.openUrlInNewTab.label"),
+  linkCopyItemLabel: STRINGS.GetStringFromName("inspector.menu.copyUrlToClipboard.label")
+}, {
+  selector: "link",
+  attributeName: "rel",
+  popupNodeSelector: ".attr-value",
+  isLinkFollowItemVisible: false,
+  isLinkCopyItemVisible: false
+}, {
+  selector: "output",
+  attributeName: "for",
+  popupNodeSelector: ".link",
+  isLinkFollowItemVisible: true,
+  isLinkCopyItemVisible: false,
+  linkFollowItemLabel: STRINGS.formatStringFromName(
+    "inspector.menu.selectElement.label", ["name"], 1)
+}, {
+  selector: "script",
+  attributeName: "src",
+  popupNodeSelector: ".link",
+  isLinkFollowItemVisible: true,
+  isLinkCopyItemVisible: true,
+  linkFollowItemLabel: STRINGS.GetStringFromName("inspector.menu.openUrlInNewTab.label"),
+  linkCopyItemLabel: STRINGS.GetStringFromName("inspector.menu.copyUrlToClipboard.label")
+}, {
+  selector: "p[for]",
+  attributeName: "for",
+  popupNodeSelector: ".attr-value",
+  isLinkFollowItemVisible: false,
+  isLinkCopyItemVisible: false
+}];
+
+add_task(function*() {
+  let {inspector} = yield addTab(TEST_URL).then(openInspector);
+
+  let linkFollow = inspector.panelDoc.getElementById("node-menu-link-follow");
+  let linkCopy = inspector.panelDoc.getElementById("node-menu-link-copy");
+
+  for (let test of TEST_DATA) {
+    info("Selecting test node " + test.selector);
+    yield selectNode(test.selector, inspector);
+
+    info("Finding the popupNode to anchor the context-menu to");
+    let {editor} = yield getContainerForSelector(test.selector, inspector);
+    let popupNode = editor.attrElements.get(test.attributeName)
+                    .querySelector(test.popupNodeSelector);
+    ok(popupNode, "Found the popupNode in attribute " + test.attributeName);
+
+    info("Simulating a context click on the popupNode");
+    contextMenuClick(popupNode);
+
+    // The contextual menu setup is async, because it needs to know if the
+    // inspector has the resolveRelativeURL method first. So call actorHasMethod
+    // here too to make sure the first call resolves first and the menu is
+    // properly setup.
+    yield inspector.target.actorHasMethod("inspector", "resolveRelativeURL");
+
+    is(linkFollow.hasAttribute("hidden"), !test.isLinkFollowItemVisible,
+      "The follow-link item display is correct");
+    is(linkCopy.hasAttribute("hidden"), !test.isLinkCopyItemVisible,
+      "The copy-link item display is correct");
+
+    if (test.isLinkFollowItemVisible) {
+      is(linkFollow.getAttribute("label"), test.linkFollowItemLabel,
+        "the follow-link label is correct");
+    }
+    if (test.isLinkCopyItemVisible) {
+      is(linkCopy.getAttribute("label"), test.linkCopyItemLabel,
+        "the copy-link label is correct");
+    }
+  }
+});
new file mode 100644
--- /dev/null
+++ b/browser/devtools/markupview/test/browser_markupview_links_05.js
@@ -0,0 +1,82 @@
+/* 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 that the contextual menu items shown when clicking on links in
+// attributes actually do the right things.
+
+const TEST_URL = TEST_URL_ROOT + "doc_markup_links.html";
+
+add_task(function*() {
+  let {inspector} = yield addTab(TEST_URL).then(openInspector);
+
+  let linkFollow = inspector.panelDoc.getElementById("node-menu-link-follow");
+  let linkCopy = inspector.panelDoc.getElementById("node-menu-link-copy");
+
+  info("Select a node with a URI attribute");
+  yield selectNode("video", inspector);
+
+  info("Set the popupNode to the node that contains the uri");
+  let {editor} = yield getContainerForSelector("video", inspector);
+  let popupNode = editor.attrElements.get("poster").querySelector(".link");
+  inspector.panelDoc.popupNode = popupNode;
+
+  info("Follow the link and wait for the new tab to open");
+  let onTabOpened = once(gBrowser.tabContainer, "TabOpen");
+  inspector.followAttributeLink();
+  let {target: tab} = yield onTabOpened;
+  yield waitForTabLoad(tab);
+
+  ok(true, "A new tab opened");
+  is(tab.linkedBrowser.currentURI.spec, TEST_URL_ROOT + "doc_markup_tooltip.png",
+    "The URL for the new tab is correct");
+  gBrowser.removeTab(tab);
+
+  info("Select a node with a IDREF attribute");
+  yield selectNode("label", inspector);
+
+  info("Set the popupNode to the node that contains the ref");
+  ({editor}) = yield getContainerForSelector("label", inspector);
+  popupNode = editor.attrElements.get("for").querySelector(".link");
+  inspector.panelDoc.popupNode = popupNode;
+
+  info("Follow the link and wait for the new node to be selected");
+  let onSelection = inspector.selection.once("new-node-front");
+  inspector.followAttributeLink();
+  yield onSelection;
+
+  ok(true, "A new node was selected");
+  is(inspector.selection.nodeFront.id, "name", "The right node was selected");
+
+  info("Select a node with an invalid IDREF attribute");
+  yield selectNode("output", inspector);
+
+  info("Set the popupNode to the node that contains the ref");
+  ({editor}) = yield getContainerForSelector("output", inspector);
+  popupNode = editor.attrElements.get("for").querySelectorAll(".link")[2];
+  inspector.panelDoc.popupNode = popupNode;
+
+  info("Try to follow the link and check that no new node were selected");
+  let onFailed = inspector.once("idref-attribute-link-failed");
+  inspector.followAttributeLink();
+  yield onFailed;
+
+  ok(true, "The node selection failed");
+  is(inspector.selection.nodeFront.tagName.toLowerCase(), "output",
+    "The <output> node is still selected");
+});
+
+function waitForTabLoad(tab) {
+  let def = promise.defer();
+  tab.addEventListener("load", function onLoad(e) {
+    // Skip load event for about:blank
+    if (tab.linkedBrowser.currentURI.spec === "about:blank") {
+      return;
+    }
+    tab.removeEventListener("load", onLoad);
+    def.resolve();
+  });
+  return def.promise;
+}
new file mode 100644
--- /dev/null
+++ b/browser/devtools/markupview/test/doc_markup_links.html
@@ -0,0 +1,42 @@
+<!DOCTYPE html>
+<html>
+  <head>
+    <meta charset="utf-8">
+    <title>Markup-view links</title>
+    <link rel="stylesheet" type="text/css" href="style.css">
+    <link rel="icon" type="image/png" sizes="196x196" href="/media/img/firefox/favicon-196.223e1bcaf067.png">
+  </head>
+  <body>
+    <form id="message-form" method="post" action="/post_message">
+      <p for="invalid-idref">
+        <label for="name">Name</label>
+        <input id="name" type="text" />
+      </p>
+      <p>
+        <label for="message">Message</label>
+        <input id="message" type="text" />
+      </p>
+      <p>
+        <button>Send message</button>
+      </p>
+      <output form="message-form" for="name message invalid">Thank you for your message!</output>
+    </form>
+    <a href="/go/somewhere/else" ping="/analytics?page=pageA /analytics?user=test">Click me, I'm a link</a>
+    <ul>
+      <li contextmenu="menu1">Item 1</li>
+      <li contextmenu="menu2">Item 2</li>
+      <li contextmenu="menu3">Item 3</li>
+    </ul>
+    <menu type="context" id="menu1">
+      <menuitem label="custom menu 1"></menuitem>
+    </menu>
+    <menu type="context" id="menu2">
+      <menuitem label="custom menu 2"></menuitem>
+    </menu>
+    <menu type="context" id="menu3">
+      <menuitem label="custom menu 3"></menuitem>
+    </menu>
+    <video controls poster="doc_markup_tooltip.png" src="code-rush.mp4"></video>
+    <script type="text/javascript" src="lib_jquery_1.0.js"></script>
+  </body>
+</html>
--- a/browser/devtools/markupview/test/head.js
+++ b/browser/devtools/markupview/test/head.js
@@ -680,8 +680,22 @@ function createTestHTTPServer() {
       destroyed.resolve();
     });
     yield destroyed.promise;
   });
 
   server.start(-1);
   return server;
 }
+
+/**
+ * A helper that simulates a contextmenu event on the given chrome DOM element.
+ */
+function contextMenuClick(element) {
+  let evt = element.ownerDocument.createEvent('MouseEvents');
+  let button = 2;  // right click
+
+  evt.initMouseEvent('contextmenu', true, true,
+       element.ownerDocument.defaultView, 1, 0, 0, 0, 0, false,
+       false, false, false, button, null);
+
+  element.dispatchEvent(evt);
+}
--- a/browser/devtools/shared/moz.build
+++ b/browser/devtools/shared/moz.build
@@ -47,16 +47,17 @@ EXTRA_JS_MODULES.devtools.shared.timelin
 
 EXTRA_JS_MODULES.devtools.shared += [
     'autocomplete-popup.js',
     'devices.js',
     'doorhanger.js',
     'frame-script-utils.js',
     'getjson.js',
     'inplace-editor.js',
+    'node-attribute-parser.js',
     'observable-object.js',
     'options-view.js',
     'source-utils.js',
     'telemetry.js',
     'theme-switching.js',
     'theme.js',
     'undo.js',
 ]
new file mode 100644
--- /dev/null
+++ b/browser/devtools/shared/node-attribute-parser.js
@@ -0,0 +1,278 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2; js-indent-level: 2 -*- */
+/* vim: set ft=javascript 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";
+
+/**
+ * This module contains a small element attribute value parser. It's primary
+ * goal is to extract link information from attribute values (like the href in
+ * <a href="/some/link.html"> for example).
+ *
+ * There are several types of linkable attribute values:
+ * - TYPE_URI: a URI (e.g. <a href="uri">).
+ * - TYPE_URI_LIST: a space separated list of URIs (e.g. <a ping="uri1 uri2">).
+ * - TYPE_IDREF: a reference to an other element in the same document via its id
+ *   (e.g. <label for="input-id"> or <key command="command-id">).
+ * - TYPE_IDREF_LIST: a space separated list of IDREFs (e.g.
+ *   <output for="id1 id2">).
+ * - TYPE_RESOURCE_URI: a URI to a javascript or css resource that can be opened
+ *   in the devtools (e.g. <script src="uri">).
+ *
+ * parseAttribute is the parser entry function, exported on this module.
+ */
+
+const TYPE_STRING = "string";
+const TYPE_URI = "uri";
+const TYPE_URI_LIST = "uriList";
+const TYPE_IDREF = "idref";
+const TYPE_IDREF_LIST = "idrefList";
+const TYPE_RESOURCE_URI = "resource";
+
+const SVG_NS = "http://www.w3.org/2000/svg";
+const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
+const HTML_NS = "http://www.w3.org/1999/xhtml";
+
+const ATTRIBUTE_TYPES = [
+  {namespaceURI: HTML_NS, attributeName: "action", tagName: "form", type: TYPE_URI},
+  {namespaceURI: HTML_NS, attributeName: "background", tagName: "body", type: TYPE_URI},
+  {namespaceURI: HTML_NS, attributeName: "cite", tagName: "blockquote", type: TYPE_URI},
+  {namespaceURI: HTML_NS, attributeName: "cite", tagName: "q", type: TYPE_URI},
+  {namespaceURI: HTML_NS, attributeName: "cite", tagName: "del", type: TYPE_URI},
+  {namespaceURI: HTML_NS, attributeName: "cite", tagName: "ins", type: TYPE_URI},
+  {namespaceURI: HTML_NS, attributeName: "classid", tagName: "object", type: TYPE_URI},
+  {namespaceURI: HTML_NS, attributeName: "codebase", tagName: "object", type: TYPE_URI},
+  {namespaceURI: HTML_NS, attributeName: "codebase", tagName: "applet", type: TYPE_URI},
+  {namespaceURI: HTML_NS, attributeName: "command", tagName: "menuitem", type: TYPE_IDREF},
+  {namespaceURI: "*", attributeName: "contextmenu", tagName: "*", type: TYPE_IDREF},
+  {namespaceURI: HTML_NS, attributeName: "data", tagName: "object", type: TYPE_URI},
+  {namespaceURI: HTML_NS, attributeName: "for", tagName: "label", type: TYPE_IDREF},
+  {namespaceURI: HTML_NS, attributeName: "for", tagName: "output", type: TYPE_IDREF_LIST},
+  {namespaceURI: HTML_NS, attributeName: "form", tagName: "button", type: TYPE_IDREF},
+  {namespaceURI: HTML_NS, attributeName: "form", tagName: "fieldset", type: TYPE_IDREF},
+  {namespaceURI: HTML_NS, attributeName: "form", tagName: "input", type: TYPE_IDREF},
+  {namespaceURI: HTML_NS, attributeName: "form", tagName: "keygen", type: TYPE_IDREF},
+  {namespaceURI: HTML_NS, attributeName: "form", tagName: "label", type: TYPE_IDREF},
+  {namespaceURI: HTML_NS, attributeName: "form", tagName: "object", type: TYPE_IDREF},
+  {namespaceURI: HTML_NS, attributeName: "form", tagName: "output", type: TYPE_IDREF},
+  {namespaceURI: HTML_NS, attributeName: "form", tagName: "select", type: TYPE_IDREF},
+  {namespaceURI: HTML_NS, attributeName: "form", tagName: "textarea", type: TYPE_IDREF},
+  {namespaceURI: HTML_NS, attributeName: "formaction", tagName: "button", type: TYPE_URI},
+  {namespaceURI: HTML_NS, attributeName: "formaction", tagName: "input", type: TYPE_URI},
+  {namespaceURI: HTML_NS, attributeName: "headers", tagName: "td", type: TYPE_IDREF_LIST},
+  {namespaceURI: HTML_NS, attributeName: "headers", tagName: "th", type: TYPE_IDREF_LIST},
+  {namespaceURI: HTML_NS, attributeName: "href", tagName: "a", type: TYPE_URI},
+  {namespaceURI: HTML_NS, attributeName: "href", tagName: "area", type: TYPE_URI},
+  {namespaceURI: "*", attributeName: "href", tagName: "link", type: TYPE_RESOURCE_URI,
+   isValid: (namespaceURI, tagName, attributes) => {
+    return getAttribute(attributes, "rel") === "stylesheet";
+   }},
+  {namespaceURI: "*", attributeName: "href", tagName: "link", type: TYPE_URI},
+  {namespaceURI: HTML_NS, attributeName: "href", tagName: "base", type: TYPE_URI},
+  {namespaceURI: HTML_NS, attributeName: "icon", tagName: "menuitem", type: TYPE_URI},
+  {namespaceURI: HTML_NS, attributeName: "list", tagName: "input", type: TYPE_IDREF},
+  {namespaceURI: HTML_NS, attributeName: "longdesc", tagName: "img", type: TYPE_URI},
+  {namespaceURI: HTML_NS, attributeName: "longdesc", tagName: "frame", type: TYPE_URI},
+  {namespaceURI: HTML_NS, attributeName: "longdesc", tagName: "iframe", type: TYPE_URI},
+  {namespaceURI: HTML_NS, attributeName: "manifest", tagName: "html", type: TYPE_URI},
+  {namespaceURI: HTML_NS, attributeName: "menu", tagName: "button", type: TYPE_IDREF},
+  {namespaceURI: HTML_NS, attributeName: "ping", tagName: "a", type: TYPE_URI_LIST},
+  {namespaceURI: HTML_NS, attributeName: "ping", tagName: "area", type: TYPE_URI_LIST},
+  {namespaceURI: HTML_NS, attributeName: "poster", tagName: "video", type: TYPE_URI},
+  {namespaceURI: HTML_NS, attributeName: "profile", tagName: "head", type: TYPE_URI},
+  {namespaceURI: "*", attributeName: "src", tagName: "script", type: TYPE_RESOURCE_URI},
+  {namespaceURI: HTML_NS, attributeName: "src", tagName: "input", type: TYPE_URI},
+  {namespaceURI: HTML_NS, attributeName: "src", tagName: "frame", type: TYPE_URI},
+  {namespaceURI: HTML_NS, attributeName: "src", tagName: "iframe", type: TYPE_URI},
+  {namespaceURI: HTML_NS, attributeName: "src", tagName: "img", type: TYPE_URI},
+  {namespaceURI: HTML_NS, attributeName: "src", tagName: "audio", type: TYPE_URI},
+  {namespaceURI: HTML_NS, attributeName: "src", tagName: "embed", type: TYPE_URI},
+  {namespaceURI: HTML_NS, attributeName: "src", tagName: "source", type: TYPE_URI},
+  {namespaceURI: HTML_NS, attributeName: "src", tagName: "track", type: TYPE_URI},
+  {namespaceURI: HTML_NS, attributeName: "src", tagName: "video", type: TYPE_URI},
+  {namespaceURI: HTML_NS, attributeName: "usemap", tagName: "img", type: TYPE_URI},
+  {namespaceURI: HTML_NS, attributeName: "usemap", tagName: "input", type: TYPE_URI},
+  {namespaceURI: HTML_NS, attributeName: "usemap", tagName: "object", type: TYPE_URI},
+  {namespaceURI: "*", attributeName: "xmlns", tagName: "*", type: TYPE_URI},
+  {namespaceURI: XUL_NS, attributeName: "command", tagName: "key", type: TYPE_IDREF},
+  {namespaceURI: XUL_NS, attributeName: "containment", tagName: "*", type: TYPE_URI},
+  {namespaceURI: XUL_NS, attributeName: "context", tagName: "*", type: TYPE_IDREF},
+  {namespaceURI: XUL_NS, attributeName: "datasources", tagName: "*", type: TYPE_URI_LIST},
+  {namespaceURI: XUL_NS, attributeName: "insertafter", tagName: "*", type: TYPE_IDREF},
+  {namespaceURI: XUL_NS, attributeName: "insertbefore", tagName: "*", type: TYPE_IDREF},
+  {namespaceURI: XUL_NS, attributeName: "menu", tagName: "*", type: TYPE_IDREF},
+  {namespaceURI: XUL_NS, attributeName: "observes", tagName: "*", type: TYPE_IDREF},
+  {namespaceURI: XUL_NS, attributeName: "popup", tagName: "*", type: TYPE_IDREF},
+  {namespaceURI: XUL_NS, attributeName: "ref", tagName: "*", type: TYPE_URI},
+  {namespaceURI: XUL_NS, attributeName: "removeelement", tagName: "*", type: TYPE_IDREF},
+  {namespaceURI: XUL_NS, attributeName: "sortResource", tagName: "*", type: TYPE_URI},
+  {namespaceURI: XUL_NS, attributeName: "sortResource2", tagName: "*", type: TYPE_URI},
+  {namespaceURI: XUL_NS, attributeName: "src", tagName: "stringbundle", type: TYPE_URI},
+  {namespaceURI: XUL_NS, attributeName: "template", tagName: "*", type: TYPE_IDREF},
+  {namespaceURI: XUL_NS, attributeName: "tooltip", tagName: "*", type: TYPE_IDREF},
+  // SVG links aren't handled yet, see bug 1158831.
+  // {namespaceURI: SVG_NS, attributeName: "fill", tagName: "*", type: },
+  // {namespaceURI: SVG_NS, attributeName: "stroke", tagName: "*", type: },
+  // {namespaceURI: SVG_NS, attributeName: "markerstart", tagName: "*", type: },
+  // {namespaceURI: SVG_NS, attributeName: "markermid", tagName: "*", type: },
+  // {namespaceURI: SVG_NS, attributeName: "markerend", tagName: "*", type: },
+  // {namespaceURI: SVG_NS, attributeName: "xlink:href", tagName: "*", type: }
+];
+
+let parsers = {
+  [TYPE_URI]: function(attributeValue) {
+    return [{
+      type: TYPE_URI,
+      value: attributeValue
+    }];
+  },
+  [TYPE_URI_LIST]: function(attributeValue) {
+    let data = splitBy(attributeValue, " ");
+    for (let token of data) {
+      if (!token.type) {
+        token.type = TYPE_URI;
+      }
+    }
+    return data;
+  },
+  [TYPE_RESOURCE_URI]: function(attributeValue) {
+    return [{
+      type: TYPE_RESOURCE_URI,
+      value: attributeValue
+    }];
+  },
+  [TYPE_IDREF]: function(attributeValue) {
+    return [{
+      type: TYPE_IDREF,
+      value: attributeValue
+    }];
+  },
+  [TYPE_IDREF_LIST]: function(attributeValue) {
+    let data = splitBy(attributeValue, " ");
+    for (let token of data) {
+      if (!token.type) {
+        token.type = TYPE_IDREF;
+      }
+    }
+    return data;
+  }
+};
+
+/**
+ * Parse an attribute value.
+ * @param {String} namespaceURI The namespaceURI of the node that has the
+ * attribute.
+ * @param {String} tagName The tagName of the node that has the attribute.
+ * @param {Array} attributes The list of all attributes of the node. This should
+ * be an array of {name, value} objects.
+ * @param {String} attributeName The name of the attribute to parse.
+ * @return {Array} An array of tokens that represents the value. Each token is
+ * an object {type: [string|uri|resource|idref], value}.
+ * For instance parsing the ping attribute in <a ping="uri1 uri2"> returns:
+ * [
+ *   {type: "uri", value: "uri2"},
+ *   {type: "string", value: " "},
+ *   {type: "uri", value: "uri1"}
+ * ]
+ */
+function parseAttribute(namespaceURI, tagName, attributes, attributeName) {
+  if (!hasAttribute(attributes, attributeName)) {
+    throw new Error(`Attribute ${attributeName} isn't part of the provided attributes`);
+  }
+
+  let type = getType(namespaceURI, tagName, attributes, attributeName);
+  if (!type) {
+    return [{
+      type: TYPE_STRING,
+      value: getAttribute(attributes, attributeName)
+    }];
+  }
+
+  return parsers[type](getAttribute(attributes, attributeName));
+}
+
+/**
+ * Get the type for links in this attribute if any.
+ * @param {String} namespaceURI The node's namespaceURI.
+ * @param {String} tagName The node's tagName.
+ * @param {Array} attributes The node's attributes, as a list of {name, value}
+ * objects.
+ * @param {String} attributeName The name of the attribute to get the type for.
+ * @return {Object} null if no type exist for this attribute on this node, the
+ * type object otherwise.
+ */
+function getType(namespaceURI, tagName, attributes, attributeName) {
+  for (let typeData of ATTRIBUTE_TYPES) {
+    let hasAttribute = attributeName === typeData.attributeName ||
+                       typeData.attributeName === "*";
+    let hasNamespace = namespaceURI === typeData.namespaceURI ||
+                       typeData.namespaceURI === "*";
+    let hasTagName = tagName.toLowerCase() === typeData.tagName ||
+                     typeData.tagName === "*";
+    let isValid = typeData.isValid
+                  ? typeData.isValid(namespaceURI, tagName, attributes, attributeName)
+                  : true;
+
+    if (hasAttribute && hasNamespace && hasTagName && isValid) {
+      return typeData.type;
+    }
+  }
+
+  return null;
+}
+
+function getAttribute(attributes, attributeName) {
+  for (let {name, value} of attributes) {
+    if (name === attributeName) {
+      return value;
+    }
+  }
+  return null;
+}
+
+function hasAttribute(attributes, attributeName) {
+  for (let {name, value} of attributes) {
+    if (name === attributeName) {
+      return true;
+    }
+  }
+  return false;
+}
+
+/**
+ * Split a string by a given character and return an array of objects parts.
+ * The array will contain objects for the split character too, marked with
+ * TYPE_STRING type.
+ * @param {String} value The string to parse.
+ * @param {String} splitChar A 1 length split character.
+ * @return {Array}
+ */
+function splitBy(value, splitChar) {
+  let data = [], i = 0, buffer = "";
+  while (i <= value.length) {
+    if (i === value.length && buffer) {
+      data.push({value: buffer});
+    }
+    if (value[i] === splitChar) {
+      if (buffer) {
+        data.push({value: buffer});
+      }
+      data.push({
+        type: TYPE_STRING,
+        value: splitChar
+      });
+      buffer = "";
+    } else {
+      buffer += value[i];
+    }
+
+    i ++;
+  }
+  return data;
+}
+
+exports.parseAttribute = parseAttribute;
+// Exported for testing only.
+exports.splitBy = splitBy;
new file mode 100644
--- /dev/null
+++ b/browser/devtools/shared/test/unit/test_attribute-parsing-01.js
@@ -0,0 +1,73 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Test splitBy from node-attribute-parser.js
+
+const Cu = Components.utils;
+let {devtools} = Cu.import("resource://gre/modules/devtools/Loader.jsm", {});
+let require = devtools.require;
+const {splitBy} = require("devtools/shared/node-attribute-parser");
+
+const TEST_DATA = [{
+  value: "this is a test",
+  splitChar: " ",
+  expected: [
+    {value: "this"},
+    {value: " ", type: "string"},
+    {value: "is"},
+    {value: " ", type: "string"},
+    {value: "a"},
+    {value: " ", type: "string"},
+    {value: "test"}
+  ]
+}, {
+  value: "/path/to/handler",
+  splitChar: " ",
+  expected: [
+    {value: "/path/to/handler"}
+  ]
+}, {
+  value: "test",
+  splitChar: " ",
+  expected: [
+    {value: "test"}
+  ]
+}, {
+  value: " test ",
+  splitChar: " ",
+  expected: [
+    {value: " ", type: "string"},
+    {value: "test"},
+    {value: " ", type: "string"}
+  ]
+}, {
+  value: "",
+  splitChar: " ",
+  expected: []
+}, {
+  value: "   ",
+  splitChar: " ",
+  expected: [
+    {value: " ", type: "string"},
+    {value: " ", type: "string"},
+    {value: " ", type: "string"}
+  ]
+}];
+
+function run_test() {
+  for (let {value, splitChar, expected} of TEST_DATA) {
+    do_print("Splitting string: " + value);
+    let tokens = splitBy(value, splitChar);
+
+    do_print("Checking that the number of parsed tokens is correct");
+    do_check_eq(tokens.length, expected.length);
+
+    for (let i = 0; i < tokens.length; i ++) {
+      do_print("Checking the data in token " + i);
+      do_check_eq(tokens[i].value, expected[i].value);
+      if (expected[i].type) {
+        do_check_eq(tokens[i].type, expected[i].type);
+      }
+    }
+  }
+}
new file mode 100644
--- /dev/null
+++ b/browser/devtools/shared/test/unit/test_attribute-parsing-02.js
@@ -0,0 +1,131 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Test parseAttribute from node-attribute-parser.js
+
+const Cu = Components.utils;
+let {devtools} = Cu.import("resource://gre/modules/devtools/Loader.jsm", {});
+let require = devtools.require;
+const {parseAttribute} = require("devtools/shared/node-attribute-parser");
+
+const TEST_DATA = [{
+  tagName: "body",
+  namespaceURI: "http://www.w3.org/1999/xhtml",
+  attributeName: "class",
+  attributeValue: "some css class names",
+  expected: [
+    {value: "some css class names", type: "string"}
+  ]
+}, {
+  tagName: "box",
+  namespaceURI: "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul",
+  attributeName: "datasources",
+  attributeValue: "/url/1?test=1#test http://mozilla.org/wow",
+  expected: [
+    {value: "/url/1?test=1#test", type: "uri"},
+    {value: " ", type: "string"},
+    {value: "http://mozilla.org/wow", type: "uri"}
+  ]
+}, {
+  tagName: "form",
+  namespaceURI: "http://www.w3.org/1999/xhtml",
+  attributeName: "action",
+  attributeValue: "/path/to/handler",
+  expected: [
+    {value: "/path/to/handler", type: "uri"}
+  ]
+}, {
+  tagName: "a",
+  namespaceURI: "http://www.w3.org/1999/xhtml",
+  attributeName: "ping",
+  attributeValue: "http://analytics.com/track?id=54 http://analytics.com/track?id=55",
+  expected: [
+    {value: "http://analytics.com/track?id=54", type: "uri"},
+    {value: " ", type: "string"},
+    {value: "http://analytics.com/track?id=55", type: "uri"}
+  ]
+}, {
+  tagName: "link",
+  namespaceURI: "http://www.w3.org/1999/xhtml",
+  attributeName: "href",
+  attributeValue: "styles.css",
+  otherAttributes: [{name: "rel", value: "stylesheet"}],
+  expected: [
+    {value: "styles.css", type: "resource"}
+  ]
+}, {
+  tagName: "link",
+  namespaceURI: "http://www.w3.org/1999/xhtml",
+  attributeName: "href",
+  attributeValue: "styles.css",
+  expected: [
+    {value: "styles.css", type: "uri"}
+  ]
+}, {
+  tagName: "output",
+  namespaceURI: "http://www.w3.org/1999/xhtml",
+  attributeName: "for",
+  attributeValue: "element-id something id",
+  expected: [
+    {value: "element-id", type: "idref"},
+    {value: " ", type: "string"},
+    {value: "something", type: "idref"},
+    {value: " ", type: "string"},
+    {value: "id", type: "idref"}
+  ]
+}, {
+  tagName: "img",
+  namespaceURI: "http://www.w3.org/1999/xhtml",
+  attributeName: "contextmenu",
+  attributeValue: "id-of-menu",
+  expected: [
+    {value: "id-of-menu", type: "idref"}
+  ]
+}, {
+  tagName: "img",
+  namespaceURI: "http://www.w3.org/1999/xhtml",
+  attributeName: "src",
+  attributeValue: "omg-thats-so-funny.gif",
+  expected: [
+    {value: "omg-thats-so-funny.gif", type: "uri"}
+  ]
+}, {
+  tagName: "key",
+  namespaceURI: "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul",
+  attributeName: "command",
+  attributeValue: "some_command_id",
+  expected: [
+    {value: "some_command_id", type: "idref"}
+  ]
+}, {
+  tagName: "script",
+  namespaceURI: "whatever",
+  attributeName: "src",
+  attributeValue: "script.js",
+  expected: [
+    {value: "script.js", type: "resource"}
+  ]
+}];
+
+function run_test() {
+  for (let {tagName, namespaceURI, attributeName,
+            otherAttributes, attributeValue, expected} of TEST_DATA) {
+    do_print("Testing <" + tagName + " " + attributeName + "='" + attributeValue + "'>");
+
+    let attributes = [...otherAttributes||[], {name: attributeName, value: attributeValue}];
+    let tokens = parseAttribute(namespaceURI, tagName, attributes, attributeName);
+    if (!expected) {
+      do_check_true(!tokens);
+      continue;
+    }
+
+    do_print("Checking that the number of parsed tokens is correct");
+    do_check_eq(tokens.length, expected.length);
+
+    for (let i = 0; i < tokens.length; i ++) {
+      do_print("Checking the data in token " + i);
+      do_check_eq(tokens[i].value, expected[i].value);
+      do_check_eq(tokens[i].type, expected[i].type);
+    }
+  }
+}
--- a/browser/devtools/shared/test/unit/xpcshell.ini
+++ b/browser/devtools/shared/test/unit/xpcshell.ini
@@ -1,11 +1,13 @@
 [DEFAULT]
 tags = devtools
 head =
 tail =
 firefox-appdir = browser
 skip-if = toolkit == 'android' || toolkit == 'gonk'
 
+[test_attribute-parsing-01.js]
+[test_attribute-parsing-02.js]
 [test_bezierCanvas.js]
 [test_cubicBezier.js]
 [test_undoStack.js]
 [test_VariablesView_getString_promise.js]
--- a/browser/locales/en-US/chrome/browser/devtools/inspector.properties
+++ b/browser/locales/en-US/chrome/browser/devtools/inspector.properties
@@ -67,8 +67,41 @@ docsTooltip.loadDocsError=Could not load
 # that collapses the right panel (rules, computed, box-model, etc...) in the
 # inspector UI.
 inspector.collapsePane=Collapse pane
 
 # LOCALIZATION NOTE (inspector.expandPane): This is the tooltip for the button
 # that expands the right panel (rules, computed, box-model, etc...) in the
 # inspector UI.
 inspector.expandPane=Expand pane
+
+# LOCALIZATION NOTE (inspector.menu.openUrlInNewTab.label): This is the label of
+# a menu item in the inspector contextual-menu that appears when the user right-
+# clicks on the attribute of a node in the inspector that is a URL, and that
+# allows to open that URL in a new tab.
+inspector.menu.openUrlInNewTab.label=Open Link in New Tab
+
+# LOCALIZATION NOTE (inspector.menu.copyUrlToClipboard.label): This is the label
+# of a menu item in the inspector contextual-menu that appears when the user
+# right-clicks on the attribute of a node in the inspector that is a URL, and
+# that allows to copy that URL in the clipboard.
+inspector.menu.copyUrlToClipboard.label=Copy Link Address
+
+# LOCALIZATION NOTE (inspector.menu.openFileInDebugger.label): This is the label
+# of a menu item in the inspector contextual-menu that appears when the user
+# right-clicks on the attribute of a node in the inspector that is a URL to a
+# javascript filename, and that allows to open the corresponding file in the
+# debugger.
+inspector.menu.openFileInDebugger.label=Open File in Debugger
+
+# LOCALIZATION NOTE (inspector.menu.openFileInStyleEditor.label): This is the
+# label of a menu item in the inspector contextual-menu that appears when the
+# user right-clicks on the attribute of a node in the inspector that is a URL to
+# a css filename, and that allows to open the corresponding file in the style
+# editor.
+inspector.menu.openFileInStyleEditor.label=Open File in Style-Editor
+
+# LOCALIZATION NOTE (inspector.menu.selectElement.label): This is the label of a
+# menu item in the inspector contextual-menu that appears when the user right-
+# clicks on the attribute of a node in the inspector that is the ID of another
+# element in the DOM (like with <label for="input-id">), and that allows to
+# select that element in the inspector.
+inspector.menu.selectElement.label=Select Element #%S
--- a/build/mobile/robocop/Makefile.in
+++ b/build/mobile/robocop/Makefile.in
@@ -1,15 +1,14 @@
 # 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/.
 
-mobile-tests := mobile/android/base/tests
+mobile-tests := mobile/android/tests/browser/robocop
 TESTPATH     := $(topsrcdir)/$(mobile-tests)
-dir-tests    := $(DEPTH)/$(mobile-tests)
 
 ANDROID_APK_NAME := robocop-debug
 
 ANDROID_EXTRA_JARS += \
   $(srcdir)/robotium-solo-4.3.1.jar \
   $(NULL)
 
 ANDROID_ASSETS_DIR := $(TESTPATH)/assets
@@ -57,18 +56,16 @@ JAVAFILES += \
   $(java-harness) \
   $(java-tests) \
   $(NULL)
 
 include $(topsrcdir)/config/rules.mk
 
 tools:: $(ANDROID_APK_NAME).apk
 
-GENERATED_DIRS += $(dir-tests)
-
 # The test APK needs to know the contents of the target APK while not
 # being linked against them.  This is a best effort to avoid getting
 # out of sync with base's build config.
 jars_dir := $(DEPTH)/mobile/android/base
 stumbler_jars_dir := $(DEPTH)/mobile/android/stumbler
 JAVA_BOOTCLASSPATH := $(JAVA_BOOTCLASSPATH):$(subst $(NULL) ,:,$(wildcard $(jars_dir)/*.jar)):$(subst $(NULL) ,:,$(wildcard $(stumbler_jars_dir)/*.jar)):$(ANDROID_COMPAT_LIB)
 # We also want to re-compile classes.dex when the associated base
 # content changes.
--- a/build/mobile/robocop/README
+++ b/build/mobile/robocop/README
@@ -4,9 +4,9 @@ Robotium is an open source tool licensed
 source can be found here:
 http://code.google.com/p/robotium/
 
 We are including robotium-solo-4.3.1.jar as a binary and are not modifying it in any way
 from the original download found at:
 http://code.google.com/p/robotium/
 
 Firefox for Android developers should read the documentation in
-mobile/android/base/tests/README.rst.
+mobile/android/tests/browser/robocop/README.rst.
--- a/build/mobile/robocop/moz.build
+++ b/build/mobile/robocop/moz.build
@@ -1,30 +1,17 @@
 # -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*-
 # vim: set filetype=python:
 # 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/.
 
 DEFINES['ANDROID_PACKAGE_NAME'] = CONFIG['ANDROID_PACKAGE_NAME']
 
-main = add_android_eclipse_project('Robocop', OBJDIR + '/AndroidManifest.xml')
-main.package_name = 'org.mozilla.roboexample.test'
-main.res = SRCDIR + '/res'
-main.recursive_make_targets += [OBJDIR + '/AndroidManifest.xml']
-main.extra_jars += [SRCDIR + '/robotium-solo-4.3.1.jar']
-main.assets = TOPSRCDIR + '/mobile/android/base/tests/assets'
-main.referenced_projects += ['Fennec']
-
-main.add_classpathentry('harness', SRCDIR,
-    dstdir='harness/org/mozilla/gecko')
-main.add_classpathentry('src', TOPSRCDIR + '/mobile/android/base/tests',
-    dstdir='src/org/mozilla/gecko/tests')
-
-base = '/mobile/android/base/tests/'
+base = '/mobile/android/tests/browser/robocop/'
 TEST_HARNESS_FILES.testing.mochitest += [
     base + 'robocop.ini',
     base + 'robocop_autophone.ini',
 ]
 TEST_HARNESS_FILES.testing.mochitest.tests.robocop += [base + x for x in [
     '*.html',
     '*.jpg',
     '*.mp4',
--- a/mobile/android/base/moz.build
+++ b/mobile/android/base/moz.build
@@ -812,17 +812,16 @@ static.referenced_projects += ['../' + g
 main = add_android_eclipse_project('Fennec', OBJDIR + '/AndroidManifest.xml')
 main.package_name = 'org.mozilla.gecko'
 
 # These values were extracted from an existing Eclipse project.  Use
 # Project > Resource > Resource Filters and inspect the resulting
 # .project file to modify this list.
 main.filtered_resources += [
     '1.0-projectRelativePath-matches-false-false-*org/mozilla/gecko/resources/**',
-    '1.0-projectRelativePath-matches-false-false-*org/mozilla/gecko/tests/**',
 ]
 
 main.recursive_make_targets += generated_recursive_make_targets
 main.recursive_make_targets += ['generated/' + f for f in mgjar.generated_sources]
 main.recursive_make_targets += ['generated/' + f for f in gbjar.generated_sources]
 
 main.extra_jars += [CONFIG['ANDROID_COMPAT_LIB']]
 main.assets = TOPOBJDIR + '/dist/' + CONFIG['MOZ_APP_NAME'] + '/assets'
@@ -832,17 +831,16 @@ main.libs = [
     (TOPOBJDIR + '/dist/' + CONFIG['MOZ_APP_NAME'] + '/lib/' + CONFIG['ANDROID_CPU_ARCH'] + '/libplugin-container.so',
      'libs/' + CONFIG['ANDROID_CPU_ARCH'] + '/libplugin-container.so'),
 ]
 main.res = None
 
 cpe = main.add_classpathentry('src', SRCDIR,
     dstdir='src/org/mozilla/gecko',
     exclude_patterns=[
-        'org/mozilla/gecko/tests/**',
         'org/mozilla/gecko/resources/**'])
 
 if not CONFIG['MOZ_CRASHREPORTER']:
     cpe.exclude_patterns += ['org/mozilla/gecko/CrashReporter.java']
 
 if CONFIG['MOZ_NATIVE_DEVICES']:
     # This is rather hacky, but: we define three Eclipse projects for appcompat,
     # mediarouter, and Google Play Services. mediarouter depends on appcompat;
--- a/mobile/android/chrome/content/SelectionHandler.js
+++ b/mobile/android/chrome/content/SelectionHandler.js
@@ -752,16 +752,53 @@ var SelectionHandler = {
             return false;
           }
 
           return SelectionHandler.isSelectionActive();
         }
       }
     },
 
+    SEARCH_ADD: {
+      id: "search_add_action",
+      label: Strings.browser.GetStringFromName("contextmenu.addSearchEngine2"),
+      icon: "drawable://ab_add_search_engine",
+
+      selector: {
+        matches: function(element) {
+          if(!(element instanceof HTMLInputElement)) {
+            return false;
+          }
+          let form = element.form;
+          if (!form || element.type == "password") {
+            return false;
+          }
+
+          // These are the following types of forms we can create keywords for:
+          //
+          // method    encoding type        can create keyword
+          // GET       *                                   YES
+          //           *                                   YES
+          // POST      *                                   YES
+          // POST      application/x-www-form-urlencoded   YES
+          // POST      text/plain                          NO ( a little tricky to do)
+          // POST      multipart/form-data                 NO
+          // POST      everything else                     YES
+          let method = form.method.toUpperCase();
+          return (method == "GET" || method == "") ||
+                 (form.enctype != "text/plain") && (form.enctype != "multipart/form-data");
+        },
+      },
+
+      action: function(element) {
+        UITelemetry.addEvent("action.1", "actionbar", null, "add_search_engine");
+        SearchEngines.addEngine(element);
+      },
+    },
+
     SEARCH: {
       label: function() {
         return Strings.browser.formatStringFromName("contextmenu.search", [Services.search.defaultEngine.name], 1);
       },
       id: "search_action",
       icon: "drawable://ab_search",
       action: function() {
         SelectionHandler.searchSelection();
--- a/mobile/android/chrome/content/browser.js
+++ b/mobile/android/chrome/content/browser.js
@@ -7089,52 +7089,16 @@ var SearchEngines = {
 
   init: function init() {
     Services.obs.addObserver(this, "SearchEngines:Add", false);
     Services.obs.addObserver(this, "SearchEngines:GetVisible", false);
     Services.obs.addObserver(this, "SearchEngines:Remove", false);
     Services.obs.addObserver(this, "SearchEngines:RestoreDefaults", false);
     Services.obs.addObserver(this, "SearchEngines:SetDefault", false);
     Services.obs.addObserver(this, "browser-search-engine-modified", false);
-
-    let filter = {
-      matches: function (aElement) {
-        // Copied from body of isTargetAKeywordField function in nsContextMenu.js
-        if(!(aElement instanceof HTMLInputElement))
-          return false;
-        let form = aElement.form;
-        if (!form || aElement.type == "password")
-          return false;
-
-        let method = form.method.toUpperCase();
-
-        // These are the following types of forms we can create keywords for:
-        //
-        // method    encoding type        can create keyword
-        // GET       *                                   YES
-        //           *                                   YES
-        // POST      *                                   YES
-        // POST      application/x-www-form-urlencoded   YES
-        // POST      text/plain                          NO ( a little tricky to do)
-        // POST      multipart/form-data                 NO
-        // POST      everything else                     YES
-        return (method == "GET" || method == "") ||
-               (form.enctype != "text/plain") && (form.enctype != "multipart/form-data");
-      }
-    };
-    SelectionHandler.addAction({
-      id: "search_add_action",
-      label: Strings.browser.GetStringFromName("contextmenu.addSearchEngine2"),
-      icon: "drawable://ab_add_search_engine",
-      selector: filter,
-      action: function(aElement) {
-        UITelemetry.addEvent("action.1", "actionbar", null, "add_search_engine");
-        SearchEngines.addEngine(aElement);
-      }
-    });
   },
 
   // Fetch list of search engines. all ? All engines : Visible engines only.
   _handleSearchEnginesGetVisible: function _handleSearchEnginesGetVisible(rv, all) {
     if (!Components.isSuccessCode(rv)) {
       Cu.reportError("Could not initialize search service, bailing out.");
       return;
     }
--- a/mobile/android/mach_commands.py
+++ b/mobile/android/mach_commands.py
@@ -113,22 +113,22 @@ class MachCommands(MachCommandBase):
         srcdir('omnijar/src/main/java/components', 'mobile/android/components')
         srcdir('omnijar/src/main/java/modules', 'mobile/android/modules')
         srcdir('omnijar/src/main/java/themes', 'mobile/android/themes')
 
         srcdir('app/build.gradle', 'mobile/android/gradle/app/build.gradle')
         objdir('app/src/main/AndroidManifest.xml', 'mobile/android/base/AndroidManifest.xml')
         objdir('app/src/androidTest/AndroidManifest.xml', 'build/mobile/robocop/AndroidManifest.xml')
         srcdir('app/src/androidTest/res', 'build/mobile/robocop/res')
-        srcdir('app/src/androidTest/assets', 'mobile/android/base/tests/assets')
+        srcdir('app/src/androidTest/assets', 'mobile/android/tests/browser/robocop/assets')
         objdir('app/src/debug/assets', 'dist/fennec/assets')
         objdir('app/src/debug/jniLibs', 'dist/fennec/lib')
         # Test code.
         srcdir('app/src/robocop_harness/org/mozilla/gecko', 'build/mobile/robocop')
-        srcdir('app/src/robocop/org/mozilla/gecko/tests', 'mobile/android/base/tests')
+        srcdir('app/src/robocop/org/mozilla/gecko/tests', 'mobile/android/tests/browser/robocop')
         srcdir('app/src/background/org/mozilla/gecko', 'mobile/android/tests/background/junit3/src')
         srcdir('app/src/browser/org/mozilla/gecko', 'mobile/android/tests/browser/junit3/src')
         # Test libraries.
         srcdir('app/libs', 'build/mobile/robocop')
 
         srcdir('base/build.gradle', 'mobile/android/gradle/base/build.gradle')
         srcdir('base/src/main/AndroidManifest.xml', 'mobile/android/gradle/base/AndroidManifest.xml')
         srcdir('base/src/main/java/org/mozilla/gecko', 'mobile/android/base')
rename from mobile/android/base/tests/AboutHomeTest.java
rename to mobile/android/tests/browser/robocop/AboutHomeTest.java
rename from mobile/android/base/tests/BaseRobocopTest.java
rename to mobile/android/tests/browser/robocop/BaseRobocopTest.java
rename from mobile/android/base/tests/BaseTest.java
rename to mobile/android/tests/browser/robocop/BaseTest.java
rename from mobile/android/base/tests/ContentContextMenuTest.java
rename to mobile/android/tests/browser/robocop/ContentContextMenuTest.java
rename from mobile/android/base/tests/ContentProviderTest.java
rename to mobile/android/tests/browser/robocop/ContentProviderTest.java
rename from mobile/android/base/tests/DatabaseHelper.java
rename to mobile/android/tests/browser/robocop/DatabaseHelper.java
rename from mobile/android/base/tests/Firefox.jpg
rename to mobile/android/tests/browser/robocop/Firefox.jpg
rename from mobile/android/base/tests/JavascriptTest.java
rename to mobile/android/tests/browser/robocop/JavascriptTest.java
rename from mobile/android/base/tests/MotionEventHelper.java
rename to mobile/android/tests/browser/robocop/MotionEventHelper.java
rename from mobile/android/base/tests/MotionEventReplayer.java
rename to mobile/android/tests/browser/robocop/MotionEventReplayer.java
rename from mobile/android/base/tests/PixelTest.java
rename to mobile/android/tests/browser/robocop/PixelTest.java
rename from mobile/android/base/tests/README.rst
rename to mobile/android/tests/browser/robocop/README.rst
rename from mobile/android/base/tests/SelectionHandlerTest.java
rename to mobile/android/tests/browser/robocop/SelectionHandlerTest.java
rename from mobile/android/base/tests/SessionTest.java
rename to mobile/android/tests/browser/robocop/SessionTest.java
rename from mobile/android/base/tests/StringHelper.java
rename to mobile/android/tests/browser/robocop/StringHelper.java
rename from mobile/android/base/tests/UITest.java
rename to mobile/android/tests/browser/robocop/UITest.java
rename from mobile/android/base/tests/UITestContext.java
rename to mobile/android/tests/browser/robocop/UITestContext.java
rename from mobile/android/base/tests/assets/README
rename to mobile/android/tests/browser/robocop/assets/README
rename from mobile/android/base/tests/assets/mock-package.zip
rename to mobile/android/tests/browser/robocop/assets/mock-package.zip
rename from mobile/android/base/tests/assets/testcheck2-motionevents
rename to mobile/android/tests/browser/robocop/assets/testcheck2-motionevents
rename from mobile/android/base/tests/components/AboutHomeComponent.java
rename to mobile/android/tests/browser/robocop/components/AboutHomeComponent.java
rename from mobile/android/base/tests/components/AppMenuComponent.java
rename to mobile/android/tests/browser/robocop/components/AppMenuComponent.java
rename from mobile/android/base/tests/components/BaseComponent.java
rename to mobile/android/tests/browser/robocop/components/BaseComponent.java
rename from mobile/android/base/tests/components/GeckoViewComponent.java
rename to mobile/android/tests/browser/robocop/components/GeckoViewComponent.java
rename from mobile/android/base/tests/components/TabStripComponent.java
rename to mobile/android/tests/browser/robocop/components/TabStripComponent.java
rename from mobile/android/base/tests/components/ToolbarComponent.java
rename to mobile/android/tests/browser/robocop/components/ToolbarComponent.java
rename from mobile/android/base/tests/devicesearch.xml
rename to mobile/android/tests/browser/robocop/devicesearch.xml
rename from mobile/android/base/tests/green.swf
rename to mobile/android/tests/browser/robocop/green.swf
rename from mobile/android/base/tests/helpers/AssertionHelper.java
rename to mobile/android/tests/browser/robocop/helpers/AssertionHelper.java
rename from mobile/android/base/tests/helpers/DeviceHelper.java
rename to mobile/android/tests/browser/robocop/helpers/DeviceHelper.java
rename from mobile/android/base/tests/helpers/FrameworkHelper.java
rename to mobile/android/tests/browser/robocop/helpers/FrameworkHelper.java
rename from mobile/android/base/tests/helpers/GeckoClickHelper.java
rename to mobile/android/tests/browser/robocop/helpers/GeckoClickHelper.java
rename from mobile/android/base/tests/helpers/GeckoHelper.java
rename to mobile/android/tests/browser/robocop/helpers/GeckoHelper.java
rename from mobile/android/base/tests/helpers/HelperInitializer.java
rename to mobile/android/tests/browser/robocop/helpers/HelperInitializer.java
rename from mobile/android/base/tests/helpers/JavascriptBridge.java
rename to mobile/android/tests/browser/robocop/helpers/JavascriptBridge.java
rename from mobile/android/base/tests/helpers/JavascriptMessageParser.java
rename to mobile/android/tests/browser/robocop/helpers/JavascriptMessageParser.java
rename from mobile/android/base/tests/helpers/NavigationHelper.java
rename to mobile/android/tests/browser/robocop/helpers/NavigationHelper.java
rename from mobile/android/base/tests/helpers/TextInputHelper.java
rename to mobile/android/tests/browser/robocop/helpers/TextInputHelper.java
rename from mobile/android/base/tests/helpers/WaitHelper.java
rename to mobile/android/tests/browser/robocop/helpers/WaitHelper.java
rename from mobile/android/base/tests/javascript_redirect.sjs
rename to mobile/android/tests/browser/robocop/javascript_redirect.sjs
rename from mobile/android/base/tests/link_discovery.html
rename to mobile/android/tests/browser/robocop/link_discovery.html
rename from mobile/android/base/tests/reader_mode_pages/basic_article.html
rename to mobile/android/tests/browser/robocop/reader_mode_pages/basic_article.html
rename from mobile/android/base/tests/reader_mode_pages/developer.mozilla.org/en/XULRunner/Build_Instructions.html
rename to mobile/android/tests/browser/robocop/reader_mode_pages/developer.mozilla.org/en/XULRunner/Build_Instructions.html
rename from mobile/android/base/tests/reader_mode_pages/not_an_article.html
rename to mobile/android/tests/browser/robocop/reader_mode_pages/not_an_article.html
rename from mobile/android/base/tests/robocop.ini
rename to mobile/android/tests/browser/robocop/robocop.ini
rename from mobile/android/base/tests/robocop_404.sjs
rename to mobile/android/tests/browser/robocop/robocop_404.sjs
rename from mobile/android/base/tests/robocop_adobe_flash.html
rename to mobile/android/tests/browser/robocop/robocop_adobe_flash.html
rename from mobile/android/base/tests/robocop_autophone.ini
rename to mobile/android/tests/browser/robocop/robocop_autophone.ini
rename from mobile/android/base/tests/robocop_big_link.html
rename to mobile/android/tests/browser/robocop/robocop_big_link.html
rename from mobile/android/base/tests/robocop_big_mailto.html
rename to mobile/android/tests/browser/robocop/robocop_big_mailto.html
rename from mobile/android/base/tests/robocop_blank_01.html
rename to mobile/android/tests/browser/robocop/robocop_blank_01.html
rename from mobile/android/base/tests/robocop_blank_02.html
rename to mobile/android/tests/browser/robocop/robocop_blank_02.html
rename from mobile/android/base/tests/robocop_blank_03.html
rename to mobile/android/tests/browser/robocop/robocop_blank_03.html
rename from mobile/android/base/tests/robocop_blank_04.html
rename to mobile/android/tests/browser/robocop/robocop_blank_04.html
rename from mobile/android/base/tests/robocop_blank_05.html
rename to mobile/android/tests/browser/robocop/robocop_blank_05.html
rename from mobile/android/base/tests/robocop_boxes.html
rename to mobile/android/tests/browser/robocop/robocop_boxes.html
rename from mobile/android/base/tests/robocop_dynamic.sjs
rename to mobile/android/tests/browser/robocop/robocop_dynamic.sjs
rename from mobile/android/base/tests/robocop_geolocation.html
rename to mobile/android/tests/browser/robocop/robocop_geolocation.html
rename from mobile/android/base/tests/robocop_getusermedia.html
rename to mobile/android/tests/browser/robocop/robocop_getusermedia.html
rename from mobile/android/base/tests/robocop_getusermedia2.html
rename to mobile/android/tests/browser/robocop/robocop_getusermedia2.html
rename from mobile/android/base/tests/robocop_head.js
rename to mobile/android/tests/browser/robocop/robocop_head.js
rename from mobile/android/base/tests/robocop_input.html
rename to mobile/android/tests/browser/robocop/robocop_input.html
rename from mobile/android/base/tests/robocop_javascript.html
rename to mobile/android/tests/browser/robocop/robocop_javascript.html
rename from mobile/android/base/tests/robocop_link_to_slow_loading.html
rename to mobile/android/tests/browser/robocop/robocop_link_to_slow_loading.html
rename from mobile/android/base/tests/robocop_login_01.html
rename to mobile/android/tests/browser/robocop/robocop_login_01.html
rename from mobile/android/base/tests/robocop_login_02.html
rename to mobile/android/tests/browser/robocop/robocop_login_02.html
rename from mobile/android/base/tests/robocop_offline_storage.html
rename to mobile/android/tests/browser/robocop/robocop_offline_storage.html
rename from mobile/android/base/tests/robocop_picture_link.html
rename to mobile/android/tests/browser/robocop/robocop_picture_link.html
rename from mobile/android/base/tests/robocop_popup.html
rename to mobile/android/tests/browser/robocop/robocop_popup.html
rename from mobile/android/base/tests/robocop_search.html
rename to mobile/android/tests/browser/robocop/robocop_search.html
rename from mobile/android/base/tests/robocop_slow_loading.html
rename to mobile/android/tests/browser/robocop/robocop_slow_loading.html
rename from mobile/android/base/tests/robocop_suggestions.sjs
rename to mobile/android/tests/browser/robocop/robocop_suggestions.sjs
rename from mobile/android/base/tests/robocop_testharness.js
rename to mobile/android/tests/browser/robocop/robocop_testharness.js
rename from mobile/android/base/tests/robocop_text_page.html
rename to mobile/android/tests/browser/robocop/robocop_text_page.html
rename from mobile/android/base/tests/robocop_tiles.sjs
rename to mobile/android/tests/browser/robocop/robocop_tiles.sjs
rename from mobile/android/base/tests/roboextender/SelectionUtils.js
rename to mobile/android/tests/browser/robocop/roboextender/SelectionUtils.js
rename from mobile/android/base/tests/roboextender/paymentsUI.html
rename to mobile/android/tests/browser/robocop/roboextender/paymentsUI.html
rename from mobile/android/base/tests/roboextender/robocop_home_banner.html
rename to mobile/android/tests/browser/robocop/roboextender/robocop_home_banner.html
rename from mobile/android/base/tests/roboextender/robocop_prompt_gridinput.html
rename to mobile/android/tests/browser/robocop/roboextender/robocop_prompt_gridinput.html
rename from mobile/android/base/tests/roboextender/testInputSelections.html
rename to mobile/android/tests/browser/robocop/roboextender/testInputSelections.html
rename from mobile/android/base/tests/roboextender/testSelectionHandler.html
rename to mobile/android/tests/browser/robocop/roboextender/testSelectionHandler.html
rename from mobile/android/base/tests/roboextender/testTextareaSelections.html
rename to mobile/android/tests/browser/robocop/roboextender/testTextareaSelections.html
rename from mobile/android/base/tests/session_formdata_sample.html
rename to mobile/android/tests/browser/robocop/session_formdata_sample.html
rename from mobile/android/base/tests/simple_redirect.sjs
rename to mobile/android/tests/browser/robocop/simple_redirect.sjs
rename from mobile/android/base/tests/simpleservice.xml
rename to mobile/android/tests/browser/robocop/simpleservice.xml
rename from mobile/android/base/tests/testANRReporter.java
rename to mobile/android/tests/browser/robocop/testANRReporter.java
rename from mobile/android/base/tests/testAboutHomePageNavigation.java
rename to mobile/android/tests/browser/robocop/testAboutHomePageNavigation.java
rename from mobile/android/base/tests/testAboutHomeVisibility.java
rename to mobile/android/tests/browser/robocop/testAboutHomeVisibility.java
rename from mobile/android/base/tests/testAboutPage.java
rename to mobile/android/tests/browser/robocop/testAboutPage.java
rename from mobile/android/base/tests/testAboutPasswords.java
rename to mobile/android/tests/browser/robocop/testAboutPasswords.java
rename from mobile/android/base/tests/testAboutPasswords.js
rename to mobile/android/tests/browser/robocop/testAboutPasswords.js
rename from mobile/android/base/tests/testAccounts.java
rename to mobile/android/tests/browser/robocop/testAccounts.java
rename from mobile/android/base/tests/testAccounts.js
rename to mobile/android/tests/browser/robocop/testAccounts.js
rename from mobile/android/base/tests/testAddSearchEngine.java
rename to mobile/android/tests/browser/robocop/testAddSearchEngine.java
rename from mobile/android/base/tests/testAddonManager.java
rename to mobile/android/tests/browser/robocop/testAddonManager.java
rename from mobile/android/base/tests/testAdobeFlash.java
rename to mobile/android/tests/browser/robocop/testAdobeFlash.java
rename from mobile/android/base/tests/testAndroidLog.java
rename to mobile/android/tests/browser/robocop/testAndroidLog.java
rename from mobile/android/base/tests/testAndroidLog.js
rename to mobile/android/tests/browser/robocop/testAndroidLog.js
rename from mobile/android/base/tests/testAppConstants.java
rename to mobile/android/tests/browser/robocop/testAppConstants.java
rename from mobile/android/base/tests/testAppConstants.js
rename to mobile/android/tests/browser/robocop/testAppConstants.js
rename from mobile/android/base/tests/testAppMenuPathways.java
rename to mobile/android/tests/browser/robocop/testAppMenuPathways.java
rename from mobile/android/base/tests/testAwesomebar.java
rename to mobile/android/tests/browser/robocop/testAwesomebar.java
rename from mobile/android/base/tests/testAxisLocking.java
rename to mobile/android/tests/browser/robocop/testAxisLocking.java
rename from mobile/android/base/tests/testBackButtonInEditMode.java
rename to mobile/android/tests/browser/robocop/testBackButtonInEditMode.java
rename from mobile/android/base/tests/testBookmark.java
rename to mobile/android/tests/browser/robocop/testBookmark.java
rename from mobile/android/base/tests/testBookmarkFolders.java
rename to mobile/android/tests/browser/robocop/testBookmarkFolders.java
rename from mobile/android/base/tests/testBookmarkKeyword.java
rename to mobile/android/tests/browser/robocop/testBookmarkKeyword.java
rename from mobile/android/base/tests/testBookmarklets.java
rename to mobile/android/tests/browser/robocop/testBookmarklets.java
rename from mobile/android/base/tests/testBookmarksPanel.java
rename to mobile/android/tests/browser/robocop/testBookmarksPanel.java
rename from mobile/android/base/tests/testBrowserDiscovery.java
rename to mobile/android/tests/browser/robocop/testBrowserDiscovery.java
rename from mobile/android/base/tests/testBrowserDiscovery.js
rename to mobile/android/tests/browser/robocop/testBrowserDiscovery.js
rename from mobile/android/base/tests/testBrowserProvider.java
rename to mobile/android/tests/browser/robocop/testBrowserProvider.java
rename from mobile/android/base/tests/testBrowserProviderPerf.java
rename to mobile/android/tests/browser/robocop/testBrowserProviderPerf.java
rename from mobile/android/base/tests/testBrowserSearchVisibility.java
rename to mobile/android/tests/browser/robocop/testBrowserSearchVisibility.java
rename from mobile/android/base/tests/testCheck.java
rename to mobile/android/tests/browser/robocop/testCheck.java
rename from mobile/android/base/tests/testCheck2.java
rename to mobile/android/tests/browser/robocop/testCheck2.java
rename from mobile/android/base/tests/testClearPrivateData.java
rename to mobile/android/tests/browser/robocop/testClearPrivateData.java
rename from mobile/android/base/tests/testDBUtils.java
rename to mobile/android/tests/browser/robocop/testDBUtils.java
rename from mobile/android/base/tests/testDebuggerServer.java
rename to mobile/android/tests/browser/robocop/testDebuggerServer.java
rename from mobile/android/base/tests/testDebuggerServer.js
rename to mobile/android/tests/browser/robocop/testDebuggerServer.js
rename from mobile/android/base/tests/testDeviceSearchEngine.java
rename to mobile/android/tests/browser/robocop/testDeviceSearchEngine.java
rename from mobile/android/base/tests/testDeviceSearchEngine.js
rename to mobile/android/tests/browser/robocop/testDeviceSearchEngine.js
rename from mobile/android/base/tests/testDistribution.java
rename to mobile/android/tests/browser/robocop/testDistribution.java
rename from mobile/android/base/tests/testDoorHanger.java
rename to mobile/android/tests/browser/robocop/testDoorHanger.java
rename from mobile/android/base/tests/testEventDispatcher.java
rename to mobile/android/tests/browser/robocop/testEventDispatcher.java
rename from mobile/android/base/tests/testEventDispatcher.js
rename to mobile/android/tests/browser/robocop/testEventDispatcher.js
rename from mobile/android/base/tests/testFilePicker.java
rename to mobile/android/tests/browser/robocop/testFilePicker.java
rename from mobile/android/base/tests/testFilePicker.js
rename to mobile/android/tests/browser/robocop/testFilePicker.js
rename from mobile/android/base/tests/testFilterOpenTab.java
rename to mobile/android/tests/browser/robocop/testFilterOpenTab.java
rename from mobile/android/base/tests/testFindInPage.java
rename to mobile/android/tests/browser/robocop/testFindInPage.java
rename from mobile/android/base/tests/testFindInPage.js
rename to mobile/android/tests/browser/robocop/testFindInPage.js
rename from mobile/android/base/tests/testFlingCorrectness.java
rename to mobile/android/tests/browser/robocop/testFlingCorrectness.java
rename from mobile/android/base/tests/testFormHistory.java
rename to mobile/android/tests/browser/robocop/testFormHistory.java
rename from mobile/android/base/tests/testGeckoProfile.java
rename to mobile/android/tests/browser/robocop/testGeckoProfile.java
rename from mobile/android/base/tests/testGeckoRequest.java
rename to mobile/android/tests/browser/robocop/testGeckoRequest.java
rename from mobile/android/base/tests/testGeckoRequest.js
rename to mobile/android/tests/browser/robocop/testGeckoRequest.js
rename from mobile/android/base/tests/testGetUserMedia.java
rename to mobile/android/tests/browser/robocop/testGetUserMedia.java
rename from mobile/android/base/tests/testHistory.java
rename to mobile/android/tests/browser/robocop/testHistory.java
rename from mobile/android/base/tests/testHistoryService.java
rename to mobile/android/tests/browser/robocop/testHistoryService.java
rename from mobile/android/base/tests/testHistoryService.js
rename to mobile/android/tests/browser/robocop/testHistoryService.js
rename from mobile/android/base/tests/testHomeBanner.java
rename to mobile/android/tests/browser/robocop/testHomeBanner.java
rename from mobile/android/base/tests/testHomeListsProvider.java
rename to mobile/android/tests/browser/robocop/testHomeListsProvider.java
rename from mobile/android/base/tests/testHomeProvider.java
rename to mobile/android/tests/browser/robocop/testHomeProvider.java
rename from mobile/android/base/tests/testHomeProvider.js
rename to mobile/android/tests/browser/robocop/testHomeProvider.js
rename from mobile/android/base/tests/testImportFromAndroid.java
rename to mobile/android/tests/browser/robocop/testImportFromAndroid.java
rename from mobile/android/base/tests/testInputConnection.java
rename to mobile/android/tests/browser/robocop/testInputConnection.java
rename from mobile/android/base/tests/testInputSelections.java
rename to mobile/android/tests/browser/robocop/testInputSelections.java
rename from mobile/android/base/tests/testInputUrlBar.java
rename to mobile/android/tests/browser/robocop/testInputUrlBar.java
rename from mobile/android/base/tests/testJNI.java
rename to mobile/android/tests/browser/robocop/testJNI.java
rename from mobile/android/base/tests/testJNI.js
rename to mobile/android/tests/browser/robocop/testJNI.js
rename from mobile/android/base/tests/testJarReader.java
rename to mobile/android/tests/browser/robocop/testJarReader.java
rename from mobile/android/base/tests/testJavascriptBridge.java
rename to mobile/android/tests/browser/robocop/testJavascriptBridge.java
rename from mobile/android/base/tests/testJavascriptBridge.js
rename to mobile/android/tests/browser/robocop/testJavascriptBridge.js
rename from mobile/android/base/tests/testLinkContextMenu.java
rename to mobile/android/tests/browser/robocop/testLinkContextMenu.java
rename from mobile/android/base/tests/testLoad.java
rename to mobile/android/tests/browser/robocop/testLoad.java
rename from mobile/android/base/tests/testMailToContextMenu.java
rename to mobile/android/tests/browser/robocop/testMailToContextMenu.java
rename from mobile/android/base/tests/testMasterPassword.java
rename to mobile/android/tests/browser/robocop/testMasterPassword.java
rename from mobile/android/base/tests/testMigrateUI.java
rename to mobile/android/tests/browser/robocop/testMigrateUI.java
rename from mobile/android/base/tests/testMigrateUI.js
rename to mobile/android/tests/browser/robocop/testMigrateUI.js
rename from mobile/android/base/tests/testMozPay.java
rename to mobile/android/tests/browser/robocop/testMozPay.java
rename from mobile/android/base/tests/testMozPay.js
rename to mobile/android/tests/browser/robocop/testMozPay.js
rename from mobile/android/base/tests/testNativeCrypto.java
rename to mobile/android/tests/browser/robocop/testNativeCrypto.java
rename from mobile/android/base/tests/testNetworkManager.java
rename to mobile/android/tests/browser/robocop/testNetworkManager.java
rename from mobile/android/base/tests/testNetworkManager.js
rename to mobile/android/tests/browser/robocop/testNetworkManager.js
rename from mobile/android/base/tests/testNewTab.java
rename to mobile/android/tests/browser/robocop/testNewTab.java
rename from mobile/android/base/tests/testOSLocale.java
rename to mobile/android/tests/browser/robocop/testOSLocale.java
rename from mobile/android/base/tests/testOfflinePage.java
rename to mobile/android/tests/browser/robocop/testOfflinePage.java
rename from mobile/android/base/tests/testOfflinePage.js
rename to mobile/android/tests/browser/robocop/testOfflinePage.js
rename from mobile/android/base/tests/testOrderedBroadcast.java
rename to mobile/android/tests/browser/robocop/testOrderedBroadcast.java
rename from mobile/android/base/tests/testOrderedBroadcast.js
rename to mobile/android/tests/browser/robocop/testOrderedBroadcast.js
rename from mobile/android/base/tests/testPan.java
rename to mobile/android/tests/browser/robocop/testPan.java
rename from mobile/android/base/tests/testPanCorrectness.java
rename to mobile/android/tests/browser/robocop/testPanCorrectness.java
rename from mobile/android/base/tests/testPasswordEncrypt.java
rename to mobile/android/tests/browser/robocop/testPasswordEncrypt.java
rename from mobile/android/base/tests/testPasswordProvider.java
rename to mobile/android/tests/browser/robocop/testPasswordProvider.java
rename from mobile/android/base/tests/testPermissions.java
rename to mobile/android/tests/browser/robocop/testPermissions.java
rename from mobile/android/base/tests/testPictureLinkContextMenu.java
rename to mobile/android/tests/browser/robocop/testPictureLinkContextMenu.java
rename from mobile/android/base/tests/testPrefsObserver.java
rename to mobile/android/tests/browser/robocop/testPrefsObserver.java
rename from mobile/android/base/tests/testPrivateBrowsing.java
rename to mobile/android/tests/browser/robocop/testPrivateBrowsing.java
rename from mobile/android/base/tests/testPromptGridInput.java
rename to mobile/android/tests/browser/robocop/testPromptGridInput.java
rename from mobile/android/base/tests/testReaderModeTitle.java
rename to mobile/android/tests/browser/robocop/testReaderModeTitle.java
rename from mobile/android/base/tests/testReaderView.java
rename to mobile/android/tests/browser/robocop/testReaderView.java
rename from mobile/android/base/tests/testReaderView.js
rename to mobile/android/tests/browser/robocop/testReaderView.js
rename from mobile/android/base/tests/testReadingListCache.java
rename to mobile/android/tests/browser/robocop/testReadingListCache.java
rename from mobile/android/base/tests/testReadingListCache.js
rename to mobile/android/tests/browser/robocop/testReadingListCache.js
rename from mobile/android/base/tests/testReadingListProvider.java
rename to mobile/android/tests/browser/robocop/testReadingListProvider.java
rename from mobile/android/base/tests/testResourceSubstitutions.java
rename to mobile/android/tests/browser/robocop/testResourceSubstitutions.java
rename from mobile/android/base/tests/testResourceSubstitutions.js
rename to mobile/android/tests/browser/robocop/testResourceSubstitutions.js
rename from mobile/android/base/tests/testRestrictedProfiles.java
rename to mobile/android/tests/browser/robocop/testRestrictedProfiles.java
rename from mobile/android/base/tests/testRestrictedProfiles.js
rename to mobile/android/tests/browser/robocop/testRestrictedProfiles.js
rename from mobile/android/base/tests/testSearchHistoryProvider.java
rename to mobile/android/tests/browser/robocop/testSearchHistoryProvider.java
rename from mobile/android/base/tests/testSearchSuggestions.java
rename to mobile/android/tests/browser/robocop/testSearchSuggestions.java
rename from mobile/android/base/tests/testSelectionHandler.java
rename to mobile/android/tests/browser/robocop/testSelectionHandler.java
rename from mobile/android/base/tests/testSessionFormData.java
rename to mobile/android/tests/browser/robocop/testSessionFormData.java
rename from mobile/android/base/tests/testSessionFormData.js
rename to mobile/android/tests/browser/robocop/testSessionFormData.js
rename from mobile/android/base/tests/testSessionHistory.java
rename to mobile/android/tests/browser/robocop/testSessionHistory.java
rename from mobile/android/base/tests/testSessionOOMRestore.java
rename to mobile/android/tests/browser/robocop/testSessionOOMRestore.java
rename from mobile/android/base/tests/testSessionOOMSave.java
rename to mobile/android/tests/browser/robocop/testSessionOOMSave.java
rename from mobile/android/base/tests/testSettingsMenuItems.java
rename to mobile/android/tests/browser/robocop/testSettingsMenuItems.java
rename from mobile/android/base/tests/testShareLink.java
rename to mobile/android/tests/browser/robocop/testShareLink.java
rename from mobile/android/base/tests/testSharedPreferences.java
rename to mobile/android/tests/browser/robocop/testSharedPreferences.java
rename from mobile/android/base/tests/testSharedPreferences.js
rename to mobile/android/tests/browser/robocop/testSharedPreferences.js
rename from mobile/android/base/tests/testSimpleDiscovery.java
rename to mobile/android/tests/browser/robocop/testSimpleDiscovery.java
rename from mobile/android/base/tests/testSimpleDiscovery.js
rename to mobile/android/tests/browser/robocop/testSimpleDiscovery.js
rename from mobile/android/base/tests/testStateWhileLoading.java
rename to mobile/android/tests/browser/robocop/testStateWhileLoading.java
rename from mobile/android/base/tests/testStumblerSetting.java
rename to mobile/android/tests/browser/robocop/testStumblerSetting.java
rename from mobile/android/base/tests/testSystemPages.java
rename to mobile/android/tests/browser/robocop/testSystemPages.java
rename from mobile/android/base/tests/testTextareaSelections.java
rename to mobile/android/tests/browser/robocop/testTextareaSelections.java
rename from mobile/android/base/tests/testThumbnails.java
rename to mobile/android/tests/browser/robocop/testThumbnails.java
rename from mobile/android/base/tests/testTitleBar.java
rename to mobile/android/tests/browser/robocop/testTitleBar.java
rename from mobile/android/base/tests/testTrackingProtection.java
rename to mobile/android/tests/browser/robocop/testTrackingProtection.java
rename from mobile/android/base/tests/testTrackingProtection.js
rename to mobile/android/tests/browser/robocop/testTrackingProtection.js
rename from mobile/android/base/tests/testUITelemetry.java
rename to mobile/android/tests/browser/robocop/testUITelemetry.java
rename from mobile/android/base/tests/testUITelemetry.js
rename to mobile/android/tests/browser/robocop/testUITelemetry.js
rename from mobile/android/base/tests/testVideoControls.java
rename to mobile/android/tests/browser/robocop/testVideoControls.java
rename from mobile/android/base/tests/testVideoControls.js
rename to mobile/android/tests/browser/robocop/testVideoControls.js
rename from mobile/android/base/tests/testVideoDiscovery.java
rename to mobile/android/tests/browser/robocop/testVideoDiscovery.java
rename from mobile/android/base/tests/testVideoDiscovery.js
rename to mobile/android/tests/browser/robocop/testVideoDiscovery.js
rename from mobile/android/base/tests/testVkbOverlap.java
rename to mobile/android/tests/browser/robocop/testVkbOverlap.java
rename from mobile/android/base/tests/test_bug720538.html
rename to mobile/android/tests/browser/robocop/test_bug720538.html
rename from mobile/android/base/tests/test_bug720538.java
rename to mobile/android/tests/browser/robocop/test_bug720538.java
rename from mobile/android/base/tests/test_viewport.sjs
rename to mobile/android/tests/browser/robocop/test_viewport.sjs
rename from mobile/android/base/tests/tracking_bad.html
rename to mobile/android/tests/browser/robocop/tracking_bad.html
rename from mobile/android/base/tests/tracking_good.html
rename to mobile/android/tests/browser/robocop/tracking_good.html
rename from mobile/android/base/tests/video-pattern.ogg
rename to mobile/android/tests/browser/robocop/video-pattern.ogg
rename from mobile/android/base/tests/video-pattern.webm
rename to mobile/android/tests/browser/robocop/video-pattern.webm
rename from mobile/android/base/tests/video_controls.html
rename to mobile/android/tests/browser/robocop/video_controls.html
rename from mobile/android/base/tests/video_discovery.html
rename to mobile/android/tests/browser/robocop/video_discovery.html
--- a/mobile/android/themes/core/aboutReaderControls.css
+++ b/mobile/android/themes/core/aboutReaderControls.css
@@ -1,13 +1,13 @@
 /* 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/. */
 
-.message {
+#reader-message {
   margin-top: 40px;
   display: none;
   text-align: center;
   width: 100%;
   font-size: 0.9em;
 }
 
 .header {
--- a/testing/mochitest/roboextender/Makefile.in
+++ b/testing/mochitest/roboextender/Makefile.in
@@ -1,14 +1,14 @@
 #
 # 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/.
 
-TESTPATH        = $(topsrcdir)/mobile/android/base/tests/roboextender
+TESTPATH = $(topsrcdir)/mobile/android/tests/browser/robocop/roboextender
 
 include $(DEPTH)/config/autoconf.mk
 
 TEST_EXTENSIONS_DIR = $(DEPTH)/_tests/testing/mochitest/extensions
 
 TEST_FILES = \
   bootstrap.js \
   install.rdf \
--- a/toolkit/components/reader/Readability.js
+++ b/toolkit/components/reader/Readability.js
@@ -103,28 +103,31 @@ Readability.prototype = {
   // The number of top candidates to consider when analysing how
   // tight the competition is among candidates.
   DEFAULT_N_TOP_CANDIDATES: 5,
 
   // The maximum number of pages to loop through before we call
   // it quits and just show a link.
   DEFAULT_MAX_PAGES: 5,
 
+  // Element tags to score by default.
+  DEFAULT_TAGS_TO_SCORE: ["SECTION", "P", "TD", "PRE"],
+
   // All of the regular expressions in use within readability.
   // Defined up here so we don't instantiate them repeatedly in loops.
   REGEXPS: {
     unlikelyCandidates: /banner|combx|comment|community|disqus|extra|foot|header|menu|remark|rss|share|shoutbox|sidebar|skyscraper|sponsor|ad-break|agegate|pagination|pager|popup/i,
     okMaybeItsACandidate: /and|article|body|column|main|shadow/i,
     positive: /article|body|content|entry|hentry|main|page|pagination|post|text|blog|story/i,
     negative: /hidden|banner|combx|comment|com-|contact|foot|footer|footnote|masthead|media|meta|outbrain|promo|related|scroll|share|shoutbox|sidebar|skyscraper|sponsor|shopping|tags|tool|widget/i,
     extraneous: /print|archive|comment|discuss|e[\-]?mail|share|reply|all|login|sign|single|utility/i,
     byline: /byline|author|dateline|writtenby/i,
     replaceFonts: /<(\/?)font[^>]*>/gi,
     normalize: /\s{2,}/g,
-    videos: /https?:\/\/(www\.)?(youtube|youtube-nocookie|player\.vimeo)\.com/i,
+    videos: /https?:\/\/(www\.)?(dailymotion|youtube|youtube-nocookie|player\.vimeo)\.com/i,
     nextLink: /(next|weiter|continue|>([^\|]|$)|»([^\|]|$))/i,
     prevLink: /(prev|earl|old|new|<|«)/i,
     whitespace: /^\s*$/,
     hasContent: /\S$/,
   },
 
   DIV_TO_P_ELEMS: [ "A", "BLOCKQUOTE", "DL", "DIV", "IMG", "OL", "P", "PRE", "TABLE", "UL", "SELECT" ],
 
@@ -181,16 +184,25 @@ Readability.prototype = {
     var slice = Array.prototype.slice;
     var args = slice.call(arguments);
     var nodeLists = args.map(function(list) {
       return slice.call(list);
     });
     return Array.prototype.concat.apply([], nodeLists);
   },
 
+  _getAllNodesWithTag: function(node, tagNames) {
+    if (node.querySelectorAll) {
+      return node.querySelectorAll(tagNames.join(','));
+    }
+    return [].concat.apply([], tagNames.map(function(tag) {
+      return node.getElementsByTagName(tag);
+    }));
+  },
+
   /**
    * Converts each <a> and <img> uri in the given element to an absolute URI.
    *
    * @param Element
    * @return void
    */
   _fixRelativeUris: function(articleContent) {
     var scheme = this._uri.scheme;
@@ -581,16 +593,28 @@ Readability.prototype = {
     if ((rel === "author" || this.REGEXPS.byline.test(matchString)) && this._isValidByline(node.textContent)) {
       this._articleByline = node.textContent.trim();
       return true;
     }
 
     return false;
   },
 
+  _getNodeAncestors: function(node, maxDepth) {
+    maxDepth = maxDepth || 0;
+    var i = 0, ancestors = [];
+    while (node.parentNode) {
+      ancestors.push(node.parentNode)
+      if (maxDepth && ++i === maxDepth)
+        break;
+      node = node.parentNode;
+    }
+    return ancestors;
+  },
+
   /***
    * grabArticle - Using a variety of metrics (content score, classname, element types), find the content that is
    *         most likely to be the stuff a user wants to read. Then return it wrapped up in a div.
    *
    * @param page a document to run upon. Needs to be a full document, complete with body.
    * @return Element
   **/
   _grabArticle: function (page) {
@@ -635,18 +659,19 @@ Readability.prototype = {
               node.tagName !== "BODY" &&
               node.tagName !== "A") {
             this.log("Removing unlikely candidate - " + matchString);
             node = this._removeAndGetNext(node);
             continue;
           }
         }
 
-        if (node.tagName === "P" || node.tagName === "TD" || node.tagName === "PRE")
+        if (this.DEFAULT_TAGS_TO_SCORE.indexOf(node.tagName) !== -1) {
           elementsToScore.push(node);
+        }
 
         // Turn all divs that don't have children block level elements into p's
         if (node.tagName === "DIV") {
           // Sites like http://mobile.slate.com encloses each paragraph with a DIV
           // element. DIVs with only a P element inside and no text content can be
           // safely converted into plain P elements to avoid confusing the scoring
           // algorithm with DIVs with are, in practice, paragraphs.
           if (this._hasSinglePInsideElement(node)) {
@@ -675,57 +700,52 @@ Readability.prototype = {
       /**
        * Loop through all paragraphs, and assign a score to them based on how content-y they look.
        * Then add their score to their parent node.
        *
        * A score is determined by things like number of commas, class names, etc. Maybe eventually link density.
       **/
       var candidates = [];
       this._forEachNode(elementsToScore, function(elementToScore) {
-        var parentNode = elementToScore.parentNode;
-        var grandParentNode = parentNode ? parentNode.parentNode : null;
-        var innerText = this._getInnerText(elementToScore);
-
-        if (!parentNode || typeof(parentNode.tagName) === 'undefined')
+        if (!elementToScore.parentNode || typeof(elementToScore.parentNode.tagName) === 'undefined')
           return;
 
         // If this paragraph is less than 25 characters, don't even count it.
+        var innerText = this._getInnerText(elementToScore);
         if (innerText.length < 25)
           return;
 
-        // Initialize readability data for the parent.
-        if (typeof parentNode.readability === 'undefined') {
-          this._initializeNode(parentNode);
-          candidates.push(parentNode);
-        }
-
-        // Initialize readability data for the grandparent.
-        if (grandParentNode &&
-          typeof(grandParentNode.readability) === 'undefined' &&
-          typeof(grandParentNode.tagName) !== 'undefined') {
-          this._initializeNode(grandParentNode);
-          candidates.push(grandParentNode);
-        }
+        // Exclude nodes with no ancestor.
+        var ancestors = this._getNodeAncestors(elementToScore, 3);
+        if (ancestors.length === 0)
+          return;
 
         var contentScore = 0;
 
         // Add a point for the paragraph itself as a base.
         contentScore += 1;
 
         // Add points for any commas within this paragraph.
         contentScore += innerText.split(',').length;
 
         // For every 100 characters in this paragraph, add another point. Up to 3 points.
         contentScore += Math.min(Math.floor(innerText.length / 100), 3);
 
-        // Add the score to the parent. The grandparent gets half.
-        parentNode.readability.contentScore += contentScore;
+        // Initialize and score ancestors.
+        this._forEachNode(ancestors, function(ancestor, level) {
+          if (!ancestor.tagName)
+            return;
 
-        if (grandParentNode)
-          grandParentNode.readability.contentScore += contentScore / 2;
+          if (typeof(ancestor.readability) === 'undefined') {
+            this._initializeNode(ancestor);
+            candidates.push(ancestor);
+          }
+
+          ancestor.readability.contentScore += contentScore / (level === 0 ? 1 : level * 2);
+        });
       });
 
       // After we've calculated scores, loop through all of the possible
       // candidate nodes we found and find the one with the highest score.
       var topCandidates = [];
       for (var c = 0, cl = candidates.length; c < cl; c += 1) {
         var candidate = candidates[c];
 
@@ -843,20 +863,16 @@ Readability.prototype = {
           if (this.ALTER_TO_DIV_EXCEPTIONS.indexOf(sibling.nodeName) === -1) {
             // We have a node that isn't a common block level element, like a form or td tag.
             // Turn it into a div so it doesn't get filtered out later by accident.
             this.log("Altering sibling:", sibling, 'to div.');
 
             sibling = this._setNodeTag(sibling, "DIV");
           }
 
-          // To ensure a node does not interfere with readability styles,
-          // remove its classnames.
-          sibling.removeAttribute("class");
-
           articleContent.appendChild(sibling);
           // siblings is a reference to the children array, and
           // sibling is removed from the array when we call appendChild().
           // As a result, we must revisit this index since the nodes
           // have been shifted.
           s -= 1;
           sl -= 1;
         }
@@ -948,17 +964,17 @@ Readability.prototype = {
     // Match Facebook's Open Graph title & description properties.
     var propertyPattern = /^\s*og\s*:\s*(description|title)\s*$/gi;
 
     // Find description tags.
     this._forEachNode(metaElements, function(element) {
       var elementName = element.getAttribute("name");
       var elementProperty = element.getAttribute("property");
 
-      if (elementName === "author") {
+      if ([elementName, elementProperty].indexOf("author") !== -1) {
         metadata.byline = element.getAttribute("content");
         return;
       }
 
       var name = null;
       if (namePattern.test(elementName)) {
         name = elementName;
       } else if (propertyPattern.test(elementProperty)) {
@@ -1592,16 +1608,17 @@ Readability.prototype = {
    * @return void
    **/
   _cleanConditionally: function(e, tag) {
     if (!this._flagIsActive(this.FLAG_CLEAN_CONDITIONALLY))
       return;
 
     var tagsList = e.getElementsByTagName(tag);
     var curTagsLength = tagsList.length;
+    var isList = tag === "ul" || tag === "ol";
 
     // Gather counts for other typical elements embedded within.
     // Traverse backwards so we can remove nodes at the same time
     // without effecting the traversal.
     //
     // TODO: Consider taking into account original contentScore here.
     for (var i = curTagsLength-1; i >= 0; i -= 1) {
       var weight = this._getClassWeight(tagsList[i]);
@@ -1627,23 +1644,23 @@ Readability.prototype = {
             embedCount += 1;
         }
 
         var linkDensity = this._getLinkDensity(tagsList[i]);
         var contentLength = this._getInnerText(tagsList[i]).length;
         var toRemove = false;
         if (img > p && !this._hasAncestorTag(tagsList[i], "figure")) {
           toRemove = true;
-        } else if (li > p && tag !== "ul" && tag !== "ol") {
+        } else if (!isList && li > p) {
           toRemove = true;
-        } else if ( input > Math.floor(p/3) ) {
+        } else if (input > Math.floor(p/3)) {
           toRemove = true;
-        } else if (contentLength < 25 && (img === 0 || img > 2) ) {
+        } else if (!isList && contentLength < 25 && (img === 0 || img > 2)) {
           toRemove = true;
-        } else if (weight < 25 && linkDensity > 0.2) {
+        } else if (!isList && weight < 25 && linkDensity > 0.2) {
           toRemove = true;
         } else if (weight >= 25 && linkDensity > 0.5) {
           toRemove = true;
         } else if ((embedCount === 1 && contentLength < 75) || embedCount > 1) {
           toRemove = true;
         }
 
         if (toRemove) {
@@ -1658,17 +1675,17 @@ Readability.prototype = {
    *
    * @param Element
    * @return void
   **/
   _cleanHeaders: function(e) {
     for (var headerIndex = 1; headerIndex < 3; headerIndex += 1) {
       var headers = e.getElementsByTagName('h' + headerIndex);
       for (var i = headers.length - 1; i >= 0; i -= 1) {
-        if (this._getClassWeight(headers[i]) < 0 || this._getLinkDensity(headers[i]) > 0.33)
+        if (this._getClassWeight(headers[i]) < 0)
           headers[i].parentNode.removeChild(headers[i]);
       }
     }
   },
 
   _flagIsActive: function(flag) {
     return (this._flags & flag) > 0;
   },
@@ -1681,42 +1698,52 @@ Readability.prototype = {
     this._flags = this._flags & ~flag;
   },
 
   /**
    * Decides whether or not the document is reader-able without parsing the whole thing.
    *
    * @return boolean Whether or not we suspect parse() will suceeed at returning an article object.
    */
-  isProbablyReaderable: function() {
-    var nodes = this._doc.getElementsByTagName("p");
-    if (nodes.length < 5) {
-      return false;
-    }
+  isProbablyReaderable: function(helperIsVisible) {
+    var nodes = this._getAllNodesWithTag(this._doc, ["p", "pre"]);
+
+    // FIXME we should have a fallback for helperIsVisible, but this is
+    // problematic because of jsdom's elem.style handling - see
+    // https://github.com/mozilla/readability/pull/186 for context.
 
-    var possibleParagraphs = 0;
-    for (var i = 0; i < nodes.length; i++) {
-      var node = nodes[i];
+    var score = 0;
+    // This is a little cheeky, we use the accumulator 'score' to decide what to return from
+    // this callback:
+    return this._someNode(nodes, function(node) {
+      if (helperIsVisible && !helperIsVisible(node))
+        return false;
       var matchString = node.className + " " + node.id;
 
       if (this.REGEXPS.unlikelyCandidates.test(matchString) &&
           !this.REGEXPS.okMaybeItsACandidate.test(matchString)) {
-        continue;
+        return false;
       }
 
-      if (node.textContent.trim().length < 100) {
-        continue;
+      if (node.matches && node.matches("li p")) {
+        return false;
       }
 
-      possibleParagraphs++;
-      if (possibleParagraphs >= 5) {
+      var textContentLength = node.textContent.trim().length;
+      if (textContentLength < 140) {
+        return false;
+      }
+
+      score += Math.sqrt(textContentLength - 140);
+
+      if (score > 20) {
         return true;
       }
-    }
-    return false;
+      return false;
+    });
   },
 
   /**
    * Runs readability.
    *
    * Workflow:
    *  1. Prep the document by removing script tags, css, etc.
    *  2. Build readability's DOM tree.
--- a/toolkit/components/reader/ReaderMode.jsm
+++ b/toolkit/components/reader/ReaderMode.jsm
@@ -116,17 +116,18 @@ this.ReaderMode = {
     if (!this._shouldCheckUri(uri)) {
       return false;
     }
 
     let utils = this.getUtilsForWin(doc.defaultView);
     // We pass in a helper function to determine if a node is visible, because
     // it uses gecko APIs that the engine-agnostic readability code can't rely
     // upon.
-    return new Readability(uri, doc).isProbablyReaderable(this.isNodeVisible.bind(this, utils));
+    // NOTE: This is currently disabled, see bug 1158228.
+    return new Readability(uri, doc).isProbablyReaderable(/*this.isNodeVisible.bind(this, utils)*/);
   },
 
   isNodeVisible: function(utils, node) {
     let bounds = utils.getBoundsWithoutFlushing(node);
     return bounds.height > 0 && bounds.width > 0;
   },
 
   getUtilsForWin: function(win) {
--- a/toolkit/components/reader/content/aboutReader.html
+++ b/toolkit/components/reader/content/aboutReader.html
@@ -24,17 +24,17 @@
 
     <div class="content">
       <style scoped>
         @import url("chrome://global/skin/aboutReaderContent.css");
       </style>
       <div id="moz-reader-content"></div>
     </div>
 
-    <div class="message">
+    <div>
       <style scoped>
         @import url("chrome://global/skin/aboutReaderControls.css");
       </style>
       <div id="reader-message"></div>
     </div>
 
     <div id="reader-footer" class="footer">
       <style scoped>
--- a/toolkit/devtools/server/actors/inspector.js
+++ b/toolkit/devtools/server/actors/inspector.js
@@ -3466,16 +3466,40 @@ var InspectorActor = exports.InspectorAc
     }
 
     img.src = url;
 
     return deferred.promise;
   }, {
     request: {url: Arg(0), maxDim: Arg(1, "nullable:number")},
     response: RetVal("imageData")
+  }),
+
+  /**
+   * Resolve a URL to its absolute form, in the scope of a given content window.
+   * @param {String} url.
+   * @param {NodeActor} node If provided, the owner window of this node will be
+   * used to resolve the URL. Otherwise, the top-level content window will be
+   * used instead.
+   * @return {String} url.
+   */
+  resolveRelativeURL: method(function(url, node) {
+    let document = isNodeDead(node)
+                   ? this.window.document
+                   : nodeDocument(node.rawNode);
+
+    if (!document) {
+      return url;
+    } else {
+      let baseURI = Services.io.newURI(document.location.href, null, null);
+      return Services.io.newURI(url, null, baseURI).spec;
+    }
+  }, {
+    request: {url: Arg(0, "string"), node: Arg(1, "nullable:domnode")},
+    response: {value: RetVal("string")}
   })
 });
 
 /**
  * Client side of the inspector actor, which is used to create
  * inspector-related actors, including the walker.
  */
 var InspectorFront = exports.InspectorFront = protocol.FrontClass(InspectorActor, {
--- a/toolkit/devtools/server/tests/mochitest/chrome.ini
+++ b/toolkit/devtools/server/tests/mochitest/chrome.ini
@@ -58,16 +58,17 @@ skip-if = buildapp == 'mulet'
 [test_inspector-mutations-attr.html]
 [test_inspector-mutations-childlist.html]
 [test_inspector-mutations-frameload.html]
 [test_inspector-mutations-value.html]
 [test_inspector-pseudoclass-lock.html]
 [test_inspector-release.html]
 [test_inspector-reload.html]
 [test_inspector-remove.html]
+[test_inspector-resolve-url.html]
 [test_inspector-retain.html]
 [test_inspector-scroll-into-view.html]
 [test_inspector-traversal.html]
 [test_makeGlobalObjectReference.html]
 [test_memory.html]
 [test_memory_allocations_01.html]
 [test_memory_allocations_02.html]
 [test_memory_allocations_03.html]
new file mode 100644
--- /dev/null
+++ b/toolkit/devtools/server/tests/mochitest/test_inspector-resolve-url.html
@@ -0,0 +1,90 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=921102
+-->
+<head>
+  <meta charset="utf-8">
+  <title>Test for Bug 921102</title>
+
+  <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+  <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+  <script type="application/javascript;version=1.8" src="inspector-helpers.js"></script>
+  <script type="application/javascript;version=1.8">
+Components.utils.import("resource://gre/modules/devtools/Loader.jsm");
+const {Promise: promise} = Components.utils.import("resource://gre/modules/Promise.jsm", {});
+const inspector = devtools.require("devtools/server/actors/inspector");
+
+window.onload = function() {
+  SimpleTest.waitForExplicitFinish();
+  runNextTest();
+}
+
+var gInspector;
+var gDoc;
+
+addTest(function() {
+  let url = document.getElementById("inspectorContent").href;
+  attachURL(url, function(err, client, tab, doc) {
+    gDoc = doc;
+    let {InspectorFront} = devtools.require("devtools/server/actors/inspector");
+    gInspector = InspectorFront(client, tab);
+    runNextTest();
+  });
+});
+
+addTest(function() {
+  info("Resolve a relative URL without providing a context node");
+  gInspector.resolveRelativeURL("test.png?id=4#wow").then(url => {
+    is(url, "chrome://mochitests/content/chrome/toolkit/devtools/server/tests/" +
+            "mochitest/test.png?id=4#wow");
+    runNextTest();
+  });
+});
+
+addTest(function() {
+  info("Resolve an absolute URL without providing a context node");
+  gInspector.resolveRelativeURL("chrome://mochitests/content/chrome/toolkit/" +
+                                "devtools/server/").then(url => {
+    is(url, "chrome://mochitests/content/chrome/toolkit/devtools/server/");
+    runNextTest();
+  });
+});
+
+addTest(function() {
+  info("Resolve a relative URL providing a context node");
+  let node = gDoc.querySelector(".big-horizontal");
+  gInspector.resolveRelativeURL("test.png?id=4#wow", node).then(url => {
+    is(url, "chrome://mochitests/content/chrome/toolkit/devtools/server/tests/" +
+            "mochitest/test.png?id=4#wow");
+    runNextTest();
+  });
+});
+
+addTest(function() {
+  info("Resolve an absolute URL providing a context node");
+  let node = gDoc.querySelector(".big-horizontal");
+  gInspector.resolveRelativeURL("chrome://mochitests/content/chrome/toolkit/" +
+                                "devtools/server/", node).then(url => {
+    is(url, "chrome://mochitests/content/chrome/toolkit/devtools/server/");
+    runNextTest();
+  });
+});
+
+addTest(function() {
+  gInspector = gDoc = null;
+  runNextTest();
+});
+  </script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=921102">Mozilla Bug 921102</a>
+<a id="inspectorContent" target="_blank" href="inspector_getImageData.html">Test Document</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+</pre>
+</body>
+</html>
--- a/toolkit/themes/shared/aboutReaderControls.css
+++ b/toolkit/themes/shared/aboutReaderControls.css
@@ -22,17 +22,17 @@
 }
 
 .serif-button {
   font-family: Georgia, "Times New Roman", serif;
 }
 
 /* Loading/error message */
 
-.message {
+#reader-message {
   margin-top: 40px;
   display: none;
   text-align: center;
   width: 100%;
   font-size: 0.9em;
 }
 
 /* Header */