diff options
author | gorhill <rhill@raymondhill.net> | 2015-06-26 15:45:54 -0400 |
---|---|---|
committer | gorhill <rhill@raymondhill.net> | 2015-06-26 15:45:54 -0400 |
commit | 7d2855180c5560ebc1d850261edc5a2f5bdf3b0c (patch) | |
tree | 594357fdb9d32485f746fab72d2bc519c6399d57 | |
parent | 631443768f2359e6fdb0bc6a3e457a0951e0a9e9 (diff) | |
download | uBlock-7d2855180c5560ebc1d850261edc5a2f5bdf3b0c.zip uBlock-7d2855180c5560ebc1d850261edc5a2f5bdf3b0c.tar.gz uBlock-7d2855180c5560ebc1d850261edc5a2f5bdf3b0c.tar.bz2 |
some refactoring of new DOM inspector code
-rw-r--r-- | platform/chromium/vapi-background.js | 25 | ||||
-rw-r--r-- | platform/firefox/vapi-background.js | 26 | ||||
-rw-r--r-- | src/js/logger-ui.js | 303 | ||||
-rw-r--r-- | src/js/messaging.js | 10 | ||||
-rw-r--r-- | src/js/scriptlets/dom-fingerprint.js | 66 | ||||
-rw-r--r-- | src/js/scriptlets/dom-highlight.js | 339 | ||||
-rw-r--r-- | src/js/scriptlets/dom-inspector.js | 691 | ||||
-rw-r--r-- | src/js/scriptlets/dom-layout.js | 406 |
8 files changed, 876 insertions, 990 deletions
diff --git a/platform/chromium/vapi-background.js b/platform/chromium/vapi-background.js index 8e67b3d..ed66264 100644 --- a/platform/chromium/vapi-background.js +++ b/platform/chromium/vapi-background.js @@ -603,7 +603,7 @@ vAPI.messaging.onPortMessage = function(request, port) { return; } - console.error('µBlock> messaging > unknown request: %o', request); + console.error('uBlock> messaging > unknown request: %o', request); // Unhandled: // Need to callback anyways in case caller expected an answer, or @@ -661,9 +661,26 @@ vAPI.messaging.broadcast = function(message) { /******************************************************************************/ -vAPI.messaging.send = function(tabId, channelName, message) { +// "Auxiliary process": any process other than main process. +// +// Main process to auxiliary processes messaging. The approach is that of +// smoke-signal messaging, so emitters have to be ready to deal with no +// response at all (i.e. use timeout where needed). +// +// Mandatory: +// - receiverTabId: Which tab to send the message. +// No target tab id means sends to all tabs. +// - receiverChannel: Which channel to send the message. +// +// Optional: +// - senderTabId: From which tab the message originates. +// - senderChannel: From which channel the message originates. +// These optional fields are useful for the target, and may be used +// to send back a response to the sender. + +vAPI.messaging.post = function(message) { var port; - var chromiumTabId = toChromiumTabId(tabId); + var chromiumTabId = toChromiumTabId(message.receiverTabId); for ( var portName in this.ports ) { if ( this.ports.hasOwnProperty(portName) === false ) { continue; @@ -673,7 +690,7 @@ vAPI.messaging.send = function(tabId, channelName, message) { continue; } port.postMessage({ - channelName: channelName, + channelName: message.receiverChannel, msg: message }); if ( chromiumTabId !== 0 ) { diff --git a/platform/firefox/vapi-background.js b/platform/firefox/vapi-background.js index f7a4d72..67bda4f 100644 --- a/platform/firefox/vapi-background.js +++ b/platform/firefox/vapi-background.js @@ -1239,10 +1239,30 @@ vAPI.messaging.setup = function(defaultHandler) { /******************************************************************************/ -vAPI.messaging.send = function(tabId, channelName, message) { - var ffTabId = tabId || ''; +// "Auxiliary process": any process other than main process. +// +// Main process to auxiliary processes messaging. The approach is that of +// smoke-signal messaging, so emitters have to be ready to deal with no +// response at all (i.e. use timeout where needed). +// +// Mandatory: +// - receiverTabId: Which tab to send the message. +// No target tab id means sends to all tabs. +// - receiverChannel: Which channel to send the message. +// +// Optional: +// - senderTabId: From which tab the message originates. +// - senderChannel: From which channel the message originates. +// These optional fields are useful for the target, and may be used +// to send back a response to the sender. + +vAPI.messaging.post = function(message) { + var ffTabId = message.receiverTabId || ''; var targetId = location.host + ':broadcast'; - var payload = JSON.stringify({ channelName: channelName, msg: message }); + var payload = JSON.stringify({ + channelName: message.receiverChannel, + msg: message + }); if ( ffTabId === '' ) { this.globalMessageManager.broadcastAsyncMessage(targetId, payload); diff --git a/src/js/logger-ui.js b/src/js/logger-ui.js index 576360a..fe07b1a 100644 --- a/src/js/logger-ui.js +++ b/src/js/logger-ui.js @@ -133,80 +133,16 @@ var tabIdFromClassName = function(className) { (function domInspector() { // Don't bother if the browser is not modern enough. - if ( typeof Map === undefined ) { + if ( typeof Map === undefined || typeof WeakMap === undefined ) { return; } - var enabled = false; - var state = ''; - var fingerprint = ''; - var fingerprintTimer = null; - var currentTabId = ''; + var inspectedTabId = ''; var currentSelector = ''; + var showdomButton = uDom.nodeFromId('showdom'); var inspector = uDom.nodeFromId('domInspector'); var tabSelector = uDom.nodeFromId('pageSelector'); - var inlineElementTags = [ - 'a', 'abbr', 'acronym', - 'b', 'bdo', 'big', 'br', 'button', - 'cite', 'code', - 'del', 'dfn', - 'em', - 'font', - 'i', 'img', 'input', 'ins', - 'kbd', - 'label', - 'map', - 'object', - 'q', - 'samp', 'select', 'small', 'span', 'strong', 'sub', 'sup', - 'textarea', 'tt', - 'u', - 'var' - ].reduce(function(p, c) { - p[c] = true; - return p; - }, Object.create(null)); - - var startTimer = function() { - if ( fingerprintTimer === null ) { - fingerprintTimer = vAPI.setTimeout(fetchFingerprint, 2000); - } - }; - - var stopTimer = function() { - if ( fingerprintTimer !== null ) { - clearTimeout(fingerprintTimer); - fingerprintTimer = null; - } - }; - - var injectHighlighter = function(tabId) { - if ( tabId === '' ) { - return; - } - messager.send({ - what: 'scriptlet', - tabId: tabId, - scriptlet: 'dom-highlight' - }); - }; - - var removeHighlighter = function(tabId) { - if ( tabId === '' ) { - return; - } - messager.send({ - what: 'sendMessageTo', - tabId: tabId, - channelName: 'scriptlets', - msg: { - what: 'dom-highlight', - action: 'shutdown' - } - }); - }; - var nodeFromDomEntry = function(entry) { var node; var li = document.createElement('li'); @@ -250,29 +186,6 @@ var tabIdFromClassName = function(className) { } }; - var expandIfBlockElement = function(node) { - var ul = node.parentElement; - if ( ul === null ) { - return; - } - var li = ul.parentElement; - if ( li === null ) { - return; - } - if ( li.classList.contains('show') ) { - return; - } - var tag = node.firstElementChild.textContent; - var pos = tag.search(/[^a-z0-9]/); - if ( pos !== -1 ) { - tag = tag.slice(0, pos); - } - if ( inlineElementTags[tag] !== undefined ) { - return; - } - li.classList.add('show'); - }; - var renderDOM = function(response) { var ul = document.createElement('ul'); var lvl = 0; @@ -314,9 +227,6 @@ var tabIdFromClassName = function(className) { removeAllChildren(inspector); inspector.appendChild(ul); - - // Check for change at regular interval. - fingerprint = entries.length !== 0 ? entries[0].fp : ''; }; var selectorFromNode = function(node, nth) { @@ -344,7 +254,7 @@ var tabIdFromClassName = function(className) { var onClick = function(ev) { ev.stopPropagation(); - if ( currentTabId === '' ) { + if ( inspectedTabId === '' ) { return; } @@ -366,12 +276,13 @@ var tabIdFromClassName = function(className) { if ( target.localName === 'code' ) { var original = target.classList.contains('filter') === false; messager.send({ - what: 'sendMessageTo', - tabId: currentTabId, - channelName: 'scriptlets', + what: 'postMessageTo', + senderTabId: null, + senderChannel: 'logger-ui.js', + receiverTabId: inspectedTabId, + receiverChannel: 'dom-inspector.js', msg: { - what: 'dom-highlight', - action: 'toggleNodes', + what: 'toggleNodes', original: original, target: original !== target.classList.toggle('off'), selector: selectorFromNode(target, original ? 1 : 2) @@ -383,12 +294,13 @@ var tabIdFromClassName = function(className) { // Highlight and scrollto if ( target.localName === 'code' ) { messager.send({ - what: 'sendMessageTo', - tabId: currentTabId, - channelName: 'scriptlets', + what: 'postMessageTo', + senderTabId: null, + senderChannel: 'logger-ui.js', + receiverTabId: inspectedTabId, + receiverChannel: 'dom-inspector.js', msg: { - what: 'dom-highlight', - action: 'highlight', + what: 'highlight', selector: selectorFromNode(target), scrollTo: true } @@ -404,12 +316,13 @@ var tabIdFromClassName = function(className) { var timerHandler = function() { mouseoverTimer = null; messager.send({ - what: 'sendMessageTo', - tabId: currentTabId, - channelName: 'scriptlets', + what: 'postMessageTo', + senderTabId: null, + senderChannel: 'logger-ui.js', + receiverTabId: inspectedTabId, + receiverChannel: 'dom-inspector.js', msg: { - what: 'dom-highlight', - action: 'highlight', + what: 'highlight', selector: selectorFromNode(mouseoverTarget), scrollTo: true } @@ -417,6 +330,10 @@ var tabIdFromClassName = function(className) { }; return function(ev) { + if ( inspectedTabId === '' ) { + return; + } + // Find closest `li` var target = ev.target; while ( target !== null ) { @@ -435,115 +352,165 @@ var tabIdFromClassName = function(className) { }; })(); - var onFingerprintFetched = function(response) { - if ( state !== 'fetchingFingerprint' ) { - return; + var pollTimer = null; + var fingerprint = null; + + var currentTabId = function() { + if ( showdomButton.classList.contains('active') === false ) { + return ''; } - state = ''; - fingerprintTimer = null; - if ( !enabled ) { + var tabId = tabIdFromClassName(tabSelector.value) || ''; + return tabId !== 'bts' ? tabId : ''; + }; + + var cancelPollTimer = function() { + if ( pollTimer !== null ) { + clearTimeout(pollTimer); + pollTimer = null; + } + }; + + var onDOMFetched = function(response) { + if ( response === undefined || currentTabId() !== inspectedTabId ) { + shutdownInspector(inspectedTabId); + injectInspectorAsync(250); return; } - if ( response === fingerprint ) { - startTimer(); + + if ( response.layout === 'NOCHANGE' ) { + fetchDOMAsync(); return; } - fingerprint = response || ''; - fetchDOM(); + + renderDOM(response.layout); + fingerprint = response.fingerprint; + + fetchDOMAsync(); }; - var fetchFingerprint = function() { + var fetchDOM = function() { messager.send({ - what: 'scriptlet', - tabId: currentTabId, - scriptlet: 'dom-fingerprint' - }, onFingerprintFetched); - state = 'fetchingFingerprint'; + what: 'postMessageTo', + senderTabId: null, + senderChannel: 'logger-ui.js', + receiverTabId: inspectedTabId, + receiverChannel: 'dom-inspector.js', + msg: { + what: 'domLayout', + fingerprint: fingerprint + } + }); + pollTimer = vAPI.setTimeout(function() { + pollTimer = null; + onDOMFetched(); + }, 1001); }; - var onDOMFetched = function(response) { - if ( state !== 'fetchingDOM' ) { + var fetchDOMAsync = function(delay) { + if ( pollTimer !== null ) { return; } - state = ''; - if ( !enabled ) { + pollTimer = vAPI.setTimeout(function() { + pollTimer = null; + fetchDOM(); + }, delay || 1001); + }; + + var injectInspector = function() { + var tabId = currentTabId(); + // No valid tab, go back + if ( tabId === '' ) { + injectInspectorAsync(); return; } - if ( Array.isArray(response) && response.length !== 0 ) { - renderDOM(response); - injectHighlighter(currentTabId); - } else { - fingerprint = ''; - } - startTimer(); + inspectedTabId = tabId; + fingerprint = null; + messager.send({ + what: 'scriptlet', + tabId: tabId, + scriptlet: 'dom-inspector' + }); + fetchDOMAsync(250); }; - var fetchDOM = function() { - if ( currentTabId === '' ) { - removeAllChildren(inspector); - startTimer(); + var injectInspectorAsync = function(delay) { + if ( pollTimer !== null ) { return; } + if ( showdomButton.classList.contains('active') === false ) { + return; + } + pollTimer = vAPI.setTimeout(function() { + pollTimer = null; + injectInspector(); + }, delay || 1001); + }; + var shutdownInspector = function(tabId) { messager.send({ - what: 'scriptlet', - tabId: currentTabId, - scriptlet: 'dom-layout' - }, onDOMFetched); - state = 'fetchingDOM'; + what: 'postMessageTo', + senderTabId: null, + senderChannel: 'logger-ui.js', + receiverTabId: tabId, + receiverChannel: 'dom-inspector.js', + msg: { what: 'shutdown', } + }); + removeAllChildren(inspector); + cancelPollTimer(); + inspectedTabId = ''; }; var onTabIdChanged = function() { - if ( !enabled ) { - return; - } - var previousTabId = currentTabId; - var tabId = tabIdFromClassName(tabSelector.value) || ''; - currentTabId = tabId !== 'bts' && tabId !== '' ? tabId : ''; - if ( currentTabId !== previousTabId ) { - removeHighlighter(previousTabId); + if ( inspectedTabId !== currentTabId() ) { + shutdownInspector(); + injectInspectorAsync(250); } - if ( state === 'fetchingDOM' ) { - return; + }; + + var onMessage = function(request) { + var msg = request.what === 'postMessageTo' ? request.msg : request; + switch ( msg.what ) { + case 'domLayout': + cancelPollTimer(); + onDOMFetched(msg); + break; + + default: + break; } - fetchDOM(); }; var toggleOn = function() { - if ( enabled ) { - return; - } - enabled = true; window.addEventListener('beforeunload', toggleOff); inspector.addEventListener('click', onClick, true); inspector.addEventListener('mouseover', onMouseOver, true); tabSelector.addEventListener('change', onTabIdChanged); - onTabIdChanged(); inspector.classList.add('enabled'); + messager.addListener(onMessage); + injectInspector(); }; var toggleOff = function() { - removeHighlighter(currentTabId); + messager.removeListener(onMessage); + cancelPollTimer(); + shutdownInspector(); window.removeEventListener('beforeunload', toggleOff); inspector.removeEventListener('click', onClick, true); inspector.removeEventListener('mouseover', onMouseOver, true); tabSelector.removeEventListener('change', onTabIdChanged); - removeAllChildren(inspector); - stopTimer(); - currentTabId = currentSelector = fingerprint = ''; - enabled = false; + currentSelector = inspectedTabId = ''; inspector.classList.remove('enabled'); }; var toggle = function() { - if ( uDom.nodeFromId('showdom').classList.toggle('active') ) { + if ( showdomButton.classList.toggle('active') ) { toggleOn(); } else { toggleOff(); } }; - uDom('#showdom').on('click', toggle); + showdomButton.addEventListener('click', toggle); })(); /******************************************************************************/ diff --git a/src/js/messaging.js b/src/js/messaging.js index 41e563b..daf0b83 100644 --- a/src/js/messaging.js +++ b/src/js/messaging.js @@ -141,6 +141,12 @@ var onMessage = function(request, sender, callback) { vAPI.tabs.open(request.details); break; + // Passthrough for auxiliary script to auxiliary script messaging + case 'postMessageTo': + request.senderTabId = tabId; + vAPI.messaging.post(request); + break; + case 'reloadTab': if ( vAPI.isBehindTheSceneTabId(request.tabId) === false ) { vAPI.tabs.reload(request.tabId); @@ -158,10 +164,6 @@ var onMessage = function(request, sender, callback) { µb.selectFilterLists(request.switches); break; - case 'sendMessageTo': - vAPI.messaging.send(request.tabId, request.channelName, request.msg); - break; - case 'toggleHostnameSwitch': µb.toggleHostnameSwitch(request); break; diff --git a/src/js/scriptlets/dom-fingerprint.js b/src/js/scriptlets/dom-fingerprint.js deleted file mode 100644 index 05a9fb9..0000000 --- a/src/js/scriptlets/dom-fingerprint.js +++ /dev/null @@ -1,66 +0,0 @@ -/******************************************************************************* - - uBlock Origin - a browser extension to block requests. - Copyright (C) 2015 Raymond Hill - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see {http://www.gnu.org/licenses/}. - - Home: https://github.com/gorhill/uBlock -*/ - -/* global vAPI, HTMLDocument */ - -/******************************************************************************/ - -(function() { - -'use strict'; - -/******************************************************************************/ - -// https://github.com/gorhill/uBlock/issues/464 -if ( document instanceof HTMLDocument === false ) { - return; -} - -// This can happen -if ( typeof vAPI !== 'object' ) { - return; -} - -/******************************************************************************/ - -// Some kind of fingerprint for the DOM, without incurring too much overhead. - -var url = window.location.href; -var pos = url.indexOf('#'); -if ( pos !== -1 ) { - url = url.slice(0, pos); -} -var fingerprint = url + '{' + document.getElementsByTagName('*').length.toString() + '}'; - -var localMessager = vAPI.messaging.channel('scriptlets'); -localMessager.send({ - what: 'scriptletResponse', - scriptlet: 'dom-fingerprint', - response: fingerprint -}, function() { - localMessager.close(); -}); - -/******************************************************************************/ - -})(); - -/******************************************************************************/ diff --git a/src/js/scriptlets/dom-highlight.js b/src/js/scriptlets/dom-highlight.js deleted file mode 100644 index 1b4c981..0000000 --- a/src/js/scriptlets/dom-highlight.js +++ /dev/null @@ -1,339 +0,0 @@ -/******************************************************************************* - - uBlock Origin - a browser extension to block requests. - Copyright (C) 2015 Raymond Hill - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see {http://www.gnu.org/licenses/}. - - Home: https://github.com/gorhill/uBlock -*/ - -/* global vAPI, HTMLDocument */ - -/******************************************************************************/ - -(function() { - -'use strict'; - -/******************************************************************************/ - -// https://github.com/gorhill/uBlock/issues/464 -if ( document instanceof HTMLDocument === false ) { - return; -} - -// This can happen -if ( typeof vAPI !== 'object' ) { - return; -} - -/******************************************************************************/ - -if ( document.querySelector('iframe.dom-highlight.' + vAPI.sessionId) !== null ) { - return; -} - -/******************************************************************************/ - -var localMessager = null; -var svgOcean = null; -var svgIslands = null; -var svgRoot = null; -var pickerRoot = null; -var currentSelector = ''; - -var toggledNodes = new Map(); - -/******************************************************************************/ - -var highlightElements = function(elems, scrollTo) { - var wv = pickerRoot.contentWindow.innerWidth; - var hv = pickerRoot.contentWindow.innerHeight; - var ocean = ['M0 0h' + wv + 'v' + hv + 'h-' + wv, 'z']; - var islands = []; - var elem, rect, poly; - var xl, xr, yt, yb, w, h, ws; - var xlu = Number.MAX_VALUE, xru = 0, ytu = Number.MAX_VALUE, ybu = 0; - - for ( var i = 0; i < elems.length; i++ ) { - elem = elems[i]; - if ( elem === pickerRoot ) { - continue; - } - if ( typeof elem.getBoundingClientRect !== 'function' ) { - continue; - } - - rect = elem.getBoundingClientRect(); - xl = rect.left; - xr = rect.right; - w = rect.width; - yt = rect.top; - yb = rect.bottom; - h = rect.height; - - ws = w.toFixed(1); - poly = 'M' + xl.toFixed(1) + ' ' + yt.toFixed(1) + - 'h' + ws + - 'v' + h.toFixed(1) + - 'h-' + ws + - 'z'; - ocean.push(poly); - islands.push(poly); - - if ( !scrollTo ) { - continue; - } - - if ( xl < xlu ) { xlu = xl; } - if ( xr > xru ) { xru = xr; } - if ( yt < ytu ) { ytu = yt; } - if ( yb > ybu ) { ybu = yb; } - } - svgOcean.setAttribute('d', ocean.join('')); - svgIslands.setAttribute('d', islands.join('') || 'M0 0'); - - if ( !scrollTo ) { - return; - } - - // Highlighted area completely within viewport - if ( xlu >= 0 && xru <= wv && ytu >= 0 && ybu <= hv ) { - return; - } - - var dx = 0, dy = 0; - - if ( xru > wv ) { - dx = xru - wv; - xlu -= dx; - } - if ( xlu < 0 ) { - dx += xlu; - } - if ( ybu > hv ) { - dy = ybu - hv; - ytu -= dy; - } - if ( ytu < 0 ) { - dy += ytu; - } - - if ( dx !== 0 || dy !== 0 ) { - window.scrollBy(dx, dy); - } -}; - -/******************************************************************************/ - -var elementsFromSelector = function(filter) { - var out = []; - try { - out = document.querySelectorAll(filter); - } catch (ex) { - } - return out; -}; - -/******************************************************************************/ - -var highlight = function(scrollTo) { - var elements = elementsFromSelector(currentSelector); - highlightElements(elements, scrollTo); -}; - -/******************************************************************************/ - -var onScrolled = function() { - highlight(); -}; - -/******************************************************************************/ - -// original, target = what to do -// any, any = restore saved display property -// any, hidden = set display to `none`, remember original state -// hidden, any = remove display property, don't remember original state -// hidden, hidden = set display to `none` - -var toggleNodes = function(selector, originalState, targetState) { - var nodes = document.querySelectorAll(selector); - var i = nodes.length; - if ( i === 0 ) { - return; - } - var node, value; - while ( i-- ) { - node = nodes[i]; - if ( originalState ) { // any, ? - if ( targetState ) { // any, any - value = toggledNodes.get(node); - if ( value === undefined ) { - continue; - } - if ( value !== null ) { - node.style.removeProperty('display'); - } else { - node.style.setProperty('display', value); - } - toggledNodes.delete(node); - } else { // any, hidden - toggledNodes.set(node, node.style.getPropertyValue('display') || null); - node.style.setProperty('display', 'none'); - } - } else { // hidden, ? - if ( targetState ) { // hidden, any - node.style.setProperty('display', 'initial', 'important'); - } else { // hidden, hidden - node.style.setProperty('display', 'none', 'important'); - } - } - } -}; - -/******************************************************************************/ - -var resetToggledNodes = function() { - // Chromium does not support destructuring as of v43. - for ( var node of toggledNodes.keys() ) { - value = toggledNodes.get(node); - if ( value !== null ) { - node.style.removeProperty('display'); - } else { - node.style.setProperty('display', value); - } - } - toggledNodes.clear(); -}; - -/******************************************************************************/ - -var shutdown = function() { - resetToggledNodes(); - localMessager.removeListener(onMessage); - localMessager.close(); - localMessager = null; - window.removeEventListener('scroll', onScrolled, true); - document.documentElement.removeChild(pickerRoot); - pickerRoot = svgRoot = svgOcean = svgIslands = null; - currentSelector = ''; -}; - -/******************************************************************************/ - -var onMessage = function(msg) { - if ( msg.what !== 'dom-highlight' ) { - return; - } - switch ( msg.action ) { - case 'highlight': - currentSelector = msg.selector; - highlight(msg.scrollTo); - break; - - case 'toggleNodes': - toggleNodes(msg.selector, msg.original, msg.target); - currentSelector = msg.selector; - highlight(true); - break; - - case 'shutdown': - shutdown(); - break; - - default: - break; - } -}; - -/******************************************************************************/ - -(function() { - pickerRoot = document.createElement('iframe'); - pickerRoot.classList.add(vAPI.sessionId); - pickerRoot.classList.add('dom-highlight'); - pickerRoot.style.cssText = [ - 'background: transparent', - 'border: 0', - 'border-radius: 0', - 'box-shadow: none', - 'display: block', - 'height: 100%', - 'left: 0', - 'margin: 0', - 'opacity: 1', - 'position: fixed', - 'outline: 0', - 'padding: 0', - 'top: 0', - 'visibility: visible', - 'width: 100%', - 'z-index: 2147483647', - '' - ].join(' !important;\n'); - - pickerRoot.onload = function() { - pickerRoot.onload = null; - var pickerDoc = this.contentDocument; - - var style = pickerDoc.createElement('style'); - style.textContent = [ - 'body {', - 'background-color: transparent;', - 'cursor: crosshair;', - '}', - 'svg {', - 'height: 100%;', - 'left: 0;', - 'position: fixed;', - 'top: 0;', - 'width: 100%;', - '}', - 'svg > path:first-child {', - 'fill: rgba(0,0,0,0.75);', - 'fill-rule: evenodd;', - '}', - 'svg > path + path {', - 'fill: rgba(0,0,255,0.1);', - 'stroke: #FFF;', - 'stroke-width: 0.5px;', - '}', - '' - ].join('\n'); - pickerDoc.body.appendChild(style); - - svgRoot = pickerDoc.createElementNS('http://www.w3.org/2000/svg', 'svg'); - svgOcean = pickerDoc.createElementNS('http://www.w3.org/2000/svg', 'path'); - svgRoot.appendChild(svgOcean); - svgIslands = pickerDoc.createElementNS('http://www.w3.org/2000/svg', 'path'); - svgRoot.appendChild(svgIslands); - pickerDoc.body.appendChild(svgRoot); - - window.addEventListener('scroll', onScrolled, true); - - localMessager = vAPI.messaging.channel('scriptlets'); - localMessager.addListener(onMessage); - - highlight(); - }; - - document.documentElement.appendChild(pickerRoot); -})(); - -/******************************************************************************/ - -})(); - -/******************************************************************************/ diff --git a/src/js/scriptlets/dom-inspector.js b/src/js/scriptlets/dom-inspector.js new file mode 100644 index 0000000..3127799 --- /dev/null +++ b/src/js/scriptlets/dom-inspector.js @@ -0,0 +1,691 @@ +/******************************************************************************* + + uBlock Origin - a browser extension to block requests. + Copyright (C) 2015 Raymond Hill + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see {http://www.gnu.org/licenses/}. + + Home: https://github.com/gorhill/uBlock +*/ + +/* global vAPI, HTMLDocument */ + +/******************************************************************************/ +/******************************************************************************/ + +(function() { + +'use strict'; + +/******************************************************************************/ + +// https://github.com/gorhill/uBlock/issues/464 +if ( document instanceof HTMLDocument === false ) { + return; +} + +// This can happen +if ( typeof vAPI !== 'object' ) { + return; +} + +/******************************************************************************/ + +if ( document.querySelector('iframe.dom-inspector.' + vAPI.sessionId) !== null ) { + return; +} + +/******************************************************************************/ +/******************************************************************************/ + +// Modified to avoid installing as a global shim -- so the scriptlet can be +// flushed from memory once no longer in use. + +/*! http://mths.be/cssescape v0.2.1 by @mathias | MIT license */ +var cssEscape = (function(root) { + + var css = root.CSS || {}; + if ( css.escape ) { + return css.escape; + } + + var InvalidCharacterError = function(message) { + this.message = message; + }; + InvalidCharacterError.prototype = new Error(); + InvalidCharacterError.prototype.name = 'InvalidCharacterError'; + + // http://dev.w3.org/csswg/cssom/#serialize-an-identifier + return function(value) { + var string = String(value); + var length = string.length; + var index = -1; + var codeUnit; + var result = ''; + var firstCodeUnit = string.charCodeAt(0); + while (++index < length) { + codeUnit = string.charCodeAt(index); + // Note: there’s no need to special-case astral symbols, surrogate + // pairs, or lone surrogates. + + // If the character is NULL (U+0000), then throw an + // `InvalidCharacterError` exception and terminate these steps. + if (codeUnit === 0x0000) { + throw new InvalidCharacterError( + 'Invalid character: the input contains U+0000.' + ); + } + + if ( + // If the character is in the range [\1-\1F] (U+0001 to U+001F) or is + // U+007F, […] + (codeUnit >= 0x0001 && codeUnit <= 0x001F) || codeUnit == 0x007F || + // If the character is the first character and is in the range [0-9] + // (U+0030 to U+0039), […] + (index === 0 && codeUnit >= 0x0030 && codeUnit <= 0x0039) || + // If the character is the second character and is in the range [0-9] + // (U+0030 to U+0039) and the first character is a `-` (U+002D), […] + ( + index == 1 && + codeUnit >= 0x0030 && codeUnit <= 0x0039 && + firstCodeUnit == 0x002D + ) + ) { + // http://dev.w3.org/csswg/cssom/#escape-a-character-as-code-point + result += '\\' + codeUnit.toString(16) + ' '; + continue; + } + + // If the character is not handled by one of the above rules and is + // greater than or equal to U+0080, is `-` (U+002D) or `_` (U+005F), or + // is in one of the ranges [0-9] (U+0030 to U+0039), [A-Z] (U+0041 to + // U+005A), or [a-z] (U+0061 to U+007A), […] + if ( + codeUnit >= 0x0080 || + codeUnit == 0x002D || + codeUnit == 0x005F || + codeUnit >= 0x0030 && codeUnit <= 0x0039 || + codeUnit >= 0x0041 && codeUnit <= 0x005A || + codeUnit >= 0x0061 && codeUnit <= 0x007A + ) { + // the character itself + result += string.charAt(index); + continue; + } + + // Otherwise, the escaped character. + // http://dev.w3.org/csswg/cssom/#escape-a-character + result += '\\' + string.charAt(index); + + } + return result; + }; + +}(self)); + +/******************************************************************************/ +/******************************************************************************/ + +var localMessager = vAPI.messaging.channel('dom-inspector.js'); + +var svgOcean = null; +var svgIslands = null; +var svgRoot = null; +var pickerRoot = null; +var currentSelector = ''; + +var toggledNodes = new Map(); + +/******************************************************************************/ + +var domLayout = (function() { + var skipTagNames = { + 'br': true, + 'link': true, + 'meta': true, + 'script': true, + 'style': true + }; + + var resourceAttrNames = { + 'a': 'href', + 'iframe': 'src', + 'img': 'src', + 'object': 'data' + }; + + // Collect all nodes which are directly affected by cosmetic filters: these + // will be reported in the layout data. + + var nodeToCosmeticFilterMap = (function() { + var out = new WeakMap(); + var styleTags = vAPI.styles || []; + var i = styleTags.length; + var selectors, styleText, j, selector, nodes, k; + while ( i-- ) { + styleText = styleTags[i].textContent; + selectors = styleText.slice(0, styleText.lastIndexOf('\n')).split(/,\n/); + j = selectors.length; + while ( j-- ) { + selector = selectors[j]; + nodes = document.querySelectorAll(selector); + k = nodes.length; + while ( k-- ) { + out.set(nodes[k], selector); + } + } + } + return out; + })(); + + var DomRoot = function() { + this.lvl = 0; + this.sel = 'body'; + var url = window.location.href; + var pos = url.indexOf('#'); + if ( pos !== -1 ) { + url = url.slice(0, pos); + } + this.src = url; + this.top = window === window.top; + this.cnt = 0; + }; + + var DomNode = function(level, selector, filter) { + this.lvl = level; + this.sel = selector; + this.cnt = 0; + this.filter = filter; + }; + + var hasManyMatches = function(node, selector) { + var fnName = matchesSelector; + if ( fnName === '' ) { + return true; + } + var child = node.firstElementChild; + var match = false; + while ( child !== null ) { + if ( child[fnName](selector) ) { + if ( match ) { + return true; + } + match = true; + } + child = child.nextElementSibling; + } + return false; + }; + + var matchesSelector = (function() { + if ( typeof Element.prototype.matches === 'function' ) { + return 'matches'; + } + if ( typeof Element.prototype.mozMatchesSelector === 'function' ) { + return 'mozMatchesSelector'; + } + if ( typeof Element.prototype.webkitMatchesSelector === 'function' ) { + return 'webkitMatchesSelector'; + } + return ''; + })(); + + var selectorFromNode = function(node) { + var str, attr, pos, sw, i; + var tag = node.localName; + var selector = cssEscape(tag); + // Id + if ( typeof node.id === 'string' ) { + str = node.id.trim(); + if ( str !== '' ) { + selector += '#' + cssEscape(str); + } + } + // Class + var cl = node.classList; + if ( cl ) { + for ( i = 0; i < cl.length; i++ ) { + selector += '.' + cssEscape(cl[i]); + } + } + // Tag-specific attributes + if ( resourceAttrNames.hasOwnProperty(tag) ) { + attr = resourceAttrNames[tag]; + str = node.getAttribute(attr) || ''; + str = str.trim(); + pos = str.indexOf('#'); + if ( pos !== -1 ) { + str = str.slice(0, pos); + sw = '^'; + } else { + sw = ''; + } + if ( str !== '' ) { + selector += '[' + attr + sw + '="' + cssEscape(str) + '"]'; + } + } + // The resulting selector must cause only one element to be selected. If + // it's not the case, further narrow using `nth-of-type` pseudo-class. + if ( hasManyMatches(node.parentElement, selector) ) { + i = 1; + while ( node.previousElementSibling ) { + node = node.previousElementSibling; + if ( node.localName === tag ) { + i += 1; + } + } + selector += ':nth-of-type(' + i + ')'; + } + return selector; + }; + + var domNodeFactory = function(level, node) { + var localName = node.localName; + if ( skipTagNames.hasOwnProperty(localName) ) { + return null; + } + // skip uBlock's own nodes + if ( node.classList.contains(vAPI.sessionId) ) { + return null; + } + if ( level === 0 && localName === 'body' ) { + return new DomRoot(); + } + var selector = selectorFromNode(node); + var filter = nodeToCosmeticFilterMap.get(node); + return new DomNode(level, selector, filter); + }; + + // Collect layout data. + + var getLayoutData = function() { + var domLayout = []; + var stack = []; + var node = document.body; + var domNode; + var lvl = 0; + + for (;;) { + domNode = domNodeFactory(lvl, node); + if ( domNode !== null ) { + domLayout.push(domNode); + } + // children + if ( node.firstElementChild !== null ) { + stack.push(node); + lvl += 1; + node = node.firstElementChild; + continue; + } + // sibling + if ( node.nextElementSibling === null ) { + do { + node = stack.pop(); + if ( !node ) { break; } + lvl -= 1; + } while ( node.nextElementSibling === null ); + if ( !node ) { break; } + } + node = node.nextElementSibling; + } + return domLayout; + }; + + // Descendant count for each node. + + var patchLayoutData = function(domLayout) { + var stack = [], ptr; + var lvl = 0; + var domNode, cnt; + var i = domLayout.length; + + while ( i-- ) { + domNode = domLayout[i]; + if ( domNode.lvl === lvl ) { + stack[ptr] += 1; + continue; + } + if ( domNode.lvl > lvl ) { + while ( lvl < domNode.lvl ) { + stack.push(0); + lvl += 1; + } + ptr = lvl - 1; + stack[ptr] += 1; + continue; + } + // domNode.lvl < lvl + cnt = stack.pop(); + domNode.cnt = cnt; + lvl -= 1; + ptr = lvl - 1; + stack[ptr] += cnt + 1; + } + return domLayout; + }; + + return function() { + return patchLayoutData(getLayoutData()); + }; +})(); + +/******************************************************************************/ + +// Some kind of fingerprint for the DOM, without incurring too much overhead. + +var domFingerprint = function() { + return vAPI.sessionId + '{' + document.getElementsByTagName('*').length + '}'; +}; + +/******************************************************************************/ + +var highlightElements = function(elems, scrollTo) { + var wv = pickerRoot.contentWindow.innerWidth; + var hv = pickerRoot.contentWindow.innerHeight; + var ocean = ['M0 0h' + wv + 'v' + hv + 'h-' + wv, 'z']; + var islands = []; + var elem, rect, poly; + var xl, xr, yt, yb, w, h, ws; + var xlu = Number.MAX_VALUE, xru = 0, ytu = Number.MAX_VALUE, ybu = 0; + + for ( var i = 0; i < elems.length; i++ ) { + elem = elems[i]; + if ( elem === pickerRoot ) { + continue; + } + if ( typeof elem.getBoundingClientRect !== 'function' ) { + continue; + } + + rect = elem.getBoundingClientRect(); + xl = rect.left; + xr = rect.right; + w = rect.width; + yt = rect.top; + yb = rect.bottom; + h = rect.height; + + ws = w.toFixed(1); + poly = 'M' + xl.toFixed(1) + ' ' + yt.toFixed(1) + + 'h' + ws + + 'v' + h.toFixed(1) + + 'h-' + ws + + 'z'; + ocean.push(poly); + islands.push(poly); + + if ( !scrollTo ) { + continue; + } + + if ( xl < xlu ) { xlu = xl; } + if ( xr > xru ) { xru = xr; } + if ( yt < ytu ) { ytu = yt; } + if ( yb > ybu ) { ybu = yb; } + } + svgOcean.setAttribute('d', ocean.join('')); + svgIslands.setAttribute('d', islands.join('') || 'M0 0'); + + if ( !scrollTo ) { + return; + } + + // Highlighted area completely within viewport + if ( xlu >= 0 && xru <= wv && ytu >= 0 && ybu <= hv ) { + return; + } + + var dx = 0, dy = 0; + + if ( xru > wv ) { + dx = xru - wv; + xlu -= dx; + } + if ( xlu < 0 ) { + dx += xlu; + } + if ( ybu > hv ) { + dy = ybu - hv; + ytu -= dy; + } + if ( ytu < 0 ) { + dy += ytu; + } + + if ( dx !== 0 || dy !== 0 ) { + window.scrollBy(dx, dy); + } +}; + +/******************************************************************************/ + +var elementsFromSelector = function(filter) { + var out = []; + try { + out = document.querySelectorAll(filter); + } catch (ex) { + } + return out; +}; + +/******************************************************************************/ + +var highlight = function(scrollTo) { + var elements = elementsFromSelector(currentSelector); + highlightElements(elements, scrollTo); +}; + +/******************************************************************************/ + +var onScrolled = function() { + highlight(); +}; + +/******************************************************************************/ + +// original, target = what to do +// any, any = restore saved display property +// any, hidden = set display to `none`, remember original state +// hidden, any = remove display property, don't remember original state +// hidden, hidden = set display to `none` + +var toggleNodes = function(selector, originalState, targetState) { + var nodes = document.querySelectorAll(selector); + var i = nodes.length; + if ( i === 0 ) { + return; + } + var node, value; + while ( i-- ) { + node = nodes[i]; + if ( originalState ) { // any, ? + if ( targetState ) { // any, any + value = toggledNodes.get(node); + if ( value === undefined ) { + continue; + } + if ( value !== null ) { + node.style.removeProperty('display'); + } else { + node.style.setProperty('display', value); + } + toggledNodes.delete(node); + } else { // any, hidden + toggledNodes.set(node, node.style.getPropertyValue('display') || null); + node.style.setProperty('display', 'none'); + } + } else { // hidden, ? + if ( targetState ) { // hidden, any + node.style.setProperty('display', 'initial', 'important'); + } else { // hidden, hidden + node.style.setProperty('display', 'none', 'important'); + } + } + } +}; + +/******************************************************************************/ + +var resetToggledNodes = function() { + var value; + // Chromium does not support destructuring as of v43. + for ( var node of toggledNodes.keys() ) { + value = toggledNodes.get(node); + if ( value !== null ) { + node.style.removeProperty('display'); + } else { + node.style.setProperty('display', value); + } + } + toggledNodes.clear(); +}; + +/******************************************************************************/ + +var shutdown = function() { + resetToggledNodes(); + localMessager.removeListener(onMessage); + localMessager.close(); + localMessager = null; + window.removeEventListener('scroll', onScrolled, true); + document.documentElement.removeChild(pickerRoot); + pickerRoot = svgRoot = svgOcean = svgIslands = null; + currentSelector = ''; +}; + +/******************************************************************************/ + +var onMessage = function(request) { + var msg = request.what === 'postMessageTo' ? request.msg : request; + var response; + + switch ( msg.what ) { + case 'domLayout': + var fingerprint = domFingerprint(); + response = { + what: 'domLayout', + layout: msg.fingerprint !== fingerprint ? domLayout() : 'NOCHANGE', + fingerprint: fingerprint + }; + break; + + case 'highlight': + currentSelector = msg.selector; + highlight(msg.scrollTo); + break; + + case 'toggleNodes': + toggleNodes(msg.selector, msg.original, msg.target); + currentSelector = msg.selector; + highlight(true); + break; + + case 'shutdown': + shutdown(); + break; + + default: + break; + } + + if ( response !== undefined && request.what === 'postMessageTo' ) { + localMessager.send({ + what: 'postMessageTo', + senderTabId: null, + senderChannel: 'dom-inspector.js', + receiverTabId: request.senderTabId, + receiverChannel: request.senderChannel, + msg: response + }); + } +}; + +/******************************************************************************/ + +// Install DOM inspector widget + +pickerRoot = document.createElement('iframe'); +pickerRoot.classList.add(vAPI.sessionId); +pickerRoot.classList.add('dom-inspector'); +pickerRoot.style.cssText = [ + 'background: transparent', + 'border: 0', + 'border-radius: 0', + 'box-shadow: none', + 'display: block', + 'height: 100%', + 'left: 0', + 'margin: 0', + 'opacity: 1', + 'position: fixed', + 'outline: 0', + 'padding: 0', + 'top: 0', + 'visibility: visible', + 'width: 100%', + 'z-index: 2147483647', + '' +].join(' !important;\n'); + +pickerRoot.onload = function() { + pickerRoot.onload = null; + var pickerDoc = this.contentDocument; + + var style = pickerDoc.createElement('style'); + style.textContent = [ + 'body {', + 'background-color: transparent;', + 'cursor: crosshair;', + '}', + 'svg {', + 'height: 100%;', + 'left: 0;', + 'position: fixed;', + 'top: 0;', + 'width: 100%;', + '}', + 'svg > path:first-child {', + 'fill: rgba(0,0,0,0.75);', + 'fill-rule: evenodd;', + '}', + 'svg > path + path {', + 'fill: rgba(0,0,255,0.1);', + 'stroke: #FFF;', + 'stroke-width: 0.5px;', + '}', + '' + ].join('\n'); + pickerDoc.body.appendChild(style); + + svgRoot = pickerDoc.createElementNS('http://www.w3.org/2000/svg', 'svg'); + svgOcean = pickerDoc.createElementNS('http://www.w3.org/2000/svg', 'path'); + svgRoot.appendChild(svgOcean); + svgIslands = pickerDoc.createElementNS('http://www.w3.org/2000/svg', 'path'); + svgRoot.appendChild(svgIslands); + pickerDoc.body.appendChild(svgRoot); + + window.addEventListener('scroll', onScrolled, true); + + highlight(); + + localMessager.addListener(onMessage); +}; + +document.documentElement.appendChild(pickerRoot); + +/******************************************************************************/ + +})(); + +/******************************************************************************/ diff --git a/src/js/scriptlets/dom-layout.js b/src/js/scriptlets/dom-layout.js deleted file mode 100644 index 1c22de8..0000000 --- a/src/js/scriptlets/dom-layout.js +++ /dev/null @@ -1,406 +0,0 @@ -/******************************************************************************* - - uBlock Origin - a browser extension to block requests. - Copyright (C) 2015 Raymond Hill - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see {http://www.gnu.org/licenses/}. - - Home: https://github.com/gorhill/uBlock -*/ - -/* global vAPI, HTMLDocument */ - -/******************************************************************************/ -/******************************************************************************/ - -/*! http://mths.be/cssescape v0.2.1 by @mathias | MIT license */ -;(function(root) { - - 'use strict'; - - if (!root.CSS) { - root.CSS = {}; - } - - var CSS = root.CSS; - - var InvalidCharacterError = function(message) { - this.message = message; - }; - InvalidCharacterError.prototype = new Error(); - InvalidCharacterError.prototype.name = 'InvalidCharacterError'; - - if (!CSS.escape) { - // http://dev.w3.org/csswg/cssom/#serialize-an-identifier - CSS.escape = function(value) { - var string = String(value); - var length = string.length; - var index = -1; - var codeUnit; - var result = ''; - var firstCodeUnit = string.charCodeAt(0); - while (++index < length) { - codeUnit = string.charCodeAt(index); - // Note: there’s no need to special-case astral symbols, surrogate - // pairs, or lone surrogates. - - // If the character is NULL (U+0000), then throw an - // `InvalidCharacterError` exception and terminate these steps. - if (codeUnit === 0x0000) { - throw new InvalidCharacterError( - 'Invalid character: the input contains U+0000.' - ); - } - - if ( - // If the character is in the range [\1-\1F] (U+0001 to U+001F) or is - // U+007F, […] - (codeUnit >= 0x0001 && codeUnit <= 0x001F) || codeUnit == 0x007F || - // If the character is the first character and is in the range [0-9] - // (U+0030 to U+0039), […] - (index === 0 && codeUnit >= 0x0030 && codeUnit <= 0x0039) || - // If the character is the second character and is in the range [0-9] - // (U+0030 to U+0039) and the first character is a `-` (U+002D), […] - ( - index == 1 && - codeUnit >= 0x0030 && codeUnit <= 0x0039 && - firstCodeUnit == 0x002D - ) - ) { - // http://dev.w3.org/csswg/cssom/#escape-a-character-as-code-point - result += '\\' + codeUnit.toString(16) + ' '; - continue; - } - - // If the character is not handled by one of the above rules and is - // greater than or equal to U+0080, is `-` (U+002D) or `_` (U+005F), or - // is in one of the ranges [0-9] (U+0030 to U+0039), [A-Z] (U+0041 to - // U+005A), or [a-z] (U+0061 to U+007A), […] - if ( - codeUnit >= 0x0080 || - codeUnit == 0x002D || - codeUnit == 0x005F || - codeUnit >= 0x0030 && codeUnit <= 0x0039 || - codeUnit >= 0x0041 && codeUnit <= 0x005A || - codeUnit >= 0x0061 && codeUnit <= 0x007A - ) { - // the character itself - result += string.charAt(index); - continue; - } - - // Otherwise, the escaped character. - // http://dev.w3.org/csswg/cssom/#escape-a-character - result += '\\' + string.charAt(index); - - } - return result; - }; - } - -}(self)); - -/******************************************************************************/ -/******************************************************************************/ - -(function() { - -'use strict'; - -/******************************************************************************/ - -// https://github.com/gorhill/uBlock/issues/464 -if ( document instanceof HTMLDocument === false ) { - return; -} - -// This can happen -if ( typeof vAPI !== 'object' ) { - return; -} - -/******************************************************************************/ - -var skipTagNames = { - 'br': true, - 'link': true, - 'meta': true, - 'script': true, - 'style': true -}; - -var resourceAttrNames = { - 'a': 'href', - 'iframe': 'src', - 'img': 'src', - 'object': 'data' -}; - -/******************************************************************************/ - -// Collect all nodes which are directly affected by cosmetic filters: these -// will be reported in the layout data. - -var nodeToCosmeticFilterMap = (function() { - var out = new WeakMap(); - var styleTags = vAPI.styles || []; - var i = styleTags.length; - var selectors, styleText, j, selector, nodes, k; - while ( i-- ) { - styleText = styleTags[i].textContent; - selectors = styleText.slice(0, styleText.lastIndexOf('\n')).split(/,\n/); - j = selectors.length; - while ( j-- ) { - selector = selectors[j]; - nodes = document.querySelectorAll(selector); - k = nodes.length; - while ( k-- ) { - out.set(nodes[k], selector); - } - } - } - return out; -})(); - -/******************************************************************************/ - -var DomRoot = function() { - this.lvl = 0; - this.sel = 'body'; - var url = window.location.href; - var pos = url.indexOf('#'); - if ( pos !== -1 ) { - url = url.slice(0, pos); - } - this.src = url; - this.top = window === window.top; - this.cnt = 0; - this.fp = fingerprint(); -}; - -var DomNode = function(level, selector, filter) { - this.lvl = level; - this.sel = selector; - this.cnt = 0; - this.filter = filter; -}; - -/******************************************************************************/ - -var hasManyMatches = function(node, selector) { - var fnName = matchesSelector; - if ( fnName === '' ) { - return true; - } - var child = node.firstElementChild; - var match = false; - while ( child !== null ) { - if ( child[fnName](selector) ) { - if ( match ) { - return true; - } - match = true; - } - child = child.nextElementSibling; - } - return false; -}; - -var matchesSelector = (function() { - if ( typeof Element.prototype.matches === 'function' ) { - return 'matches'; - } - if ( typeof Element.prototype.mozMatchesSelector === 'function' ) { - return 'mozMatchesSelector'; - } - if ( typeof Element.prototype.webkitMatchesSelector === 'function' ) { - return 'webkitMatchesSelector'; - } - return ''; -})(); - -/******************************************************************************/ - -var selectorFromNode = function(node) { - var str, attr, pos, sw, i; - var tag = node.localName; - var selector = CSS.escape(tag); - // Id - if ( typeof node.id === 'string' ) { - str = node.id.trim(); - if ( str !== '' ) { - selector += '#' + CSS.escape(str); - } - } - // Class - var cl = node.classList; - if ( cl ) { - for ( i = 0; i < cl.length; i++ ) { - selector += '.' + CSS.escape(cl[i]); - } - } - // Tag-specific attributes - if ( resourceAttrNames.hasOwnProperty(tag) ) { - attr = resourceAttrNames[tag]; - str = node.getAttribute(attr) || ''; - str = str.trim(); - pos = str.indexOf('#'); - if ( pos !== -1 ) { - str = str.slice(0, pos); - sw = '^'; - } else { - sw = ''; - } - if ( str !== '' ) { - selector += '[' + attr + sw + '="' + CSS.escape(str) + '"]'; - } - } - // The resulting selector must cause only one element to be selected. If - // it's not the case, further narrow using `nth-of-type` pseudo-class. - if ( hasManyMatches(node.parentElement, selector) ) { - i = 1; - while ( node.previousElementSibling ) { - node = node.previousElementSibling; - if ( node.localName === tag ) { - i += 1; - } - } - selector += ':nth-of-type(' + i + ')'; - } - return selector; -}; - -/******************************************************************************/ - -var domNodeFactory = function(level, node) { - var localName = node.localName; - if ( skipTagNames.hasOwnProperty(localName) ) { - return null; - } - // skip uBlock's own nodes - if ( node.classList.contains(vAPI.sessionId) ) { - return null; - } - if ( level === 0 && localName === 'body' ) { - return new DomRoot(); - } - var selector = selectorFromNode(node); - var filter = nodeToCosmeticFilterMap.get(node); - return new DomNode(level, selector, filter); -}; - -/******************************************************************************/ - -// Some kind of fingerprint for the DOM, without incurring too much -// overhead. - -var fingerprint = function() { - var url = window.location.href; - var pos = url.indexOf('#'); - if ( pos !== -1 ) { - url = url.slice(0, pos); - } - return url + '{' + document.getElementsByTagName('*').length.toString() + '}'; -}; - -/******************************************************************************/ - -// Collect layout data. - -var domLayout = []; - -(function() { - var dom = domLayout; - var stack = []; - var node = document.body; - var domNode; - var lvl = 0; - - for (;;) { - domNode = domNodeFactory(lvl, node); - if ( domNode !== null ) { - dom.push(domNode); - } - // children - if ( node.firstElementChild !== null ) { - stack.push(node); - lvl += 1; - node = node.firstElementChild; - continue; - } - // sibling - if ( node.nextElementSibling === null ) { - do { - node = stack.pop(); - if ( !node ) { break; } - lvl -= 1; - } while ( node.nextElementSibling === null ); - if ( !node ) { break; } - } - node = node.nextElementSibling; - } -})(); - -/******************************************************************************/ - -// Descendant count for each node. - -(function() { - var dom = domLayout; - var stack = [], ptr; - var lvl = 0; - var domNode, cnt; - var i = dom.length; - - while ( i-- ) { - domNode = dom[i]; - if ( domNode.lvl === lvl ) { - stack[ptr] += 1; - continue; - } - if ( domNode.lvl > lvl ) { - while ( lvl < domNode.lvl ) { - stack.push(0); - lvl += 1; - } - ptr = lvl - 1; - stack[ptr] += 1; - continue; - } - // domNode.lvl < lvl - cnt = stack.pop(); - domNode.cnt = cnt; - lvl -= 1; - ptr = lvl - 1; - stack[ptr] += cnt + 1; - } -})(); - -/******************************************************************************/ - -var localMessager = vAPI.messaging.channel('scriptlets'); -localMessager.send({ - what: 'scriptletResponse', - scriptlet: 'dom-layout', - response: domLayout -}, function() { - localMessager.close(); - localMessager = null; -}); - -/******************************************************************************/ - -})(); - -/******************************************************************************/ |