diff --git a/docs/packages/transformers.md b/docs/packages/transformers.md index 08581fd91..3683029c4 100644 --- a/docs/packages/transformers.md +++ b/docs/packages/transformers.md @@ -150,6 +150,33 @@ console.log('goodbye') --- +### `transformerNotationDiffWord` + +Use `[!code ++:text]` and `[!code --:text]` to mark added and removed text in the code. + +````md +```ts +// [\!code --:hello] +// [\!code ++:hola] +console.log('hello') +``` +```` + +Renders (with custom CSS rules): + +```ts +// [!code --:hello] +// [!code ++:hola] +console.log('hello') +``` + +- `// [!code ++:text]` outputs: `text` +- `// [!code --:text]` outputs: `text` + +You can also specify the number of lines to highlight matched words on, similar to `transformerNotationWordHighlight`. + +--- + ### `transformerNotationHighlight` Use `[!code highlight]` to highlight a line. diff --git a/packages/transformers/src/index.ts b/packages/transformers/src/index.ts index 7bc630870..7ec49352d 100644 --- a/packages/transformers/src/index.ts +++ b/packages/transformers/src/index.ts @@ -4,6 +4,7 @@ export * from './transformers/compact-line-options' export * from './transformers/meta-highlight' export * from './transformers/meta-highlight-word' export * from './transformers/notation-diff' +export * from './transformers/notation-diff-word' export * from './transformers/notation-error-level' export * from './transformers/notation-focus' export * from './transformers/notation-highlight' diff --git a/packages/transformers/src/transformers/notation-diff-word.ts b/packages/transformers/src/transformers/notation-diff-word.ts new file mode 100644 index 000000000..b2de00732 --- /dev/null +++ b/packages/transformers/src/transformers/notation-diff-word.ts @@ -0,0 +1,53 @@ +import type { ShikiTransformer, ShikiTransformerContext } from '@shikijs/core' +import type { Element } from 'hast' +import type { MatchAlgorithmOptions } from '../shared/notation-transformer' +import { highlightWordInLine } from '../shared/highlight-word' +import { createCommentNotationTransformer } from '../shared/notation-transformer' + +export interface TransformerNotationDiffWordOptions extends MatchAlgorithmOptions { + /** + * Class for added words + */ + classActiveWordAdd?: string + /** + * Class for removed words + */ + classActiveWordRemove?: string + /** + * Class added to the root element when the current code has diff + */ + classActivePre?: string +} + +export function transformerNotationDiffWord( + options: TransformerNotationDiffWordOptions = {}, +): ShikiTransformer { + const { + classActiveWordAdd = 'diff add', + classActiveWordRemove = 'diff remove', + classActivePre = 'has-diff', + } = options + + return createCommentNotationTransformer( + '@shikijs/transformers:notation-diff-word', + // Matches: [!code ++:word] or [!code --:word] or [!code ++:word:1] + /\s*\[!code (\+\+|--):((?:\\.|[^:\]])+)(:\d+)?\]/, + function (this: ShikiTransformerContext, [_, type, word, range]: string[], _line: Element, comment: Element, lines: Element[], index: number) { + const lineNum = range ? Number.parseInt(range.slice(1), 10) : lines.length + + // escape backslashes + word = word.replace(/\\(.)/g, '$1') + const className = type === '++' ? classActiveWordAdd : classActiveWordRemove + + for (let i = index; i < Math.min(index + lineNum, lines.length); i++) { + highlightWordInLine.call(this, lines[i], comment, word, className) + } + + if (classActivePre) + this.addClassToHast(this.pre, classActivePre) + + return true + }, + options.matchAlgorithm, + ) +} diff --git a/packages/transformers/test/fixtures.test.ts b/packages/transformers/test/fixtures.test.ts index 75b4184bc..bcab1ed9e 100644 --- a/packages/transformers/test/fixtures.test.ts +++ b/packages/transformers/test/fixtures.test.ts @@ -6,6 +6,7 @@ import { describe, expect, it } from 'vitest' import { transformerCompactLineOptions, transformerNotationDiff, + transformerNotationDiffWord, transformerNotationErrorLevel, transformerNotationFocus, transformerNotationHighlight, @@ -88,6 +89,23 @@ body { margin: 0; } `, ) +suite( + 'diff-word', + import.meta.glob('./fixtures/diff-word/*.*', { query: '?raw', import: 'default', eager: true }), + [ + transformerNotationDiffWord({ matchAlgorithm: 'v3' }), + transformerRemoveLineBreak(), + ], + code => `${code} +`, +) + suite( 'focus', import.meta.glob('./fixtures/focus/*.*', { query: '?raw', import: 'default', eager: true }), diff --git a/packages/transformers/test/fixtures/diff-word/basic.js b/packages/transformers/test/fixtures/diff-word/basic.js new file mode 100644 index 000000000..243bb66b5 --- /dev/null +++ b/packages/transformers/test/fixtures/diff-word/basic.js @@ -0,0 +1,8 @@ + +// [!code ++:foo] +const foo = 'bar' +// [!code --:bar] +const bar = 'baz' +// [!code ++:console.log:2] +console.log('hello') +console.log('world') diff --git a/packages/transformers/test/fixtures/diff-word/basic.js.output.html b/packages/transformers/test/fixtures/diff-word/basic.js.output.html new file mode 100644 index 000000000..01d2c253f --- /dev/null +++ b/packages/transformers/test/fixtures/diff-word/basic.js.output.html @@ -0,0 +1,8 @@ +
const foo = 'bar'const bar = 'baz'console.log('hello')console.log('world')
+
\ No newline at end of file
diff --git a/test/exports/@shikijs/transformers.yaml b/test/exports/@shikijs/transformers.yaml
index c4bf27d20..e4091665c 100644
--- a/test/exports/@shikijs/transformers.yaml
+++ b/test/exports/@shikijs/transformers.yaml
@@ -7,6 +7,7 @@
transformerMetaHighlight: function
transformerMetaWordHighlight: function
transformerNotationDiff: function
+ transformerNotationDiffWord: function
transformerNotationErrorLevel: function
transformerNotationFocus: function
transformerNotationHighlight: function