diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 57c84334..ee283f5e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -440,6 +440,9 @@ importers: '@sentry/astro': specifier: ^10.32.1 version: 10.42.0(astro@5.18.0(@netlify/blobs@10.7.0)(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(rollup@4.59.0)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.2))(rollup@4.59.0) + '@shikijs/transformers': + specifier: ^4.0.2 + version: 4.0.2 '@tailwindcss/vite': specifier: ^4.2.1 version: 4.2.1(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(tsx@4.21.0)(yaml@2.8.2)) @@ -2945,6 +2948,10 @@ packages: '@shikijs/core@3.23.0': resolution: {integrity: sha512-NSWQz0riNb67xthdm5br6lAkvpDJRTgB36fxlo37ZzM2yq0PQFFzbd8psqC2XMPgCzo1fW6cVi18+ArJ44wqgA==} + '@shikijs/core@4.0.2': + resolution: {integrity: sha512-hxT0YF4ExEqB8G/qFdtJvpmHXBYJ2lWW7qTHDarVkIudPFE6iCIrqdgWxGn5s+ppkGXI0aEGlibI0PAyzP3zlw==} + engines: {node: '>=20'} + '@shikijs/engine-javascript@3.23.0': resolution: {integrity: sha512-aHt9eiGFobmWR5uqJUViySI1bHMqrAgamWE1TYSUoftkAeCCAiGawPMwM+VCadylQtF4V3VNOZ5LmfItH5f3yA==} @@ -2954,12 +2961,24 @@ packages: '@shikijs/langs@3.23.0': resolution: {integrity: sha512-2Ep4W3Re5aB1/62RSYQInK9mM3HsLeB91cHqznAJMuylqjzNVAVCMnNWRHFtcNHXsoNRayP9z1qj4Sq3nMqYXg==} + '@shikijs/primitive@4.0.2': + resolution: {integrity: sha512-M6UMPrSa3fN5ayeJwFVl9qWofl273wtK1VG8ySDZ1mQBfhCpdd8nEx7nPZ/tk7k+TYcpqBZzj/AnwxT9lO+HJw==} + engines: {node: '>=20'} + '@shikijs/themes@3.23.0': resolution: {integrity: sha512-5qySYa1ZgAT18HR/ypENL9cUSGOeI2x+4IvYJu4JgVJdizn6kG4ia5Q1jDEOi7gTbN4RbuYtmHh0W3eccOrjMA==} + '@shikijs/transformers@4.0.2': + resolution: {integrity: sha512-1+L0gf9v+SdDXs08vjaLb3mBFa8U7u37cwcBQIv/HCocLwX69Tt6LpUCjtB+UUTvQxI7BnjZKhN/wMjhHBcJGg==} + engines: {node: '>=20'} + '@shikijs/types@3.23.0': resolution: {integrity: sha512-3JZ5HXOZfYjsYSk0yPwBrkupyYSLpAE26Qc0HLghhZNGTZg/SKxXIIgoxOpmmeQP0RRSDJTk1/vPfw9tbw+jSQ==} + '@shikijs/types@4.0.2': + resolution: {integrity: sha512-qzbeRooUTPnLE+sHD/Z8DStmaDgnbbc/pMrU203950aRqjX/6AFHeDYT+j00y2lPdz0ywJKx7o/7qnqTivtlXg==} + engines: {node: '>=20'} + '@shikijs/vscode-textmate@10.0.2': resolution: {integrity: sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==} @@ -10302,6 +10321,14 @@ snapshots: '@types/hast': 3.0.4 hast-util-to-html: 9.0.5 + '@shikijs/core@4.0.2': + dependencies: + '@shikijs/primitive': 4.0.2 + '@shikijs/types': 4.0.2 + '@shikijs/vscode-textmate': 10.0.2 + '@types/hast': 3.0.4 + hast-util-to-html: 9.0.5 + '@shikijs/engine-javascript@3.23.0': dependencies: '@shikijs/types': 3.23.0 @@ -10317,15 +10344,31 @@ snapshots: dependencies: '@shikijs/types': 3.23.0 + '@shikijs/primitive@4.0.2': + dependencies: + '@shikijs/types': 4.0.2 + '@shikijs/vscode-textmate': 10.0.2 + '@types/hast': 3.0.4 + '@shikijs/themes@3.23.0': dependencies: '@shikijs/types': 3.23.0 + '@shikijs/transformers@4.0.2': + dependencies: + '@shikijs/core': 4.0.2 + '@shikijs/types': 4.0.2 + '@shikijs/types@3.23.0': dependencies: '@shikijs/vscode-textmate': 10.0.2 '@types/hast': 3.0.4 + '@shikijs/types@4.0.2': + dependencies: + '@shikijs/vscode-textmate': 10.0.2 + '@types/hast': 3.0.4 + '@shikijs/vscode-textmate@10.0.2': {} '@simple-libs/stream-utils@1.2.0': {} @@ -10804,7 +10847,7 @@ snapshots: std-env: 3.10.0 test-exclude: 7.0.2 tinyrainbow: 2.0.0 - vitest: 3.2.4(@types/debug@4.1.12)(@types/node@22.19.15)(@vitest/browser@3.2.4)(@vitest/ui@3.2.4)(happy-dom@18.0.1)(jiti@2.6.1)(jsdom@26.1.0)(lightningcss@1.32.0)(tsx@4.21.0)(yaml@2.8.2) + vitest: 3.2.4(@types/debug@4.1.12)(@types/node@22.19.15)(@vitest/browser@3.2.4)(@vitest/ui@3.2.4)(happy-dom@18.0.1)(jiti@2.6.1)(jsdom@27.4.0)(lightningcss@1.32.0)(tsx@4.21.0)(yaml@2.8.2) optionalDependencies: '@vitest/browser': 3.2.4(playwright@1.58.2)(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(tsx@4.21.0)(yaml@2.8.2))(vitest@3.2.4) transitivePeerDependencies: diff --git a/site/astro.config.mjs b/site/astro.config.mjs index 6696b690..8a21849d 100644 --- a/site/astro.config.mjs +++ b/site/astro.config.mjs @@ -15,6 +15,7 @@ import pagefind from './integrations/pagefind'; import rehypePrepareCodeBlocks from './src/utils/rehypePrepareCodeBlocks'; import remarkConditionalHeadings from './src/utils/remarkConditionalHeadings'; import { remarkReadingTime } from './src/utils/remarkReadingTime.mjs'; +import { shikiNotationTransformers } from './src/utils/shikiNotationTransformers'; import shikiTransformMetadata from './src/utils/shikiTransformMetadata'; // On production deploys, use the custom domain — DEPLOY_PRIME_URL always returns @@ -98,8 +99,7 @@ export default defineConfig({ light: 'gruvbox-dark-hard', dark: 'gruvbox-dark-soft', }, - // TODO more shiki transformers - transformers: [shikiTransformMetadata], + transformers: [shikiTransformMetadata, ...shikiNotationTransformers], }, remarkPlugins: [remarkConditionalHeadings, remarkReadingTime], rehypePlugins: [rehypePrepareCodeBlocks], diff --git a/site/package.json b/site/package.json index 7bf06a57..3e968e39 100644 --- a/site/package.json +++ b/site/package.json @@ -27,6 +27,7 @@ "@nanostores/react": "^1.0.0", "@pagefind/default-ui": "^1.4.0", "@sentry/astro": "^10.32.1", + "@shikijs/transformers": "^4.0.2", "@tailwindcss/vite": "^4.2.1", "@videojs/html": "workspace:*", "@videojs/react": "workspace:*", diff --git a/site/src/components/Code/Shared.tsx b/site/src/components/Code/Shared.tsx index 027ac8c4..47481467 100644 --- a/site/src/components/Code/Shared.tsx +++ b/site/src/components/Code/Shared.tsx @@ -1,6 +1,7 @@ import clsx from 'clsx'; import type { BundledLanguage, Highlighter } from 'shiki'; import { hastToHtml } from 'shiki'; +import { shikiNotationTransformers } from '@/utils/shikiNotationTransformers'; export interface SharedProps { code: string; @@ -15,6 +16,7 @@ export default function Shared({ code, lang, highlighter }: SharedProps) { light: 'gruvbox-dark-hard', dark: 'gruvbox-dark-soft', }, + transformers: shikiNotationTransformers, }); // shiki gives us a root > pre > code > text structure @@ -39,7 +41,7 @@ export default function Shared({ code, lang, highlighter }: SharedProps) { const { class: codeClassName } = codeProps; return ( -
+    
       
     
); diff --git a/site/src/content/docs/how-to/write-guides.mdx b/site/src/content/docs/how-to/write-guides.mdx index 160f41ee..26643c8b 100644 --- a/site/src/content/docs/how-to/write-guides.mdx +++ b/site/src/content/docs/how-to/write-guides.mdx @@ -216,6 +216,44 @@ we can use 3 backticks ``` in new line and write snippet and close with 3 backti ``` +### Code annotations + +You can annotate code blocks with special comments to highlight, diff, focus, or word-highlight specific lines. The annotation comments are stripped from the rendered output (and from copy/paste). + +#### Diff + +Show added and removed lines with `// [!code ++]` and `// [!code --]`: + +```ts +function greet(name: string) { + console.log('Hello') // [!code --] + console.log(`Hello, ${name}!`) // [!code ++] + console.log(`It's great to meet you.`) // [!code ++] +} +``` + +#### Focus + +Dim everything except the focused lines with `// [!code focus]`. Hovering the code block reveals all lines: + +```ts +function greet(name: string) { + const message = `hello, ${name}!` + console.log(message) // [!code focus] +} +``` + +#### Word highlight + +Highlight a specific word or phrase with `// [!code word:term]`. For example, `// [!code word:name]` looks like this: + +```ts +// [!code word:name] +function greet(name: string) { + console.log(`hello, ${name}!`) +} +``` + ### List types #### Ordered list diff --git a/site/src/styles/globals.css b/site/src/styles/globals.css index 2f28f3ec..cfa71aaa 100644 --- a/site/src/styles/globals.css +++ b/site/src/styles/globals.css @@ -1,4 +1,5 @@ @import "tailwindcss"; +@import "./shiki-transformers.css"; /** * @license diff --git a/site/src/styles/shiki-transformers.css b/site/src/styles/shiki-transformers.css new file mode 100644 index 00000000..64867ba9 --- /dev/null +++ b/site/src/styles/shiki-transformers.css @@ -0,0 +1,72 @@ +@layer base { + .astro-code code { + display: flex; + flex-direction: column; + } + .astro-code .line { + display: inline-flex; + position: relative; + line-height: 1.75; + } + .astro-code .line.diff, + .astro-code .line.focused { + width: calc(100% + var(--spacing) * 12); + margin-inline: calc(var(--spacing) * -6); + padding-inline: calc(var(--spacing) * 6); + } + + /* Diff: added lines */ + .astro-code .line.diff.add { + background-color: rgba(184, 187, 38, 0.12); + } + + .astro-code .line.diff.add::before, + .astro-code .line.diff.remove::before { + position: absolute; + font-weight: bold; + left: 0; + top: 0; + bottom: 0; + width: calc(var(--spacing) * 6); + display: flex; + align-items: center; + justify-content: center; + } + + .astro-code .line.diff.add::before { + content: "+"; + color: rgba(184, 187, 31, 1); + } + + /* Diff: removed lines */ + .astro-code .line.diff.remove { + background-color: rgba(251, 73, 52, 0.12); + } + + .astro-code .line.diff.remove::before { + content: "-"; + color: rgba(251, 73, 52, 1); + } + + /* Word highlight */ + .astro-code .highlighted-word { + background-color: var(--color-soot); + border-bottom: 1px solid var(--color-manila-dark); + padding-inline: 0.2em; + } + + :where(.dark, .dark *) .astro-code .highlighted-word { + background-color: var(--color-faded-black); + } + + /* Focus: mute unfocused lines, reveal on hover */ + .astro-code:has(.line.focused) .line:not(.focused) span { + opacity: 0.4; + transition: opacity 0.3s; + } + + .astro-code:has(.line.focused):hover .line:not(.focused) span, + .astro-code:has(.line.focused):focus-within .line:not(.focused) span { + opacity: 1; + } +} diff --git a/site/src/utils/shikiNotationTransformers.ts b/site/src/utils/shikiNotationTransformers.ts new file mode 100644 index 00000000..abfb25d4 --- /dev/null +++ b/site/src/utils/shikiNotationTransformers.ts @@ -0,0 +1,12 @@ +import { + transformerNotationDiff, + transformerNotationFocus, + transformerNotationWordHighlight, +} from '@shikijs/transformers'; +import type { ShikiTransformer } from 'shiki'; + +export const shikiNotationTransformers: ShikiTransformer[] = [ + transformerNotationDiff({ matchAlgorithm: 'v3' }), + transformerNotationFocus({ matchAlgorithm: 'v3' }), + transformerNotationWordHighlight({ matchAlgorithm: 'v3' }), +];