Skip to content

Commit

Permalink
fix(core): fix decoration offset edge cases (#728)
Browse files Browse the repository at this point in the history
  • Loading branch information
timothycohen authored Jul 26, 2024
1 parent 193f73f commit 1349bb4
Show file tree
Hide file tree
Showing 5 changed files with 64 additions and 7 deletions.
9 changes: 9 additions & 0 deletions packages/core/src/transformer-decorations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,21 @@ export function transformerDecorations(): ShikiTransformer {

function normalizePosition(p: OffsetOrPosition): ResolvedPosition {
if (typeof p === 'number') {
if (p < 0 || p > shiki.source.length)
throw new ShikiError(`Invalid decoration offset: ${p}. Code length: ${shiki.source.length}`)

return {
...converter.indexToPos(p),
offset: p,
}
}
else {
const line = converter.lines[p.line]
if (line === undefined)
throw new ShikiError(`Invalid decoration position ${JSON.stringify(p)}. Lines length: ${converter.lines.length}`)
if (p.character < 0 || p.character > line.length)
throw new ShikiError(`Invalid decoration position ${JSON.stringify(p)}. Line ${p.line} length: ${line.length}`)

return {
...p,
offset: converter.posToIndex(p.line, p.character),
Expand Down
2 changes: 0 additions & 2 deletions packages/core/src/types/decorations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,6 @@ export interface DecorationItem {
start: OffsetOrPosition
/**
* End offset or position of the decoration.
*
* If the
*/
end: OffsetOrPosition
/**
Expand Down
11 changes: 10 additions & 1 deletion packages/core/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ export function toArray<T>(x: MaybeArray<T>): T[] {
}

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

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

function indexToPos(index: number): Position {
if (index === code.length) {
return {
line: lines.length - 1,
character: lines[lines.length - 1].length,
}
}

let character = index
let line = 0
for (const lineText of lines) {
Expand Down
47 changes: 44 additions & 3 deletions packages/shiki/test/decorations.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ export function codeToHtml(
let result = hastToHtml(codeToHast(internal, code, options, context))
return result
}
`
// final`

describe('decorations', () => {
it('works', async () => {
Expand Down Expand Up @@ -78,6 +78,13 @@ describe('decorations', () => {
end: { line: 8, character: 25 },
properties: { class: 'highlighted' },
},
// "// final"
// Testing offset === code.length edge case
{
start: code.length - 8,
end: code.length,
properties: { class: 'highlighted' },
},
],
})

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

it('throws when chars overflow', async () => {
Expand All @@ -139,6 +146,40 @@ describe('decorations errors', () => {
],
})
}).rejects
.toThrowErrorMatchingInlineSnapshot(`[ShikiError: Failed to find end index for decoration {"line":0,"character":10,"offset":10}]`)
.toThrowErrorMatchingInlineSnapshot(`[ShikiError: Invalid decoration position {"line":0,"character":10}. Line 0 length: 4]`)

expect(async () => {
await codeToHtml(code, {
theme: 'vitesse-light',
lang: 'ts',
decorations: [
{
start: { line: 2, character: 1 },
end: { line: 1, character: 36 }, // actual position is { line: 2, character: 3, offset 40 }
},
],
})
}).rejects
.toThrowErrorMatchingInlineSnapshot(`[ShikiError: Invalid decoration position {"line":1,"character":36}. Line 1 length: 33]`)
})

it('throws when offset underflows/overflows', async () => {
expect(async () => {
await codeToHtml(code, {
theme: 'vitesse-light',
lang: 'ts',
decorations: [{ start: 1, end: 1000 }],
})
}).rejects
.toThrowErrorMatchingInlineSnapshot(`[ShikiError: Invalid decoration offset: 1000. Code length: 252]`)

expect(async () => {
await codeToHtml(code, {
theme: 'vitesse-light',
lang: 'ts',
decorations: [{ start: -3, end: 5 }],
})
}).rejects
.toThrowErrorMatchingInlineSnapshot(`[ShikiError: Invalid decoration offset: -3. Code length: 252]`)
})
})
2 changes: 1 addition & 1 deletion packages/shiki/test/out/decorations/basic.html

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit 1349bb4

Please sign in to comment.