diff --git a/src/epicker.html b/src/epicker.html
index a1008e1a7..35c45982f 100644
--- a/src/epicker.html
+++ b/src/epicker.html
@@ -83,6 +83,7 @@ html#ublock0-epicker,
padding: 2px !important;
resize: none !important;
width: 100% !important;
+ word-break: break-all !important;
}
#ublock0-epicker #resultsetCount {
background-color: #aaa !important;
diff --git a/src/js/scriptlets/element-picker.js b/src/js/scriptlets/element-picker.js
index 8e6593303..8b32e7f41 100644
--- a/src/js/scriptlets/element-picker.js
+++ b/src/js/scriptlets/element-picker.js
@@ -272,104 +272,118 @@ var highlightElements = function(elems, force) {
/******************************************************************************/
+// TODO: Investigate why diff'ing these two strings returns an incorrect result:
+// |
+// /articles/5c1a7aae1854f30006cb26f7/lede/1545239527833-shutterstock_726017572-copy.jpeg?crop=0.8889xw%3A0.9988xh%3B0.1089xw%2C0xh&resize=650%3A*&output-quality=55
+// /articles/5c1aaea91854f30006cb2f1e/lede/1545253629235-shutterstock_1063990172-copy.jpeg?crop=0.7749xw%3A1xh%3B0.0391xw%2C0xh&resize=650%3A*&output-quality=55
+// |
+// This appears to be an issue in the differ, need to investigate it.
+
+const mergeStrings = function(urls) {
+ if ( urls.length === 0 ) { return ''; }
+ if (
+ urls.length === 1 ||
+ self.diff_match_patch instanceof Function === false
+ ) {
+ return urls[0];
+ }
+ const differ = new self.diff_match_patch();
+ let merged = urls[0];
+ for ( let i = 1; i < urls.length; i++ ) {
+ // The differ works at line granularity: we insert a linefeed after
+ // each character to trick the differ to work at character granularity.
+ const diffs = differ.diff_main(
+ //urls[i].replace(/.(?=.)/g, '$&\n'),
+ //merged.replace(/.(?=.)/g, '$&\n')
+ urls[i].split('').join('\n'),
+ merged.split('').join('\n')
+ );
+ const result = [];
+ for ( const diff of diffs ) {
+ if ( diff[0] !== 0 ) {
+ result.push('*');
+ } else {
+ result.push(diff[1].charAt(0));
+ }
+ }
+ // Keep usage of wildcards to a sane level, too many of them can cause
+ // high overhead filters
+ merged =
+ result.join('')
+ .replace(/\*+$/, '')
+ .replace(/\*{2,}/g, '*')
+ .replace(/([^*]{1,2}\*)(?:[^*]{1,2}\*)+/g, '$1');
+ }
+ return merged;
+};
+
+/******************************************************************************/
+
// https://github.com/gorhill/uBlock/issues/1897
// Ignore `data:` URI, they can't be handled by an HTTP observer.
-var backgroundImageURLFromElement = function(elem) {
- var style = window.getComputedStyle(elem),
- bgImg = style.backgroundImage || '',
- matches = /^url\((["']?)([^"']+)\1\)$/.exec(bgImg),
- url = matches !== null && matches.length === 3 ? matches[2] : '';
+const backgroundImageURLFromElement = function(elem) {
+ const style = window.getComputedStyle(elem);
+ const bgImg = style.backgroundImage || '';
+ const matches = /^url\((["']?)([^"']+)\1\)$/.exec(bgImg);
+ const url = matches !== null && matches.length === 3 ? matches[2] : '';
return url.lastIndexOf('data:', 0) === -1 ? url.slice(0, 1024) : '';
};
/******************************************************************************/
// https://github.com/gorhill/uBlock/issues/1725#issuecomment-226479197
-// Limit returned string to 1024 characters.
-// Also, return only URLs which will be seen by an HTTP observer.
+// Limit returned string to 1024 characters.
+// Also, return only URLs which will be seen by an HTTP observer.
-var resourceURLFromElement = function(elem) {
- var tagName = elem.localName, s;
- if (
- (s = netFilter1stSources[tagName]) ||
- (s = netFilter2ndSources[tagName])
- ) {
- s = elem[s];
- if ( typeof s === 'string' && /^https?:\/\//.test(s) ) {
- return s.slice(0, 1024);
+const resourceURLFromElement = function(elem) {
+ const tagName = elem.localName;
+ const prop = netFilter1stSources[tagName];
+ if ( prop ) {
+ let src = '';
+ {
+ let s = elem[prop];
+ if ( typeof s === 'string' && /^https?:\/\//.test(s) ) {
+ src = s.slice(0, 1024);
+ }
}
+ if ( typeof elem.srcset === 'string' && elem.srcset !== '' ) {
+ const ss = [];
+ for ( let s of elem.srcset.split(/\s*,\s+/) ) {
+ const pos = s.indexOf(' ');
+ if ( pos !== -1 ) { s = s.slice(0, pos); }
+ const parsedURL = new URL(s, document.baseURI);
+ if ( parsedURL.pathname.length > 1 ) {
+ ss.push(parsedURL.href);
+ }
+ }
+ if ( ss.length !== 0 ) {
+ if ( src !== '' ) {
+ ss.push(src);
+ }
+ src = mergeStrings(ss);
+ }
+ }
+ return src;
}
return backgroundImageURLFromElement(elem);
};
/******************************************************************************/
-var netFilterFromUnion = (function() {
- var reTokenizer = /[^0-9a-z%*]+|[0-9a-z%]+|\*/gi;
- var a = document.createElement('a');
+const netFilterFromUnion = function(toMergeURL, out) {
+ const parsedURL = new URL(toMergeURL, document.baseURI);
- return function(to, out) {
- a.href= to;
- to = a.pathname + a.search;
- var from = lastNetFilterUnion;
+ toMergeURL = parsedURL.pathname + parsedURL.search;
- // Reset reference filter when dealing with unrelated URLs
- if ( from === '' || a.host === '' || a.host !== lastNetFilterHostname ) {
- lastNetFilterHostname = a.host;
- lastNetFilterUnion = to;
- vAPI.messaging.send(
- 'elementPicker',
- {
- what: 'elementPickerEprom',
- lastNetFilterSession: lastNetFilterSession,
- lastNetFilterHostname: lastNetFilterHostname,
- lastNetFilterUnion: lastNetFilterUnion
- }
- );
- return;
- }
-
- // Related URLs
- lastNetFilterHostname = a.host;
-
- var fromTokens = from.match(reTokenizer);
- var toTokens = to.match(reTokenizer);
- var toCount = toTokens.length, toIndex = 0;
- var fromToken, pos;
-
- for ( var fromIndex = 0; fromIndex < fromTokens.length; fromIndex += 1 ) {
- fromToken = fromTokens[fromIndex];
- if ( fromToken === '*' ) {
- continue;
- }
- pos = toTokens.indexOf(fromToken, toIndex);
- if ( pos === -1 ) {
- fromTokens[fromIndex] = '*';
- continue;
- }
- if ( pos !== toIndex ) {
- fromTokens.splice(fromIndex, 0, '*');
- fromIndex += 1;
- }
- toIndex = pos + 1;
- if ( toIndex === toCount ) {
- fromTokens = fromTokens.slice(0, fromIndex + 1);
- break;
- }
- }
- from = fromTokens.join('').replace(/\*\*+/g, '*');
- if ( from !== '/*' && from !== to ) {
- var filter = '||' + lastNetFilterHostname + from;
- if ( out.indexOf(filter) === -1 ) {
- out.push(filter);
- }
- } else {
- from = to;
- }
- lastNetFilterUnion = from;
-
- // Remember across element picker sessions
+ // Reset reference filter when dealing with unrelated URLs
+ if (
+ lastNetFilterUnion === '' ||
+ parsedURL.host === '' ||
+ parsedURL.host !== lastNetFilterHostname
+ ) {
+ lastNetFilterHostname = parsedURL.host;
+ lastNetFilterUnion = toMergeURL;
vAPI.messaging.send(
'elementPicker',
{
@@ -379,14 +393,40 @@ var netFilterFromUnion = (function() {
lastNetFilterUnion: lastNetFilterUnion
}
);
- };
-})();
+ return;
+ }
+
+ // Related URLs
+ lastNetFilterHostname = parsedURL.host;
+
+ let mergedURL = mergeStrings([ toMergeURL, lastNetFilterUnion ]);
+ if ( mergedURL !== '/*' && mergedURL !== toMergeURL ) {
+ const filter = '||' + lastNetFilterHostname + mergedURL;
+ if ( out.indexOf(filter) === -1 ) {
+ out.push(filter);
+ }
+ } else {
+ mergedURL = toMergeURL;
+ }
+ lastNetFilterUnion = mergedURL;
+
+ // Remember across element picker sessions
+ vAPI.messaging.send(
+ 'elementPicker',
+ {
+ what: 'elementPickerEprom',
+ lastNetFilterSession: lastNetFilterSession,
+ lastNetFilterHostname: lastNetFilterHostname,
+ lastNetFilterUnion: lastNetFilterUnion
+ }
+ );
+};
/******************************************************************************/
// Extract the best possible net filter, i.e. as specific as possible.
-var netFilterFromElement = function(elem) {
+const netFilterFromElement = function(elem) {
if ( elem === null ) { return 0; }
if ( elem.nodeType !== 1 ) { return 0; }
let src = resourceURLFromElement(elem);
@@ -429,7 +469,7 @@ var netFilterFromElement = function(elem) {
return candidates.length - len;
};
-var netFilter1stSources = {
+const netFilter1stSources = {
'audio': 'src',
'embed': 'src',
'iframe': 'src',
@@ -438,11 +478,11 @@ var netFilter1stSources = {
'video': 'src'
};
-var netFilter2ndSources = {
+const netFilter2ndSources = {
'img': 'srcset'
};
-var filterTypes = {
+const filterTypes = {
'audio': 'media',
'embed': 'object',
'iframe': 'subdocument',
@@ -1498,7 +1538,7 @@ var stopPicker = function() {
/******************************************************************************/
-var startPicker = function(details) {
+const startPicker = function(details) {
pickerRoot.addEventListener('load', stopPicker);
const frameDoc = pickerRoot.contentDocument;
@@ -1611,7 +1651,7 @@ var startPicker = function(details) {
/******************************************************************************/
-var bootstrapPicker = function() {
+const bootstrapPicker = function() {
pickerRoot.removeEventListener('load', bootstrapPicker);
vAPI.shutdown.add(stopPicker);
vAPI.messaging.send(
@@ -1626,7 +1666,7 @@ var bootstrapPicker = function() {
pickerRoot = document.createElement('iframe');
pickerRoot.id = vAPI.sessionId;
-var pickerCSSStyle = [
+const pickerCSSStyle = [
'background: transparent',
'border: 0',
'border-radius: 0',
@@ -1649,12 +1689,12 @@ var pickerCSSStyle = [
].join(' !important;');
pickerRoot.style.cssText = pickerCSSStyle;
-var pickerCSS1 = [
+const pickerCSS1 = [
'#' + pickerRoot.id + ' {',
pickerCSSStyle,
'}'
].join('\n');
-var pickerCSS2 = [
+const pickerCSS2 = [
'[' + pickerRoot.id + '-clickblind] {',
'pointer-events: none !important;',
'}'
diff --git a/src/js/ublock.js b/src/js/ublock.js
index d6ae70a0e..758a6c7c1 100644
--- a/src/js/ublock.js
+++ b/src/js/ublock.js
@@ -407,6 +407,16 @@ var matchBucket = function(url, hostname, bucket, start) {
this.epickerTarget = targetElement || '';
this.epickerZap = zap || false;
+ // https://github.com/uBlockOrigin/uBlock-issues/issues/40
+ // The element picker needs this library
+ vAPI.tabs.injectScript(
+ tabId,
+ {
+ file: '/lib/diff/swatinem_diff.js',
+ runAt: 'document_end'
+ }
+ );
+
// https://github.com/uBlockOrigin/uBlock-issues/issues/168
// Force activate the target tab once the element picker has been
// injected.