From 703c525b01aa3fb9dab94d6a9918a0a69c6d18da Mon Sep 17 00:00:00 2001 From: Raymond Hill Date: Sat, 14 Mar 2020 13:19:41 -0400 Subject: [PATCH] Support line continuation in filter lists If a line in a filter list ends with a space (ASCII code 32) followed by a backslash (ASCII code 92), those two characters will be removed, the line will be trimmed and the next line will be trimmed and concatenated to form a new, longer line. The purpose is to give filter list authors a way to visually break apart unduly long filters and thus make maintenance easier. When line continuation is used, it is suggested that the extra lines are prepended with four space so as to make it more visually obvious that the extra line(s) are the continuation of a previous line. Related issue: - https://github.com/uBlockOrigin/uBlock-issues/issues/943 The filter referenced in the above issue was the motivation to implement this feature: - https://hg.adblockplus.org/ruadlist/rev/f362910bc9a0 I verified and could not find any instance in major filter lists of lines ending with ` \`, thus the change should be safe. --- src/js/codemirror/ubo-static-filtering.js | 145 ++++++++++++++-------- src/js/storage.js | 7 +- 2 files changed, 95 insertions(+), 57 deletions(-) diff --git a/src/js/codemirror/ubo-static-filtering.js b/src/js/codemirror/ubo-static-filtering.js index 5bfb54716..655caa462 100644 --- a/src/js/codemirror/ubo-static-filtering.js +++ b/src/js/codemirror/ubo-static-filtering.js @@ -24,76 +24,113 @@ 'use strict'; CodeMirror.defineMode("ubo-static-filtering", function() { - const reDirective = /^\s*!#(?:if|endif)\b/; + const reDirective = /^\s*!#(?:if|endif|include)\b/; const reComment1 = /^\s*!/; const reComment2 = /^\s*#/; - const reExt = /^(\s*[^#]*)(#@?(?:\$\??|\?)?#)(.+)$/; - const reNet = /^(.*?)(?:(\$)([^$]+)?)?$/; - const reNetAllow = /^\s*@@/; + const reExt = /(#@?(?:\$\??|\?)?#)(?!##)/; + const reNet = /^\s*(?:@@)?.*(?:(\$)(?:[^$]+)?)?$/; let lineStyle = null; - let lineMatches = null; + let anchorOptPos = null; + + const lines = []; + let iLine = 0; - const lineStyles = new Map([ - [ 'staticext', [ '', 'staticOpt', '' ] ], - [ 'staticnetAllow', [ '', 'staticOpt', '' ] ], - [ 'staticnetBlock', [ '', 'staticOpt', '' ] ], - ]); + const lineFromLineBuffer = function() { + return lines.length === 1 + ? lines[0] + : lines.filter(a => a.replace(/^\s*|\s+\\$/g, '')).join(''); + }; - const styleFromStream = function(stream) { - for ( let i = 1, l = 0; i < lineMatches.length; i++ ) { - if ( typeof lineMatches[i] !== 'string' ) { continue; } - l += lineMatches[i].length; - if ( stream.pos < l ) { - stream.pos = l; - let style = lineStyle; - const xstyle = lineStyles.get(style)[i-1]; - if ( xstyle !== '' ) { style += ' ' + xstyle; } - return style; + const parseExtFilter = function() { + lineStyle = 'staticext'; + for ( let i = 0; i < lines.length; i++ ) { + const match = reExt.exec(lines[i]); + if ( match === null ) { continue; } + anchorOptPos = { y: i, x: match.index, l: match[1].length }; + break; + } + }; + + const parseNetFilter = function() { + lineStyle = lineFromLineBuffer().startsWith('@@') + ? 'staticnetAllow' + : 'staticnetBlock'; + let i = lines.length; + while ( i-- ) { + const pos = lines[i].lastIndexOf('$'); + if ( pos === -1 ) { continue; } + anchorOptPos = { y: i, x: pos, l: 1 }; + break; + } + }; + + const highlight = function(stream) { + if ( anchorOptPos !== null && iLine === anchorOptPos.y ) { + if ( stream.pos === anchorOptPos.x ) { + stream.pos += anchorOptPos.l; + return `${lineStyle} staticOpt`; + } + if ( stream.pos < anchorOptPos.x ) { + stream.pos = anchorOptPos.x; + return lineStyle; } } stream.skipToEnd(); - return ''; + return lineStyle; + }; + + const parseMultiLine = function() { + anchorOptPos = null; + const line = lineFromLineBuffer(); + if ( reDirective.test(line) ) { + lineStyle = 'directive'; + return; + } + if ( reComment1.test(line) ) { + lineStyle = 'comment'; + return; + } + if ( line.indexOf('#') !== -1 ) { + if ( reExt.test(line) ) { + return parseExtFilter(); + } + if ( reComment2.test(line) ) { + lineStyle = 'comment'; + return; + } + } + if ( reNet.test(line) ) { + return parseNetFilter(); + } + lineStyle = null; }; return { + startState: function() { + }, token: function(stream) { - if ( stream.sol() ) { - lineStyle = null; - lineMatches = null; - } else if ( lineStyle !== null ) { - return styleFromStream(stream); + if ( iLine === lines.length || stream.string !== lines[iLine] ) { + iLine = 0; } - if ( reDirective.test(stream.string) ) { - stream.skipToEnd(); - return 'directive'; - } - if ( reComment1.test(stream.string) ) { - stream.skipToEnd(); - return 'comment'; - } - if ( stream.string.indexOf('#') !== -1 ) { - lineMatches = reExt.exec(stream.string); - if ( - lineMatches !== null && - lineMatches[3].startsWith('##') === false - ) { - lineStyle = 'staticext'; - return styleFromStream(stream); + if ( iLine === 0 ) { + if ( lines.length > 1 ) { + lines.length = 1; } - if ( reComment2.test(stream.string) ) { - stream.skipToEnd(); - return 'comment'; + let line = stream.string; + lines[0] = line; + if ( line.endsWith(' \\') ) { + do { + line = stream.lookAhead(lines.length); + lines.push(line); + } while ( line.endsWith(' \\') ); } + parseMultiLine(); } - lineMatches = reNet.exec(stream.string); - if ( lineMatches !== null ) { - lineStyle = reNetAllow.test(stream.string) ? - 'staticnetAllow' : - 'staticnetBlock'; - return styleFromStream(stream); + const style = highlight(stream); + if ( stream.eol() ) { + iLine += 1; } - stream.skipToEnd(); - return null; - } + return style; + }, }; }); diff --git a/src/js/storage.js b/src/js/storage.js index 3ebd3da2e..7130ee28a 100644 --- a/src/js/storage.js +++ b/src/js/storage.js @@ -804,12 +804,13 @@ self.addEventListener('hiddenSettingsChanged', ( ) => { const lineIter = new this.LineIterator(this.processDirectives(rawText)); while ( lineIter.eot() === false ) { - // rhill 2014-04-18: The trim is important here, as without it there - // could be a lingering `\r` which would cause problems in the - // following parsing code. let line = lineIter.next().trim(); if ( line.length === 0 ) { continue; } + while ( line.endsWith(' \\') ) { + line = line.slice(0, -2).trim() + lineIter.next().trim(); + } + // Strip comments const c = line.charAt(0); if ( c === '!' || c === '[' ) { continue; }