From bc21a1fe72e4af6f6a3f3046a9dd0072c467efb0 Mon Sep 17 00:00:00 2001 From: Raymond Hill Date: Thu, 6 Apr 2023 13:14:42 -0400 Subject: [PATCH] Use custom linter gutter as container for code-folding widgets This allows to reduce the horizontal size of the gutter; more efficient management of folding since we already need to parse each line for the linter; and eventually this will also allows to detect unbalanced !#if-!#endif directives and report such cases as errors. Additionally, keep incrementally improving error reporting details in the linter. --- src/1p-filters.html | 2 - src/asset-viewer.html | 2 - src/css/codemirror.css | 38 ++- src/css/popup-fenix.css | 3 + src/js/1p-filters.js | 3 +- src/js/asset-viewer.js | 3 +- src/js/codemirror/search.js | 14 +- src/js/codemirror/ubo-static-filtering.js | 344 +++++++++++++++++----- src/js/static-filtering-parser.js | 36 ++- src/js/support.js | 4 +- 10 files changed, 343 insertions(+), 106 deletions(-) diff --git a/src/1p-filters.html b/src/1p-filters.html index 7d5cb9664..fa153ba56 100644 --- a/src/1p-filters.html +++ b/src/1p-filters.html @@ -6,7 +6,6 @@ uBlock — Your filters - @@ -44,7 +43,6 @@ - diff --git a/src/asset-viewer.html b/src/asset-viewer.html index 70858725a..9bda40011 100644 --- a/src/asset-viewer.html +++ b/src/asset-viewer.html @@ -5,7 +5,6 @@ - @@ -29,7 +28,6 @@ - diff --git a/src/css/codemirror.css b/src/css/codemirror.css index b1b6d70f8..0e3516249 100644 --- a/src/css/codemirror.css +++ b/src/css/codemirror.css @@ -23,8 +23,9 @@ } .CodeMirror-foldmarker { color: var(--cm-foldmarker-ink); - font-size: large; - text-shadow: none; + cursor: pointer; + font-family: sans-serif; + font-weight: bold; } .CodeMirror-foldgutter-folded::after { content: '\25B6'; @@ -213,7 +214,7 @@ display: none; flex-grow: 1; } -.cm-linter-widget.hasErrors { +.cm-linter-widget:not([data-lint="0"]) { display: inline-flex; } .cm-linter-widget .cm-linter-widget-count { @@ -273,25 +274,44 @@ } .CodeMirror-lintmarker { - background-color: var(--sf-error-ink); height: calc(var(--font-size) - 2px); margin-top: 1px; position: relative; } -.CodeMirror-lintmarker > span { +.CodeMirror-lintmarker > * { + position: absolute; + } +.CodeMirror-lintmarker[data-lint="error"] { + background-color: var(--sf-error-ink); + } +.CodeMirror-lintmarker[data-lint="error"] .msg { display: none; } -.CodeMirror-lintmarker > span { +.CodeMirror-lintmarker[data-lint="error"] .msg { background-color: var(--surface-0); border: 1px solid var(--sf-error-ink); color: var(--ink-1); left: 100%; padding: var(--default-gap-xsmall); - position: absolute; top: -2px; white-space: pre; } -.CodeMirror-lintmarker:hover > span, -.CodeMirror-lintmarker > span:hover { +.CodeMirror-lintmarker svg { + height: 70%; + left: 15%; + top: 15%; + width: 70%; + } +.CodeMirror-lintmarker[data-fold="start"] { + fill: var(--cm-foldmarker-ink); + } +.CodeMirror-lintmarker[data-fold="start"].folded svg { + transform: rotate(-90deg); + } +.CodeMirror-lintmarker[data-fold="end"] { + fill: var(--border-2); + } +.CodeMirror-lintmarker[data-lint="error"]:hover > span, +.CodeMirror-lintmarker[data-lint="error"] > span:hover { display: initial; } diff --git a/src/css/popup-fenix.css b/src/css/popup-fenix.css index 7800d0d26..28672bc53 100644 --- a/src/css/popup-fenix.css +++ b/src/css/popup-fenix.css @@ -320,6 +320,9 @@ body.mobile.no-tooltips .toolRibbon .tool { border-bottom: 1px dotted #ddd; padding: 0.25em; } +#firewall > section .filterExpressions div:last-of-type { + border-bottom: 0; + } #firewall > section .filterExpressions span { cursor: default; display: inline-flex; diff --git a/src/js/1p-filters.js b/src/js/1p-filters.js index c33acfd32..4a31ace32 100644 --- a/src/js/1p-filters.js +++ b/src/js/1p-filters.js @@ -38,9 +38,8 @@ const cmEditor = new CodeMirror(qs$('#userFilters'), { }, foldGutter: true, gutters: [ - 'CodeMirror-foldgutter', 'CodeMirror-linenumbers', - { className: 'CodeMirror-lintgutter', style: 'width: 10px' }, + { className: 'CodeMirror-lintgutter', style: 'width: 11px' }, ], lineNumbers: true, lineWrapping: true, diff --git a/src/js/asset-viewer.js b/src/js/asset-viewer.js index 20f1cff66..060ea20a3 100644 --- a/src/js/asset-viewer.js +++ b/src/js/asset-viewer.js @@ -53,9 +53,8 @@ import './codemirror/ubo-static-filtering.js'; autofocus: true, foldGutter: true, gutters: [ - 'CodeMirror-foldgutter', 'CodeMirror-linenumbers', - { className: 'CodeMirror-lintgutter', style: 'width: 10px' }, + { className: 'CodeMirror-lintgutter', style: 'width: 11px' }, ], lineNumbers: true, lineWrapping: true, diff --git a/src/js/codemirror/search.js b/src/js/codemirror/search.js index 7f42b1f1b..882e7afd4 100644 --- a/src/js/codemirror/search.js +++ b/src/js/codemirror/search.js @@ -27,7 +27,7 @@ 'use strict'; -import { dom } from '../dom.js'; +import { dom, qs$ } from '../dom.js'; import { i18n$ } from '../i18n.js'; { @@ -382,7 +382,9 @@ import { i18n$ } from '../i18n.js'; doc.eachLine(start, end, lineHandle => { const markers = lineHandle.gutterMarkers || null; if ( markers === null ) { return; } - if ( markers['CodeMirror-lintgutter'] === undefined ) { return; } + const marker = markers['CodeMirror-lintgutter']; + if ( marker === undefined ) { return; } + if ( marker.dataset.lint !== 'error' ) { return; } const line = lineHandle.lineNo(); if ( dir < 0 ) { found = line; @@ -476,7 +478,7 @@ import { i18n$ } from '../i18n.js'; 'angle-up ' + '' + '' + - '' + + '' + ' ' + 'angle-up ' + 'angle-up ' + @@ -497,10 +499,12 @@ import { i18n$ } from '../i18n.js'; CodeMirror.defineInitHook(function(cm) { getSearchState(cm); cm.on('linterDone', details => { + const linterWidget = qs$('.cm-linter-widget'); const count = details.errorCount; - dom.cl.toggle('.cm-linter-widget', 'hasErrors', count !== 0); + if ( linterWidget.dataset.lint === `${count}` ) { return; } + linterWidget.dataset.lint = `${count}`; dom.text( - '.cm-linter-widget .cm-linter-widget-count', + qs$(linterWidget, '.cm-linter-widget-count'), i18n$('linterMainReport').replace('{{count}}', count.toLocaleString()) ); }); diff --git a/src/js/codemirror/ubo-static-filtering.js b/src/js/codemirror/ubo-static-filtering.js index 5f647b748..9811a5c26 100644 --- a/src/js/codemirror/ubo-static-filtering.js +++ b/src/js/codemirror/ubo-static-filtering.js @@ -26,6 +26,7 @@ /******************************************************************************/ import * as sfp from '../static-filtering-parser.js'; +import { dom, qs$ } from '../dom.js'; /******************************************************************************/ @@ -670,91 +671,189 @@ CodeMirror.registerHelper('fold', 'ubo-static-filtering', (( ) => { const changeset = []; let changesetTimer; + + const includeset = new Set(); let errorCount = 0; - const addChange = (doc, change) => { - changeset.push(change); - processChangesetAsync(doc); - }; - - const processChangesetAsync = doc => { - if ( changesetTimer !== undefined ) { return; } - changesetTimer = self.requestIdleCallback(deadline => { - changesetTimer = undefined; - processChangeset(doc, deadline); - }); - }; - - const extractError = ( ) => { - if ( astParser.hasError() === false ) { return; } - const error = 'Invalid filter'; - switch ( astParser.astError ) { - case sfp.AST_ERROR_REGEX: - return `${error}: Bad regular expression`; - case sfp.AST_ERROR_PATTERN: - return `${error}: Bad pattern`; - case sfp.AST_ERROR_DOMAIN_NAME: - return `${error}: Bad domain name`; - case sfp.AST_ERROR_OPTION_DUPLICATE: - return `${error}: Duplicate filter option`; - case sfp.AST_ERROR_OPTION_UNKNOWN: - return `${error}: Unsupported filter option`; - case sfp.AST_ERROR_IF_TOKEN_UNKNOWN: - return `${error}: Unknown preparsing token`; - default: - break; + const extractMarkerDetails = (doc, lineHandle) => { + if ( astParser.isUnsupported() ) { + return { value: 'error', msg: 'Unsupported filter syntax' }; } - if ( astParser.isCosmeticFilter() && astParser.result.error ) { - return `${error}: ${astParser.result.error}`; + if ( astParser.hasError() ) { + let msg = 'Invalid filter'; + switch ( astParser.astError ) { + case sfp.AST_ERROR_UNSUPPORTED: + msg = `${msg}: Unsupported filter syntax`; + break; + case sfp.AST_ERROR_REGEX: + msg = `${msg}: Bad regular expression`; + break; + case sfp.AST_ERROR_PATTERN: + msg = `${msg}: Bad pattern`; + break; + case sfp.AST_ERROR_DOMAIN_NAME: + msg = `${msg}: Bad domain name`; + break; + case sfp.AST_ERROR_OPTION_DUPLICATE: + msg = `${msg}: Duplicate filter option`; + break; + case sfp.AST_ERROR_OPTION_UNKNOWN: + msg = `${msg}: Unsupported filter option`; + break; + case sfp.AST_ERROR_IF_TOKEN_UNKNOWN: + msg = `${msg}: Unknown preparsing token`; + break; + default: + if ( astParser.isCosmeticFilter() && astParser.result.error ) { + msg = `${msg}: ${astParser.result.error}`; + } + break; + } + return { value: 'error', msg }; } - return error; + if ( astParser.astType !== sfp.AST_TYPE_COMMENT ) { return; } + if ( astParser.astTypeFlavor !== sfp.AST_TYPE_COMMENT_PREPARSER ) { + if ( astParser.raw.startsWith('! <<<<<<<< ') === false ) { return; } + for ( const include of includeset ) { + if ( astParser.raw.endsWith(include) === false ) { continue; } + includeset.delete(include); + return { value: 'include-end' }; + } + return; + } + if ( /^\s*!#if \S+/.test(astParser.raw) ) { + return { value: 'if-start' }; + } + if ( /^\s*!#endif\b/.test(astParser.raw) ) { + return { value: 'if-end' }; + } + const match = /^\s*!#include\s*(\S+)/.exec(astParser.raw); + if ( match === null ) { return; } + const nextLineHandle = doc.getLineHandle(lineHandle.lineNo() + 1); + if ( nextLineHandle === undefined ) { return; } + if ( nextLineHandle.text.startsWith('! >>>>>>>> ') === false ) { return; } + const includeToken = `/${match[1]}`; + if ( nextLineHandle.text.endsWith(includeToken) === false ) { return; } + includeset.add(includeToken); + return { value: 'include-start' }; }; const extractMarker = lineHandle => { const markers = lineHandle.gutterMarkers || null; - if ( markers === null ) { return; } - return markers['CodeMirror-lintgutter'] || undefined; + return markers !== null + ? markers['CodeMirror-lintgutter'] || null + : null; }; - const markerTemplate = (( ) => { - const marker = document.createElement('div'); - marker.classList.add('CodeMirror-lintmarker'); - marker.textContent = '\xA0'; - const info = document.createElement('span'); - marker.append(info); - return marker; - })(); + const markerTemplates = { + 'error': { + node: null, + html: [ + '
 ', + '', + '
', + ], + }, + 'if-start': { + node: null, + html: [ + '
 ', + '', + '', + '', + '
', + ], + }, + 'if-end': { + node: null, + html: [ + '
 ', + '', + '', + '', + '
', + ], + }, + 'include-start': { + node: null, + html: [ + '
 ', + '', + '', + '', + '
', + ], + }, + 'include-end': { + node: null, + html: [ + '
 ', + '', + '', + '', + '
', + ], + }, + }; - const makeMarker = (doc, lineHandle, marker, error) => { - if ( marker === undefined ) { - marker = addMarker(doc, lineHandle); + const markerFromTemplate = which => { + const template = markerTemplates[which]; + if ( template.node === null ) { + const domParser = new DOMParser(); + const doc = domParser.parseFromString(template.html.join(''), 'text/html'); + template.node = document.adoptNode(qs$(doc, '.CodeMirror-lintmarker')); } - marker.children[0].textContent = error; + return template.node.cloneNode(true); }; - const addMarker = (doc, lineHandle) => { - const marker = markerTemplate.cloneNode(true); - doc.setGutterMarker(lineHandle, 'CodeMirror-lintgutter', marker); - lineHandle.on('delete', deleteMarker); - errorCount += 1; - return marker; + const addMarker = (doc, lineHandle, marker, details) => { + if ( marker !== null && marker.dataset.lint !== details.value ) { + doc.setGutterMarker(lineHandle, 'CodeMirror-lintgutter', null); + if ( marker.dataset.lint === 'error' ) { + errorCount -= 1; + } + marker = null; + } + if ( marker === null ) { + marker = markerFromTemplate(details.value); + doc.setGutterMarker(lineHandle, 'CodeMirror-lintgutter', marker); + if ( details.value === 'error' ) { + errorCount += 1; + } + } + const msgElem = qs$(marker, '.msg'); + if ( msgElem === null ) { return; } + msgElem.textContent = details.msg || ''; }; - const deleteMarker = ( ) => { - errorCount -= 1; + const removeMarker = (doc, lineHandle, marker) => { + doc.setGutterMarker(lineHandle, 'CodeMirror-lintgutter', null); + if ( marker.dataset.lint === 'error' ) { + errorCount -= 1; + } }; - const processChange = (doc, deadline, change) => { + const processDeletion = (doc, change) => { + let { from, to } = change; + doc.eachLine(from.line, to.line, lineHandle => { + const marker = extractMarker(lineHandle); + if ( marker === null ) { return; } + if ( marker.dataset.lint === 'error' ) { + errorCount -= 1; + } + }); + }; + + const processInsertion = (doc, deadline, change) => { let { from, to } = change; doc.eachLine(from, to, lineHandle => { astParser.parse(lineHandle.text); - const error = extractError(); + const markerDetails = extractMarkerDetails(doc, lineHandle); const marker = extractMarker(lineHandle); - if ( error === undefined && marker ) { - doc.setGutterMarker(lineHandle, 'CodeMirror-lintgutter', null); - deleteMarker(); - } else if ( error !== undefined ) { - makeMarker(doc, lineHandle, marker, error); + if ( markerDetails === undefined && marker !== null ) { + removeMarker(doc, lineHandle, marker); + } else if ( markerDetails !== undefined ) { + addMarker(doc, lineHandle, marker, markerDetails); } from += 1; if ( (from & 0x0F) !== 0 ) { return; } @@ -770,7 +869,7 @@ CodeMirror.registerHelper('fold', 'ubo-static-filtering', (( ) => { const cm = doc.getEditor(); cm.startOperation(); while ( changeset.length !== 0 ) { - const change = processChange(doc, deadline, changeset.shift()); + const change = processInsertion(doc, deadline, changeset.shift()); if ( change === undefined ) { continue; } changeset.unshift(change); break; @@ -779,17 +878,120 @@ CodeMirror.registerHelper('fold', 'ubo-static-filtering', (( ) => { if ( changeset.length !== 0 ) { return processChangesetAsync(doc); } + includeset.clear(); CodeMirror.signal(doc.getEditor(), 'linterDone', { errorCount }); }; - CodeMirror.defineInitHook(cm => { - cm.on('changes', function(cm, changes) { - const doc = cm.getDoc(); - for ( const change of changes ) { - const from = change.from.line; - const to = from + change.text.length; - addChange(doc, { from, to }); + const processChangesetAsync = doc => { + if ( changesetTimer !== undefined ) { return; } + changesetTimer = self.requestIdleCallback(deadline => { + changesetTimer = undefined; + processChangeset(doc, deadline); + }); + }; + + const onChanges = (cm, changes) => { + const doc = cm.getDoc(); + for ( const change of changes ) { + const from = change.from.line; + const to = from + change.text.length; + changeset.push({ from, to }); + processChangesetAsync(doc); + } + }; + + const onBeforeChanges = (cm, change) => { + const doc = cm.getDoc(); + processDeletion(doc, change); + }; + + const foldRangeFinder = (cm, from) => { + const lineNo = from.line; + const lineHandle = cm.getDoc().getLineHandle(lineNo); + const marker = extractMarker(lineHandle); + if ( marker === null ) { return; } + if ( marker.dataset.fold === undefined ) { return; } + const foldName = marker.dataset.lint; + from.ch = lineHandle.text.length; + const to = { line: 0, ch: 0 }; + const doc = cm.getDoc(); + let depth = 0; + doc.eachLine(from.line, doc.lineCount(), lineHandle => { + const marker = extractMarker(lineHandle); + if ( marker === null ) { return; } + if ( marker.dataset.lint === foldName && marker.dataset.fold === 'start' ) { + depth += 1; + return; } + if ( marker.dataset.lint !== foldName ) { return; } + if ( marker.dataset.fold !== 'end' ) { return; } + depth -= 1; + if ( depth !== 0 ) { return; } + to.line = lineHandle.lineNo(); + return true; + }); + return { from, to }; + }; + + const onGutterClick = (cm, lineNo, gutterId, ev) => { + if ( ev.button !== 0 ) { return; } + if ( gutterId !== 'CodeMirror-lintgutter' ) { return; } + const doc = cm.getDoc(); + const lineHandle = doc.getLineHandle(lineNo); + const marker = extractMarker(lineHandle); + if ( marker === null ) { return; } + if ( marker.dataset.fold === 'start' ) { + if ( ev.ctrlKey ) { + if ( dom.cl.has(marker, 'folded') ) { + CodeMirror.commands.unfoldAll(cm); + } else { + CodeMirror.commands.foldAll(cm); + } + doc.setCursor(lineNo); + return; + } + cm.foldCode(lineNo, { + widget: '\u00A0\u22EF\u00A0', + rangeFinder: foldRangeFinder, + }); + return; + } + if ( marker.dataset.fold === 'end' ) { + let depth = 1; + let lineNo = lineHandle.lineNo(); + while ( lineNo-- ) { + const prevLineHandle = doc.getLineHandle(lineNo); + const markerFrom = extractMarker(prevLineHandle); + if ( markerFrom === null ) { continue; } + if ( markerFrom.dataset.fold === 'end' ) { + depth += 1; + } else if ( markerFrom.dataset.fold === 'start' ) { + depth -= 1; + if ( depth === 0 ) { + doc.setCursor(lineNo); + break; + } + } + } + return; + } + }; + + CodeMirror.defineInitHook(cm => { + cm.on('changes', onChanges); + cm.on('beforeChange', onBeforeChanges); + cm.on('gutterClick', onGutterClick); + cm.on('fold', function(cm, from) { + const doc = cm.getDoc(); + const lineHandle = doc.getLineHandle(from.line); + const marker = extractMarker(lineHandle); + dom.cl.add(marker, 'folded'); + }); + cm.on('unfold', function(cm, from) { + const doc = cm.getDoc(); + const lineHandle = doc.getLineHandle(from.line); + const marker = extractMarker(lineHandle); + dom.cl.remove(marker, 'folded'); }); }); } diff --git a/src/js/static-filtering-parser.js b/src/js/static-filtering-parser.js index 39258e9cb..1f2cbbe69 100644 --- a/src/js/static-filtering-parser.js +++ b/src/js/static-filtering-parser.js @@ -727,6 +727,7 @@ export class AstFilterParser { this.astType = AST_TYPE_NONE; this.astTypeFlavor = AST_TYPE_NONE; this.astFlags = 0; + this.astError = 0; this.nodeTypeRegister = []; this.nodeTypeRegisterPtr = 0; this.nodeTypeLookupTable = new Uint32Array(NODE_TYPE_COUNT); @@ -1068,7 +1069,7 @@ export class AstFilterParser { const head = this.allocTypedNode(NODE_TYPE_NET_RAW, parentBeg, tailStart); if ( this.linkDown(head, this.parseNet(head)) === 0 ) { this.astType = AST_TYPE_UNKNOWN; - this.addFlags(AST_FLAG_UNSUPPORTED); + this.addFlags(AST_FLAG_UNSUPPORTED | AST_FLAG_HAS_ERROR); } if ( tail !== 0 ) { this.linkRight(head, tail); @@ -2085,7 +2086,7 @@ export class AstFilterParser { if ( c === 0x40 /* @ */ ) { return AST_FLAG_IS_EXCEPTION | this.extFlagsFromAnchor(anchorBeg+1); } - return AST_FLAG_UNSUPPORTED; + return AST_FLAG_UNSUPPORTED | AST_FLAG_HAS_ERROR; } validateExt() { @@ -2395,6 +2396,10 @@ export class AstFilterParser { return (this.astFlags & AST_FLAG_HAS_ERROR) !== 0; } + isUnsupported() { + return (this.astFlags & AST_FLAG_UNSUPPORTED) !== 0; + } + hasOptions() { return (this.astFlags & AST_FLAG_HAS_OPTIONS) !== 0; } @@ -3015,7 +3020,7 @@ class ExtSelectorCompiler { this.reIsRelativeSelector = /^\s*[+>~]/; this.reExtendedSyntax = /\[-(?:abp|ext)-[a-z-]+=(['"])(?:.+?)(?:\1)\]/; this.reExtendedSyntaxReplacer = /\[-(?:abp|ext)-([a-z-]+)=(['"])(.+?)\2\]/g; - this.abpProceduralOpReplacer = /:-abp-(?:contains|has)\(/g; + this.abpProceduralOpReplacer = /:-abp-(?:[a-z]+)\(/g; this.nativeCssHas = instanceOptions.nativeCssHas === true; // https://www.w3.org/TR/css-syntax-3/#typedef-ident-token this.reInvalidIdentifier = /^\d/; @@ -3048,15 +3053,14 @@ class ExtSelectorCompiler { return `:${op}(${a3})`; }); } else { + let asProcedural = false; raw = raw.replace(this.abpProceduralOpReplacer, match => { - if ( match === ':-abp-contains(' ) { - return ':has-text('; - } else if ( match === ':-abp-has(' ) { - this.asProcedural = false; - return ':has('; - } + if ( match === ':-abp-contains(' ) { return ':has-text('; } + if ( match === ':-abp-has(' ) { return ':has('; } + asProcedural = true; return match; }); + this.asProcedural = asProcedural; } } @@ -3068,9 +3072,10 @@ class ExtSelectorCompiler { return true; } + this.error = undefined; out.compiled = this.compileSelector(raw); if ( out.compiled === undefined ) { - out.error = this.error || undefined; + out.error = this.error; return false; } @@ -3116,7 +3121,13 @@ class ExtSelectorCompiler { parseValue: false, }); } catch(reason) { - this.error = reason && reason.message || undefined; + const lines = [ reason.message ]; + const extra = reason.sourceFragment().split('\n'); + if ( extra.length !== 0 ) { lines.push(''); } + const match = /^[^|]+\|/.exec(extra[0]); + const beg = match !== null ? match[0].length : 0; + lines.push(...extra.map(a => a.slice(beg))); + this.error = lines.join('\n'); return; } const parts = []; @@ -3177,6 +3188,7 @@ class ExtSelectorCompiler { } if ( data.type !== 'PseudoClassSelector' ) { return; } if ( data.name.startsWith('-abp-') && this.asProcedural === false ) { + this.error = `${data.name} requires '#?#' separator syntax`; return; } // Post-analysis, mind: @@ -3189,6 +3201,7 @@ class ExtSelectorCompiler { data.type = 'ActionSelector'; } else if ( data.name.startsWith('-abp-') ) { data.type = 'Error'; + this.error = `${data.name} is not supported`; return; } if ( this.maybeProceduralOperatorNames.has(data.name) === false ) { @@ -3196,6 +3209,7 @@ class ExtSelectorCompiler { } if ( this.astHasType(args, 'ActionSelector') ) { data.type = 'Error'; + this.error = 'invalid use of action operator'; return; } if ( this.astHasType(args, 'ProceduralSelector') ) { diff --git a/src/js/support.js b/src/js/support.js index 3b878b035..c144d6383 100644 --- a/src/js/support.js +++ b/src/js/support.js @@ -152,12 +152,12 @@ function showData() { if ( reportedPage !== null ) { shownData.popupPanel = reportedPage.popupPanel; } - const text = JSON.stringify(shownData, null, 2) + const text = JSON.stringify(shownData, null, 1) .split('\n') .slice(1, -1) .map(v => { return v - .replace(/^( *?) "/, '$1') + .replace(/^( *?) "/, '$1') .replace(/^( *.*[^\\])(?:": "|": \{$|": \[$|": )/, '$1: ') .replace(/(?:",?|\},?|\],?|,)$/, ''); })