Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
88 changes: 54 additions & 34 deletions src/__tests__/highlightSpans.test.ts
Original file line number Diff line number Diff line change
@@ -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 + `<span class="remark-code-span-highlighted">${c}</span>`;
const pattern = /(?<![`\\])`([^`]+?)`/g;
return html.replace(pattern, (_m, c) => {
return `<span class="remark-code-span-highlighted">${c}</span>`;
});
}

describe('highlightSpans regex (issue #2)', () => {
it('highlights a span preceded by a non-backtick character', () => {
const result = applyHighlightPattern('call `method`(arg)');
expect(result).toContain('<span class="remark-code-span-highlighted">method</span>');
});
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('<span class="remark-code-span-highlighted">method</span>');
expect(result).toContain('(arg)');
});

it('highlights a span at the very start of a string', () => {
const result = applyHighlightPattern('`highlightedMethod`(arg1, arg2)');
expect(result).toContain('<span class="remark-code-span-highlighted">highlightedMethod</span>');
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('<span class="remark-code-span-highlighted">highlightedMethod</span>');
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('<span class="remark-code-span-highlighted">highlightedMethod</span>');
});

it('does not double-highlight adjacent backtick spans', () => {
const result = applyHighlightPattern('`a` and `b`');
expect(result).toContain('<span class="remark-code-span-highlighted">a</span>');
expect(result).toContain('<span class="remark-code-span-highlighted">b</span>');
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('<span class="remark-code-span-highlighted">highlightedMethod</span>');
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('<span class="remark-code-span-highlighted">class Token</span>');
expect(result).toMatch(/<\/span>\(value\)/);
});

it('correctly highlights both spans in `foo`(`bar`)', () => {
const result = applyHighlightPattern('`foo`(`bar`)');
expect(result).toContain('<span class="remark-code-span-highlighted">foo</span>');
expect(result).toContain('<span class="remark-code-span-highlighted">bar</span>');
// ( must not be inside either span
expect(result).not.toContain('remark-code-span-highlighted">(');
expect(result).not.toContain('(</span>');
});

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');
});
});
});
16 changes: 11 additions & 5 deletions src/mdeck/views/slideView.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = /(?<![\`\\])`([^`]+?)`/g;
} else if (highlightSpans instanceof RegExp) {
if (!highlightSpans.global) throw new Error('highlightSpans RegExp must have /g flag');
pattern = new RegExp('(^|[\\s\\S])' + highlightSpans.source, (highlightSpans.flags || 'g') + 'm');
pattern = new RegExp('(?<![\\`\\\\])' + highlightSpans.source, highlightSpans.flags || 'g');
} else {
throw new Error('Illegal value for highlightSpans');
}
Array.from(block.childNodes).forEach((node) => {
if (node instanceof HTMLElement) {
node.innerHTML = node.innerHTML.replace(pattern, (m, e, c) => {
if (e === '\\') return m.slice(1);
return e + `<span class="remark-code-span-highlighted mdeck-code-span-highlighted">${c}</span>`;
node.innerHTML = node.innerHTML.replace(pattern, (_m, c) => {
return `<span class="remark-code-span-highlighted mdeck-code-span-highlighted">${c}</span>`;
});
}
});
Expand Down