diff --git a/docs/packages/transformers.md b/docs/packages/transformers.md index 08581fd91..d0d99b6f8 100644 --- a/docs/packages/transformers.md +++ b/docs/packages/transformers.md @@ -404,6 +404,35 @@ pre.shiki .indent::before { --- +### `transformerMetaDiff` + +Mark lines as added or removed based on the meta string provided on the code snippet. + +````md +```js {1+, 3-5-} +console.log('added') +console.log('kept') +console.log('removed') +console.log('removed') +console.log('removed') +``` +```` + +Renders (with custom CSS rules): + +```js {1+, 3-5-} +console.log('added') +console.log('kept') +console.log('removed') +console.log('removed') +console.log('removed') +``` + +- `1+` outputs: `` +- `3-5-` outputs: `` + +--- + ### `transformerMetaHighlight` Highlight lines based on the [meta string](/guide/transformers#meta) provided on the code snippet. diff --git a/packages/transformers/src/index.ts b/packages/transformers/src/index.ts index 7bc630870..311c1ee4b 100644 --- a/packages/transformers/src/index.ts +++ b/packages/transformers/src/index.ts @@ -1,6 +1,7 @@ export { createCommentNotationTransformer } from './shared/notation-transformer' export * from './transformers/compact-line-options' +export * from './transformers/meta-diff' export * from './transformers/meta-highlight' export * from './transformers/meta-highlight-word' export * from './transformers/notation-diff' diff --git a/packages/transformers/src/transformers/meta-diff.ts b/packages/transformers/src/transformers/meta-diff.ts new file mode 100644 index 000000000..4a3410ea9 --- /dev/null +++ b/packages/transformers/src/transformers/meta-diff.ts @@ -0,0 +1,99 @@ +import type { ShikiTransformer } from '@shikijs/types' + +export interface TransformerMetaDiffOptions { + /** + * Class for added lines + * + * @default 'diff add' + */ + classLineAdd?: string + /** + * Class for removed lines + * + * @default 'diff remove' + */ + classLineRemove?: string +} + +const symbol = Symbol('diff-lines') + +/** + * Allow using `{1+,3-5-}` in the code snippet meta to mark added/removed lines. + */ +export function transformerMetaDiff( + options: TransformerMetaDiffOptions = {}, +): ShikiTransformer { + const { + classLineAdd = 'diff add', + classLineRemove = 'diff remove', + } = options + + return { + name: '@shikijs/transformers:meta-diff', + line(node, lineNumber) { + if (!this.options.meta?.__raw) + return + + const meta = this.meta as { [symbol]: Map | null } + meta[symbol] ??= parseMetaDiffString(this.options.meta.__raw) + + const diffs = meta[symbol] + if (!diffs) + return + + const type = diffs.get(lineNumber) + if (type === '+') + this.addClassToHast(node, classLineAdd) + else if (type === '-') + this.addClassToHast(node, classLineRemove) + }, + } +} + +export function parseMetaDiffString(meta: string): Map | null { + if (!meta) + return null + + const match = meta.match(/\{([^}]+)\}/) + if (!match) + return null + + const map = new Map() + + // Split by comma + const parts = match[1].split(',') + for (const part of parts) { + const trimmed = part.trim() + if (!trimmed) + continue + + // Check for + or - suffix + const type = trimmed.endsWith('+') + ? '+' + : trimmed.endsWith('-') + ? '-' + : null + if (!type) + continue + + const content = trimmed.slice(0, -1) + + // Parse range or number + const range = content.split('-').map(n => Number.parseInt(n, 10)) + if (range.some(Number.isNaN)) + continue + + const lines = range.length === 1 + ? [range[0]] + : Array.from({ length: range[1] - range[0] + 1 }, (_, i) => range[0] + i) + + for (const line of lines) { + map.set(line, type) + } + } + + if (map.size === 0) + return null + + return map +} diff --git a/packages/transformers/test/fixtures.test.ts b/packages/transformers/test/fixtures.test.ts index 75b4184bc..5bbb0d67f 100644 --- a/packages/transformers/test/fixtures.test.ts +++ b/packages/transformers/test/fixtures.test.ts @@ -5,6 +5,7 @@ import { codeToHtml } from 'shiki' import { describe, expect, it } from 'vitest' import { transformerCompactLineOptions, + transformerMetaDiff, transformerNotationDiff, transformerNotationErrorLevel, transformerNotationFocus, @@ -88,6 +89,29 @@ body { margin: 0; } `, ) +suite( + 'meta-diff', + import.meta.glob('./fixtures/meta-diff/*.*', { query: '?raw', import: 'default', eager: true }), + [ + transformerMetaDiff(), + transformerRemoveLineBreak(), + ], + code => `${code} +`, + undefined, + { + meta: { + __raw: '{1+, 3-, 5+}', + }, + }, +) + suite( 'focus', import.meta.glob('./fixtures/focus/*.*', { query: '?raw', import: 'default', eager: true }), diff --git a/packages/transformers/test/fixtures/meta-diff/basic.js b/packages/transformers/test/fixtures/meta-diff/basic.js new file mode 100644 index 000000000..ee87e9647 --- /dev/null +++ b/packages/transformers/test/fixtures/meta-diff/basic.js @@ -0,0 +1,5 @@ +const a = 1 +const b = 2 +const c = 3 +const d = 4 +const e = 5 diff --git a/packages/transformers/test/fixtures/meta-diff/basic.js.output.html b/packages/transformers/test/fixtures/meta-diff/basic.js.output.html new file mode 100644 index 000000000..349b3df84 --- /dev/null +++ b/packages/transformers/test/fixtures/meta-diff/basic.js.output.html @@ -0,0 +1,8 @@ +
const a = 1const b = 2const c = 3const d = 4const e = 5
+ \ No newline at end of file diff --git a/test/exports/@shikijs/transformers.yaml b/test/exports/@shikijs/transformers.yaml index c4bf27d20..7c8e5c8dd 100644 --- a/test/exports/@shikijs/transformers.yaml +++ b/test/exports/@shikijs/transformers.yaml @@ -1,9 +1,11 @@ .: createCommentNotationTransformer: function findAllSubstringIndexes: function + parseMetaDiffString: function parseMetaHighlightString: function parseMetaHighlightWords: function transformerCompactLineOptions: function + transformerMetaDiff: function transformerMetaHighlight: function transformerMetaWordHighlight: function transformerNotationDiff: function