Fix syntax worker based highlighting
HighlightJS produces HTML highlighting in the form of
`<span class="something">text</span>`. But if such an HTML string is
the *input* for syntax highlighting, then our parser would choke on
translating the highlighted HTML back to ranges. Because at the end
it would also interpret the actual code as being a syntax highlighting
marker.
The fix is to keep track of where we are within a line of highlighted
code instead of trying to always match from the beginning of the line.
Google-Bug-Id: b/222020522
Release-Notes: skip
Change-Id: Ie977bd0964df42b7bb7bff870bb064ee45c4f4be
diff --git a/polygerrit-ui/app/utils/syntax-util.ts b/polygerrit-ui/app/utils/syntax-util.ts
index 9cba96d..2710ead 100644
--- a/polygerrit-ui/app/utils/syntax-util.ts
+++ b/polygerrit-ui/app/utils/syntax-util.ts
@@ -22,7 +22,7 @@
* is really that simple:
* https://github.com/highlightjs/highlight.js/blob/main/src/lib/html_renderer.js
*/
-const openingSpan = new RegExp('<span class="(.*?)">');
+const openingSpan = new RegExp('<span class="([^"]*?)">');
const closingSpan = new RegExp('</span>');
/**
@@ -65,7 +65,12 @@
// For each closing </span> close the latest unclosed range.
let removal: SpanRemoval | undefined;
line = unescapeHTML(line);
- while ((removal = removeFirstSpan(line)) !== undefined) {
+ // We are keeping track of where we are within the line going from left to
+ // right, because the "decoded" string may end up looking like a
+ // highlighting span. Thus `removeFirstSpan()` must not keep matching from
+ // the beginning of the line once it has started removing already.
+ let minOffset = 0;
+ while ((removal = removeFirstSpan(line, minOffset)) !== undefined) {
if (removal.type === SpanType.OPENING) {
ranges.push({
start: removal.offset,
@@ -76,6 +81,7 @@
const unclosed = lastUnclosed(ranges);
unclosed.length = removal.offset - unclosed.start;
}
+ minOffset = removal.offset;
line = removal.lineAfter;
}
@@ -126,25 +132,34 @@
/**
* Finds the first <span ...> or </span>, removes it from the line and returns
* details about the removal. Returns `undefined`, if neither is found.
+ *
+ * @param minOffset Searches for matches only beyond this offset.
*/
-export function removeFirstSpan(line: string): SpanRemoval | undefined {
- const openingMatch = openingSpan.exec(line);
+export function removeFirstSpan(
+ line: string,
+ minOffset = 0
+): SpanRemoval | undefined {
+ const partialLine = line.slice(minOffset);
+ const openingMatch = openingSpan.exec(partialLine);
const openingIndex = openingMatch?.index ?? Number.MAX_VALUE;
- const closingMatch = closingSpan.exec(line);
+ const closingMatch = closingSpan.exec(partialLine);
const closingIndex = closingMatch?.index ?? Number.MAX_VALUE;
if (openingIndex === Number.MAX_VALUE && closingIndex === Number.MAX_VALUE) {
return undefined;
}
const type =
openingIndex < closingIndex ? SpanType.OPENING : SpanType.CLOSING;
- const offset = type === SpanType.OPENING ? openingIndex : closingIndex;
+ const partialOffset = type === SpanType.OPENING ? openingIndex : closingIndex;
const match = type === SpanType.OPENING ? openingMatch : closingMatch;
if (match === null) return undefined;
const length = match[0].length;
const removal: SpanRemoval = {
type,
- lineAfter: line.slice(0, offset) + line.slice(offset + length),
- offset,
+ lineAfter:
+ line.slice(0, minOffset) +
+ partialLine.slice(0, partialOffset) +
+ partialLine.slice(partialOffset + length),
+ offset: minOffset + partialOffset,
class: type === SpanType.OPENING ? match[1] : undefined,
};
return removal;
diff --git a/polygerrit-ui/app/utils/syntax-util_test.ts b/polygerrit-ui/app/utils/syntax-util_test.ts
index fef908a..226f962 100644
--- a/polygerrit-ui/app/utils/syntax-util_test.ts
+++ b/polygerrit-ui/app/utils/syntax-util_test.ts
@@ -99,6 +99,27 @@
);
});
+ test('one complex line with escaped HTML', async () => {
+ assert.deepEqual(
+ highlightedStringToRanges(
+ ' <span class="tag"><<span class="name">span</span> <span class="attr">class</span>=<span class="string">"title"</span>></span>[[name]]<span class="tag"></<span class="name">span</span>></span>'
+ ),
+ [
+ {
+ ranges: [
+ // ' <span class="title">[[name]]</span>'
+ {start: 2, length: 20, className: 'tag'},
+ {start: 3, length: 4, className: 'name'},
+ {start: 8, length: 5, className: 'attr'},
+ {start: 14, length: 7, className: 'string'},
+ {start: 30, length: 7, className: 'tag'},
+ {start: 32, length: 4, className: 'name'},
+ ],
+ },
+ ]
+ );
+ });
+
test('two lines, one span each', async () => {
assert.deepEqual(
highlightedStringToRanges(