diff --git a/src/_locales/en/messages.json b/src/_locales/en/messages.json
index ec47ffeb7..aaf0ad1b5 100644
--- a/src/_locales/en/messages.json
+++ b/src/_locales/en/messages.json
@@ -1217,6 +1217,10 @@
"message": "Temporarily allow large media elements",
"description": "A context menu entry, present when large media elements have been blocked on the current site"
},
+ "contextMenuViewSource": {
+ "message": "View source…",
+ "description": "A context menu entry, to view the source code of the target resource"
+ },
"shortcutCapturePlaceholder": {
"message": "Type a shortcut",
"description": "Placeholder string for input field used to capture a keyboard shortcut"
diff --git a/src/code-viewer.html b/src/code-viewer.html
index f721be412..d2e0ce339 100644
--- a/src/code-viewer.html
+++ b/src/code-viewer.html
@@ -15,6 +15,10 @@
+
diff --git a/src/css/code-viewer.css b/src/css/code-viewer.css
index a3c4d0f70..0d296c886 100644
--- a/src/css/code-viewer.css
+++ b/src/css/code-viewer.css
@@ -8,6 +8,43 @@ body {
padding: 0;
width: 100vw;
}
+#header {
+ background-color: var(--cm-gutter-surface);
+ border-bottom: 1px solid var(--surface-1);
+ padding: var(--default-gap-xsmall);
+ position: relative;
+ z-index: 1000000;
+ }
+#header input[type="url"] {
+ box-sizing: border-box;
+ font-size: var(--font-size-smaller);
+ width: 100%;
+ }
+#header:focus-within #pastURLs {
+ display: flex;
+ }
+#pastURLs {
+ background-color: var(--surface-0);
+ border: 1px solid var(--border-1);
+ display: none;
+ flex-direction: column;
+ font-size: var(--font-size-smaller);
+ position: absolute;
+ }
+#pastURLs > span {
+ cursor: pointer;
+ overflow: hidden;
+ padding: 2px 4px;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ width: 75vw;
+ }
+#pastURLs > span.selected {
+ font-weight: bold;
+ }
+#pastURLs > span:hover {
+ background-color: var(--surface-1);
+ }
#content {
flex-grow: 1;
}
diff --git a/src/js/code-viewer.js b/src/js/code-viewer.js
index 15a9698c6..6479672c9 100644
--- a/src/js/code-viewer.js
+++ b/src/js/code-viewer.js
@@ -29,107 +29,161 @@ import { dom, qs$ } from './dom.js';
/******************************************************************************/
-(async ( ) => {
- const params = new URLSearchParams(document.location.search);
- const url = params.get('url');
+const urlToTextMap = new Map();
+const params = new URLSearchParams(document.location.search);
+let fromURL = '';
- const a = qs$('.cm-search-widget .sourceURL');
- dom.attr(a, 'href', url);
- dom.attr(a, 'title', url);
+const cmEditor = new CodeMirror(qs$('#content'), {
+ autofocus: true,
+ gutters: [ 'CodeMirror-linenumbers' ],
+ lineNumbers: true,
+ lineWrapping: true,
+ matchBrackets: true,
+ styleActiveLine: {
+ nonEmpty: true,
+ },
+});
- const response = await fetch(url);
- const text = await response.text();
+uBlockDashboard.patchCodeMirrorEditor(cmEditor);
+if ( dom.cl.has(dom.html, 'dark') ) {
+ dom.cl.add('#content .cm-s-default', 'cm-s-night');
+ dom.cl.remove('#content .cm-s-default', 'cm-s-default');
+}
+// Convert resource URLs into clickable links to code viewer
+cmEditor.addOverlay({
+ re: /\b(?:href|src)=["']([^"']+)["']/g,
+ match: null,
+ token: function(stream) {
+ if ( stream.sol() ) {
+ this.re.lastIndex = 0;
+ this.match = this.re.exec(stream.string);
+ }
+ if ( this.match === null ) {
+ stream.skipToEnd();
+ return null;
+ }
+ const end = this.re.lastIndex - 1;
+ const beg = end - this.match[1].length;
+ if ( stream.pos < beg ) {
+ stream.pos = beg;
+ return null;
+ }
+ if ( stream.pos < end ) {
+ stream.pos = end;
+ return 'href';
+ }
+ if ( stream.pos < this.re.lastIndex ) {
+ stream.pos = this.re.lastIndex;
+ this.match = this.re.exec(stream.string);
+ return null;
+ }
+ stream.skipToEnd();
+ return null;
+ },
+});
+
+/******************************************************************************/
+
+async function fetchResource(url) {
+ if ( urlToTextMap.has(url) ) {
+ return urlToTextMap.get(url);
+ }
+ let response, text;
+ try {
+ response = await fetch(url);
+ text = await response.text();
+ } catch(reason) {
+ return;
+ }
let mime = response.headers.get('Content-Type') || '';
mime = mime.replace(/\s*;.*$/, '').trim();
- let value = '';
switch ( mime ) {
case 'text/css':
- value = beautifier.css(text, { indent_size: 2 });
+ text = beautifier.css(text, { indent_size: 2 });
break;
case 'text/html':
case 'application/xhtml+xml':
case 'application/xml':
case 'image/svg+xml':
- value = beautifier.html(text, { indent_size: 2 });
+ text = beautifier.html(text, { indent_size: 2 });
break;
case 'text/javascript':
case 'application/javascript':
case 'application/x-javascript':
- value = beautifier.js(text, { indent_size: 4 });
+ text = beautifier.js(text, { indent_size: 4 });
break;
case 'application/json':
- value = beautifier.js(text, { indent_size: 2 });
+ text = beautifier.js(text, { indent_size: 2 });
break;
default:
- value = text;
break;
}
+ urlToTextMap.set(url, { mime, text });
+ return { mime, text };
+}
- const cmEditor = new CodeMirror(qs$('#content'), {
- autofocus: true,
- gutters: [ 'CodeMirror-linenumbers' ],
- lineNumbers: true,
- lineWrapping: true,
- matchBrackets: true,
- mode: mime,
- styleActiveLine: {
- nonEmpty: true,
- },
- value,
- });
+/******************************************************************************/
- uBlockDashboard.patchCodeMirrorEditor(cmEditor);
- if ( dom.cl.has(dom.html, 'dark') ) {
- dom.cl.add('#content .cm-s-default', 'cm-s-night');
- dom.cl.remove('#content .cm-s-default', 'cm-s-default');
+function updatePastURLs(url) {
+ const list = qs$('#pastURLs');
+ let current;
+ for ( let i = 0; i < list.children.length; i++ ) {
+ const span = list.children[i];
+ dom.cl.remove(span, 'selected');
+ if ( span.textContent !== url ) { continue; }
+ current = span;
}
+ if ( current === undefined ) {
+ current = document.createElement('span');
+ current.textContent = url;
+ list.prepend(current);
+ }
+ dom.cl.add(current, 'selected');
+}
- // Convert resource URLs into clickable links to code viewer
- cmEditor.addOverlay({
- re: /\b(?:href|src)=["']([^"']+)["']/g,
- match: null,
- token: function(stream) {
- if ( stream.sol() ) {
- this.re.lastIndex = 0;
- this.match = this.re.exec(stream.string);
- }
- if ( this.match === null ) {
- stream.skipToEnd();
- return null;
- }
- const end = this.re.lastIndex - 1;
- const beg = end - this.match[1].length;
- if ( stream.pos < beg ) {
- stream.pos = beg;
- return null;
- }
- if ( stream.pos < end ) {
- stream.pos = end;
- return 'href';
- }
- if ( stream.pos < this.re.lastIndex ) {
- stream.pos = this.re.lastIndex;
- this.match = this.re.exec(stream.string);
- return null;
- }
- stream.skipToEnd();
- return null;
- },
- });
+/******************************************************************************/
- dom.on('#content', 'click', '.cm-href', ev => {
- const href = ev.target.textContent;
- try {
- const toURL = new URL(href, url);
- vAPI.messaging.send('codeViewer', {
- what: 'gotoURL',
- details: {
- url: `code-viewer.html?url=${encodeURIComponent(toURL.href)}`,
- select: true,
- },
- });
- } catch(ex) {
- }
- });
-})();
+async function setURL(resourceURL) {
+ const input = qs$('#header input[type="url"]');
+ let to;
+ try {
+ to = new URL(resourceURL, fromURL || undefined);
+ } catch(ex) {
+ }
+ if ( to === undefined ) { return; }
+ if ( /^https?:\/\/./.test(to.href) === false ) { return; }
+ if ( to.href === fromURL ) { return; }
+ let r;
+ try {
+ r = await fetchResource(to.href);
+ } catch(reason) {
+ }
+ if ( r === undefined ) { return; }
+ fromURL = to.href;
+ dom.attr(input, 'value', to.href);
+ input.value = to;
+ const a = qs$('.cm-search-widget .sourceURL');
+ dom.attr(a, 'href', to);
+ dom.attr(a, 'title', to);
+ cmEditor.setOption('mode', r.mime || '');
+ cmEditor.setValue(r.text);
+ updatePastURLs(to.href);
+ cmEditor.focus();
+}
+
+/******************************************************************************/
+
+setURL(params.get('url'));
+
+dom.on('#header input[type="url"]', 'change', ev => {
+ setURL(ev.target.value);
+});
+
+dom.on('#pastURLs', 'mousedown', 'span', ev => {
+ setURL(ev.target.textContent);
+});
+
+dom.on('#content', 'click', '.cm-href', ev => {
+ setURL(ev.target.textContent);
+});
diff --git a/src/js/contextmenu.js b/src/js/contextmenu.js
index c62f3053d..486e721df 100644
--- a/src/js/contextmenu.js
+++ b/src/js/contextmenu.js
@@ -40,6 +40,14 @@ if ( vAPI.contextMenu === undefined ) {
/******************************************************************************/
+const BLOCK_ELEMENT_BIT = 0b00001;
+const BLOCK_RESOURCE_BIT = 0b00010;
+const TEMP_ALLOW_LARGE_MEDIA_BIT = 0b00100;
+const SUBSCRIBE_TO_LIST_BIT = 0b01000;
+const VIEW_SOURCE_BIT = 0b10000;
+
+/******************************************************************************/
+
const onBlockElement = function(details, tab) {
if ( tab === undefined ) { return; }
if ( /^https?:\/\//.test(tab.url) === false ) { return; }
@@ -112,6 +120,18 @@ const onTemporarilyAllowLargeMediaElements = function(details, tab) {
/******************************************************************************/
+const onViewSource = function(details, tab) {
+ if ( tab === undefined ) { return; }
+ const url = details.linkUrl || details.frameUrl || details.pageUrl || '';
+ if ( /^https?:\/\//.test(url) === false ) { return; }
+ µb.openNewTab({
+ url: `code-viewer.html?url=${self.encodeURIComponent(url)}`,
+ select: true,
+ });
+};
+
+/******************************************************************************/
+
const onEntryClicked = function(details, tab) {
if ( details.menuItemId === 'uBlock0-blockElement' ) {
return onBlockElement(details, tab);
@@ -128,6 +148,9 @@ const onEntryClicked = function(details, tab) {
if ( details.menuItemId === 'uBlock0-temporarilyAllowLargeMediaElements' ) {
return onTemporarilyAllowLargeMediaElements(details, tab);
}
+ if ( details.menuItemId === 'uBlock0-viewSource' ) {
+ return onViewSource(details, tab);
+ }
};
/******************************************************************************/
@@ -162,7 +185,14 @@ const menuEntries = {
title: i18n$('contextMenuTemporarilyAllowLargeMediaElements'),
contexts: [ 'all' ],
documentUrlPatterns: [ 'http://*/*', 'https://*/*' ],
- }
+ },
+ viewSource: {
+ id: 'uBlock0-viewSource',
+ title: i18n$('contextMenuViewSource'),
+ contexts: [ 'page', 'frame', 'link' ],
+ documentUrlPatterns: [ 'http://*/*', 'https://*/*' ],
+ targetUrlPatterns: [ 'http://*/*', 'https://*/*' ],
+ },
};
/******************************************************************************/
@@ -175,32 +205,38 @@ const update = function(tabId = undefined) {
const pageStore = µb.pageStoreFromTabId(tabId);
if ( pageStore && pageStore.getNetFilteringSwitch() ) {
if ( pageStore.shouldApplySpecificCosmeticFilters(0) ) {
- newBits |= 0b0001;
+ newBits |= BLOCK_ELEMENT_BIT;
} else {
- newBits |= 0b0010;
+ newBits |= BLOCK_RESOURCE_BIT;
}
if ( pageStore.largeMediaCount !== 0 ) {
- newBits |= 0b0100;
+ newBits |= TEMP_ALLOW_LARGE_MEDIA_BIT;
}
}
- newBits |= 0b1000;
+ newBits |= SUBSCRIBE_TO_LIST_BIT;
+ }
+ if ( µb.hiddenSettings.filterAuthorMode ) {
+ newBits |= VIEW_SOURCE_BIT;
}
if ( newBits === currentBits ) { return; }
currentBits = newBits;
const usedEntries = [];
- if ( newBits & 0b0001 ) {
+ if ( (newBits & BLOCK_ELEMENT_BIT) !== 0 ) {
usedEntries.push(menuEntries.blockElement);
usedEntries.push(menuEntries.blockElementInFrame);
}
- if ( newBits & 0b0010 ) {
+ if ( (newBits & BLOCK_RESOURCE_BIT) !== 0 ) {
usedEntries.push(menuEntries.blockResource);
}
- if ( newBits & 0b0100 ) {
+ if ( (newBits & TEMP_ALLOW_LARGE_MEDIA_BIT) !== 0 ) {
usedEntries.push(menuEntries.temporarilyAllowLargeMediaElements);
}
- if ( newBits & 0b1000 ) {
+ if ( (newBits & SUBSCRIBE_TO_LIST_BIT) !== 0 ) {
usedEntries.push(menuEntries.subscribeToList);
}
+ if ( (newBits & VIEW_SOURCE_BIT) !== 0 ) {
+ usedEntries.push(menuEntries.viewSource);
+ }
vAPI.contextMenu.setEntries(usedEntries, onEntryClicked);
};