Skip to content

Commit

Permalink
Add 'View source...' entry in context menu
Browse files Browse the repository at this point in the history
This new context menu entry will be available only when the
advanced setting `filterAuthorMode` is set to `true`. See:

https://github.com/gorhill/uBlock/wiki/Advanced-settings#filterauthormode

The purpose is for filter list maintainers to easily access
the source code of web pages when investigating filter issues,
without having to necessarily go through the logger.

Additionally an input field to enter URL directly has been
added to the code viewer for convenience.
  • Loading branch information
gorhill committed Mar 11, 2023
1 parent bed362d commit e2dd008
Show file tree
Hide file tree
Showing 5 changed files with 224 additions and 89 deletions.
4 changes: 4 additions & 0 deletions src/_locales/en/messages.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
4 changes: 4 additions & 0 deletions src/code-viewer.html
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@
</head>
<body class="loading">

<div id="header">
<div><input type="url" value="" spellcheck="false" autofocus="false"></div>
<div id="pastURLs"></div>
</div>
<div id="content" class="codeMirrorContainer codeMirrorBreakAll"></div>

<script src="lib/codemirror/lib/codemirror.js"></script>
Expand Down
37 changes: 37 additions & 0 deletions src/css/code-viewer.css
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
214 changes: 134 additions & 80 deletions src/js/code-viewer.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,107 +29,161 @@ import { dom, qs$ } from './dom.js';

/******************************************************************************/

(async ( ) => {
const params = new URLSearchParams(document.location.search);
const url = params.get('url');

const a = qs$('.cm-search-widget .sourceURL');
dom.attr(a, 'href', url);
dom.attr(a, 'title', url);
const urlToTextMap = new Map();
const params = new URLSearchParams(document.location.search);
let fromURL = '';

const cmEditor = new CodeMirror(qs$('#content'), {
autofocus: true,
gutters: [ 'CodeMirror-linenumbers' ],
lineNumbers: true,
lineWrapping: true,
matchBrackets: true,
styleActiveLine: {
nonEmpty: true,
},
});

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;
},
});

const response = await fetch(url);
const text = await response.text();
/******************************************************************************/

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);
});
Loading

8 comments on commit e2dd008

@u-RraaLL
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new colors on Dark Mode now make code difficult to read.

@uBlock-user
Copy link
Contributor

@uBlock-user uBlock-user commented on e2dd008 Mar 12, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

'View source...' entry

No such entry appearing on my Nightly in the context menu with the latest dev build 😕

Edit: No Block Element... context menu entry on 3rd party iframes - https://fmovies.to/series/babylon-5-8251q/1-20

@gorhill
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new colors on Dark Mode now make code difficult to read.

You are talking about the code viewer only? I used an official CodeMirror theme as I do not want to go through all the coloring stuff again. You can suggest one from there: https://codemirror.net/5/demo/theme.html.

@gorhill
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No such entry appearing on my Nightly

Yeah I saw that yesterday. Firefox's behavior does not match the documentation, there definitely should be an entry when using page.

@uBlock-user
Copy link
Contributor

@uBlock-user uBlock-user commented on e2dd008 Mar 12, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No Block Element... context menu entry on 3rd party iframes

That's the second issue I found....

@gorhill
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is fixed in Firefox when I remove targetUrlPatterns.

@u-RraaLL
Copy link
Contributor

@u-RraaLL u-RraaLL commented on e2dd008 Mar 12, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You are talking about the code viewer only?

Yes.

I used an official CodeMirror theme as I do not want to go through all the coloring stuff again. You can suggest one from there: https://codemirror.net/5/demo/theme.html.

I went though the themes, they're all readable. They're not what I see in b7 (and now b8).

This is what I see in b7-8:

image

This is what I see in b6:

image

Though, since you're asking, my personal favorite would be seti.


uBlockOrigin/uBlock-issues#2542

@gorhill
Copy link
Owner Author

@gorhill gorhill commented on e2dd008 Mar 12, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm going to look into this. We should move to a discussion though, here is not ideal.


Caused by a race condition when trying to detect whether dark mode is active.

Please sign in to comment.