From 83c01fb3525bbede86c54fe06caa3eb8bc8eb0ef Mon Sep 17 00:00:00 2001 From: Raymond Hill Date: Wed, 8 Jul 2020 09:52:27 -0400 Subject: [PATCH] Add syntax highlighting/auto-completion for preparsing directives Related issue: - https://github.com/uBlockOrigin/uBlock-issues/issues/1134 Invalid values for `!#if ...` will be highlighted as errors. Auto completion is now supported for both the directives themselves and the valid values for `!#if ...`. For examples, when pressing ctrl-space: - `!#e` will auto-complete to `!#endif` - `!#i` will offer to choose between `!#if ` or `!#include ` - `!#if fir` will auto-complete to `!#if env_firefox` Additionally, support for some of AdGuard preparsing directives, i.e. `!#if adguard` is now a valid and will be honoured -- it always evaluate to `false` in uBO. --- src/1p-filters.html | 2 +- src/js/1p-filters.js | 10 ++ src/js/asset-viewer.js | 10 ++ src/js/assets.js | 2 +- src/js/codemirror/ubo-static-filtering.js | 115 ++++++++++++++++------ src/js/messaging.js | 7 +- src/js/storage.js | 11 ++- 7 files changed, 122 insertions(+), 35 deletions(-) diff --git a/src/1p-filters.html b/src/1p-filters.html index 02c7a338d..c27c21ff9 100644 --- a/src/1p-filters.html +++ b/src/1p-filters.html @@ -49,6 +49,7 @@ + @@ -59,7 +60,6 @@ - diff --git a/src/js/1p-filters.js b/src/js/1p-filters.js index fb6bee311..5874598d8 100644 --- a/src/js/1p-filters.js +++ b/src/js/1p-filters.js @@ -45,6 +45,16 @@ const cmEditor = new CodeMirror(document.getElementById('userFilters'), { uBlockDashboard.patchCodeMirrorEditor(cmEditor); +vAPI.messaging.send('dashboard', { + what: 'getAutoCompleteDetails' +}).then(response => { + if ( response instanceof Object === false ) { return; } + const mode = cmEditor.getMode(); + if ( mode.setHints instanceof Function ) { + mode.setHints(response); + } +}); + let cachedUserFilters = ''; /******************************************************************************/ diff --git a/src/js/asset-viewer.js b/src/js/asset-viewer.js index 30c64f301..b2ff29135 100644 --- a/src/js/asset-viewer.js +++ b/src/js/asset-viewer.js @@ -42,6 +42,16 @@ uBlockDashboard.patchCodeMirrorEditor(cmEditor); + const hints = await vAPI.messaging.send('dashboard', { + what: 'getAutoCompleteDetails' + }); + if ( hints instanceof Object ) { + const mode = cmEditor.getMode(); + if ( mode.setHints instanceof Function ) { + mode.setHints(hints); + } + } + const details = await vAPI.messaging.send('default', { what : 'getAssetContent', url: assetKey, diff --git a/src/js/assets.js b/src/js/assets.js index 175535e1a..be85a9e47 100644 --- a/src/js/assets.js +++ b/src/js/assets.js @@ -255,7 +255,7 @@ api.fetchFilterList = async function(mainlistURL) { } if ( result instanceof Object === false ) { continue; } const content = result.content; - const slices = µBlock.processDirectives.split(content); + const slices = µBlock.preparseDirectives.split(content); for ( let i = 0, n = slices.length - 1; i < n; i++ ) { const slice = content.slice(slices[i+0], slices[i+1]); if ( (i & 1) !== 0 ) { diff --git a/src/js/codemirror/ubo-static-filtering.js b/src/js/codemirror/ubo-static-filtering.js index 82a2cf5ca..2d21c0176 100644 --- a/src/js/codemirror/ubo-static-filtering.js +++ b/src/js/codemirror/ubo-static-filtering.js @@ -25,6 +25,17 @@ /******************************************************************************/ +{ +// >>>>> start of local scope + +/******************************************************************************/ + +const redirectNames = new Map(); +const scriptletNames = new Map(); +const preparseDirectiveNames = new Set(); + +/******************************************************************************/ + CodeMirror.defineMode('ubo-static-filtering', function() { const StaticFilteringParser = typeof vAPI === 'object' ? vAPI.StaticFilteringParser @@ -32,10 +43,35 @@ CodeMirror.defineMode('ubo-static-filtering', function() { if ( StaticFilteringParser instanceof Object === false ) { return; } const parser = new StaticFilteringParser({ interactive: true }); - const reDirective = /^!#(?:if|endif|include)\b/; + const rePreparseDirectives = /^!#(?:if|endif|include)\b/; + const rePreparseIfDirective = /^(!#if !?)(.+)$/; let parserSlot = 0; let netOptionValueMode = false; + const colorCommentSpan = function(stream) { + if ( rePreparseDirectives.test(stream.string) === false ) { + stream.skipToEnd(); + return 'comment'; + } + const match = rePreparseIfDirective.exec(stream.string); + if ( match === null ) { + stream.skipToEnd(); + return 'variable strong'; + } + if ( stream.pos < match[1].length ) { + stream.pos = match[1].length; + return 'variable strong'; + } + stream.skipToEnd(); + if ( + preparseDirectiveNames.size === 0 || + preparseDirectiveNames.has(match[2].trim()) + ) { + return 'variable strong'; + } + return 'error strong'; + }; + const colorExtHTMLPatternSpan = function(stream) { const { i } = parser.patternSpan; if ( stream.pos === parser.slices[i+1] ) { @@ -202,10 +238,7 @@ CodeMirror.defineMode('ubo-static-filtering', function() { return 'comment'; } if ( parser.category === parser.CATComment ) { - stream.skipToEnd(); - return reDirective.test(stream.string) - ? 'variable strong' - : 'comment'; + return colorCommentSpan(stream); } if ( (parser.slices[parserSlot] & parser.BITIgnore) !== 0 ) { stream.pos += parser.slices[parserSlot+2]; @@ -243,6 +276,23 @@ CodeMirror.defineMode('ubo-static-filtering', function() { style = style.trim(); return style !== '' ? style : null; }, + setHints: function(details) { + for ( const [ name, desc ] of details.redirectResources ) { + const displayText = desc.aliasOf !== '' + ? `${name} (${desc.aliasOf})` + : ''; + if ( desc.canRedirect ) { + redirectNames.set(name, displayText); + } + if ( desc.canInject && name.endsWith('.js') ) { + scriptletNames.set(name.slice(0, -3), displayText); + } + } + details.preparseDirectives.forEach(a => { + preparseDirectiveNames.add(a); + }); + initHints(); + }, }; }); @@ -251,17 +301,13 @@ CodeMirror.defineMode('ubo-static-filtering', function() { // Following code is for auto-completion. Reference: // https://codemirror.net/demo/complete.html -(( ) => { - if ( typeof vAPI !== 'object' ) { return; } - +const initHints = function() { const StaticFilteringParser = typeof vAPI === 'object' ? vAPI.StaticFilteringParser : self.StaticFilteringParser; if ( StaticFilteringParser instanceof Object === false ) { return; } const parser = new StaticFilteringParser(); - const redirectNames = new Map(); - const scriptletNames = new Map(); const proceduralOperatorNames = new Map( Array.from(parser.proceduralOperatorTokens).filter(item => { return (item[1] & 0b01) !== 0; @@ -380,7 +426,28 @@ CodeMirror.defineMode('ubo-static-filtering', function() { return pickBestHints(cursor, matchLeft[1], matchRight[1], hints); }; - const getHints = function(cm) { + const getCommentHints = function(cursor, line) { + const beg = cursor.ch; + if ( line.startsWith('!#if ') ) { + const matchLeft = /^!#if !?(\w*)$/.exec(line.slice(0, beg)); + const matchRight = /^\w*/.exec(line.slice(beg)); + if ( matchLeft === null || matchRight === null ) { return; } + const hints = []; + for ( const hint of preparseDirectiveNames ) { + hints.push(hint); + } + return pickBestHints(cursor, matchLeft[1], matchRight[0], hints); + } + if ( line.startsWith('!#') && line !== '!#endif' ) { + const matchLeft = /^!#(\w*)$/.exec(line.slice(0, beg)); + const matchRight = /^\w*/.exec(line.slice(beg)); + if ( matchLeft === null || matchRight === null ) { return; } + const hints = [ 'if ', 'endif\n', 'include ' ]; + return pickBestHints(cursor, matchLeft[1], matchRight[0], hints); + } + }; + + CodeMirror.registerHelper('hint', 'ubo-static-filtering', function(cm) { const cursor = cm.getCursor(); const line = cm.getLine(cursor.line); parser.analyze(line); @@ -393,25 +460,15 @@ CodeMirror.defineMode('ubo-static-filtering', function() { if ( parser.category === parser.CATStaticNetFilter ) { return getNetHints(cursor, line); } - }; - - vAPI.messaging.send('dashboard', { - what: 'getResourceDetails' - }).then(response => { - if ( Array.isArray(response) === false ) { return; } - for ( const [ name, details ] of response ) { - const displayText = details.aliasOf !== '' - ? `${name} (${details.aliasOf})` - : ''; - if ( details.canRedirect ) { - redirectNames.set(name, displayText); - } - if ( details.canInject && name.endsWith('.js') ) { - scriptletNames.set(name.slice(0, -3), displayText); - } + if ( parser.category === parser.CATComment ) { + return getCommentHints(cursor, line); } - CodeMirror.registerHelper('hint', 'ubo-static-filtering', getHints); }); -})(); +}; + +/******************************************************************************/ + +// <<<<< end of local scope +} /******************************************************************************/ diff --git a/src/js/messaging.js b/src/js/messaging.js index 53379fd50..a814a3727 100644 --- a/src/js/messaging.js +++ b/src/js/messaging.js @@ -1151,8 +1151,11 @@ const onMessage = function(request, sender, callback) { response = µb.canUpdateShortcuts; break; - case 'getResourceDetails': - response = µb.redirectEngine.getResourceDetails(); + case 'getAutoCompleteDetails': + response = { + redirectResources: µb.redirectEngine.getResourceDetails(), + preparseDirectives: Array.from(µb.preparseDirectives.tokens.keys()), + }; break; case 'getRules': diff --git a/src/js/storage.js b/src/js/storage.js index 2bafe60de..3ac401e3a 100644 --- a/src/js/storage.js +++ b/src/js/storage.js @@ -802,7 +802,7 @@ self.addEventListener('hiddenSettingsChanged', ( ) => { // https://adblockplus.org/en/filters const staticNetFilteringEngine = this.staticNetFilteringEngine; const staticExtFilteringEngine = this.staticExtFilteringEngine; - const lineIter = new this.LineIterator(this.processDirectives.prune(rawText)); + const lineIter = new this.LineIterator(this.preparseDirectives.prune(rawText)); const parser = new vAPI.StaticFilteringParser(); parser.setMaxTokenLength(this.urlTokenizer.MAX_TOKEN_LENGTH); @@ -857,7 +857,7 @@ self.addEventListener('hiddenSettingsChanged', ( ) => { // https://github.com/AdguardTeam/AdguardBrowserExtension/issues/917 -µBlock.processDirectives = { +µBlock.preparseDirectives = { // This method returns an array of indices, corresponding to position in // the content string which should alternatively be parsed and discarded. split: function(content) { @@ -929,6 +929,13 @@ self.addEventListener('hiddenSettingsChanged', ( ) => { [ 'cap_html_filtering', 'html_filtering' ], [ 'cap_user_stylesheet', 'user_stylesheet' ], [ 'false', 'false' ], + // Compatibility with other blockers + // https://kb.adguard.com/en/general/how-to-create-your-own-ad-filters#adguard-specific + [ 'adguard', 'adguard' ], + [ 'adguard_ext_chromium', 'chromium' ], + [ 'adguard_ext_edge', 'edge' ], + [ 'adguard_ext_firefox', 'firefox' ], + [ 'adguard_ext_opera', 'chromium' ], ]), };