From d5088bc3db70a7253ff476867145c64076467136 Mon Sep 17 00:00:00 2001 From: Daniel Pecos Martinez Date: Thu, 16 Apr 2026 13:41:09 +0200 Subject: [PATCH] fix: use lookbehind in highlightSpans to fix adjacent spans and start-of-line The old pattern ([^`])`([^`]+?)` required consuming a preceding character as group 1, which caused two bugs: - Issue #2: spans at the start of a line were never matched because there was no preceding character to consume. - Issue #3: adjacent spans like `foo`(`bar`) were broken because the pattern consumed the last char of the first span ('o' from 'foo') and then treated that span's closing backtick as the opening of a new span, capturing '(' as the highlighted content. Replace the consuming group with a negative lookbehind (? { - if (e === '\\') return m.slice(1); - return e + `${c}`; + const pattern = /(? { + return `${c}`; }); } -describe('highlightSpans regex (issue #2)', () => { - it('highlights a span preceded by a non-backtick character', () => { - const result = applyHighlightPattern('call `method`(arg)'); - expect(result).toContain('method'); - }); +describe('highlightSpans regex', () => { + describe('basic highlighting', () => { + it('highlights a span preceded by a non-backtick character', () => { + const result = applyHighlightPattern('call `method`(arg)'); + expect(result).toContain('method'); + expect(result).toContain('(arg)'); + }); - it('highlights a span at the very start of a string', () => { - const result = applyHighlightPattern('`highlightedMethod`(arg1, arg2)'); - expect(result).toContain('highlightedMethod'); + it('does not treat fenced code delimiters (```) as highlight spans', () => { + const result = applyHighlightPattern('```js\ncode\n```'); + expect(result).not.toContain('remark-code-span-highlighted'); + }); }); - it('highlights a span at the start of a new line', () => { - const html = 'normalMethod(a)\n`highlightedMethod`(b)'; - const result = applyHighlightPattern(html); - expect(result).toContain('highlightedMethod'); - expect(result).toContain('normalMethod(a)'); - }); + describe('issue #2 – span at start of line', () => { + it('highlights a span at the very start of a string', () => { + const result = applyHighlightPattern('`highlightedMethod`(arg1, arg2)'); + expect(result).toContain('highlightedMethod'); + }); - it('does not double-highlight adjacent backtick spans', () => { - const result = applyHighlightPattern('`a` and `b`'); - expect(result).toContain('a'); - expect(result).toContain('b'); + it('highlights a span at the start of a new line', () => { + const html = 'normalMethod(a)\n`highlightedMethod`(b)'; + const result = applyHighlightPattern(html); + expect(result).toContain('highlightedMethod'); + expect(result).toContain('normalMethod(a)'); + }); }); - it('does not highlight a backtick span that is escaped', () => { - const result = applyHighlightPattern('\\`notHighlighted`'); - expect(result).not.toContain('remark-code-span-highlighted'); + describe('issue #3 – trailing parenthesis not captured', () => { + it('does not include ( after closing backtick in the highlighted span', () => { + const result = applyHighlightPattern('case `class Token`(value)'); + expect(result).toContain('class Token'); + expect(result).toMatch(/<\/span>\(value\)/); + }); + + it('correctly highlights both spans in `foo`(`bar`)', () => { + const result = applyHighlightPattern('`foo`(`bar`)'); + expect(result).toContain('foo'); + expect(result).toContain('bar'); + // ( must not be inside either span + expect(result).not.toContain('remark-code-span-highlighted">('); + expect(result).not.toContain('('); + }); + + it('does not capture ( when it follows the closing backtick with spaces', () => { + const result = applyHighlightPattern('`method` (arg)'); + expect(result).toMatch(/<\/span> \(arg\)/); + }); }); - it('does not treat fenced code delimiters (```) as highlight spans', () => { - const result = applyHighlightPattern('```js\ncode\n```'); - expect(result).not.toContain('remark-code-span-highlighted'); + describe('escape handling', () => { + it('does not highlight a backtick span preceded by a backslash', () => { + const result = applyHighlightPattern('\\`notHighlighted`'); + expect(result).not.toContain('remark-code-span-highlighted'); + }); }); }); diff --git a/src/mdeck/views/slideView.ts b/src/mdeck/views/slideView.ts index a28fc34..02bb3f9 100644 --- a/src/mdeck/views/slideView.ts +++ b/src/mdeck/views/slideView.ts @@ -211,18 +211,24 @@ function highlightBlockLines(block: HTMLElement, lines: number[]): void { function highlightBlockSpans(block: HTMLElement, highlightSpans: boolean | RegExp): void { let pattern: RegExp; if (highlightSpans === true) { - pattern = /(^|[^`])`([^`]+?)`/gm; + // Use a lookbehind so the opening backtick is not consumed along with its + // preceding character. This fixes two bugs: + // - #2: spans at the start of a line were never matched (no preceding char) + // - #3: adjacent spans like `foo`(`bar`) incorrectly captured ( as content + // because the old ([^`]) group consumed the last char of the first span, + // treating its closing backtick as the opening of a new span. + // The lookbehind also handles escape: \` is not treated as an opening backtick. + pattern = /(? { if (node instanceof HTMLElement) { - node.innerHTML = node.innerHTML.replace(pattern, (m, e, c) => { - if (e === '\\') return m.slice(1); - return e + `${c}`; + node.innerHTML = node.innerHTML.replace(pattern, (_m, c) => { + return `${c}`; }); } });