From d138f31db5d3727e6112e20e91d95ea98a9bb2d7 Mon Sep 17 00:00:00 2001 From: Adam Bull Date: Wed, 22 Oct 2025 13:42:02 +0100 Subject: [PATCH 1/2] fix: remove codehike in favour of shiki --- CLAUDE.md | 4 +- package.json | 2 +- pnpm-lock.yaml | 171 +++++++++++++----- src/components/CodeHighlight/Pre.tsx | 63 +++++++ .../CodePlayground/index.stories.tsx | 8 - src/components/CodePlayground/index.tsx | 108 ++++------- src/components/CodePlayground/lineNumbers.tsx | 14 -- .../CodePlayground/tokenTransitions.tsx | 58 ------ src/components/CodePlayground/wordWrap.tsx | 24 --- src/components/CodeSnippet/index.stories.tsx | 67 +------ src/components/CodeSnippet/index.tsx | 35 ++-- src/index.ts | 12 ++ src/lib/codeUtils.test.ts | 53 +++--- src/lib/codeUtils.ts | 101 ++++++----- types/utilities.d.ts | 15 +- 15 files changed, 352 insertions(+), 383 deletions(-) create mode 100644 src/components/CodeHighlight/Pre.tsx delete mode 100644 src/components/CodePlayground/lineNumbers.tsx delete mode 100644 src/components/CodePlayground/tokenTransitions.tsx delete mode 100644 src/components/CodePlayground/wordWrap.tsx diff --git a/CLAUDE.md b/CLAUDE.md index 073d18b5..227c8ced 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -125,7 +125,7 @@ Add new `@source` declarations in global.css: ## Available Utility Classes -This section is auto-generated from the CSS files. Last updated: 2025-09-17T00:27:34.139Z +This section is auto-generated from the CSS files. Last updated: 2025-10-22T12:39:42.229Z ### Typography Utilities @@ -1101,6 +1101,8 @@ This section is auto-generated from the CSS files. Last updated: 2025-09-17T00:2 ```css .bg-btn-brand-disabled { background-color: var(--color-base-white); + opacity: 1; + color: var(--text-muted); /* dark variant */ background-color: var(--color-base-black); diff --git a/package.json b/package.json index 14088798..c943123d 100644 --- a/package.json +++ b/package.json @@ -124,8 +124,8 @@ "class-variance-authority": "^0.7.0", "clsx": "^2.1.1", "cmdk": "^1.1.1", - "codehike": "^1.0.4", "date-fns": "^4.1.0", + "shiki": "^1.27.0", "motion": "^12.6.3", "react-markdown": "^10.1.0", "react-resizable-panels": "^2.1.7", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7308a97a..4bf3dc99 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -59,9 +59,6 @@ importers: cmdk: specifier: ^1.1.1 version: 1.1.1(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - codehike: - specifier: ^1.0.4 - version: 1.0.4 date-fns: specifier: ^4.1.0 version: 4.1.0 @@ -89,6 +86,9 @@ importers: remark-gfm: specifier: ^4.0.1 version: 4.0.1 + shiki: + specifier: ^1.27.0 + version: 1.29.2 tailwind-merge: specifier: ^3.0.2 version: 3.0.2 @@ -423,9 +423,6 @@ packages: peerDependencies: storybook: ^0.0.0-0 || ^9.0.0 || ^9.1.0-0 || ^9.2.0-0 || ^10.0.0-0 - '@code-hike/lighter@1.0.1': - resolution: {integrity: sha512-mccvcsk5UTScRrE02oBz1/qzckyhD8YE3VQlQv++2bSVVZgNuCUX8MpokSCi5OmfRAAxbj6kmNiqq1Um8eXPrw==} - '@colors/colors@1.5.0': resolution: {integrity: sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==} engines: {node: '>=0.1.90'} @@ -1606,6 +1603,27 @@ packages: peerDependencies: semantic-release: '>=20.1.0' + '@shikijs/core@1.29.2': + resolution: {integrity: sha512-vju0lY9r27jJfOY4Z7+Rt/nIOjzJpZ3y+nYpqtUZInVoXQ/TJZcfGnNOGnKjFdVZb8qexiCuSlZRKcGfhhTTZQ==} + + '@shikijs/engine-javascript@1.29.2': + resolution: {integrity: sha512-iNEZv4IrLYPv64Q6k7EPpOCE/nuvGiKl7zxdq0WFuRPF5PAE9PRo2JGq/d8crLusM59BRemJ4eOqrFrC4wiQ+A==} + + '@shikijs/engine-oniguruma@1.29.2': + resolution: {integrity: sha512-7iiOx3SG8+g1MnlzZVDYiaeHe7Ez2Kf2HrJzdmGwkRisT7r4rak0e655AcM/tF9JG/kg5fMNYlLLKglbN7gBqA==} + + '@shikijs/langs@1.29.2': + resolution: {integrity: sha512-FIBA7N3LZ+223U7cJDUYd5shmciFQlYkFXlkKVaHsCPgfVLiO+e12FmQE6Tf9vuyEsFe3dIl8qGWKXgEHL9wmQ==} + + '@shikijs/themes@1.29.2': + resolution: {integrity: sha512-i9TNZlsq4uoyqSbluIcZkmPL9Bfi3djVxRnofUHwvx/h6SRW3cwgBC5SML7vsDcWyukY0eCzVN980rqP6qNl9g==} + + '@shikijs/types@1.29.2': + resolution: {integrity: sha512-VJjK0eIijTZf0QSTODEXCqinjBn0joAHQ+aPSBzrv4O2d/QSbsMw+ZeSRx03kV34Hy7NzUvV/7NqfYGRLrASmw==} + + '@shikijs/vscode-textmate@10.0.2': + resolution: {integrity: sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==} + '@sindresorhus/is@4.6.0': resolution: {integrity: sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==} engines: {node: '>=10'} @@ -2215,9 +2233,6 @@ packages: resolution: {integrity: sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==} engines: {node: '>=12'} - ansi-sequence-parser@1.1.1: - resolution: {integrity: sha512-vJXt3yiaUL4UU546s3rPXlsry/RnM730G1+HkpKE012AN0sx1eOrxSu95oKDIonskeLTijMgqWZ3uDEe3NFvyg==} - ansi-styles@3.2.1: resolution: {integrity: sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==} engines: {node: '>=4'} @@ -2428,9 +2443,6 @@ packages: react: ^18 || ^19 || ^19.0.0-rc react-dom: ^18 || ^19 || ^19.0.0-rc - codehike@1.0.4: - resolution: {integrity: sha512-mG/YJiK5J9tFHp/2seoliZpT4uxzjcbwDWXWXcYPRQKgQa4iRtwzVsOp/L4FnEX1J9LceZjCe3+ztSiktrcV1w==} - color-convert@1.9.3: resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==} @@ -2631,10 +2643,6 @@ packages: diff-match-patch@1.0.5: resolution: {integrity: sha512-IayShXAgj/QMXgB0IWmKx+rOPuGMhqm5w6jvFxmVenXKIzRqTAAsbBPT3kWQeGANj3jGgvcvv4yK6SxqYmikgw==} - diff@5.2.0: - resolution: {integrity: sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A==} - engines: {node: '>=0.3.1'} - dir-glob@3.0.1: resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} engines: {node: '>=8'} @@ -2678,6 +2686,9 @@ packages: electron-to-chromium@1.5.66: resolution: {integrity: sha512-pI2QF6+i+zjPbqRzJwkMvtvkdI7MjVbSh2g8dlMguDJIXEPw+kwasS1Jl+YGPEBfGVxsVgGUratAKymPdPo2vQ==} + emoji-regex-xs@1.0.0: + resolution: {integrity: sha512-LRlerrMYoIDrT6jgpeZ2YYl/L8EulRTt5hQcYjy5AInh7HWXKimpqx68aknBFpGL2+/IcogTcaydJEgaTmOpDg==} + emoji-regex@8.0.0: resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} @@ -2831,9 +2842,6 @@ packages: estree-util-is-identifier-name@3.0.0: resolution: {integrity: sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg==} - estree-util-visit@2.0.0: - resolution: {integrity: sha512-m5KgiH85xAhhW8Wta0vShLcUvOsh3LLPI2YVwcbio1l7E09NTLL1EyMZFM1OyWowoH0skScNbhOPl4kcBgzTww==} - estree-walker@2.0.2: resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} @@ -3063,6 +3071,9 @@ packages: resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} engines: {node: '>= 0.4'} + hast-util-to-html@9.0.5: + resolution: {integrity: sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw==} + hast-util-to-jsx-runtime@2.3.6: resolution: {integrity: sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg==} @@ -3095,6 +3106,9 @@ packages: html-url-attributes@3.0.1: resolution: {integrity: sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ==} + html-void-elements@3.0.0: + resolution: {integrity: sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==} + http-proxy-agent@7.0.2: resolution: {integrity: sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==} engines: {node: '>= 14'} @@ -3915,6 +3929,9 @@ packages: resolution: {integrity: sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==} engines: {node: '>=12'} + oniguruma-to-es@2.3.0: + resolution: {integrity: sha512-bwALDxriqfKGfUufKGGepCzu9x7nJQuoRoAFp4AnwehhC2crqrDIAP/uN2qdlsAvSMpeRC3+Yzhqc7hLmle5+g==} + open@8.4.2: resolution: {integrity: sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==} engines: {node: '>=12'} @@ -4280,6 +4297,15 @@ packages: regenerator-runtime@0.14.1: resolution: {integrity: sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==} + regex-recursion@5.1.1: + resolution: {integrity: sha512-ae7SBCbzVNrIjgSbh7wMznPcQel1DNlDtzensnFxpiNpXt1U2ju/bHugH422r+4LAVS1FpW1YCwilmnNsjum9w==} + + regex-utilities@2.3.0: + resolution: {integrity: sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng==} + + regex@5.1.1: + resolution: {integrity: sha512-dN5I359AVGPnwzJm2jN1k0W9LPZ+ePvoOeVMMfqIMFz53sSwXkxaJoxr50ptnsC771lK95BnTrVSZxq0b9yCGw==} + registry-auth-token@5.0.3: resolution: {integrity: sha512-1bpc9IyC+e+CNFRaWyn77tk4xGG4PPUyfakSmA6F6cvUDjrm58dfyJ3II+9yb10EDkHoy1LaPSmHaWLOH3m6HA==} engines: {node: '>=14'} @@ -4400,6 +4426,9 @@ packages: resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} engines: {node: '>=8'} + shiki@1.29.2: + resolution: {integrity: sha512-njXuliz/cP+67jU2hukkxCNuH1yUi4QfdZZY+sMr5PPrIyXSu5iTb/qYC4BiWWB0vZ+7TbdvYUCeL23zpwCfbg==} + siginfo@2.0.0: resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} @@ -5314,10 +5343,6 @@ snapshots: - '@chromatic-com/cypress' - '@chromatic-com/playwright' - '@code-hike/lighter@1.0.1': - dependencies: - ansi-sequence-parser: 1.1.1 - '@colors/colors@1.5.0': optional: true @@ -6360,6 +6385,41 @@ snapshots: transitivePeerDependencies: - supports-color + '@shikijs/core@1.29.2': + dependencies: + '@shikijs/engine-javascript': 1.29.2 + '@shikijs/engine-oniguruma': 1.29.2 + '@shikijs/types': 1.29.2 + '@shikijs/vscode-textmate': 10.0.2 + '@types/hast': 3.0.4 + hast-util-to-html: 9.0.5 + + '@shikijs/engine-javascript@1.29.2': + dependencies: + '@shikijs/types': 1.29.2 + '@shikijs/vscode-textmate': 10.0.2 + oniguruma-to-es: 2.3.0 + + '@shikijs/engine-oniguruma@1.29.2': + dependencies: + '@shikijs/types': 1.29.2 + '@shikijs/vscode-textmate': 10.0.2 + + '@shikijs/langs@1.29.2': + dependencies: + '@shikijs/types': 1.29.2 + + '@shikijs/themes@1.29.2': + dependencies: + '@shikijs/types': 1.29.2 + + '@shikijs/types@1.29.2': + dependencies: + '@shikijs/vscode-textmate': 10.0.2 + '@types/hast': 3.0.4 + + '@shikijs/vscode-textmate@10.0.2': {} + '@sindresorhus/is@4.6.0': {} '@sindresorhus/merge-streams@2.3.0': {} @@ -7062,8 +7122,6 @@ snapshots: ansi-regex@6.1.0: {} - ansi-sequence-parser@1.1.1: {} - ansi-styles@3.2.1: dependencies: color-convert: 1.9.3 @@ -7251,16 +7309,6 @@ snapshots: - '@types/react' - '@types/react-dom' - codehike@1.0.4: - dependencies: - '@code-hike/lighter': 1.0.1 - diff: 5.2.0 - estree-util-visit: 2.0.0 - mdast-util-mdx-jsx: 3.1.3 - unist-util-visit: 5.0.0 - transitivePeerDependencies: - - supports-color - color-convert@1.9.3: dependencies: color-name: 1.1.3 @@ -7430,8 +7478,6 @@ snapshots: diff-match-patch@1.0.5: {} - diff@5.2.0: {} - dir-glob@3.0.1: dependencies: path-type: 4.0.0 @@ -7479,6 +7525,8 @@ snapshots: electron-to-chromium@1.5.66: {} + emoji-regex-xs@1.0.0: {} + emoji-regex@8.0.0: {} emoji-regex@9.2.2: {} @@ -7680,11 +7728,6 @@ snapshots: estree-util-is-identifier-name@3.0.0: {} - estree-util-visit@2.0.0: - dependencies: - '@types/estree-jsx': 1.0.5 - '@types/unist': 3.0.3 - estree-walker@2.0.2: {} estree-walker@3.0.3: @@ -7935,6 +7978,20 @@ snapshots: dependencies: function-bind: 1.1.2 + hast-util-to-html@9.0.5: + dependencies: + '@types/hast': 3.0.4 + '@types/unist': 3.0.3 + ccount: 2.0.1 + comma-separated-tokens: 2.0.3 + hast-util-whitespace: 3.0.0 + html-void-elements: 3.0.0 + mdast-util-to-hast: 13.2.0 + property-information: 7.0.0 + space-separated-tokens: 2.0.2 + stringify-entities: 4.0.4 + zwitch: 2.0.4 + hast-util-to-jsx-runtime@2.3.6: dependencies: '@types/estree': 1.0.6 @@ -7979,6 +8036,8 @@ snapshots: html-url-attributes@3.0.1: {} + html-void-elements@3.0.0: {} + http-proxy-agent@7.0.2: dependencies: agent-base: 7.1.1 @@ -8856,6 +8915,12 @@ snapshots: dependencies: mimic-fn: 4.0.0 + oniguruma-to-es@2.3.0: + dependencies: + emoji-regex-xs: 1.0.0 + regex: 5.1.1 + regex-recursion: 5.1.1 + open@8.4.2: dependencies: define-lazy-prop: 2.0.0 @@ -9186,6 +9251,17 @@ snapshots: regenerator-runtime@0.14.1: {} + regex-recursion@5.1.1: + dependencies: + regex: 5.1.1 + regex-utilities: 2.3.0 + + regex-utilities@2.3.0: {} + + regex@5.1.1: + dependencies: + regex-utilities: 2.3.0 + registry-auth-token@5.0.3: dependencies: '@pnpm/npm-conf': 2.3.1 @@ -9375,6 +9451,17 @@ snapshots: shebang-regex@3.0.0: {} + shiki@1.29.2: + dependencies: + '@shikijs/core': 1.29.2 + '@shikijs/engine-javascript': 1.29.2 + '@shikijs/engine-oniguruma': 1.29.2 + '@shikijs/langs': 1.29.2 + '@shikijs/themes': 1.29.2 + '@shikijs/types': 1.29.2 + '@shikijs/vscode-textmate': 10.0.2 + '@types/hast': 3.0.4 + siginfo@2.0.0: {} signal-exit@4.1.0: {} diff --git a/src/components/CodeHighlight/Pre.tsx b/src/components/CodeHighlight/Pre.tsx new file mode 100644 index 00000000..e7c39d4c --- /dev/null +++ b/src/components/CodeHighlight/Pre.tsx @@ -0,0 +1,63 @@ +import { forwardRef, HTMLAttributes } from 'react' +import { HighlightedCode } from '@/lib/codeUtils' +import { cn } from '@/lib/utils' + +export interface PreProps + extends Omit, 'children'> { + code: HighlightedCode + showLineNumbers?: boolean + wordWrap?: boolean +} + +export const Pre = forwardRef( + ( + { code, showLineNumbers = false, wordWrap = false, className, ...props }, + ref + ) => { + return ( +
+        
+          {code.lines.map((line, lineIndex) => (
+            
+ {showLineNumbers && ( + + {lineIndex + 1} + + )} + + {line.tokens.map((token, tokenIndex) => ( + + {token.content} + + ))} + +
+ ))} +
+
+ ) + } +) + +Pre.displayName = 'Pre' diff --git a/src/components/CodePlayground/index.stories.tsx b/src/components/CodePlayground/index.stories.tsx index ae6608a5..d3fa4661 100644 --- a/src/components/CodePlayground/index.stories.tsx +++ b/src/components/CodePlayground/index.stories.tsx @@ -323,14 +323,6 @@ export const NoLineNumbers: Story = { }, } -export const NoAnimations: Story = { - ...Default, - args: { - ...Default.args, - animateOnLanguageChange: false, - }, -} - export const OverflowingCode: Story = { ...Default, args: { diff --git a/src/components/CodePlayground/index.tsx b/src/components/CodePlayground/index.tsx index 0cc8bdd1..a4e2ec2e 100644 --- a/src/components/CodePlayground/index.tsx +++ b/src/components/CodePlayground/index.tsx @@ -1,4 +1,3 @@ -import { AnnotationHandler, HighlightedCode, Pre } from 'codehike/code' import { useCallback, useEffect, @@ -18,15 +17,21 @@ import { SelectValue, } from '@/components/Select' import { prettyLanguageName, SupportedLanguage } from '@/types' -import { wordWrapping } from './wordWrap' import '@/styles/codeSyntax.css' import { motion } from 'motion/react' import { cn } from '@/lib/utils' import { AnimatePresence } from 'motion/react' import { Icon } from '@/components/Icon' import { Skeleton } from '@/components/Skeleton' -import { highlightCode, getCodeHandlers } from '@/lib/codeUtils' +import { + highlightCode, + HighlightedCode, + LIGHT_THEME, + DARK_THEME, +} from '@/lib/codeUtils' import React from 'react' +import { Pre } from '../CodeHighlight/Pre' +import { useConfig } from '@/hooks/useConfig' const copyIconVariants = { hidden: { opacity: 0, scale: 0.5 }, @@ -99,13 +104,6 @@ export interface CodePlaygroundProps { */ onChangeLanguage?: (language: SupportedLanguage) => void - /** - * Whether to animate the code when the language is changed. - * - * @default true - */ - animateOnLanguageChange?: boolean - /** * Whether to show the language selector. */ @@ -127,7 +125,6 @@ const CodePlayground = ({ className, onChangeLanguage, error, - animateOnLanguageChange = true, showLineNumbers = true, showLanguageSelector = true, wordWrap = true, @@ -158,22 +155,8 @@ const CodePlayground = ({ ) const [highlighted, setHighlighted] = useState(null) - const selectedCode = useMemo( - () => snippets[selectedLanguage]!, - [selectedLanguage, snippets] - ) - - const preHandlers = useMemo(() => { - // Get the base handlers (lineNumbers and tokenTransitions) - const handlers = getCodeHandlers(showLineNumbers, animateOnLanguageChange) - - // Add wordWrap handler if needed - if (wordWrap) { - handlers.push(wordWrapping) - } - - return handlers - }, [animateOnLanguageChange, showLineNumbers, wordWrap]) + const selectedCode = snippets[selectedLanguage]! + const { theme } = useConfig() const loadingSkeleton = useMemo(() => { // Try to measure the existing height of the code container if code has @@ -193,19 +176,18 @@ const CodePlayground = ({ ) }, [codeRef.current]) - const codeContents = useMemo(() => { - return error ? ( - error - ) : selectedCode.loading ? ( -
{loadingSkeleton}
- ) : highlighted ? ( -
-    ) : null
-  }, [selectedCode.loading, highlighted, error])
+  const codeContents = error ? (
+    error
+  ) : selectedCode.loading ? (
+    
{loadingSkeleton}
+ ) : highlighted ? ( +
+  ) : null
 
   const foundCustomCodeContainer = useMemo(
     () =>
@@ -218,35 +200,29 @@ const CodePlayground = ({
     [validChildren]
   )
 
-  const code = useMemo(
-    () =>
-      foundCustomCodeContainer ? (
-        React.cloneElement(foundCustomCodeContainer as React.ReactElement, {
-          __children__: codeContents,
-          ref: codeRef,
-        })
-      ) : (
-        
-      ),
-    [foundCustomCodeContainer, codeContents]
+  const code = foundCustomCodeContainer ? (
+    React.cloneElement(foundCustomCodeContainer as React.ReactElement, {
+      __children__: codeContents,
+      ref: codeRef,
+    })
+  ) : (
+    
   )
-  const footer = useMemo(
-    () =>
-      validChildren.find(
-        (child) =>
-          isValidElement(child) &&
-          (child.type as { displayName?: string }).displayName ===
-            'CodePlayground.Footer'
-      ),
-    [validChildren]
+
+  const footer = validChildren.find(
+    (child) =>
+      isValidElement(child) &&
+      (child.type as { displayName?: string }).displayName ===
+        'CodePlayground.Footer'
   )
 
   const updateHighlighted = useCallback(
     async (code: string, language: SupportedLanguage) => {
-      const highlighted = await highlightCode(code, language)
+      const shikiTheme = theme === 'dark' ? DARK_THEME : LIGHT_THEME
+      const highlighted = await highlightCode(code, language, shikiTheme)
       setHighlighted(highlighted)
     },
-    []
+    [theme]
   )
 
   const [copying, setCopying] = useState(false)
@@ -263,13 +239,7 @@ const CodePlayground = ({
     if (selectedCode.code) {
       updateHighlighted(selectedCode.code, selectedLanguage)
     }
-  }, [selectedCode, selectedLanguage])
-
-  useEffect(() => {
-    if (selectedCode.code) {
-      updateHighlighted(selectedCode.code, selectedLanguage)
-    }
-  }, [selectedCode, selectedLanguage])
+  }, [selectedCode, selectedLanguage, updateHighlighted])
 
   const handleChangeLanguage = useCallback(
     (language: SupportedLanguage) => {
diff --git a/src/components/CodePlayground/lineNumbers.tsx b/src/components/CodePlayground/lineNumbers.tsx
deleted file mode 100644
index 2be036d3..00000000
--- a/src/components/CodePlayground/lineNumbers.tsx
+++ /dev/null
@@ -1,14 +0,0 @@
-import { AnnotationHandler, InnerLine } from 'codehike/code'
-
-export const lineNumbers: AnnotationHandler = {
-  name: 'lineNumbers',
-  Line: (props) => {
-    const { lineNumber } = props
-    return (
-      
-
{lineNumber}
- -
- ) - }, -} diff --git a/src/components/CodePlayground/tokenTransitions.tsx b/src/components/CodePlayground/tokenTransitions.tsx deleted file mode 100644 index 6f13ce6f..00000000 --- a/src/components/CodePlayground/tokenTransitions.tsx +++ /dev/null @@ -1,58 +0,0 @@ -import { CustomPreProps, InnerPre, getPreRef } from 'codehike/code' -import { - TokenTransitionsSnapshot, - calculateTransitions, - getStartingSnapshot, -} from 'codehike/utils/token-transitions' -import React from 'react' -import { AnnotationHandler, InnerToken } from 'codehike/code' - -const MAX_TRANSITION_DURATION = 900 // milliseconds - -class SmoothPre extends React.Component { - ref: React.RefObject - constructor(props: CustomPreProps) { - super(props) - this.ref = getPreRef(this.props) - } - - render() { - return - } - - getSnapshotBeforeUpdate() { - return getStartingSnapshot(this.ref.current!) - } - - componentDidUpdate( - prevProps: never, - prevState: never, - snapshot: TokenTransitionsSnapshot - ) { - const transitions = calculateTransitions(this.ref.current!, snapshot) - transitions.forEach(({ element, keyframes, options }) => { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const { translateX, translateY, ...kf } = keyframes as any - if (translateX && translateY) { - kf.translate = [ - `${translateX[0]}px ${translateY[0]}px`, - `${translateX[1]}px ${translateY[1]}px`, - ] - } - element.animate(kf, { - duration: options.duration * MAX_TRANSITION_DURATION, - delay: options.delay * MAX_TRANSITION_DURATION, - easing: options.easing, - fill: 'both', - }) - }) - } -} - -export const tokenTransitions: AnnotationHandler = { - name: 'token-transitions', - PreWithRef: SmoothPre, - Token: (props) => ( - - ), -} diff --git a/src/components/CodePlayground/wordWrap.tsx b/src/components/CodePlayground/wordWrap.tsx deleted file mode 100644 index f4db1e6c..00000000 --- a/src/components/CodePlayground/wordWrap.tsx +++ /dev/null @@ -1,24 +0,0 @@ -import { - AnnotationHandler, - InnerLine, - InnerPre, - InnerToken, -} from 'codehike/code' - -export const wordWrapping: AnnotationHandler = { - name: 'word-wrap', - Pre: (props) => , - Line: (props) => ( - -
- {props.children} -
-
- ), - Token: (props) => , -} diff --git a/src/components/CodeSnippet/index.stories.tsx b/src/components/CodeSnippet/index.stories.tsx index 017a0f84..38c73aee 100644 --- a/src/components/CodeSnippet/index.stories.tsx +++ b/src/components/CodeSnippet/index.stories.tsx @@ -3,7 +3,6 @@ import { userEvent } from 'storybook/test' import { CodeSnippet } from '.' import { Meta, StoryObj } from '@storybook/react-vite' import { fn } from 'storybook/test' -import { InnerLine } from 'codehike/code' const meta: Meta = { component: CodeSnippet, @@ -126,7 +125,7 @@ export const UnsupportedLanguage: Story = { } /** - * Codehike supports many languages that Speakeasy does not. + * Shiki supports many languages that Speakeasy does not. */ export const SemiSupportedLanguage: Story = { args: { @@ -215,70 +214,6 @@ console.log("Hello, world!")`, }, } -export const CustomHandler: Story = { - args: { - code: `function greet(name) { - // Say hello to the user - console.log("Hello, " + name); - return true; -}`, - language: 'javascript', - copyable: true, - showLineNumbers: true, - customHandlers: [ - { - // Always give your handler a descriptive name for debugging - name: 'simple-line-highlight', - - /** - * Custom Line component that highlights line 3 with a background color - * - * The Line handler receives these important props: - * - lineNumber: The 1-based line number - * - children: The content of the line - * - indentation: The number of spaces for indentation - */ - Line: (props) => { - // Check if this is the line to highlight (line 3) - if (props.lineNumber === 3) { - return ( -
- {/* InnerLine with merge is critical for proper handler composition */} - -
- ) - } - - // For other lines, just use InnerLine with all props - return - }, - }, - ], - }, -} - -export const WithAnnotations: Story = { - args: { - code: `function greet(name) { - // Say hello to the user - console.log("Hello, " + name); - - # !callout - # This is a callout - console.log("Hello, " + name); - }`, - language: 'javascript', - copyable: true, - showLineNumbers: true, - }, -} - export const WithSnippetClassName: Story = { args: { code: `const reallyLongLine = 'this is a really long line of code that will overflow the container'`, diff --git a/src/components/CodeSnippet/index.tsx b/src/components/CodeSnippet/index.tsx index 815d19aa..8dcad090 100644 --- a/src/components/CodeSnippet/index.tsx +++ b/src/components/CodeSnippet/index.tsx @@ -1,13 +1,18 @@ -import { useCallback, useState, useEffect, useRef, useMemo } from 'react' +import { useCallback, useState, useEffect, useRef } from 'react' import { cn } from '@/lib/utils' import { ProgrammingLanguage, Size } from '@/types' -import { AnnotationHandler, HighlightedCode, Pre } from 'codehike/code' import { AnimatePresence, motion } from 'motion/react' import '@/styles/codeSyntax.css' import './codeSnippet.css' import { Icon } from '../Icon' -import { highlightCode, getCodeHandlers } from '@/lib/codeUtils' +import { + highlightCode, + HighlightedCode, + LIGHT_THEME, + DARK_THEME, +} from '@/lib/codeUtils' import { useConfig } from '@/hooks/useConfig' +import { Pre } from '../CodeHighlight/Pre' export interface CodeSnippetProps { /** @@ -19,8 +24,8 @@ export interface CodeSnippetProps { */ copyable?: boolean /** - * One of the known Speakeasy target languages, or a language that Codehike supports. - * The full list of supported languages is available at https://codehike.org/docs/concepts/code#languages + * One of the known Speakeasy target languages, or a language that Shiki supports. + * The full list of supported languages is available at https://shiki.style/languages */ language: ProgrammingLanguage | string /** @@ -53,13 +58,9 @@ export interface CodeSnippetProps { className?: string /** - * Additional CSS classes to apply to the code snippet inner container (e.g the Codehike Pre component). + * Additional CSS classes to apply to the code snippet inner container (e.g the Pre component). */ snippetClassName?: string - /** - * Custom annotation handlers to be added to the default handlers. - */ - customHandlers?: AnnotationHandler[] } const fontSizeMap: Record = { @@ -87,7 +88,6 @@ export function CodeSnippet({ className, snippetClassName, showLineNumbers = false, - customHandlers = [], }: CodeSnippetProps) { const [copying, setCopying] = useState(false) const [containerWidth, setContainerWidth] = useState(0) @@ -122,21 +122,16 @@ export function CodeSnippet({ const isMultiline = code.split('\n').length > 1 const { theme } = useConfig() - // Get the code handlers for line numbers and animations, then add custom handlers - const preHandlers = useMemo(() => { - const defaultHandlers = getCodeHandlers(showLineNumbers, true) - return [...defaultHandlers, ...customHandlers] - }, [showLineNumbers, customHandlers]) - // Directly highlight the code when code or language changes useEffect(() => { if (!language) return + const shikiTheme = theme === 'dark' ? DARK_THEME : LIGHT_THEME // Use the highlightCode utility directly - highlightCode(code, language).then((highlighted) => { + highlightCode(code, language, shikiTheme).then((highlighted) => { setHighlightedCodeState(highlighted) }) - }, [code, language]) + }, [code, language, theme]) const handleCopy = useCallback(() => { setCopying(true) @@ -179,7 +174,7 @@ export function CodeSnippet({ snippetClassName )} onBeforeInput={handleBeforeInput} - handlers={preHandlers} + showLineNumbers={showLineNumbers} /> )} diff --git a/src/index.ts b/src/index.ts index 9a2c5b43..f41a33c3 100644 --- a/src/index.ts +++ b/src/index.ts @@ -25,6 +25,18 @@ export { export { UserAvatar, type UserAvatarProps } from '@/components/UserAvatar' export { Subnav, type SubnavItem, type SubnavProps } from '@/components/Subnav' export { CodeSnippet, type CodeSnippetProps } from '@/components/CodeSnippet' +export { Pre, type PreProps } from '@/components/CodeHighlight/Pre' +export { + highlightCode, + getMappedLanguage, + isProgrammingLanguage, + removeCodeHikeAnnotations, + LIGHT_THEME, + DARK_THEME, + type HighlightedCode, + type CodeLine, + type CodeToken, +} from '@/lib/codeUtils' export { LoggedInUserMenu, type LoggedInUserProps, diff --git a/src/lib/codeUtils.test.ts b/src/lib/codeUtils.test.ts index 818b0293..c3d6181a 100644 --- a/src/lib/codeUtils.test.ts +++ b/src/lib/codeUtils.test.ts @@ -2,10 +2,10 @@ import { describe, it, expect } from 'vitest' import { removeCodeHikeAnnotations, highlightCode, - getCodeHandlers, getMappedLanguage, isProgrammingLanguage, - CODEHIKE_THEME, + LIGHT_THEME, + DARK_THEME, } from './codeUtils' import { ProgrammingLanguage } from '@/types' @@ -97,33 +97,28 @@ const x = 1` describe('highlightCode', () => { it('should highlight code with supported language', async () => { const result = await highlightCode('console.log("test")', 'typescript') - expect(result).toHaveProperty('value') + expect(result).toHaveProperty('lines') + expect(result).toHaveProperty('code') + expect(result).toHaveProperty('lang') + expect(result.lines).toBeInstanceOf(Array) + expect(result.code).toBe('console.log("test")') }) - it('should handle unsupported language', async () => { + it('should handle unsupported language gracefully', async () => { const result = await highlightCode('console.log("test")', 'unsupported') - expect(result).toHaveProperty('value') - }) - }) - - describe('getCodeHandlers', () => { - it('should return only transitions by default', () => { - const handlers = getCodeHandlers() - expect(handlers).toHaveLength(1) - expect(handlers[0]).toBeDefined() + expect(result).toHaveProperty('lines') + expect(result).toHaveProperty('code') + expect(result.code).toBe('console.log("test")') }) - it('should include line numbers and transitions when line numbers are enabled', () => { - const handlers = getCodeHandlers(true) - expect(handlers).toHaveLength(2) - expect(handlers[0]).toBeDefined() - expect(handlers[1]).toBeDefined() - }) - - it('should include only transitions when transitions are enabled', () => { - const handlers = getCodeHandlers(false, true) - expect(handlers).toHaveLength(1) - expect(handlers[0]).toBeDefined() + it('should use specified theme', async () => { + const result = await highlightCode( + 'const x = 1', + 'javascript', + DARK_THEME + ) + expect(result).toHaveProperty('lines') + expect(result.lines[0].tokens).toBeDefined() }) }) @@ -136,7 +131,8 @@ const x = 1` bash: 'bash', json: 'json', go: 'go', - dotnet: 'c#', + dotnet: 'csharp', + csharp: 'csharp', java: 'java', } as const @@ -178,9 +174,10 @@ const x = 1` }) }) - describe('CODEHIKE_THEME', () => { - it('should have the correct theme value', () => { - expect(CODEHIKE_THEME).toBe('github-from-css') + describe('themes', () => { + it('should have correct theme values', () => { + expect(LIGHT_THEME).toBe('github-light') + expect(DARK_THEME).toBe('github-dark') }) }) }) diff --git a/src/lib/codeUtils.ts b/src/lib/codeUtils.ts index df448320..8679b2ef 100644 --- a/src/lib/codeUtils.ts +++ b/src/lib/codeUtils.ts @@ -1,53 +1,70 @@ -import { highlight, AnnotationHandler, RawCode } from 'codehike/code' +import { codeToTokens, BundledLanguage, BundledTheme } from 'shiki' import { ProgrammingLanguage, SupportedLanguage } from '@/types' -import { lineNumbers } from '@/components/CodePlayground/lineNumbers' -import { tokenTransitions } from '@/components/CodePlayground/tokenTransitions' -export const CODEHIKE_THEME = 'github-from-css' as const +export const LIGHT_THEME = 'github-light' as const +export const DARK_THEME = 'github-dark' as const -/** - * Highlights code using the standardized theme across the application - */ -export async function highlightCode( - code: string, - language: SupportedLanguage | string -) { - // Clean the code by removing annotations - const cleanCode = removeCodeHikeAnnotations(code) +export interface CodeToken { + content: string + color?: string + fontStyle?: number +} - const rawCode: RawCode = { - value: code, // Use original code for highlighting - lang: isProgrammingLanguage(language) - ? getMappedLanguage(language) - : language, - meta: '', - } - const highlighted = await highlight(rawCode, CODEHIKE_THEME) +export interface CodeLine { + tokens: CodeToken[] +} - return { - ...highlighted, - code: cleanCode, // Return the clean code without annotations - } +export interface HighlightedCode { + lines: CodeLine[] + code: string + lang: string } /** - * Returns an array of handlers for code highlighting with consistent options + * Highlights code using Shiki */ -export function getCodeHandlers( - showLineNumbers = false, - useTransitions = true -): AnnotationHandler[] { - const handlers: AnnotationHandler[] = [] - - if (showLineNumbers) { - handlers.push(lineNumbers) - } +export async function highlightCode( + code: string, + language: SupportedLanguage | string, + theme: BundledTheme = LIGHT_THEME +): Promise { + // Clean the code by removing annotations + const cleanCode = removeCodeHikeAnnotations(code) - if (useTransitions) { - handlers.push(tokenTransitions) + const lang = isProgrammingLanguage(language) + ? getMappedLanguage(language) + : (language as BundledLanguage) + + try { + const tokens = await codeToTokens(cleanCode, { + lang, + theme, + }) + + const lines: CodeLine[] = tokens.tokens.map((line) => ({ + tokens: line.map((token) => ({ + content: token.content, + color: token.color, + fontStyle: token.fontStyle, + })), + })) + + return { + lines, + code: cleanCode, + lang, + } + } catch (error) { + console.error('Error highlighting code:', error) + // Fallback to plain text + return { + lines: cleanCode.split('\n').map((line) => ({ + tokens: [{ content: line || '\n' }], + })), + code: cleanCode, + lang, + } } - - return handlers } /** @@ -55,7 +72,7 @@ export function getCodeHandlers( */ export function getMappedLanguage( language: ProgrammingLanguage | SupportedLanguage -): string { +): BundledLanguage { switch (language) { case 'javascript': return 'js' @@ -71,7 +88,7 @@ export function getMappedLanguage( return 'go' case 'dotnet': case 'csharp': - return 'c#' + return 'csharp' case 'java': return 'java' case 'ruby': @@ -83,7 +100,7 @@ export function getMappedLanguage( case 'terraform': return 'hcl' default: - return language as string + return language as BundledLanguage } } diff --git a/types/utilities.d.ts b/types/utilities.d.ts index 8b407f07..166291b4 100644 --- a/types/utilities.d.ts +++ b/types/utilities.d.ts @@ -1,6 +1,6 @@ /** * Auto-generated TypeScript definitions for Moonshine utilities - * Generated on: 2025-09-17T00:27:34.137Z + * Generated on: 2025-10-22T12:39:42.228Z * * DO NOT EDIT THIS FILE DIRECTLY * Run 'pnpm generate:docs' to update @@ -232,7 +232,7 @@ export type MoonshineUtilities = | 'bg-warning-highlight' | 'bg-warning-default' | 'bg-warning-muted' - | 'bg-warning-softest' + | 'bg-warning-softest'; /** * Semantic color utilities available from theme @@ -458,19 +458,14 @@ export type MoonshineColorUtilities = | 'text-body-muted' | 'border-body-muted' | 'ring-body-muted' - | 'divide-body-muted' + | 'divide-body-muted'; /** * All available Moonshine utility classes */ -export type MoonshineClasses = MoonshineUtilities | MoonshineColorUtilities +export type MoonshineClasses = MoonshineUtilities | MoonshineColorUtilities; /** * Helper type for className props */ -export type MoonshineClassName = - | MoonshineClasses - | MoonshineClasses[] - | undefined - | null - | false +export type MoonshineClassName = MoonshineClasses | MoonshineClasses[] | undefined | null | false; From 4c29eaf0e5fc17e2fcf8f57ca2e2f03723468970 Mon Sep 17 00:00:00 2001 From: Adam Bull Date: Wed, 22 Oct 2025 13:43:20 +0100 Subject: [PATCH 2/2] fix: lint --- types/utilities.d.ts | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/types/utilities.d.ts b/types/utilities.d.ts index 166291b4..8dd2392c 100644 --- a/types/utilities.d.ts +++ b/types/utilities.d.ts @@ -232,7 +232,7 @@ export type MoonshineUtilities = | 'bg-warning-highlight' | 'bg-warning-default' | 'bg-warning-muted' - | 'bg-warning-softest'; + | 'bg-warning-softest' /** * Semantic color utilities available from theme @@ -458,14 +458,19 @@ export type MoonshineColorUtilities = | 'text-body-muted' | 'border-body-muted' | 'ring-body-muted' - | 'divide-body-muted'; + | 'divide-body-muted' /** * All available Moonshine utility classes */ -export type MoonshineClasses = MoonshineUtilities | MoonshineColorUtilities; +export type MoonshineClasses = MoonshineUtilities | MoonshineColorUtilities /** * Helper type for className props */ -export type MoonshineClassName = MoonshineClasses | MoonshineClasses[] | undefined | null | false; +export type MoonshineClassName = + | MoonshineClasses + | MoonshineClasses[] + | undefined + | null + | false