Skip to content

Commit 1349bb4

Browse files
authoredJul 26, 2024··
fix(core): fix decoration offset edge cases (#728)
1 parent 193f73f commit 1349bb4

File tree

5 files changed

+64
-7
lines changed

5 files changed

+64
-7
lines changed
 

‎packages/core/src/transformer-decorations.ts

+9
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,21 @@ export function transformerDecorations(): ShikiTransformer {
2121

2222
function normalizePosition(p: OffsetOrPosition): ResolvedPosition {
2323
if (typeof p === 'number') {
24+
if (p < 0 || p > shiki.source.length)
25+
throw new ShikiError(`Invalid decoration offset: ${p}. Code length: ${shiki.source.length}`)
26+
2427
return {
2528
...converter.indexToPos(p),
2629
offset: p,
2730
}
2831
}
2932
else {
33+
const line = converter.lines[p.line]
34+
if (line === undefined)
35+
throw new ShikiError(`Invalid decoration position ${JSON.stringify(p)}. Lines length: ${converter.lines.length}`)
36+
if (p.character < 0 || p.character > line.length)
37+
throw new ShikiError(`Invalid decoration position ${JSON.stringify(p)}. Line ${p.line} length: ${line.length}`)
38+
3039
return {
3140
...p,
3241
offset: converter.posToIndex(p.line, p.character),

‎packages/core/src/types/decorations.ts

-2
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,6 @@ export interface DecorationItem {
1414
start: OffsetOrPosition
1515
/**
1616
* End offset or position of the decoration.
17-
*
18-
* If the
1917
*/
2018
end: OffsetOrPosition
2119
/**

‎packages/core/src/utils.ts

+10-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ export function toArray<T>(x: MaybeArray<T>): T[] {
77
}
88

99
/**
10-
* Slipt a string into lines, each line preserves the line ending.
10+
* Split a string into lines, each line preserves the line ending.
1111
*/
1212
export function splitLines(code: string, preserveEnding = false): [string, number][] {
1313
const parts = code.split(/(\r?\n)/g)
@@ -192,11 +192,20 @@ export function stringifyTokenStyle(token: Record<string, string>) {
192192

193193
/**
194194
* Creates a converter between index and position in a code block.
195+
*
196+
* Overflow/underflow are unchecked.
195197
*/
196198
export function createPositionConverter(code: string) {
197199
const lines = splitLines(code, true).map(([line]) => line)
198200

199201
function indexToPos(index: number): Position {
202+
if (index === code.length) {
203+
return {
204+
line: lines.length - 1,
205+
character: lines[lines.length - 1].length,
206+
}
207+
}
208+
200209
let character = index
201210
let line = 0
202211
for (const lineText of lines) {

‎packages/shiki/test/decorations.test.ts

+44-3
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ export function codeToHtml(
2323
let result = hastToHtml(codeToHast(internal, code, options, context))
2424
return result
2525
}
26-
`
26+
// final`
2727

2828
describe('decorations', () => {
2929
it('works', async () => {
@@ -78,6 +78,13 @@ describe('decorations', () => {
7878
end: { line: 8, character: 25 },
7979
properties: { class: 'highlighted' },
8080
},
81+
// "// final"
82+
// Testing offset === code.length edge case
83+
{
84+
start: code.length - 8,
85+
end: code.length,
86+
properties: { class: 'highlighted' },
87+
},
8188
],
8289
})
8390

@@ -126,7 +133,7 @@ describe('decorations errors', () => {
126133
],
127134
})
128135
}).rejects
129-
.toThrowErrorMatchingInlineSnapshot(`[TypeError: Cannot read properties of undefined (reading 'length')]`)
136+
.toThrowErrorMatchingInlineSnapshot(`[ShikiError: Invalid decoration position {"line":100,"character":0}. Lines length: 12]`)
130137
})
131138

132139
it('throws when chars overflow', async () => {
@@ -139,6 +146,40 @@ describe('decorations errors', () => {
139146
],
140147
})
141148
}).rejects
142-
.toThrowErrorMatchingInlineSnapshot(`[ShikiError: Failed to find end index for decoration {"line":0,"character":10,"offset":10}]`)
149+
.toThrowErrorMatchingInlineSnapshot(`[ShikiError: Invalid decoration position {"line":0,"character":10}. Line 0 length: 4]`)
150+
151+
expect(async () => {
152+
await codeToHtml(code, {
153+
theme: 'vitesse-light',
154+
lang: 'ts',
155+
decorations: [
156+
{
157+
start: { line: 2, character: 1 },
158+
end: { line: 1, character: 36 }, // actual position is { line: 2, character: 3, offset 40 }
159+
},
160+
],
161+
})
162+
}).rejects
163+
.toThrowErrorMatchingInlineSnapshot(`[ShikiError: Invalid decoration position {"line":1,"character":36}. Line 1 length: 33]`)
164+
})
165+
166+
it('throws when offset underflows/overflows', async () => {
167+
expect(async () => {
168+
await codeToHtml(code, {
169+
theme: 'vitesse-light',
170+
lang: 'ts',
171+
decorations: [{ start: 1, end: 1000 }],
172+
})
173+
}).rejects
174+
.toThrowErrorMatchingInlineSnapshot(`[ShikiError: Invalid decoration offset: 1000. Code length: 252]`)
175+
176+
expect(async () => {
177+
await codeToHtml(code, {
178+
theme: 'vitesse-light',
179+
lang: 'ts',
180+
decorations: [{ start: -3, end: 5 }],
181+
})
182+
}).rejects
183+
.toThrowErrorMatchingInlineSnapshot(`[ShikiError: Invalid decoration offset: -3. Code length: 252]`)
143184
})
144185
})

‎packages/shiki/test/out/decorations/basic.html

+1-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)
Please sign in to comment.