diff --git a/docs/userGuide/syntax/code.md b/docs/userGuide/syntax/code.md index 96a01eb264..85e6330353 100644 --- a/docs/userGuide/syntax/code.md +++ b/docs/userGuide/syntax/code.md @@ -24,6 +24,10 @@ Features: * Line highlighting * Code block headers + +Tabs (i.e. "\t") are automatically converted to 4 whitespaces by default. + + ##### Syntax coloring To enable syntax coloring, specify a language next to the backticks before the fenced code block. @@ -124,7 +128,7 @@ function subtract(a, b) { **Character-bounded highlight** -```js {.line-numbers highlight-lines="1[0:3], 1[6:10], 2[5:], 3[:6]"} +```js {.line-numbers highlight-lines="1[0:3], 1[6:10], 2[5:], 3[:6], +4[:6]"} function multiply(a, b) { const product = a * b; console.log('Product = ${product}'); @@ -203,7 +207,7 @@ Type | Format | Example -----|--------|-------- **Full text highlight**
Highlights the entirety of the text portion of the line | The line numbers as-is (subject to the starting line number set in `start-from`). | `3`, `5` **Substring highlight**
Highlights _all_ occurrences of a substring in the line | `lineNumber[part]`

_Limitations_: `part` must be wrapped in quotes. If `part` contains a quote, escape it with a backslash (`\`). | `3['Inventory']`,`4['It\'s designed']` -**Character-bounded highlight**
Highlights a specific range of characters in the line | `lineNumber[start:end]`, highlights from character position `start` up to (but not including) `end`.

Character positions start from `0` as the first non-whitespace character, upwards.

Omit either `start`/`end` to highlight from the start / up to the end, respectively. | `19[1:5]`,`30[10:]`,`35[:20]` +**Character-bounded highlight**
Highlights a specific range of characters in the line | `lineNumber[start:end]`, highlights from character position `start` up to (but not including) `end`.

Character positions start from `0` as the first non-whitespace character, upwards.

Omit either `start`/`end` to highlight from the start / up to the end, respectively.

To use absolute positions (i.e. including leading whitespace), add a `+` symbol to the beginning of the rule: `+lineNumber[start:end]` | `19[1:5]`,`30[10:]`,`35[:20]`, `+3[1:4]` **Word-bounded highlight**
Highlights a specific range of words in the line | `lineNumber[start::end]`, highlights from word position `start` up to (but not including) `end`.

Word positions start from `0` as the first word (sequence of non-whitespace characters), upwards.

Omit either `start`/`end` to highlight from the start / up to the end, respectively. | `5[2::4]`,`9[1::]`,`11[::5]` **Full line highlight**
Highlights the entirety of the line | `lineNumber[:]` | `7[:]` diff --git a/packages/core/src/lib/markdown-it/highlight/HighlightRuleComponent.ts b/packages/core/src/lib/markdown-it/highlight/HighlightRuleComponent.ts index 61e16cd3a9..d6ed452dfc 100644 --- a/packages/core/src/lib/markdown-it/highlight/HighlightRuleComponent.ts +++ b/packages/core/src/lib/markdown-it/highlight/HighlightRuleComponent.ts @@ -1,6 +1,6 @@ import { splitCodeAndIndentation } from './helper'; -const LINESLICE_CHAR_REGEX = /(\d+)\[(\d*):(\d*)]/; +const LINESLICE_CHAR_REGEX = /(\+?)(\d+)\[(\d*):(\d*)]/; const LINESLICE_WORD_REGEX = /(\d+)\[(\d*)::(\d*)]/; const LINEPART_REGEX = /(\d+)\[(["'])((?:\\.|[^\\])*?)\2]/; const UNBOUNDED = -1; @@ -43,9 +43,15 @@ export class HighlightRuleComponent { const linesliceWordMatch = compString.match(LINESLICE_WORD_REGEX); const sliceMatch = linesliceCharMatch || linesliceWordMatch; if (sliceMatch) { - // There are four capturing groups: [full match, line number, start bound, end bound] + // There are four/five capturing groups: [full match, is absolute indexing (for char match), + // line number, start bound, end bound] const groups = sliceMatch.slice(1); // discard full match + let isAbsoluteIndexing = false; + if (sliceMatch === linesliceCharMatch) { + isAbsoluteIndexing = groups.shift() === '+'; + } + const lineNumber = HighlightRuleComponent .isValidLineNumber(groups.shift() ?? '', 1, lines.length, lineNumberOffset); if (!lineNumber) return null; @@ -58,7 +64,7 @@ export class HighlightRuleComponent { let bound = groups.map(x => (x !== '' ? parseInt(x, 10) : UNBOUNDED)) as [number, number]; const isCharSlice = sliceMatch === linesliceCharMatch; bound = isCharSlice - ? HighlightRuleComponent.computeCharBounds(bound, lines[lineNumber - 1]) + ? HighlightRuleComponent.computeCharBounds(bound, lines[lineNumber - 1], isAbsoluteIndexing) : HighlightRuleComponent.computeWordBounds(bound, lines[lineNumber - 1]); return new HighlightRuleComponent(lineNumber, true, [bound]); @@ -95,38 +101,30 @@ export class HighlightRuleComponent { * comparing the bounds and the line's range. * * If the bound does not specify either the start or the end bound, the computed bound will default - * to the start or end of line, excluding leading whitespaces. + * to the start or end of line. The bound can be either absolute or relative to the indentation level. * * @param bound The user-defined bound * @param line The given line + * @param isAbsoluteIndexing Whether the bound is absolute or relative to the indentation level * @returns {[number, number]} The actual bound computed */ - static computeCharBounds(bound: [number, number], line: string): [number, number] { + static computeCharBounds(bound: [number, number], line: string, + isAbsoluteIndexing: boolean): [number, number] { const [indents] = splitCodeAndIndentation(line); let [start, end] = bound; if (start === UNBOUNDED) { - start = indents.length; + start = isAbsoluteIndexing ? 0 : indents.length; } else { - start += indents.length; - // Clamp values - if (start < indents.length) { - start = indents.length; - } else if (start > line.length) { - start = line.length; - } + start = isAbsoluteIndexing ? start : Math.max(start + indents.length, indents.length); + start = Math.min(start, line.length); } if (end === UNBOUNDED) { end = line.length; } else { - end += indents.length; - // Clamp values - if (end < indents.length) { - end = indents.length; - } else if (end > line.length) { - end = line.length; - } + end = isAbsoluteIndexing ? end : Math.max(end + indents.length, indents.length); + end = Math.min(end, line.length); } return [start, end]; diff --git a/packages/core/src/lib/markdown-it/highlight/Highlighter.ts b/packages/core/src/lib/markdown-it/highlight/Highlighter.ts index ee6cc2325b..3743930b86 100644 --- a/packages/core/src/lib/markdown-it/highlight/Highlighter.ts +++ b/packages/core/src/lib/markdown-it/highlight/Highlighter.ts @@ -18,6 +18,7 @@ export class Highlighter { */ const mergedBounds = collateAllIntervals(bounds); const dataStr = mergedBounds.map(bound => bound.join('-')).join(','); - return `${code}\n`; + const formattedCode = code.replace(/\t/g, ' '); // Convert tabs to 4 spaces by default + return `${formattedCode}\n`; } } diff --git a/packages/core/src/lib/markdown-it/index.ts b/packages/core/src/lib/markdown-it/index.ts index 565d2d0e4a..91d355c5cc 100644 --- a/packages/core/src/lib/markdown-it/index.ts +++ b/packages/core/src/lib/markdown-it/index.ts @@ -72,6 +72,7 @@ markdownIt.renderer.rules.fence = (tokens: Token[], const token = tokens[idx]; const lang = token.info || ''; let str = token.content; + str = str.replace(/\t/g, ' '); // Convert tabs to 4 spaces by default let highlighted = false; let lines: string[] = []; diff --git a/packages/core/test/unit/lib/markdown-it/highlight/HighlightRuleComponent.test.ts b/packages/core/test/unit/lib/markdown-it/highlight/HighlightRuleComponent.test.ts index 593cb30fe6..8ee9420f9a 100644 --- a/packages/core/test/unit/lib/markdown-it/highlight/HighlightRuleComponent.test.ts +++ b/packages/core/test/unit/lib/markdown-it/highlight/HighlightRuleComponent.test.ts @@ -44,28 +44,55 @@ describe('parseRuleComponent', () => { }); }); -describe('computeCharBounds', () => { +describe('computeCharBounds, relative to indent-level', () => { test('computes character bounds correctly', () => { - const bounds = HighlightRuleComponent.computeCharBounds([2, 5], ' some text'); + const bounds = HighlightRuleComponent.computeCharBounds([2, 5], ' some text', false); expect(bounds).toEqual([4, 7]); }); test('handles unbounded start correctly', () => { - const bounds = HighlightRuleComponent.computeCharBounds([-1, 4], ' some text'); + const bounds = HighlightRuleComponent.computeCharBounds([-1, 4], ' some text', false); expect(bounds).toEqual([2, 6]); }); test('handles unbounded end correctly', () => { - const bounds = HighlightRuleComponent.computeCharBounds([3, -1], ' some text'); + const bounds = HighlightRuleComponent.computeCharBounds([3, -1], ' some text', false); expect(bounds).toEqual([5, ' some text'.length]); }); test('handles out-of-range bounds correctly', () => { - const bounds = HighlightRuleComponent.computeCharBounds([30, 40], ' some text'); + const bounds = HighlightRuleComponent.computeCharBounds([30, 40], ' some text', false); expect(bounds).toEqual([' some text'.length, ' some text'.length]); }); }); +describe('computeCharBounds, absolute value bounds', () => { + test('computes character bounds correctly', () => { + const bounds = HighlightRuleComponent.computeCharBounds([2, 5], ' some text', true); + expect(bounds).toEqual([2, 5]); + }); + + test('handles unbounded start correctly', () => { + const bounds = HighlightRuleComponent.computeCharBounds([-1, 4], ' some text', true); + expect(bounds).toEqual([0, 4]); + }); + + test('handles unbounded end correctly', () => { + const bounds = HighlightRuleComponent.computeCharBounds([3, -1], ' some text', true); + expect(bounds).toEqual([3, ' some text'.length]); + }); + + test('handles out-of-range bounds correctly', () => { + const bounds = HighlightRuleComponent.computeCharBounds([30, 40], ' some text', true); + expect(bounds).toEqual([' some text'.length, ' some text'.length]); + }); + + test('handles bounds spanning from start to line length correctly', () => { + const bounds = HighlightRuleComponent.computeCharBounds([0, 11], ' some text', true); + expect(bounds).toEqual([0, 11]); + }); +}); + describe('computeWordBounds', () => { test('computes word bounds correctly', () => { const bounds = HighlightRuleComponent.computeWordBounds([1, 2], ' some text here'); diff --git a/packages/core/test/unit/lib/markdown-it/highlight/Highlighter.test.ts b/packages/core/test/unit/lib/markdown-it/highlight/Highlighter.test.ts index 83f742abc0..44b712ff3c 100644 --- a/packages/core/test/unit/lib/markdown-it/highlight/Highlighter.test.ts +++ b/packages/core/test/unit/lib/markdown-it/highlight/Highlighter.test.ts @@ -36,4 +36,11 @@ describe('highlightPartOfText', () => { const result = Highlighter.highlightPartOfText(code, bounds); expect(result).toBe('const x = 10;\n'); }); + + test('handles tabs in code correctly', () => { + const code = '\tconst x = 10;'; + const bounds: Array<[number, number]> = [[0, 4], [8, 10]]; + const result = Highlighter.highlightPartOfText(code, bounds); + expect(result).toBe(' const x = 10;\n'); + }); });