From 6d8b310d9422d869315717c5cf31b636484a1a6e Mon Sep 17 00:00:00 2001 From: Raymond Hill Date: Sun, 14 Jun 2020 12:05:42 -0400 Subject: [PATCH] Minor code review of static filtering parser Rename `l` property to `len`, to avoid ambiguity as `l` could mean _left_ or _length_. Typically `l` is to be used for _left_ (whereas `r` is to be used for _right_). Additionally, add CodeMirror's bracket-matching and bracket auto-closing to _My filters_ pane and and bracket-matching to asset viewer page. --- src/1p-filters.html | 2 + src/asset-viewer.html | 1 + src/css/codemirror.css | 8 + src/js/1p-filters.js | 18 +- src/js/asset-viewer.js | 19 +- src/js/codemirror/ubo-static-filtering.js | 89 +++++-- src/js/static-filtering-parser.js | 238 +++++++++--------- .../codemirror/addon/edit/closebrackets.js | 191 ++++++++++++++ .../codemirror/addon/edit/matchbrackets.js | 150 +++++++++++ 9 files changed, 555 insertions(+), 161 deletions(-) create mode 100644 src/lib/codemirror/addon/edit/closebrackets.js create mode 100644 src/lib/codemirror/addon/edit/matchbrackets.js diff --git a/src/1p-filters.html b/src/1p-filters.html index fbb016f57..13fc9792f 100644 --- a/src/1p-filters.html +++ b/src/1p-filters.html @@ -38,6 +38,8 @@ + + diff --git a/src/asset-viewer.html b/src/asset-viewer.html index b54ec865b..42874e0e2 100644 --- a/src/asset-viewer.html +++ b/src/asset-viewer.html @@ -30,6 +30,7 @@ body { + diff --git a/src/css/codemirror.css b/src/css/codemirror.css index d31e7d6d4..182551b4d 100644 --- a/src/css/codemirror.css +++ b/src/css/codemirror.css @@ -48,6 +48,14 @@ .cm-staticnetAllow { color: #004f00; } .cm-staticOpt { background-color: #ddd; font-weight: bold; } +div.CodeMirror span.CodeMirror-matchingbracket { + color: unset; + } +.CodeMirror-matchingbracket { + color: inherit !important; + font-weight: bold; + } + .cm-search-widget { align-items: center; background-color: var(--bg-code); diff --git a/src/js/1p-filters.js b/src/js/1p-filters.js index 28217dd5a..9a3894177 100644 --- a/src/js/1p-filters.js +++ b/src/js/1p-filters.js @@ -29,15 +29,15 @@ /******************************************************************************/ -const cmEditor = new CodeMirror( - document.getElementById('userFilters'), - { - autofocus: true, - lineNumbers: true, - lineWrapping: true, - styleActiveLine: true, - } -); +const cmEditor = new CodeMirror(document.getElementById('userFilters'), { + autoCloseBrackets: true, + autofocus: true, + lineNumbers: true, + lineWrapping: true, + matchBrackets: true, + maxScanLines: 1, + styleActiveLine: true, +}); uBlockDashboard.patchCodeMirrorEditor(cmEditor); diff --git a/src/js/asset-viewer.js b/src/js/asset-viewer.js index 8ea3eb5a5..30c64f301 100644 --- a/src/js/asset-viewer.js +++ b/src/js/asset-viewer.js @@ -30,16 +30,15 @@ const assetKey = params.get('url'); if ( assetKey === null ) { return; } - const cmEditor = new CodeMirror( - document.getElementById('content'), - { - autofocus: true, - lineNumbers: true, - lineWrapping: true, - readOnly: true, - styleActiveLine: true, - } - ); + const cmEditor = new CodeMirror(document.getElementById('content'), { + autofocus: true, + lineNumbers: true, + lineWrapping: true, + matchBrackets: true, + maxScanLines: 1, + readOnly: true, + styleActiveLine: true, + }); uBlockDashboard.patchCodeMirrorEditor(cmEditor); diff --git a/src/js/codemirror/ubo-static-filtering.js b/src/js/codemirror/ubo-static-filtering.js index a762536e6..ae5fe9492 100644 --- a/src/js/codemirror/ubo-static-filtering.js +++ b/src/js/codemirror/ubo-static-filtering.js @@ -29,6 +29,48 @@ CodeMirror.defineMode("ubo-static-filtering", function() { let parserSlot = 0; let netOptionValueMode = false; + const colorExtHTMLPatternSpan = function(stream) { + const { i } = parser.patternSpan; + if ( stream.pos === parser.slices[i+1] ) { + stream.pos += 1; + return 'def'; + } + stream.skipToEnd(); + return 'variable'; + }; + + const colorExtScriptletPatternSpan = function(stream) { + const { i, len } = parser.patternSpan; + if ( stream.pos === parser.slices[i+1] ) { + stream.pos += 4; + return 'def'; + } + if ( len > 3 ) { + const r = parser.slices[i+len+1] - 1; + if ( stream.pos < r ) { + stream.pos = r; + return 'variable'; + } + if ( stream.pos === r ) { + stream.pos += 1; + return 'def'; + } + } + stream.skipToEnd(); + return 'variable'; + }; + + const colorExtPatternSpan = function(stream) { + if ( (parser.flavorBits & parser.BITFlavorExtScriptlet) !== 0 ) { + return colorExtScriptletPatternSpan(stream); + } + if ( (parser.flavorBits & parser.BITFlavorExtHTML) !== 0 ) { + return colorExtHTMLPatternSpan(stream); + } + stream.skipToEnd(); + return 'variable'; + }; + const colorExtSpan = function(stream) { if ( parserSlot < parser.optionsAnchorSpan.i ) { const style = (parser.slices[parserSlot] & parser.BITComma) === 0 @@ -49,23 +91,25 @@ CodeMirror.defineMode("ubo-static-filtering", function() { parserSlot += 3; return `${style} strong`; } - if ( parserSlot >= parser.patternSpan.i ) { - stream.skipToEnd(); - return 'variable'; + if ( + parserSlot >= parser.patternSpan.i && + parserSlot < parser.rightSpaceSpan.i + ) { + return colorExtPatternSpan(stream); } stream.skipToEnd(); - return ''; + return null; }; const colorNetSpan = function(stream) { if ( parserSlot < parser.exceptionSpan.i ) { stream.pos += parser.slices[parserSlot+2]; parserSlot += 3; - return ''; + return null; } if ( parserSlot === parser.exceptionSpan.i && - parser.exceptionSpan.l !== 0 + parser.exceptionSpan.len !== 0 ) { stream.pos += parser.slices[parserSlot+2]; parserSlot += 3; @@ -73,9 +117,9 @@ CodeMirror.defineMode("ubo-static-filtering", function() { } if ( parserSlot === parser.patternLeftAnchorSpan.i && - parser.patternLeftAnchorSpan.l !== 0 || + parser.patternLeftAnchorSpan.len !== 0 || parserSlot === parser.patternRightAnchorSpan.i && - parser.patternRightAnchorSpan.l !== 0 + parser.patternRightAnchorSpan.len !== 0 ) { stream.pos += parser.slices[parserSlot+2]; parserSlot += 3; @@ -83,31 +127,30 @@ CodeMirror.defineMode("ubo-static-filtering", function() { } if ( parserSlot >= parser.patternSpan.i && - parserSlot < parser.patternRightAnchorSpan.i + parserSlot < parser.optionsAnchorSpan.i ) { - const isRegex = parser.patternIsRegex(); - if ( - (isRegex === false) && - (parser.slices[parserSlot] & (parser.BITAsterisk | parser.BITCaret)) !== 0 - ) { + if ( parser.patternIsRegex() ) { + stream.pos = parser.slices[parser.optionsAnchorSpan.i+1]; + parserSlot += parser.optionsAnchorSpan.i; + return 'variable regex'; + } + if ( (parser.slices[parserSlot] & (parser.BITAsterisk | parser.BITCaret)) !== 0 ) { stream.pos += parser.slices[parserSlot+2]; parserSlot += 3; return 'keyword strong'; } - let style = 'variable'; - if ( isRegex ) { style += ' regex'; } const nextSlot = parser.skipUntil( - parserSlot, + parserSlot + 3, parser.patternRightAnchorSpan.i, parser.BITAsterisk | parser.BITCaret ); stream.pos = parser.slices[nextSlot+1]; parserSlot = nextSlot; - return style; + return 'variable'; } if ( parserSlot === parser.optionsAnchorSpan.i && - parser.optionsAnchorSpan.l !== 0 + parserSlot < parser.optionsSpan.i !== 0 ) { stream.pos += parser.slices[parserSlot+2]; parserSlot += 3; @@ -115,7 +158,7 @@ CodeMirror.defineMode("ubo-static-filtering", function() { } if ( parserSlot >= parser.optionsSpan.i && - parser.optionsSpan.l !== 0 + parserSlot < parser.rightSpaceSpan.i ) { const bits = parser.slices[parserSlot]; let style; @@ -137,13 +180,13 @@ CodeMirror.defineMode("ubo-static-filtering", function() { } if ( parserSlot >= parser.commentSpan.i && - parser.commentSpan.l !== 0 + parser.commentSpan.len !== 0 ) { stream.skipToEnd(); return 'comment'; } stream.skipToEnd(); - return ''; + return null; }; const colorSpan = function(stream) { @@ -185,7 +228,7 @@ CodeMirror.defineMode("ubo-static-filtering", function() { parserSlot = 0; netOptionValueMode = false; } - let style = colorSpan(stream); + let style = colorSpan(stream) || ''; if ( (parser.flavorBits & parser.BITFlavorError) !== 0 ) { style += ' line-background-error'; } diff --git a/src/js/static-filtering-parser.js b/src/js/static-filtering-parser.js index 47b78ae0b..0b3da5679 100644 --- a/src/js/static-filtering-parser.js +++ b/src/js/static-filtering-parser.js @@ -129,7 +129,7 @@ const Parser = class { analyze(raw) { this.slice(raw); - let slot = this.leftSpaceSpan.l; + let slot = this.leftSpaceSpan.len; if ( slot === this.rightSpaceSpan.i ) { return; } // test for `!`, `#`, or `[` @@ -158,7 +158,7 @@ const Parser = class { for (;;) { if ( hasBits(this.slices[hashSlot-3], BITSpace) ) { this.commentSpan.i = hashSlot-3; - this.commentSpan.l = this.rightSpaceSpan.i - hashSlot; + this.commentSpan.len = this.rightSpaceSpan.i - hashSlot; break; } hashSlot = this.findFirstMatch(hashSlot + 6, BITHash); @@ -194,25 +194,25 @@ const Parser = class { analyzeExt(from) { let end = this.rightSpaceSpan.i; // Number of consecutive #s. - const l = this.slices[from+2]; + const len = this.slices[from+2]; // More than 3 #s is likely to be a comment in a hosts file. - if ( l > 3 ) { return; } - if ( l !== 1 ) { + if ( len > 3 ) { return; } + if ( len !== 1 ) { // If a space immediately follows 2 #s, assume a comment. - if ( l === 2 ) { + if ( len === 2 ) { if ( from+3 === end || hasBits(this.slices[from+3], BITSpace) ) { return; } - } else /* l === 3 */ { + } else /* len === 3 */ { this.splitSlot(from, 2); end = this.rightSpaceSpan.i; } - this.optionsSpan.i = this.leftSpaceSpan.i + this.leftSpaceSpan.l; - this.optionsSpan.l = from - this.optionsSpan.i; + this.optionsSpan.i = this.leftSpaceSpan.i + this.leftSpaceSpan.len; + this.optionsSpan.len = from - this.optionsSpan.i; this.optionsAnchorSpan.i = from; - this.optionsAnchorSpan.l = 3; + this.optionsAnchorSpan.len = 3; this.patternSpan.i = from + 3; - this.patternSpan.l = this.rightSpaceSpan.i - this.patternSpan.i; + this.patternSpan.len = this.rightSpaceSpan.i - this.patternSpan.i; this.category = CATStaticExtFilter; this.analyzeExtPattern(); return; @@ -257,12 +257,12 @@ const Parser = class { this.splitSlot(to, 1); } to += 3; - this.optionsSpan.i = this.leftSpaceSpan.i + this.leftSpaceSpan.l; - this.optionsSpan.l = from - this.optionsSpan.i; + this.optionsSpan.i = this.leftSpaceSpan.i + this.leftSpaceSpan.len; + this.optionsSpan.len = from - this.optionsSpan.i; this.optionsAnchorSpan.i = from; - this.optionsAnchorSpan.l = to - this.optionsAnchorSpan.i; + this.optionsAnchorSpan.len = to - this.optionsAnchorSpan.i; this.patternSpan.i = to; - this.patternSpan.l = this.rightSpaceSpan.i - to; + this.patternSpan.len = this.rightSpaceSpan.i - to; this.flavorBits = flavorBits; this.category = CATStaticExtFilter; this.analyzeExtPattern(); @@ -305,11 +305,11 @@ const Parser = class { } } - // Use in syntax highlighting contexts + // Use in syntax highlighting contexts analyzeExtExtra() { if ( this.hasOptions() ) { - const { i, l } = this.optionsSpan; - this.analyzeDomainList(i, i + l, BITComma, 0b11); + const { i, len } = this.optionsSpan; + this.analyzeDomainList(i, i + len, BITComma, 0b11); } if ( hasBits(this.flavorBits, BITFlavorUnsupported) ) { this.markSpan(this.patternSpan, BITError); @@ -341,19 +341,19 @@ const Parser = class { let islice = this.leftSpaceSpan.i; // Assume no exception - this.exceptionSpan.i = this.leftSpaceSpan.l; + this.exceptionSpan.i = this.leftSpaceSpan.len; // Exception? if ( islice < this.commentSpan.i && hasBits(this.slices[islice], BITAt) ) { - const l = this.slices[islice+2]; + const len = this.slices[islice+2]; // @@@*, ... => @@, @*, ... - if ( l >= 2 ) { - if ( l > 2 ) { + if ( len >= 2 ) { + if ( len > 2 ) { this.splitSlot(islice, 2); } - this.exceptionSpan.l = 3; + this.exceptionSpan.len = 3; islice += 3; this.flavorBits |= BITFlavorException; } @@ -364,17 +364,17 @@ const Parser = class { // Assume all is part of pattern this.patternSpan.i = islice; - this.patternSpan.l = this.optionsAnchorSpan.i - islice; + this.patternSpan.len = this.optionsAnchorSpan.i - islice; let patternStartIsRegex = islice < this.optionsAnchorSpan.i && hasBits(this.slices[islice], BITSlash); let patternIsRegex = patternStartIsRegex; if ( patternStartIsRegex ) { - const { i, l } = this.patternSpan; + const { i, len } = this.patternSpan; patternIsRegex = ( - l === 3 && this.slices[i+2] > 2 || - l > 3 && hasBits(this.slices[i+l-3], BITSlash) + len === 3 && this.slices[i+2] > 2 || + len > 3 && hasBits(this.slices[i+len-3], BITSlash) ); } @@ -390,8 +390,8 @@ const Parser = class { optionsBits |= bits; } if ( i >= islice ) { - const l = this.slices[i+2]; - if ( l > 1 ) { + const len = this.slices[i+2]; + if ( len > 1 ) { // https://github.com/gorhill/uBlock/issues/952 // AdGuard-specific `$$` filters => unsupported. if ( this.findFirstOdd(0, BITHostname | BITComma | BITAsterisk) === i ) { @@ -400,22 +400,22 @@ const Parser = class { this.markSlices(i, i+3, BITError); } } else { - this.splitSlot(i, l - 1); + this.splitSlot(i, len - 1); i += 3; } } - this.patternSpan.l = i - this.patternSpan.i; + this.patternSpan.len = i - this.patternSpan.i; this.optionsAnchorSpan.i = i; - this.optionsAnchorSpan.l = 3; + this.optionsAnchorSpan.len = 3; i += 3; this.optionsSpan.i = i; - this.optionsSpan.l = this.commentSpan.i - i; + this.optionsSpan.len = this.commentSpan.i - i; this.optionsBits = optionsBits; if ( patternStartIsRegex ) { - const { i, l } = this.patternSpan; + const { i, len } = this.patternSpan; patternIsRegex = ( - l === 3 && this.slices[i+2] > 2 || - l > 3 && hasBits(this.slices[i+l-3], BITSlash) + len === 3 && this.slices[i+2] > 2 || + len > 3 && hasBits(this.slices[i+len-3], BITSlash) ); } } @@ -437,19 +437,19 @@ const Parser = class { // `|`: anchor to start of URL // `||`: anchor to left of a hostname label if ( - this.patternSpan.l !== 0 && + this.patternSpan.len !== 0 && hasBits(this.slices[this.patternSpan.i], BITPipe) ) { - this.patternLeftAnchorSpan.l = 3; - const l = this.slices[this.patternSpan.i+2]; + this.patternLeftAnchorSpan.len = 3; + const len = this.slices[this.patternSpan.i+2]; // |||*, ... => ||, |*, ... - if ( l > 2 ) { + if ( len > 2 ) { this.splitSlot(this.patternSpan.i, 2); } else { - this.patternSpan.l -= 3; + this.patternSpan.len -= 3; } this.patternSpan.i += 3; - this.flavorBits |= l === 1 + this.flavorBits |= len === 1 ? BITFlavorNetLeftURLAnchor : BITFlavorNetLeftHnAnchor; } @@ -459,21 +459,21 @@ const Parser = class { // fulfilled: // the pattern is hostname-anchored on the left // the pattern is made only of hostname characters - if ( this.patternSpan.l !== 0 ) { - const lastPatternSlice = this.patternSpan.l > 3 + if ( this.patternSpan.len !== 0 ) { + const lastPatternSlice = this.patternSpan.len > 3 ? this.patternRightAnchorSpan.i - 3 : this.patternSpan.i; const bits = this.slices[lastPatternSlice]; if ( (bits & BITPipe) !== 0 ) { this.patternRightAnchorSpan.i = lastPatternSlice; - this.patternRightAnchorSpan.l = 3; - const l = this.slices[this.patternRightAnchorSpan.i+2]; + this.patternRightAnchorSpan.len = 3; + const len = this.slices[this.patternRightAnchorSpan.i+2]; // ..., ||* => ..., |*, | - if ( l > 1 ) { - this.splitSlot(this.patternRightAnchorSpan.i, l - 1); + if ( len > 1 ) { + this.splitSlot(this.patternRightAnchorSpan.i, len - 1); this.patternRightAnchorSpan.i += 3; } else { - this.patternSpan.l -= 3; + this.patternSpan.len -= 3; } this.flavorBits |= BITFlavorNetRightURLAnchor; } else if ( @@ -487,8 +487,8 @@ const Parser = class { ) === lastPatternSlice ) { this.patternRightAnchorSpan.i = lastPatternSlice; - this.patternRightAnchorSpan.l = 3; - this.patternSpan.l -= 3; + this.patternRightAnchorSpan.len = 3; + this.patternSpan.len -= 3; this.flavorBits |= BITFlavorNetRightHnAnchor; } } @@ -502,8 +502,8 @@ const Parser = class { // if a pattern contains space characters, the pattern will be only // the part following the last space occurrence. { - const { i, l } = this.patternSpan; - let j = l; + const { i, len } = this.patternSpan; + let j = len; for (;;) { if ( j === 0 ) { break; } j -= 3; @@ -513,7 +513,7 @@ const Parser = class { } if ( j !== 0 ) { this.patternSpan.i += j + 3; - this.patternSpan.l -= j + 3; + this.patternSpan.len -= j + 3; if ( this.reIsLocalhostRedirect.test(this.getNetPattern()) ) { this.flavorBits |= BITFlavorIgnore; } @@ -539,41 +539,41 @@ const Parser = class { // https://github.com/gorhill/uBlock/issues/3034 // We can remove anchoring if we need to match all at the end. { - let { i, l } = this.patternSpan; + let { i, len } = this.patternSpan; // Pointless leading wildcard if ( - l > 3 && + len > 3 && hasBits(this.slices[i], BITAsterisk) && hasNoBits(this.slices[i+3], BITPatternToken) ) { this.slices[i] |= BITIgnore; - i += 3; l -= 3; + i += 3; len -= 3; this.patternSpan.i = i; - this.patternSpan.l = l; + this.patternSpan.len = len; // We can ignore left-hand pattern anchor - if ( this.patternLeftAnchorSpan.l !== 0 ) { + if ( this.patternLeftAnchorSpan.len !== 0 ) { this.slices[this.patternLeftAnchorSpan.i] |= BITIgnore; this.flavorBits &= ~BITFlavorNetLeftAnchor; } } // Pointless trailing wildcard if ( - l > 3 && - hasBits(this.slices[i+l-3], BITAsterisk) && - hasNoBits(this.slices[i+l-6], BITPatternToken) + len > 3 && + hasBits(this.slices[i+len-3], BITAsterisk) && + hasNoBits(this.slices[i+len-6], BITPatternToken) ) { // Ignore only if the pattern would not end up looking like // a regex. if ( hasNoBits(this.slices[i], BITSlash) || - hasNoBits(this.slices[i+l-6], BITSlash) + hasNoBits(this.slices[i+len-6], BITSlash) ) { - this.slices[i+l-3] |= BITIgnore; + this.slices[i+len-3] |= BITIgnore; } - l -= 3; - this.patternSpan.l = l; + len -= 3; + this.patternSpan.len = len; // We can ignore right-hand pattern anchor - if ( this.patternRightAnchorSpan.l !== 0 ) { + if ( this.patternRightAnchorSpan.len !== 0 ) { this.slices[this.patternRightAnchorSpan.i] |= BITIgnore; this.flavorBits &= ~BITFlavorNetRightAnchor; } @@ -581,8 +581,8 @@ const Parser = class { // Pointless left-hand pattern anchoring if ( ( - l === 0 || - l !== 0 && hasBits(this.slices[i], BITAsterisk) + len === 0 || + len !== 0 && hasBits(this.slices[i], BITAsterisk) ) && hasBits(this.flavorBits, BITFlavorNetLeftAnchor) ) { @@ -592,8 +592,8 @@ const Parser = class { // Pointless right-hand pattern anchoring if ( ( - l === 0 || - l !== 0 && hasBits(this.slices[i+l-3], BITAsterisk) + len === 0 || + len !== 0 && hasBits(this.slices[i+len-3], BITAsterisk) ) && hasBits(this.flavorBits, BITFlavorNetRightAnchor) ) { @@ -747,9 +747,9 @@ const Parser = class { ptr += 3; // Trim left if ( (slices[0] & BITSpace) !== 0 ) { - this.leftSpaceSpan.l = 3; + this.leftSpaceSpan.len = 3; } else { - this.leftSpaceSpan.l = 0; + this.leftSpaceSpan.len = 0; } // Trim right const lastSlice = this.eolSpan.i - 3; @@ -758,25 +758,25 @@ const Parser = class { (slices[lastSlice] & BITSpace) !== 0 ) { this.rightSpaceSpan.i = lastSlice; - this.rightSpaceSpan.l = 3; + this.rightSpaceSpan.len = 3; } else { this.rightSpaceSpan.i = this.eolSpan.i; - this.rightSpaceSpan.l = 0; + this.rightSpaceSpan.len = 0; } // Quit cleanly this.sliceWritePtr = ptr; this.allBits = allBits; } - splitSlot(slot, l) { + splitSlot(slot, len) { this.sliceWritePtr += 3; if ( this.sliceWritePtr > this.slices.length ) { this.slices.push(0, 0, 0); } this.slices.copyWithin(slot + 3, slot, this.sliceWritePtr - 3); - this.slices[slot+3+1] = this.slices[slot+1] + l; - this.slices[slot+3+2] = this.slices[slot+2] - l; - this.slices[slot+2] = l; + this.slices[slot+3+1] = this.slices[slot+1] + len; + this.slices[slot+3+2] = this.slices[slot+2] - len; + this.slices[slot+2] = len; for ( const span of this.spans ) { if ( span.i > slot ) { span.i += 3; @@ -792,8 +792,8 @@ const Parser = class { } markSpan(span, bits) { - const { i, l } = span; - this.markSlices(i, i + l, bits); + const { i, len } = span; + this.markSlices(i, i + len, bits); } unmarkSlices(beg, end, bits) { @@ -847,9 +847,9 @@ const Parser = class { } strFromSpan(span) { - if ( span.l === 0 ) { return ''; } + if ( span.len === 0 ) { return ''; } const beg = span.i; - return this.strFromSlices(beg, beg + span.l - 3); + return this.strFromSlices(beg, beg + span.len - 3); } isBlank() { @@ -857,25 +857,25 @@ const Parser = class { } hasOptions() { - return this.optionsSpan.l !== 0; + return this.optionsSpan.len !== 0; } getPattern() { if ( this.pattern !== '' ) { return this.pattern; } - const { i, l } = this.patternSpan; - if ( l === 0 ) { return ''; } + const { i, len } = this.patternSpan; + if ( len === 0 ) { return ''; } let beg = this.slices[i+1]; - let end = this.slices[i+l+1]; + let end = this.slices[i+len+1]; this.pattern = this.raw.slice(beg, end); return this.pattern; } getNetPattern() { if ( this.pattern !== '' ) { return this.pattern; } - const { i, l } = this.patternSpan; - if ( l === 0 ) { return ''; } + const { i, len } = this.patternSpan; + if ( len === 0 ) { return ''; } let beg = this.slices[i+1]; - let end = this.slices[i+l+1]; + let end = this.slices[i+len+1]; if ( hasBits(this.flavorBits, BITFlavorNetRegex) ) { beg += 1; end -= 1; } @@ -888,15 +888,15 @@ const Parser = class { // - Single character other than `*` wildcard patternIsDubious() { return this.patternBits !== BITAsterisk && - this.optionsSpan.l === 0 && - this.patternSpan.l === 3 && + this.optionsSpan.len === 0 && + this.patternSpan.len === 3 && this.slices[this.patternSpan.i+2] === 1; } patternIsMatchAll() { - const { l } = this.patternSpan; - return l === 0 || - l === 3 && hasBits(this.patternBits, BITAsterisk); + const { len } = this.patternSpan; + return len === 0 || + len === 3 && hasBits(this.patternBits, BITAsterisk); } patternIsPlainHostname() { @@ -908,9 +908,9 @@ const Parser = class { ) { return false; } - const { i, l } = this.patternSpan; + const { i, len } = this.patternSpan; return hasBits(this.slices[i], BITAlphaNum) && - hasBits(this.slices[i+l-3], BITAlphaNum); + hasBits(this.slices[i+len-3], BITAlphaNum); } patternIsLeftHostnameAnchored() { @@ -954,17 +954,17 @@ const Parser = class { if ( hasUpper === false && this.pattern !== '' ) { return this.pattern; } - const { i, l } = this.patternSpan; - if ( l === 0 ) { return ''; } + const { i, len } = this.patternSpan; + if ( len === 0 ) { return ''; } const beg = this.slices[i+1]; - const end = this.slices[i+l+1]; + const end = this.slices[i+len+1]; this.pattern = this.pattern || this.raw.slice(beg, end); if ( hasUpper === false ) { return this.pattern; } this.pattern = this.pattern.toLowerCase(); this.raw = this.raw.slice(0, beg) + this.pattern + this.raw.slice(end); - this.unmarkSlices(i, i+l, BITUppercase); + this.unmarkSlices(i, i + len, BITUppercase); this.patternBits &= ~BITUppercase; return this.pattern; } @@ -977,16 +977,16 @@ const Parser = class { if ( hasBits(this.patternBits, BITAsterisk) === false ) { return false; } - const { i, l } = this.patternSpan; - return l !== 0 && hasBits(this.slices[i], BITAsterisk); + const { i, len } = this.patternSpan; + return len !== 0 && hasBits(this.slices[i], BITAsterisk); } patternHasTrailingWildcard() { if ( hasBits(this.patternBits, BITAsterisk) === false ) { return false; } - const { i, l } = this.patternSpan; - return l !== 0 && hasBits(this.slices[i+l-1], BITAsterisk); + const { i, len } = this.patternSpan; + return len !== 0 && hasBits(this.slices[i+len-1], BITAsterisk); } optionHasUnicode() { @@ -1008,8 +1008,8 @@ const Parser = class { return []; } - setMaxTokenLength(l) { - this.maxTokenLength = l; + setMaxTokenLength(len) { + this.maxTokenLength = len; } hasUnicode() { @@ -1025,8 +1025,8 @@ const Parser = class { toPunycode() { if ( this.patternHasUnicode() === false ) { return true; } - const { i, l } = this.patternSpan; - if ( l === 0 ) { return true; } + const { i, len } = this.patternSpan; + if ( len === 0 ) { return true; } let pattern = this.getNetPattern(); const match = this.reHostname.exec(this.pattern); if ( match === null ) { return; } @@ -1038,7 +1038,7 @@ const Parser = class { const punycoded = this.punycoder.hostname.replace(/__asterisk__/g, '*'); pattern = punycoded + this.pattern.slice(match.index + match[0].length); const beg = this.slices[i+1]; - const end = this.slices[i+l+1]; + const end = this.slices[i+len+1]; const raw = this.raw.slice(0, beg) + pattern + this.raw.slice(end); this.analyze(raw); return true; @@ -1936,7 +1936,7 @@ const Span = class { this.reset(); } reset() { - this.i = this.l = 0; + this.i = this.len = 0; } }; @@ -1963,7 +1963,7 @@ const NetOptionsIterator = class { } init() { this.readPtr = this.writePtr = 0; - this.done = this.parser.optionsSpan.l === 0; + this.done = this.parser.optionsSpan.len === 0; if ( this.done ) { this.value = undefined; return this; @@ -1981,7 +1981,7 @@ const NetOptionsIterator = class { // At index 0 is the option descriptor. // At indices 1-5 is a slice index. const lopts = this.parser.optionsSpan.i; - const ropts = lopts + this.parser.optionsSpan.l; + const ropts = lopts + this.parser.optionsSpan.len; const slices = this.parser.slices; const optSlices = this.optSlices; let typeCount = 0; @@ -2206,12 +2206,12 @@ const PatternTokenIterator = class { this.done = true; } [Symbol.iterator]() { - const { i, l } = this.parser.patternSpan; - if ( l === 0 ) { + const { i, len } = this.parser.patternSpan; + if ( len === 0 ) { return this.end(); } this.l = i; - this.r = i + l; + this.r = i + len; this.i = i; this.done = false; this.value = { token: '', pos: 0 }; @@ -2269,14 +2269,14 @@ const ExtOptionsIterator = class { this.done = true; } [Symbol.iterator]() { - const { i, l } = this.parser.optionsSpan; - if ( l === 0 ) { + const { i, len } = this.parser.optionsSpan; + if ( len === 0 ) { this.l = this.r = 0; this.done = true; this.value = undefined; } else { this.l = i; - this.r = i + l; + this.r = i + len; this.done = false; this.value = { hn: undefined, not: false, bad: false }; } diff --git a/src/lib/codemirror/addon/edit/closebrackets.js b/src/lib/codemirror/addon/edit/closebrackets.js new file mode 100644 index 000000000..4415c3938 --- /dev/null +++ b/src/lib/codemirror/addon/edit/closebrackets.js @@ -0,0 +1,191 @@ +// CodeMirror, copyright (c) by Marijn Haverbeke and others +// Distributed under an MIT license: https://codemirror.net/LICENSE + +(function(mod) { + if (typeof exports == "object" && typeof module == "object") // CommonJS + mod(require("../../lib/codemirror")); + else if (typeof define == "function" && define.amd) // AMD + define(["../../lib/codemirror"], mod); + else // Plain browser env + mod(CodeMirror); +})(function(CodeMirror) { + var defaults = { + pairs: "()[]{}''\"\"", + closeBefore: ")]}'\":;>", + triples: "", + explode: "[]{}" + }; + + var Pos = CodeMirror.Pos; + + CodeMirror.defineOption("autoCloseBrackets", false, function(cm, val, old) { + if (old && old != CodeMirror.Init) { + cm.removeKeyMap(keyMap); + cm.state.closeBrackets = null; + } + if (val) { + ensureBound(getOption(val, "pairs")) + cm.state.closeBrackets = val; + cm.addKeyMap(keyMap); + } + }); + + function getOption(conf, name) { + if (name == "pairs" && typeof conf == "string") return conf; + if (typeof conf == "object" && conf[name] != null) return conf[name]; + return defaults[name]; + } + + var keyMap = {Backspace: handleBackspace, Enter: handleEnter}; + function ensureBound(chars) { + for (var i = 0; i < chars.length; i++) { + var ch = chars.charAt(i), key = "'" + ch + "'" + if (!keyMap[key]) keyMap[key] = handler(ch) + } + } + ensureBound(defaults.pairs + "`") + + function handler(ch) { + return function(cm) { return handleChar(cm, ch); }; + } + + function getConfig(cm) { + var deflt = cm.state.closeBrackets; + if (!deflt || deflt.override) return deflt; + var mode = cm.getModeAt(cm.getCursor()); + return mode.closeBrackets || deflt; + } + + function handleBackspace(cm) { + var conf = getConfig(cm); + if (!conf || cm.getOption("disableInput")) return CodeMirror.Pass; + + var pairs = getOption(conf, "pairs"); + var ranges = cm.listSelections(); + for (var i = 0; i < ranges.length; i++) { + if (!ranges[i].empty()) return CodeMirror.Pass; + var around = charsAround(cm, ranges[i].head); + if (!around || pairs.indexOf(around) % 2 != 0) return CodeMirror.Pass; + } + for (var i = ranges.length - 1; i >= 0; i--) { + var cur = ranges[i].head; + cm.replaceRange("", Pos(cur.line, cur.ch - 1), Pos(cur.line, cur.ch + 1), "+delete"); + } + } + + function handleEnter(cm) { + var conf = getConfig(cm); + var explode = conf && getOption(conf, "explode"); + if (!explode || cm.getOption("disableInput")) return CodeMirror.Pass; + + var ranges = cm.listSelections(); + for (var i = 0; i < ranges.length; i++) { + if (!ranges[i].empty()) return CodeMirror.Pass; + var around = charsAround(cm, ranges[i].head); + if (!around || explode.indexOf(around) % 2 != 0) return CodeMirror.Pass; + } + cm.operation(function() { + var linesep = cm.lineSeparator() || "\n"; + cm.replaceSelection(linesep + linesep, null); + cm.execCommand("goCharLeft"); + ranges = cm.listSelections(); + for (var i = 0; i < ranges.length; i++) { + var line = ranges[i].head.line; + cm.indentLine(line, null, true); + cm.indentLine(line + 1, null, true); + } + }); + } + + function contractSelection(sel) { + var inverted = CodeMirror.cmpPos(sel.anchor, sel.head) > 0; + return {anchor: new Pos(sel.anchor.line, sel.anchor.ch + (inverted ? -1 : 1)), + head: new Pos(sel.head.line, sel.head.ch + (inverted ? 1 : -1))}; + } + + function handleChar(cm, ch) { + var conf = getConfig(cm); + if (!conf || cm.getOption("disableInput")) return CodeMirror.Pass; + + var pairs = getOption(conf, "pairs"); + var pos = pairs.indexOf(ch); + if (pos == -1) return CodeMirror.Pass; + + var closeBefore = getOption(conf,"closeBefore"); + + var triples = getOption(conf, "triples"); + + var identical = pairs.charAt(pos + 1) == ch; + var ranges = cm.listSelections(); + var opening = pos % 2 == 0; + + var type; + for (var i = 0; i < ranges.length; i++) { + var range = ranges[i], cur = range.head, curType; + var next = cm.getRange(cur, Pos(cur.line, cur.ch + 1)); + if (opening && !range.empty()) { + curType = "surround"; + } else if ((identical || !opening) && next == ch) { + if (identical && stringStartsAfter(cm, cur)) + curType = "both"; + else if (triples.indexOf(ch) >= 0 && cm.getRange(cur, Pos(cur.line, cur.ch + 3)) == ch + ch + ch) + curType = "skipThree"; + else + curType = "skip"; + } else if (identical && cur.ch > 1 && triples.indexOf(ch) >= 0 && + cm.getRange(Pos(cur.line, cur.ch - 2), cur) == ch + ch) { + if (cur.ch > 2 && /\bstring/.test(cm.getTokenTypeAt(Pos(cur.line, cur.ch - 2)))) return CodeMirror.Pass; + curType = "addFour"; + } else if (identical) { + var prev = cur.ch == 0 ? " " : cm.getRange(Pos(cur.line, cur.ch - 1), cur) + if (!CodeMirror.isWordChar(next) && prev != ch && !CodeMirror.isWordChar(prev)) curType = "both"; + else return CodeMirror.Pass; + } else if (opening && (next.length === 0 || /\s/.test(next) || closeBefore.indexOf(next) > -1)) { + curType = "both"; + } else { + return CodeMirror.Pass; + } + if (!type) type = curType; + else if (type != curType) return CodeMirror.Pass; + } + + var left = pos % 2 ? pairs.charAt(pos - 1) : ch; + var right = pos % 2 ? ch : pairs.charAt(pos + 1); + cm.operation(function() { + if (type == "skip") { + cm.execCommand("goCharRight"); + } else if (type == "skipThree") { + for (var i = 0; i < 3; i++) + cm.execCommand("goCharRight"); + } else if (type == "surround") { + var sels = cm.getSelections(); + for (var i = 0; i < sels.length; i++) + sels[i] = left + sels[i] + right; + cm.replaceSelections(sels, "around"); + sels = cm.listSelections().slice(); + for (var i = 0; i < sels.length; i++) + sels[i] = contractSelection(sels[i]); + cm.setSelections(sels); + } else if (type == "both") { + cm.replaceSelection(left + right, null); + cm.triggerElectric(left + right); + cm.execCommand("goCharLeft"); + } else if (type == "addFour") { + cm.replaceSelection(left + left + left + left, "before"); + cm.execCommand("goCharRight"); + } + }); + } + + function charsAround(cm, pos) { + var str = cm.getRange(Pos(pos.line, pos.ch - 1), + Pos(pos.line, pos.ch + 1)); + return str.length == 2 ? str : null; + } + + function stringStartsAfter(cm, pos) { + var token = cm.getTokenAt(Pos(pos.line, pos.ch + 1)) + return /\bstring/.test(token.type) && token.start == pos.ch && + (pos.ch == 0 || !/\bstring/.test(cm.getTokenTypeAt(pos))) + } +}); diff --git a/src/lib/codemirror/addon/edit/matchbrackets.js b/src/lib/codemirror/addon/edit/matchbrackets.js new file mode 100644 index 000000000..2a147282c --- /dev/null +++ b/src/lib/codemirror/addon/edit/matchbrackets.js @@ -0,0 +1,150 @@ +// CodeMirror, copyright (c) by Marijn Haverbeke and others +// Distributed under an MIT license: https://codemirror.net/LICENSE + +(function(mod) { + if (typeof exports == "object" && typeof module == "object") // CommonJS + mod(require("../../lib/codemirror")); + else if (typeof define == "function" && define.amd) // AMD + define(["../../lib/codemirror"], mod); + else // Plain browser env + mod(CodeMirror); +})(function(CodeMirror) { + var ie_lt8 = /MSIE \d/.test(navigator.userAgent) && + (document.documentMode == null || document.documentMode < 8); + + var Pos = CodeMirror.Pos; + + var matching = {"(": ")>", ")": "(<", "[": "]>", "]": "[<", "{": "}>", "}": "{<", "<": ">>", ">": "<<"}; + + function bracketRegex(config) { + return config && config.bracketRegex || /[(){}[\]]/ + } + + function findMatchingBracket(cm, where, config) { + var line = cm.getLineHandle(where.line), pos = where.ch - 1; + var afterCursor = config && config.afterCursor + if (afterCursor == null) + afterCursor = /(^| )cm-fat-cursor($| )/.test(cm.getWrapperElement().className) + var re = bracketRegex(config) + + // A cursor is defined as between two characters, but in in vim command mode + // (i.e. not insert mode), the cursor is visually represented as a + // highlighted box on top of the 2nd character. Otherwise, we allow matches + // from before or after the cursor. + var match = (!afterCursor && pos >= 0 && re.test(line.text.charAt(pos)) && matching[line.text.charAt(pos)]) || + re.test(line.text.charAt(pos + 1)) && matching[line.text.charAt(++pos)]; + if (!match) return null; + var dir = match.charAt(1) == ">" ? 1 : -1; + if (config && config.strict && (dir > 0) != (pos == where.ch)) return null; + var style = cm.getTokenTypeAt(Pos(where.line, pos + 1)); + + var found = scanForBracket(cm, Pos(where.line, pos + (dir > 0 ? 1 : 0)), dir, style || null, config); + if (found == null) return null; + return {from: Pos(where.line, pos), to: found && found.pos, + match: found && found.ch == match.charAt(0), forward: dir > 0}; + } + + // bracketRegex is used to specify which type of bracket to scan + // should be a regexp, e.g. /[[\]]/ + // + // Note: If "where" is on an open bracket, then this bracket is ignored. + // + // Returns false when no bracket was found, null when it reached + // maxScanLines and gave up + function scanForBracket(cm, where, dir, style, config) { + var maxScanLen = (config && config.maxScanLineLength) || 10000; + var maxScanLines = (config && config.maxScanLines) || 1000; + + var stack = []; + var re = bracketRegex(config) + var lineEnd = dir > 0 ? Math.min(where.line + maxScanLines, cm.lastLine() + 1) + : Math.max(cm.firstLine() - 1, where.line - maxScanLines); + for (var lineNo = where.line; lineNo != lineEnd; lineNo += dir) { + var line = cm.getLine(lineNo); + if (!line) continue; + var pos = dir > 0 ? 0 : line.length - 1, end = dir > 0 ? line.length : -1; + if (line.length > maxScanLen) continue; + if (lineNo == where.line) pos = where.ch - (dir < 0 ? 1 : 0); + for (; pos != end; pos += dir) { + var ch = line.charAt(pos); + if (re.test(ch) && (style === undefined || cm.getTokenTypeAt(Pos(lineNo, pos + 1)) == style)) { + var match = matching[ch]; + if (match && (match.charAt(1) == ">") == (dir > 0)) stack.push(ch); + else if (!stack.length) return {pos: Pos(lineNo, pos), ch: ch}; + else stack.pop(); + } + } + } + return lineNo - dir == (dir > 0 ? cm.lastLine() : cm.firstLine()) ? false : null; + } + + function matchBrackets(cm, autoclear, config) { + // Disable brace matching in long lines, since it'll cause hugely slow updates + var maxHighlightLen = cm.state.matchBrackets.maxHighlightLineLength || 1000; + var marks = [], ranges = cm.listSelections(); + for (var i = 0; i < ranges.length; i++) { + var match = ranges[i].empty() && findMatchingBracket(cm, ranges[i].head, config); + if (match && cm.getLine(match.from.line).length <= maxHighlightLen) { + var style = match.match ? "CodeMirror-matchingbracket" : "CodeMirror-nonmatchingbracket"; + marks.push(cm.markText(match.from, Pos(match.from.line, match.from.ch + 1), {className: style})); + if (match.to && cm.getLine(match.to.line).length <= maxHighlightLen) + marks.push(cm.markText(match.to, Pos(match.to.line, match.to.ch + 1), {className: style})); + } + } + + if (marks.length) { + // Kludge to work around the IE bug from issue #1193, where text + // input stops going to the textare whever this fires. + if (ie_lt8 && cm.state.focused) cm.focus(); + + var clear = function() { + cm.operation(function() { + for (var i = 0; i < marks.length; i++) marks[i].clear(); + }); + }; + if (autoclear) setTimeout(clear, 800); + else return clear; + } + } + + function doMatchBrackets(cm) { + cm.operation(function() { + if (cm.state.matchBrackets.currentlyHighlighted) { + cm.state.matchBrackets.currentlyHighlighted(); + cm.state.matchBrackets.currentlyHighlighted = null; + } + cm.state.matchBrackets.currentlyHighlighted = matchBrackets(cm, false, cm.state.matchBrackets); + }); + } + + CodeMirror.defineOption("matchBrackets", false, function(cm, val, old) { + if (old && old != CodeMirror.Init) { + cm.off("cursorActivity", doMatchBrackets); + if (cm.state.matchBrackets && cm.state.matchBrackets.currentlyHighlighted) { + cm.state.matchBrackets.currentlyHighlighted(); + cm.state.matchBrackets.currentlyHighlighted = null; + } + } + if (val) { + cm.state.matchBrackets = typeof val == "object" ? val : {}; + cm.on("cursorActivity", doMatchBrackets); + } + }); + + CodeMirror.defineExtension("matchBrackets", function() {matchBrackets(this, true);}); + CodeMirror.defineExtension("findMatchingBracket", function(pos, config, oldConfig){ + // Backwards-compatibility kludge + if (oldConfig || typeof config == "boolean") { + if (!oldConfig) { + config = config ? {strict: true} : null + } else { + oldConfig.strict = config + config = oldConfig + } + } + return findMatchingBracket(this, pos, config) + }); + CodeMirror.defineExtension("scanForBracket", function(pos, dir, style, config){ + return scanForBracket(this, pos, dir, style, config); + }); +});