diff --git a/src/__tests__/highlightSpans.test.ts b/src/__tests__/highlightSpans.test.ts index 207a8de..45823c9 100644 --- a/src/__tests__/highlightSpans.test.ts +++ b/src/__tests__/highlightSpans.test.ts @@ -1,50 +1,70 @@ import { describe, it, expect } from 'vitest'; -// Test the highlight-spans regex behaviour directly. -// The bug (issue #2): a backtick span at the very start of a line was not -// recognised because the pattern required a non-backtick character before the -// opening backtick. The fix adds `^` as an alternative via the `m` flag. - +// Mirror the fixed pattern from slideView.ts highlightBlockSpans. +// Uses a lookbehind so no preceding character is consumed, fixing: +// issue #2 – spans at the start of a line were never matched +// issue #3 – adjacent spans like `foo`(`bar`) incorrectly captured ( function applyHighlightPattern(html: string): string { - // Mirrors the fixed pattern in highlightBlockSpans (highlightSpans === true). - const pattern = /(^|[^`])`([^`]+?)`/gm; - return html.replace(pattern, (m, e, c) => { - 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}`; }); } });