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');
+ });
});