├── README.md ├── pnpm-workspace.yaml ├── packages ├── remark-sugar-high │ ├── .gitignore │ ├── tsconfig.json │ ├── package.json │ ├── README.md │ └── src │ │ └── index.ts └── sugar-high │ ├── lib │ ├── presets │ │ ├── index.js │ │ ├── index.d.ts │ │ └── lang │ │ │ ├── rust.js │ │ │ ├── css.js │ │ │ └── python.js │ ├── index.d.ts │ └── index.js │ ├── package.json │ ├── test │ ├── testing-utils.ts │ ├── tokenize.test.ts │ ├── newline-handling.test.ts │ └── ast.test.ts │ ├── README.md │ └── pnpm-lock.yaml ├── .gitignore ├── apps └── docs │ ├── app │ ├── components │ │ ├── copy-button.css │ │ ├── copy-button.tsx │ │ ├── install-banner.css │ │ └── install-banner.tsx │ ├── editor │ │ └── page.tsx │ ├── remark │ │ ├── languages │ │ │ ├── javascript.ts │ │ │ ├── css.ts │ │ │ ├── html.ts │ │ │ ├── python.ts │ │ │ └── rust.ts │ │ ├── markdown.ts │ │ ├── page.css │ │ └── page.tsx │ ├── page.tsx │ ├── lib │ │ └── copy-image.ts │ ├── icon.svg │ ├── layout.tsx │ ├── global.css │ ├── carousel.tsx │ ├── live-editor.tsx │ └── styles.css │ ├── tsconfig.json │ └── package.json ├── .github └── workflows │ └── test.yml └── package.json /README.md: -------------------------------------------------------------------------------- 1 | packages/sugar-high/README.md -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - "packages/*" 3 | - "apps/*" -------------------------------------------------------------------------------- /packages/remark-sugar-high/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | .next 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | .next 4 | next-env.d.ts 5 | .DS_Store 6 | -------------------------------------------------------------------------------- /packages/sugar-high/lib/presets/index.js: -------------------------------------------------------------------------------- 1 | export * as css from './lang/css.js' 2 | export * as rust from './lang/rust.js' 3 | export * as python from './lang/python.js' 4 | -------------------------------------------------------------------------------- /packages/remark-sugar-high/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "ESNext", 4 | "jsx": "preserve", 5 | "lib": ["ESNext"], 6 | "moduleResolution": "bundler" 7 | } 8 | } -------------------------------------------------------------------------------- /apps/docs/app/components/copy-button.css: -------------------------------------------------------------------------------- 1 | .copy-button { 2 | background-color:transparent; 3 | border:none; 4 | cursor:pointer; 5 | padding:0; 6 | border-radius:0; 7 | } 8 | 9 | .copy-button svg { 10 | vertical-align: middle; 11 | } 12 | -------------------------------------------------------------------------------- /apps/docs/app/editor/page.tsx: -------------------------------------------------------------------------------- 1 | import LiveEditor from '../live-editor' 2 | 3 | const code = `` 4 | 5 | export default function Page() { 6 | return ( 7 | 11 | ) 12 | } 13 | -------------------------------------------------------------------------------- /apps/docs/app/remark/languages/javascript.ts: -------------------------------------------------------------------------------- 1 | export const code = `\ 2 | \`\`\`javascript {2,5} 3 | // Here is a simple function 4 | async function hello() { 5 | console.log('Hello, world from JavaScript!') 6 | return 123 // return a number 7 | } 8 | 9 | await hello() 10 | \`\`\` 11 | ` 12 | -------------------------------------------------------------------------------- /packages/sugar-high/lib/presets/index.d.ts: -------------------------------------------------------------------------------- 1 | type LanguageConfig = { 2 | keywords: Set 3 | onCommentStart(curr: string, next: string): 0 | 1 | 2 4 | onCommentEnd(prev: string, curr: string): 0 | 1 | 2 5 | } 6 | 7 | export const css: LanguageConfig 8 | export const rust: LanguageConfig 9 | export const python: LanguageConfig 10 | -------------------------------------------------------------------------------- /apps/docs/app/remark/languages/css.ts: -------------------------------------------------------------------------------- 1 | export const code = `\ 2 | \`\`\`css 3 | body { 4 | font-family: 'Geist Sans', sans-serif; 5 | background-color: #f0f0f0; 6 | color: #333; /* text color */ 7 | } 8 | 9 | h1 { 10 | color: #0070f3; 11 | } 12 | 13 | @media (max-width: 600px) { 14 | body { 15 | font-size: 14px; 16 | } 17 | } 18 | \`\`\` 19 | ` 20 | -------------------------------------------------------------------------------- /apps/docs/app/remark/languages/html.ts: -------------------------------------------------------------------------------- 1 | export const code = `\ 2 | \`\`\`html 3 | 4 | 5 | 6 | 7 | 8 | Document 9 | 10 | 11 |

Hello, world!

12 | 13 | 14 | \`\`\` 15 | ` 16 | -------------------------------------------------------------------------------- /apps/docs/app/remark/languages/python.ts: -------------------------------------------------------------------------------- 1 | export const code = `\ 2 | \`\`\`python 3 | """ 4 | This is a multi-line comment. 5 | Demonstrating functions, loops, and type hints. 6 | """ 7 | 8 | def factorial(n: int) -> int: 9 | """Compute the factorial of a number.""" 10 | result = 1 11 | for i in range(2, n + 1): 12 | result *= i 13 | return result 14 | 15 | print(factorial(5)) # Output: 120 16 | \`\`\` 17 | ` 18 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | 9 | jobs: 10 | test: 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - uses: actions/checkout@v3 15 | - uses: pnpm/action-setup@v4 16 | 17 | 18 | - uses: actions/setup-node@v3 19 | with: 20 | node-version: '18' 21 | - run: pnpm install 22 | - run: pnpm test 23 | -------------------------------------------------------------------------------- /apps/docs/app/remark/markdown.ts: -------------------------------------------------------------------------------- 1 | import { remark } from 'remark' 2 | import { highlight } from 'remark-sugar-high' 3 | import html from 'remark-html' 4 | import gfm from 'remark-gfm' 5 | 6 | export async function renderMarkdown(input: string) { 7 | const markdown = await remark() 8 | .use(gfm) 9 | .use(highlight) 10 | // @ts-expect-error ignore ts checking 11 | .use(html, { sanitize: false }) 12 | .process(input) 13 | 14 | return markdown.toString() 15 | } 16 | -------------------------------------------------------------------------------- /packages/sugar-high/lib/presets/lang/rust.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | export const keywords = new Set([ 3 | 'as', 'break', 'const', 'continue', 'crate', 'else', 'enum', 'extern', 'false', 'fn', 'for', 4 | 'if', 'impl', 'in', 'let', 'loop', 'match', 'mod', 'move', 'mut', 'pub', 'ref', 'return', 'self', 5 | 'Self', 'static', 'struct', 'super', 'trait', 'true', 'type', 'unsafe', 'use', 'where', 'while', 6 | 'async', 'await', 'dyn', 'abstract', 'become', 'box', 'do', 'final', 'macro', 'override', 7 | 'priv', 'typeof', 'unsized', 'virtual', 'yield', 'try', 8 | ]) 9 | -------------------------------------------------------------------------------- /apps/docs/app/page.tsx: -------------------------------------------------------------------------------- 1 | import Carousel from './carousel' 2 | import LiveEditor from './live-editor' 3 | import InstallBanner from './components/install-banner' 4 | 5 | export default function Page() { 6 | return ( 7 | <> 8 |
9 |

10 | Sugar High 11 |

12 |

Super lightweight syntax highlighter

13 |
14 | 15 | 16 | 17 | 18 | 19 | ) 20 | } 21 | -------------------------------------------------------------------------------- /apps/docs/app/lib/copy-image.ts: -------------------------------------------------------------------------------- 1 | export async function copyImageDataUrl(dataUrl: string) { 2 | try { 3 | if (navigator.clipboard && window.ClipboardItem) { 4 | // Modern browsers: Use Clipboard API 5 | const blob = await (await fetch(dataUrl)).blob() 6 | const item = new ClipboardItem({ 'image/png': blob }) 7 | await navigator.clipboard.write([item]) 8 | return Promise.resolve() 9 | } else { 10 | return Promise.reject('Clipboard API not available') 11 | } 12 | } catch (error) { 13 | return Promise.reject(error) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /apps/docs/app/remark/languages/rust.ts: -------------------------------------------------------------------------------- 1 | export const code = `\ 2 | \`\`\`rust 3 | 4 | use std::fs; // Import file system utilities 5 | use std::io::{self, Write}; // Import I/O utilities 6 | 7 | /* Read file content into a string */ 8 | fn read_file(path: &str) -> io::Result { 9 | fs::read_to_string(path) // Read file content into a String 10 | } 11 | 12 | fn main() { 13 | match read_file("example.txt") { 14 | Ok(content) => println!("File content:\n{}", content), 15 | Err(e) => eprintln!("Error reading file: {}", e), 16 | } 17 | } 18 | \`\`\` 19 | ` 20 | -------------------------------------------------------------------------------- /packages/sugar-high/lib/presets/lang/css.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | export const keywords = new Set([ 3 | // css keywords like @media, @import, @keyframes, etc. 4 | '@media', '@import', '@keyframes', '@font-face', '@supports', '@page', '@counter-style', 5 | '@font-feature-values', '@viewport', '@counter-style', '@font-feature-values', '@document', 6 | ]) 7 | 8 | export const onCommentStart = (currentChar, nextChar) => { 9 | return '/*' === (currentChar + nextChar) ? 1 : 0 10 | } 11 | 12 | export const onCommentEnd = (prevChar, currChar) => { 13 | return '*/' === (prevChar + currChar) ? 1 : 0 14 | } 15 | -------------------------------------------------------------------------------- /packages/sugar-high/lib/presets/lang/python.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | export const keywords = new Set([ 3 | 'and', 'as', 'assert', 'async', 'await', 'break', 'class', 'continue', 'def', 'del', 'elif', 4 | 'else', 'except', 'finally', 'for', 'from', 'global', 'if', 'import', 'in', 'is', 'lambda', 5 | 'nonlocal', 'not', 'or', 'pass', 'raise', 'return', 'try', 'while', 'with', 'yield', 6 | ]) 7 | 8 | export const onCommentStart = (currentChar, _nextChar) => { 9 | return currentChar === '#' ? 1 : 0 10 | } 11 | 12 | export const onCommentEnd = (_prevChar, currChar) => { 13 | return currChar === '\n' ? 1 : 0 14 | } 15 | -------------------------------------------------------------------------------- /packages/sugar-high/lib/index.d.ts: -------------------------------------------------------------------------------- 1 | type HighlightOptions = { 2 | keywords?: Set 3 | onCommentStart?: (curr: string, next: string) => number | boolean 4 | onCommentEnd?: (curr: string, prev: string) => number | boolean 5 | } 6 | 7 | export function highlight(code: string, options?: HighlightOptions): string 8 | export function tokenize(code: string, options?: HighlightOptions): Array<[number, string]> 9 | export function generate(tokens: Array<[number, string]>): Array 10 | export const SugarHigh: { 11 | TokenTypes: { 12 | [key: number]: string 13 | } 14 | TokenMap: Map 15 | } 16 | -------------------------------------------------------------------------------- /apps/docs/app/icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /apps/docs/app/components/copy-button.tsx: -------------------------------------------------------------------------------- 1 | import "./copy-button.css" 2 | 3 | export function CopyButton({ codeSnippet, ...props }) { 4 | return ( 5 | 14 | ) 15 | } 16 | -------------------------------------------------------------------------------- /apps/docs/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2017", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "allowJs": true, 10 | "skipLibCheck": true, 11 | "strict": false, 12 | "noEmit": true, 13 | "incremental": true, 14 | "module": "esnext", 15 | "esModuleInterop": true, 16 | "moduleResolution": "node", 17 | "resolveJsonModule": true, 18 | "isolatedModules": true, 19 | "jsx": "react-jsx", 20 | "plugins": [ 21 | { 22 | "name": "next" 23 | } 24 | ] 25 | }, 26 | "include": [ 27 | "next-env.d.ts", 28 | ".next/types/**/*.ts", 29 | "**/*.ts", 30 | "**/*.tsx", 31 | ".next/dev/types/**/*.ts" 32 | ], 33 | "exclude": [ 34 | "node_modules" 35 | ] 36 | } 37 | -------------------------------------------------------------------------------- /packages/sugar-high/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sugar-high", 3 | "version": "0.9.5", 4 | "type": "module", 5 | "types": "./lib/index.d.ts", 6 | "main": "./lib/index.js", 7 | "exports": { 8 | ".": { 9 | "types": "./lib/index.d.ts", 10 | "default": "./lib/index.js" 11 | }, 12 | "./presets": { 13 | "types": "./lib/presets/index.d.ts", 14 | "default": "./lib/presets/index.js" 15 | } 16 | }, 17 | "description": "Super lightweight JSX syntax highlighter", 18 | "files": [ 19 | "lib" 20 | ], 21 | "license": "MIT", 22 | "scripts": { 23 | "test": "vitest", 24 | "build": "echo 'package requires no build'" 25 | }, 26 | "devDependencies": { 27 | "@types/node": "22.12.0", 28 | "typescript": "5.7.3", 29 | "vitest": "^3.0.2" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /packages/remark-sugar-high/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "remark-sugar-high", 3 | "version": "0.6.0", 4 | "description": "remark plugin of sugar high syntax highlighter", 5 | "type": "module", 6 | "main": "./dist/index.js", 7 | "types": "./dist/index.d.ts", 8 | "scripts": { 9 | "build": "bunchee", 10 | "prepublishOnly": "pnpm build" 11 | }, 12 | "files": [ 13 | "dist" 14 | ], 15 | "keywords": [ 16 | "remark-plugin" 17 | ], 18 | "author": "huozhi", 19 | "license": "MIT", 20 | "packageManager": "pnpm@9.12.2", 21 | "dependencies": { 22 | "parse-numeric-range": "^1.3.0", 23 | "sugar-high": "workspace:*", 24 | "unist-util-map": "^4.0.0" 25 | }, 26 | "devDependencies": { 27 | "@types/node": "^22.12.0", 28 | "bunchee": "^6.3.1", 29 | "typescript": "^5.7.2" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /apps/docs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "docs", 3 | "version": "1.0.0", 4 | "private": true, 5 | "description": "Documentation website for sugar-high packages", 6 | "type": "module", 7 | "scripts": { 8 | "dev": "next dev", 9 | "build": "next build", 10 | "start": "next start" 11 | }, 12 | "dependencies": { 13 | "sugar-high": "workspace:*", 14 | "remark-sugar-high": "workspace:*", 15 | "codice": "^1.5.4", 16 | "dom-to-image": "^2.6.0", 17 | "next": "^16.0.7", 18 | "react": "^19.2.0", 19 | "react-dom": "^19.2.0", 20 | "remark": "^15.0.1", 21 | "remark-gfm": "^4.0.0", 22 | "remark-html": "^16.0.1" 23 | }, 24 | "devDependencies": { 25 | "@types/node": "22.12.0", 26 | "@types/react": "19.0.7", 27 | "typescript": "5.7.3" 28 | }, 29 | "packageManager": "pnpm@9.12.2" 30 | } 31 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sugar-high-monorepo", 3 | "private": true, 4 | "description": "Monorepo for sugar-high and remark-sugar-high packages", 5 | "type": "module", 6 | "scripts": { 7 | "build": "pnpm -r run build", 8 | "test": "pnpm -r run test", 9 | "dev": "pnpm -r run dev", 10 | "clean": "pnpm -r run clean", 11 | "site": "pnpm --filter docs dev", 12 | "docs:build": "pnpm build && pnpm --filter docs build" 13 | }, 14 | "packageManager": "pnpm@9.12.2", 15 | "license": "MIT", 16 | "author": "huozhi", 17 | "devDependencies": { 18 | "next": "^16.0.7" 19 | }, 20 | "pnpm": { 21 | "overrides": { 22 | "sugar-high": "workspace:*" 23 | }, 24 | "packageExtensions": { 25 | "codice@*": { 26 | "dependencies": { 27 | "sugar-high": "workspace:*" 28 | } 29 | } 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /apps/docs/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import './global.css' 2 | import './styles.css' 3 | 4 | const imgUrl = 'https://repository-images.githubusercontent.com/453236442/aa0db684-bad3-4cd3-a420-f4e53b8c6757' 5 | 6 | export default function Layout({ children }) { 7 | return ( 8 | 9 | 10 | {children} 11 | 12 | 13 | ) 14 | } 15 | 16 | export const metadata = { 17 | metadataBase: new URL('https://sugar-high.vercel.app'), 18 | title: 'Sugar High', 19 | authors: [{ name: '@huozhi' }], 20 | description: 'Super lightweight JSX syntax highlighter, around 1KB after minified and gzipped', 21 | twitter: { 22 | card: 'summary_large_image', 23 | images: imgUrl, 24 | title: 'Sugar High', 25 | description: 'Super lightweight JSX syntax highlighter, around 1KB after minified and gzipped', 26 | }, 27 | openGraph: { 28 | images: imgUrl, 29 | title: 'Sugar High', 30 | description: 'Super lightweight JSX syntax highlighter, around 1KB after minified and gzipped', 31 | }, 32 | } 33 | 34 | export const viewport = { 35 | width: 'device-width', 36 | initialScale: 1, 37 | maximumScale: 1, 38 | } 39 | -------------------------------------------------------------------------------- /packages/sugar-high/test/testing-utils.ts: -------------------------------------------------------------------------------- 1 | import { SugarHigh } from '..' 2 | 3 | function getTypeName(token) { 4 | return SugarHigh.TokenTypes[token[0]] 5 | } 6 | 7 | function getTokenValues(tokens) { 8 | return tokens.map((tk) => tk[1]) 9 | } 10 | 11 | function mergeSpaces(str) { 12 | return str.trim().replace(/^[\s]{2,}$/g, ' ') 13 | } 14 | 15 | function filterSpaces(arr) { 16 | return arr 17 | .map(t => mergeSpaces(t)) 18 | .filter(Boolean) 19 | } 20 | 21 | function extractTokenValues(tokens) { 22 | return filterSpaces(getTokenValues(tokens)) 23 | } 24 | 25 | function getTokenArray(tokens) { 26 | return tokens.map((tk) => [tk[1], getTypeName(tk)]); 27 | } 28 | 29 | function extractTokenArray(tokens, options: { filterSpaces?: boolean } = {}) { 30 | const { filterSpaces = true } = options 31 | return tokens 32 | .map((tk) => [mergeSpaces(tk[1]), getTypeName(tk)]) 33 | .filter(([_, type]) => 34 | filterSpaces 35 | ? type !== 'space' && type !== 'break' 36 | : true 37 | ) 38 | } 39 | 40 | // Generate the string representation of the tokens 41 | function getTokensAsString(tokens: any[], options: { filterSpaces?: boolean } = {}) { 42 | const extracted = extractTokenArray(tokens, options) 43 | return extracted.map(([value, type]) => `${value} => ${type}`) 44 | } 45 | 46 | export { 47 | extractTokenArray, 48 | extractTokenValues, 49 | getTokenValues, 50 | getTokensAsString, 51 | getTokenArray, 52 | } -------------------------------------------------------------------------------- /apps/docs/app/remark/page.css: -------------------------------------------------------------------------------- 1 | .remark-page { 2 | font-family: var(--font-geist-sans), system-ui, -apple-system, 3 | BlinkMacSystemFont, Segoe UI, Roboto, Helvetica Neue, Arial, Noto Sans, 4 | sans-serif, Apple Color Emoji, Segoe UI Emoji, Segoe UI Symbol, 5 | Noto Color Emoji; 6 | font-feature-settings: 'liga', 'clig', 'calt'; 7 | 8 | padding: 2rem; 9 | background-color: #f7fcfc; 10 | 11 | /* theme */ 12 | --sh-class: #2d5e9d; 13 | --sh-identifier: #354150; 14 | --sh-sign: #8996a3; 15 | --sh-property: #0550ae; 16 | --sh-entity: #9eb8d6; 17 | --sh-jsxliterals: #6266d1; 18 | --sh-string: #73747c; 19 | --sh-keyword: #2876db; 20 | --sh-comment: #a19595; 21 | /* codice */ 22 | --codice-code-padding: 0; 23 | } 24 | 25 | .remark-page main { 26 | width: 600px; 27 | } 28 | 29 | .remark-page a { 30 | color: #888; 31 | } 32 | .remark-page a:hover { 33 | color: #333; 34 | } 35 | 36 | .remark-page pre { 37 | background-color: #ffffff; 38 | } 39 | 40 | .code { 41 | margin-top: 1rem; 42 | margin-bottom: 1rem; 43 | width: 100%; 44 | } 45 | div[data-codice-editor-header="true"] { 46 | background-color: #d6e1eb; 47 | padding: 4px 6px; 48 | color: #507a99; 49 | margin: 0; 50 | } 51 | /* Override */ 52 | [data-codice-code] pre, 53 | [data-codice-code] code { 54 | width: 100%; 55 | } 56 | .code[data-codice-code="true"] pre { 57 | padding: 8px 0; 58 | } 59 | .sh__line { 60 | display: block; 61 | padding: 0 0.5em; 62 | } 63 | .sh__line--highlighted { 64 | background-color: #dff1ff; 65 | } -------------------------------------------------------------------------------- /apps/docs/app/components/install-banner.css: -------------------------------------------------------------------------------- 1 | .install-banner { 2 | display: flex; 3 | flex-direction: column; 4 | justify-content: center; 5 | max-width: 100vw; 6 | min-height: 200px; 7 | background-color: #2b2b2b; 8 | color: #ccc; 9 | margin: 0; 10 | padding: 32px 0 16px 0; 11 | } 12 | 13 | .install-banner ::selection { 14 | background-color: #fff; 15 | color: #000; 16 | } 17 | 18 | .install-banner__command { 19 | text-align: center; 20 | } 21 | 22 | .install-banner__command a, 23 | .install-banner__command a:visited { 24 | color: #fff; 25 | } 26 | 27 | .install-banner__code { 28 | position: relative; 29 | background-color: #383838; 30 | font-size: 1rem; 31 | padding: 16px; 32 | width: 600px; 33 | max-width: calc(100vw - 48px); 34 | border-radius: 2px; 35 | } 36 | 37 | .install-banner__block { 38 | position: relative; 39 | font-size: 1rem; 40 | padding: 8px 16px; 41 | width: 600px; 42 | max-width: calc(100vw - 48px); 43 | border-radius: 2px; 44 | } 45 | 46 | /* copy code button */ 47 | .install-banner__code button { 48 | position: absolute; 49 | top: 8px; 50 | right: 8px; 51 | background-color: #383838; 52 | color: #fff; 53 | border: none; 54 | cursor: pointer; 55 | padding: 4px 8px; 56 | border-radius: 2px; 57 | font-size: 0.75rem; 58 | } 59 | 60 | .install-banner__code--dimmed { 61 | color: #afafaf; 62 | } 63 | 64 | .install-banner a:hover { 65 | color: #fff; 66 | } 67 | 68 | @media screen and (max-width: 600px) { 69 | .install-banner__code { 70 | font-size: 0.75rem; 71 | } 72 | } -------------------------------------------------------------------------------- /packages/sugar-high/test/tokenize.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from 'vitest' 2 | import { tokenize } from '..' 3 | import { getTokensAsString } from './testing-utils' 4 | 5 | describe('tokenize - customized keywords', () => { 6 | it('should tokenize the input string with the given keywords', () => { 7 | const input = 'def f(): return 1' 8 | const keywords = new Set(['def', 'return']) 9 | const actual = getTokensAsString(tokenize(input, { keywords })) 10 | expect(actual).toMatchInlineSnapshot(` 11 | [ 12 | "def => keyword", 13 | "f => identifier", 14 | "( => sign", 15 | ") => sign", 16 | ": => sign", 17 | "return => keyword", 18 | "1 => class", 19 | ] 20 | `) 21 | }) 22 | }) 23 | 24 | 25 | describe('tokenize - customized comment rule', () => { 26 | it('should tokenize the input string with the given comment rule', () => { 27 | const input = `\ 28 | # define a function 29 | def f(): 30 | return 2 # this is a comment 31 | ` 32 | const keywords = new Set(['def', 'return']) 33 | const onCommentStart = (curr, next) => { 34 | return curr === '#' 35 | } 36 | const onCommentEnd = (prev, curr) => { 37 | return curr === '\n' 38 | } 39 | const actual = getTokensAsString(tokenize(input, { 40 | keywords, 41 | onCommentStart, 42 | onCommentEnd 43 | })) 44 | expect(actual).toMatchInlineSnapshot(` 45 | [ 46 | "# define a function => comment", 47 | "def => keyword", 48 | "f => identifier", 49 | "( => sign", 50 | ") => sign", 51 | ": => sign", 52 | "return => keyword", 53 | "2 => class", 54 | "# this is a comment => comment", 55 | ] 56 | `) 57 | }) 58 | }) -------------------------------------------------------------------------------- /apps/docs/app/global.css: -------------------------------------------------------------------------------- 1 | /** CSS Reset from https://www.joshwcomeau.com/css/custom-css-reset <3 */ 2 | *, 3 | *::before, 4 | *::after { 5 | box-sizing: border-box; 6 | } 7 | 8 | * { 9 | margin: 0; 10 | } 11 | 12 | body { 13 | line-height: 1.5; 14 | -webkit-font-smoothing: antialiased; 15 | } 16 | 17 | img, 18 | picture, 19 | video, 20 | canvas, 21 | svg { 22 | display: block; 23 | max-width: 100%; 24 | } 25 | 26 | input, 27 | button, 28 | textarea, 29 | select { 30 | font: inherit; 31 | } 32 | 33 | p, 34 | h1, 35 | h2, 36 | h3, 37 | h4, 38 | h5, 39 | h6 { 40 | overflow-wrap: break-word; 41 | } 42 | 43 | /** Styles */ 44 | body { 45 | font-variant: common-ligatures contextual; 46 | letter-spacing: -0.01em; 47 | font-size: 15px; 48 | line-height: 1.5; 49 | } 50 | 51 | h1 { 52 | font-size: 1.5rem; 53 | margin-bottom: 1rem; 54 | } 55 | 56 | h2 { 57 | font-size: 1.15rem; 58 | margin-top: 1.5rem; 59 | margin-bottom: 1rem; 60 | } 61 | 62 | p, 63 | pre { 64 | margin-bottom: 0.75rem; 65 | max-width: 100%; 66 | } 67 | 68 | a { 69 | text-decoration: underline solid currentColor; 70 | text-underline-position: from-font; 71 | text-decoration-thickness: from-font; 72 | } 73 | 74 | code { 75 | font-family: var(--font-geist-mono), menlo, 'Courier New', Courier, monospace; 76 | letter-spacing: 0; 77 | font-size: 0.95em; 78 | } 79 | 80 | pre { 81 | display: inline-block; 82 | border-radius: 3px; 83 | padding: 0.3em 0; 84 | max-width: 100%; 85 | white-space: pre-wrap; 86 | } 87 | 88 | pre code { 89 | padding: 0; 90 | background: none; 91 | border-radius: 0; 92 | } 93 | 94 | footer { 95 | margin-top: 2rem; 96 | padding-top: 1rem; 97 | max-width: 100%; 98 | } 99 | 100 | ul { 101 | padding-left: 1rem; 102 | } 103 | 104 | li { 105 | list-style: '• '; 106 | } 107 | -------------------------------------------------------------------------------- /apps/docs/app/components/install-banner.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import { CopyButton } from './copy-button' 4 | import { Code } from 'codice' 5 | import './install-banner.css' 6 | import Link from 'next/link' 7 | 8 | const cssCode = `\ 9 | /* styles.css */ 10 | :root { 11 | --sh-class: #2d5e9d; 12 | --sh-identifier: #354150; 13 | --sh-sign: #8996a3; 14 | --sh-property: #0550ae; 15 | --sh-entity: #249a97; 16 | --sh-jsxliterals: #6266d1; 17 | --sh-string: #00a99a; 18 | --sh-keyword: #f47067; 19 | --sh-comment: #a19595; 20 | }` 21 | 22 | const usageCode = `\ 23 | import { highlight } from 'sugar-high' 24 | 25 | const html = highlight(code)` 26 | 27 | export default function InstallBanner() { 28 | return ( 29 |
30 | 43 |

44 | Highlight your code with{' '} 45 | 46 | sugar-high 47 | 48 |

49 |
50 |
51 | 52 | {usageCode} 53 | 54 | 55 |
56 |
57 |
58 | 59 | {cssCode} 60 | 61 | 62 |
63 | 64 |
65 |

Usage with remark.js

66 |

67 | Remark.js{' '} 68 | is a powerful markdown processor, you can use the sugar-high remark plugin with remark.js to highlight code blocks in markdown. 69 |

70 |
71 |
72 |
73 | ) 74 | } 75 | -------------------------------------------------------------------------------- /apps/docs/app/remark/page.tsx: -------------------------------------------------------------------------------- 1 | import { renderMarkdown } from './markdown' 2 | import { code as jsCode } from './languages/javascript' 3 | import { code as cssCode } from './languages/css' 4 | import { code as htmlCode } from './languages/html' 5 | import { code as pythonCode } from './languages/python' 6 | import { code as rustCode } from './languages/rust' 7 | import { Code } from 'codice' 8 | import './page.css' 9 | import Link from 'next/link' 10 | 11 | const usageCode = `\ 12 | \`\`\`javascript {2,9} 13 | import { remark } from 'remark' 14 | import { highlight } from 'remark-sugar-high' 15 | import html from 'remark-html' 16 | import gfm from 'remark-gfm' 17 | 18 | async function renderMarkdown(input) { 19 | const markdown = await remark() 20 | .use(gfm) 21 | .use(highlight) 22 | .use(html, { sanitize: false }) 23 | .process(input) 24 | 25 | return markdown.toString() 26 | } 27 | 28 | export default async Preview({ markdown }) { 29 | const html = await renderMarkdown(markdown) 30 | return ( 31 |
32 | ) 33 | } 34 | \`\`\` 35 | ` 36 | 37 | async function CodeExample({ 38 | filename, 39 | code, 40 | }: { 41 | filename: string 42 | code: string 43 | }) { 44 | const html = await renderMarkdown(code) 45 | return ( 46 | 47 | {html} 48 | 49 | ) 50 | } 51 | 52 | export default async function RemarkPage() { 53 | return ( 54 |
55 |
56 |

Remark Sugar High

57 | 58 |

59 | Use{' '} 60 | 61 | Sugar High Syntax Highlighting 62 | with Remark Plugin.{' '} 63 | 64 | Source Code ↗ 65 | 66 |

67 | 68 |

Usage

69 |

Install

70 |
71 |           {`npm install remark remark-html remark-gfm remark-sugar-high`}
72 |         
73 | 74 |

API

75 | 76 | 77 | 78 |

Examples

79 | 80 | 81 | 82 | 83 | 84 | 85 |
86 |
87 | ) 88 | } -------------------------------------------------------------------------------- /packages/sugar-high/README.md: -------------------------------------------------------------------------------- 1 | # Sugar High 2 | 3 | [![Build][build-badge]][build] 4 | 5 | ### Introduction 6 | 7 | Super lightweight JSX syntax highlighter, around 1KB after minified and gzipped 8 | 9 | ![img](https://repository-images.githubusercontent.com/453236442/aa0db684-bad3-4cd3-a420-f4e53b8c6757) 10 | 11 | ### Usage 12 | 13 | ```sh 14 | npm install --save sugar-high 15 | ``` 16 | 17 | ```js 18 | import { highlight } from 'sugar-high' 19 | 20 | const codeHTML = highlight(code) 21 | 22 | document.querySelector('pre > code').innerHTML = codeHTML 23 | ``` 24 | 25 | ### Highlight with CSS 26 | 27 | Then make your own theme with customized colors by token type and put in global CSS. The corresponding class names start with `--sh-` prefix. 28 | 29 | ```css 30 | /** 31 | * Types that sugar-high have: 32 | * 33 | * identifier 34 | * keyword 35 | * string 36 | * Class, number and null 37 | * property 38 | * entity 39 | * jsx literals 40 | * sign 41 | * comment 42 | * break 43 | * space 44 | */ 45 | :root { 46 | --sh-class: #2d5e9d; 47 | --sh-identifier: #354150; 48 | --sh-sign: #8996a3; 49 | --sh-property: #0550ae; 50 | --sh-entity: #249a97; 51 | --sh-jsxliterals: #6266d1; 52 | --sh-string: #00a99a; 53 | --sh-keyword: #f47067; 54 | --sh-comment: #a19595; 55 | } 56 | ``` 57 | 58 | ### Features 59 | 60 | #### Line number 61 | 62 | Sugar high provide `.sh_line` class name for each line. To display line number, define the `.sh_line::before` element with CSS will enable line numbers automatically. 63 | 64 | ```css 65 | pre code { 66 | counter-reset: sh-line-number; 67 | } 68 | 69 | .sh__line::before { 70 | counter-increment: sh-line-number 1; 71 | content: counter(sh-line-number); 72 | margin-right: 24px; 73 | text-align: right; 74 | color: #a4a4a4; 75 | } 76 | ``` 77 | 78 | ### Line Highlight 79 | 80 | Use `.sh__line:nth-child()` to highlight specific line. 81 | 82 | ```css 83 | .sh__line:nth-child(5) { 84 | background: #f5f5f5; 85 | } 86 | ``` 87 | 88 | #### CSS Class Names 89 | 90 | You can use `.sh__token--` to customize the output node of each token. 91 | 92 | ```css 93 | .sh__token--keyword { 94 | background: #f47067; 95 | } 96 | ``` 97 | 98 | ### Use With Remark.js 99 | 100 | [Remark.js](https://remark.js.org/) is a powerful markdown processor, you can use the [sugar-high remark plugin](https://sugar-high.vercel.app/remark) with remark.js to highlight code blocks in markdown. 101 | 102 | Check out the [readme](https://github.com/huozhi/sugar-high/tree/main/packages/remark-sugar-high) for more details. 103 | 104 | ### LICENSE 105 | 106 | MIT 107 | 108 | 109 | 110 | [build-badge]: https://github.com/huozhi/sugar-high/workflows/Test/badge.svg 111 | 112 | [build]: https://github.com/huozhi/sugar-high/actions 113 | 114 | [coverage-badge]: https://badge.fury.io/js/sugar-high.svg 115 | 116 | [coverage]: https://codecov.io/github/huozhi/sugar-high 117 | 118 | [downloads-badge]: https://img.shields.io/npm/dm/sugar-high.svg 119 | 120 | [downloads]: https://www.npmjs.com/package/sugar-high 121 | -------------------------------------------------------------------------------- /packages/sugar-high/test/newline-handling.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, it, expect } from 'vitest' 2 | import { tokenize, generate, highlight } from '../lib/index.js' 3 | 4 | describe('newline handling', () => { 5 | it('should create new line in HTML when switching to new line in editor', () => { 6 | const codeWithNewlines = `function test() { 7 | console.log('hello'); 8 | return true; 9 | }` 10 | 11 | const tokens = tokenize(codeWithNewlines) 12 | const lines = generate(tokens) 13 | const html = highlight(codeWithNewlines) 14 | 15 | // Should generate 4 lines for 4 input lines 16 | expect(lines).toHaveLength(4) 17 | expect(html.split('\n')).toHaveLength(4) 18 | 19 | // Verify line contents 20 | const lineContents = lines.map(line => 21 | line.children.map(child => child.children[0].value).join('') 22 | ) 23 | 24 | expect(lineContents[0]).toBe('function test() {') 25 | expect(lineContents[1]).toBe(' console.log(\'hello\');') 26 | expect(lineContents[2]).toBe(' return true;') 27 | expect(lineContents[3]).toBe('}') 28 | }) 29 | 30 | it('should preserve empty lines in HTML output', () => { 31 | const codeWithEmptyLines = `function test() { 32 | console.log('first'); 33 | 34 | console.log('after empty'); 35 | 36 | 37 | console.log('after two empty'); 38 | }` 39 | 40 | const lines = generate(tokenize(codeWithEmptyLines)) 41 | const html = highlight(codeWithEmptyLines) 42 | 43 | // Should generate 8 lines (including empty lines) 44 | expect(lines).toHaveLength(8) 45 | expect(html.split('\n')).toHaveLength(8) 46 | 47 | // Verify empty lines are preserved 48 | const lineContents = lines.map(line => 49 | line.children.map(child => child.children[0].value).join('') 50 | ) 51 | 52 | expect(lineContents[2]).toBe('') // First empty line 53 | expect(lineContents[4]).toBe('') // Second empty line 54 | expect(lineContents[5]).toBe('') // Third empty line 55 | }) 56 | 57 | it('should create empty line when code ends with newline', () => { 58 | const codeEndingWithNewline = 'const x = 1;\n' 59 | 60 | const tokens = tokenize(codeEndingWithNewline) 61 | const lines = generate(tokens) 62 | const html = highlight(codeEndingWithNewline) 63 | 64 | // Should generate 2 lines: one with code, one empty 65 | expect(lines).toHaveLength(2) 66 | expect(html.split('\n')).toHaveLength(2) 67 | 68 | const lineContents = lines.map(line => 69 | line.children.map(child => child.children[0].value).join('') 70 | ) 71 | 72 | expect(lineContents[0]).toBe('const x = 1;') 73 | expect(lineContents[1]).toBe('') // Empty line from trailing newline 74 | }) 75 | 76 | it('should handle multiple trailing newlines', () => { 77 | const codeWithMultipleTrailingNewlines = 'const x = 1;\n\n\n' 78 | 79 | const lines = generate(tokenize(codeWithMultipleTrailingNewlines)) 80 | const html = highlight(codeWithMultipleTrailingNewlines) 81 | 82 | // Should generate 4 lines: one with code, three empty 83 | expect(lines).toHaveLength(4) 84 | expect(html.split('\n')).toHaveLength(4) 85 | 86 | const lineContents = lines.map(line => 87 | line.children.map(child => child.children[0].value).join('') 88 | ) 89 | 90 | expect(lineContents[0]).toBe('const x = 1;') 91 | expect(lineContents[1]).toBe('') // First empty line 92 | expect(lineContents[2]).toBe('') // Second empty line 93 | expect(lineContents[3]).toBe('') // Third empty line 94 | }) 95 | 96 | it('should handle code with only newlines', () => { 97 | const onlyNewlines = '\n\n' 98 | 99 | const lines = generate(tokenize(onlyNewlines)) 100 | const html = highlight(onlyNewlines) 101 | 102 | // Should generate 3 empty lines 103 | expect(lines).toHaveLength(3) 104 | expect(html.split('\n')).toHaveLength(3) 105 | 106 | const lineContents = lines.map(line => 107 | line.children.map(child => child.children[0].value).join('') 108 | ) 109 | 110 | expect(lineContents.every(content => content === '')).toBe(true) 111 | }) 112 | }) 113 | -------------------------------------------------------------------------------- /packages/remark-sugar-high/README.md: -------------------------------------------------------------------------------- 1 | # remark-sugar-high 2 | 3 | Remark plugin for [Sugar High](https://sugar-high.vercel.app) syntax highlighter. 4 | 5 | [Website](https://sugar-high.vercel.app/remark) 6 | 7 | 8 | ## Installation 9 | 10 | ```bash 11 | $ npm i -S remark-sugar-high 12 | ``` 13 | 14 | ## Usage 15 | 16 | Input markdown file: 17 | 18 | ``` 19 | \`\`\`javascript {2,5} 20 | // Here is a simple function 21 | async function hello() { 22 | console.log('Hello, world from JavaScript!') 23 | return 123 // return a number 24 | } 25 | 26 | await hello() 27 | \`\`\` 28 | ``` 29 | 30 | Using [remark](https://github.com/remarkjs/remark): 31 | 32 | ```js 33 | const { highlight } = require('remark-sugar-high'); 34 | 35 | await remark() 36 | .use(highlight) 37 | .use(require('remark-html')) 38 | .process(file, (err, file) => console.log(String(file))); 39 | ``` 40 | 41 |
42 | Output HTML 43 |

44 | 45 | ```html 46 |

// Here is a simple function
49 | async function hello() {
50 |     console.log('Hello, world from JavaScript!')
51 |     return 123 // return a number
52 | }
53 | 
54 | await hello()
55 | 
56 | ``` 57 |

58 | 59 |
60 | 61 | Customize the color theme with sugar-high CSS variables. Check [sugar-high highlight-with-css section](https://github.com/huozhi/sugar-high#highlight-with-css) for more details. 62 | 63 | 64 | ## License 65 | 66 | MIT 67 | -------------------------------------------------------------------------------- /packages/remark-sugar-high/src/index.ts: -------------------------------------------------------------------------------- 1 | import { tokenize, generate, type HighlightOptions } from 'sugar-high' 2 | import * as languagePresets from 'sugar-high/presets' 3 | import { map as unistMap } from 'unist-util-map' 4 | import rangeParser from 'parse-numeric-range' 5 | 6 | function cx(...args: any[]): string { 7 | return args.filter(Boolean).join(' ') 8 | } 9 | 10 | type HighlightRange = number | [number, number] 11 | 12 | /** 13 | * Parses highlight information from the `meta` field of a Remark code block node. 14 | * @param meta The `meta` string containing highlight information, e.g., "{1,3-5}". 15 | * @returns An array of highlight ranges. Single numbers represent single lines, and tuples represent ranges. 16 | */ 17 | function parseHighlightMeta(meta?: string): HighlightRange[] { 18 | if (!meta) return [] 19 | 20 | const highlightRegex = /{([\d,-]+)}/ // Matches {1,3-5} 21 | const match = meta.match(highlightRegex) 22 | 23 | if (!match) return [] 24 | 25 | const ranges = match[1].split(',').map((range) => { 26 | if (range.includes('-')) { 27 | const [start, end] = range.split('-').map(Number) 28 | return [start, end] as [number, number] 29 | } 30 | return Number(range) 31 | }) 32 | 33 | return ranges 34 | } 35 | 36 | const parseLang = (str) => { 37 | const match = (regexp) => { 38 | const m = (str || '').match(regexp) 39 | return Array.isArray(m) ? m.filter(Boolean) : [] 40 | } 41 | 42 | const [lang = 'unknown'] = match(/^[a-zA-Z\d-]*/g) 43 | 44 | const range = rangeParser( 45 | match(/\{(.*?)\}$/g) 46 | .join(',') 47 | .replace(/^\{/, '') 48 | .replace(/\}$/, '') 49 | ) 50 | 51 | return { 52 | lang, 53 | range, 54 | } 55 | } 56 | 57 | const h = (type, attrs, children) => { 58 | return { 59 | type: 'element', 60 | tagName: type, 61 | data: { 62 | hName: type, 63 | hProperties: attrs, 64 | hChildren: children, 65 | }, 66 | properties: attrs, 67 | children, 68 | } 69 | } 70 | 71 | const highlight = () => (tree) => { 72 | return unistMap(tree, (node) => { 73 | const { type, tagName } = node 74 | if (tagName !== 'code' && type !== 'code') return node 75 | 76 | const { lang } = parseLang(node.lang) 77 | 78 | const highlightRanges = parseHighlightMeta(node.meta) 79 | const highlightLineNumbers = new Set() 80 | highlightRanges.forEach((range) => { 81 | if (Array.isArray(range)) { 82 | for (let i = range[0]; i <= range[1]; i++) { 83 | highlightLineNumbers.add(i) 84 | } 85 | } else { 86 | highlightLineNumbers.add(range) 87 | } 88 | }) 89 | 90 | let options: HighlightOptions | undefined = undefined 91 | 92 | if (lang in languagePresets) { 93 | options = languagePresets[lang] 94 | } 95 | 96 | const codeText = 97 | node.value || 98 | node.children 99 | .filter(({ type }) => type === 'text') 100 | .map(({ value }) => value) 101 | .pop() 102 | 103 | const childrenLines = generate(tokenize(codeText, options)) 104 | 105 | let lineIndex = 1 106 | for (let i = 0; i < childrenLines.length; i++) { 107 | const line = childrenLines[i] 108 | // if it's highlighted lines, add a classname `sh__line--highlighted` 109 | let highLightClassName = '' 110 | let isCurrentLineHighlighted = highlightLineNumbers.has(lineIndex) 111 | 112 | if (isCurrentLineHighlighted) { 113 | highLightClassName = 'sh__line--highlighted' 114 | } 115 | 116 | for (let j = 0; j < line.children.length; j++) { 117 | const token = line.children[j] 118 | // normalize token's style object to string 119 | if (token.properties && typeof token.properties.style === 'object') { 120 | let styleString = '' 121 | for (const [key, value] of Object.entries(token.properties.style)) { 122 | styleString += `${key}:${value};` 123 | } 124 | if (styleString) { 125 | token.properties.style = styleString 126 | } 127 | } 128 | } 129 | 130 | // add line break 131 | line.children.push( 132 | h( 133 | 'span', 134 | { 135 | className: `sh__token--line`, 136 | }, 137 | [{ type: 'text', value: '\n' }] 138 | ) 139 | ) 140 | 141 | // add class to line 142 | line.properties.className = cx( 143 | line.properties.className, 144 | highLightClassName 145 | ) 146 | 147 | lineIndex++ 148 | } 149 | 150 | const code = h( 151 | 'code', 152 | { 153 | className: `sh-lang--${lang}`, 154 | ['data-sh-language']: `${lang}`, 155 | }, 156 | childrenLines 157 | ) 158 | 159 | const pre = h( 160 | 'pre', 161 | { 162 | className: `sh-lang--${lang}`, 163 | }, 164 | [code] 165 | ) 166 | 167 | return pre 168 | }) 169 | } 170 | 171 | export { highlight } 172 | export default highlight 173 | -------------------------------------------------------------------------------- /apps/docs/app/carousel.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import { startTransition, useActionState, useEffect, useState } from 'react' 4 | import domToImage from 'dom-to-image' 5 | import { Code } from 'codice' 6 | import { copyImageDataUrl } from './lib/copy-image' 7 | 8 | const EXAMPLE_PAIRS = [ 9 | [ 10 | 'install.js', 11 | `\ 12 | // npm i -S sugar-high 13 | 14 | import { highlight } from 'sugar-high' 15 | 16 | const html = highlight(code) 17 | 18 | document.querySelector('pre > code').innerHTML = html 19 | `, 20 | { 21 | highlightedLines: [5] 22 | }, 23 | ], 24 | 25 | [ 26 | `app.jsx`, 27 | `\ 28 | const element = ( 29 | <> 30 | 33 | }}> 34 | 35 | {/* jsx comment */} 36 |

37 | Read{' '} 38 | 39 | this page! - {Date.now()} 40 | 41 |

42 | 43 | ) 44 | `, 45 | { 46 | highlightedLines: [7] 47 | } 48 | ], 49 | [ 50 | `hello.js`, 51 | `\ 52 | const nums = [ 53 | 1000_000_000, 1.2e3, 0x1f, .14, 1n 54 | ].filter(Boolean) 55 | 56 | function* foo(index) { 57 | do { 58 | yield index++; 59 | return void 0 60 | } while (index < 2) 61 | } 62 | `, 63 | { 64 | highlightedLines: [2] 65 | } 66 | ], 67 | 68 | [ 69 | `klass.js`, 70 | `\ 71 | /** 72 | * @param {string} names 73 | * @return {Promise} 74 | */ 75 | async function notify(names) { 76 | const tags = [] 77 | for (let i = 0; i < names.length; i++) { 78 | tags.push('@' + names[i]) 79 | } 80 | await ping(tags) 81 | } 82 | 83 | class SuperArray extends Array { 84 | static core = Object.create(null) 85 | 86 | constructor(...args) { super(...args); } 87 | 88 | bump(value) { 89 | return this.map( 90 | x => x == undefined ? x + 1 : 0 91 | ).concat(value) 92 | } 93 | } 94 | `, 95 | { 96 | highlightedLines: [7] 97 | } 98 | ], 99 | 100 | [ 101 | `regex.js`, 102 | `\ 103 | export const test = (str) => /^\\/[0-5]\\/$/g.test(str) 104 | 105 | // This is a super lightweight javascript syntax highlighter npm package 106 | 107 | // This is a inline comment / <- a slash 108 | /// // reference comment 109 | /* This is another comment */ alert('good') // <- alerts 110 | 111 | // Invalid calculation: regex and numbers 112 | const _in = 123 - /555/ + 444; 113 | const _iu = /* evaluate */ (19) / 234 + 56 / 7; 114 | `, 115 | { 116 | highlightedLines: [9] 117 | } 118 | ] 119 | ] as const 120 | 121 | function CodeFrame( 122 | { 123 | code, 124 | title = 'Untitled', 125 | index, 126 | highlightedLines = [] 127 | }: { 128 | code: string, 129 | title: string, 130 | index: number, 131 | highlightedLines: readonly number[] | number[] 132 | }) { 133 | return ( 134 |
135 | 143 | 149 | {code} 150 | 151 |
152 | ) 153 | } 154 | 155 | 156 | export default function Carousel() { 157 | const examples = EXAMPLE_PAIRS 158 | const [selected, setSelected] = useState(Math.ceil(examples.length / 2)) 159 | 160 | return ( 161 |
162 | 203 | <> 204 | {examples.map((_, i) => ( 205 | setSelected(i)} 213 | /> 214 | ))} 215 | 216 |
217 |

Showcase

218 |

Code highlight examples built with sugar-high

219 |
220 |
221 | {examples.map((_, i) => ( 222 |
229 |
230 | {examples.map(([name, code, config], i) => { 231 | function handleCopyImage() { 232 | const domNode = document.querySelector(`#code-frame-${i}`) 233 | return domToImage.toPng(domNode).then(dataUrl => { 234 | return copyImageDataUrl(dataUrl).then( 235 | () => { 236 | return true 237 | }, () => { 238 | return false 239 | } 240 | ) 241 | }) 242 | } 243 | 244 | return ( 245 | 259 | )} 260 | )} 261 |
262 |
263 | ) 264 | } 265 | 266 | function CameraIcon({ ...props }: React.SVGProps) { 267 | return ( 268 | 269 | 270 | 271 | 272 | ) 273 | } 274 | 275 | function cx(...args: any[]) { 276 | return args.filter(Boolean).join(' ') 277 | } 278 | 279 | 280 | function CopyImageButton({ onCopy } : { onCopy: () => Promise }) { 281 | function handleActionState(state, action) { 282 | if (action === 'copy') { 283 | return onCopy().then( 284 | result => result ? 1 : 2, 285 | ) 286 | } else if (action === 'reset') { 287 | return 0 288 | } 289 | return state 290 | } 291 | // 0: idle, 1: success, 2: error 292 | const [copyState, dispatch, isPending] = useActionState(handleActionState, 0) 293 | function copy() { 294 | startTransition(() => { 295 | dispatch('copy') 296 | }) 297 | } 298 | const reset = () => dispatch('reset') 299 | 300 | useEffect(() => { 301 | if (copyState === 1) { 302 | const timer = setTimeout(() => { 303 | reset() 304 | }, 2000) 305 | return () => clearTimeout(timer) 306 | } 307 | }) 308 | 309 | return ( 310 | 336 | ) 337 | } -------------------------------------------------------------------------------- /apps/docs/app/live-editor.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import { useState, useEffect, useRef, useMemo } from 'react' 4 | import { tokenize, SugarHigh } from 'sugar-high' 5 | import { Editor } from 'codice' 6 | import { CopyButton } from './components/copy-button' 7 | 8 | // Original colorful theme 9 | const defaultColorPlateColors = { 10 | class: '#8d85ff', 11 | identifier: '#354150', 12 | sign: '#8996a3', 13 | entity: '#6eafad', 14 | property: '#4e8fdf', 15 | jsxliterals: '#bf7db6', 16 | string: '#00a99a', 17 | keyword: '#f47067', 18 | comment: '#a19595', 19 | break: '#ffffff', 20 | space: '#ffffff', 21 | } 22 | 23 | // Minimal gray theme with 3 colors 24 | const minimalGrayTheme = { 25 | name: 'Minimal Gray', 26 | colors: { 27 | primary: '#2d2d2d', // Dark gray for keywords, class 28 | secondary: '#6b6b6b', // Medium gray for identifiers, properties 29 | tertiary: '#9a9a9a', // Light gray for comments, signs 30 | } 31 | } 32 | 33 | // Map token types to minimal gray theme colors 34 | function getMinimalThemeColors() { 35 | const { primary, secondary, tertiary } = minimalGrayTheme.colors 36 | return { 37 | class: primary, 38 | identifier: secondary, 39 | sign: tertiary, 40 | entity: secondary, 41 | property: secondary, 42 | jsxliterals: primary, 43 | string: secondary, 44 | keyword: primary, 45 | comment: tertiary, 46 | break: '#ffffff', 47 | space: '#ffffff', 48 | } 49 | } 50 | 51 | const themes = [ 52 | { name: 'Stylish', colors: defaultColorPlateColors }, 53 | { name: 'Minimal', colors: getMinimalThemeColors() }, 54 | ] 55 | 56 | const customizableColors = Object.entries(SugarHigh.TokenTypes) 57 | .filter(([, tokenTypeName]) => tokenTypeName !== 'break' && tokenTypeName !== 'space') 58 | .sort((a, b) => Number(a) - Number(b)) 59 | 60 | function debounce(func, timeout = 200) { 61 | let timer 62 | return (...args) => { 63 | clearTimeout(timer) 64 | timer = setTimeout(() => { 65 | func.apply(this, args) 66 | }, timeout) 67 | } 68 | } 69 | 70 | const DEFAULT_LIVE_CODE = `\ 71 | export default function App() { 72 | return ( 73 | <> 74 |

75 | Hello 76 | world 77 |

78 |
79 | 80 | ) 81 | } 82 | 83 | ` 84 | 85 | function useTextTypingAnimation(targetText, delay, enableTypingAnimation, onReady) { 86 | if (!enableTypingAnimation) { 87 | return { 88 | text: targetText, 89 | isTyping: false, 90 | setText: () => {}, 91 | } 92 | } 93 | const [text, setText] = useState('') 94 | const [isTyping, setIsTyping] = useState(true) 95 | const animationDuration = delay / targetText.length 96 | let timeoutId = useRef(null) 97 | 98 | useEffect(() => { 99 | if (isTyping && targetText.length) { 100 | if (text.length < targetText.length) { 101 | const nextText = targetText.substring(0, text.length + 1) 102 | if (timeoutId.current) { 103 | clearTimeout(timeoutId.current) 104 | timeoutId.current = null 105 | } 106 | timeoutId.current = setTimeout(() => { 107 | setText(nextText) 108 | }, animationDuration) 109 | } else if (text.length === targetText.length) { 110 | setIsTyping(false) 111 | onReady() 112 | } 113 | } 114 | return () => { 115 | if (timeoutId.current) { 116 | clearTimeout(timeoutId.current) 117 | timeoutId.current = null 118 | } 119 | } 120 | }, [targetText, text, timeoutId.current]) 121 | 122 | return { text, isTyping, setText } 123 | } 124 | 125 | const DEFAULT_LIVE_CODE_KEY = '$saved-live-code' 126 | function useDefaultLiveCode(defaultCodeText) { 127 | const [defaultCode, setCode] = useState(defaultCodeText || '') 128 | 129 | useEffect(() => { 130 | if (defaultCode) return 131 | 132 | setCode(window.localStorage.getItem(DEFAULT_LIVE_CODE_KEY) || DEFAULT_LIVE_CODE) 133 | }, [defaultCode]) 134 | 135 | const setDefaultLiveCode = (code) => window.localStorage.setItem(DEFAULT_LIVE_CODE_KEY, code) 136 | 137 | return { 138 | defaultLiveCode: defaultCode, 139 | setDefaultLiveCode, 140 | } 141 | } 142 | 143 | export default function LiveEditor({ 144 | enableTypingAnimation = true, 145 | defaultCode = DEFAULT_LIVE_CODE, 146 | }) { 147 | const editorRef = useRef(null) 148 | const [currentThemeIndex, setCurrentThemeIndex] = useState(0) 149 | const [colorPlateColors, setColorPlateColors] = useState(() => themes[0].colors) 150 | const [textareaColor, setTextareaColor] = useState('transparent') 151 | 152 | const currentTheme = themes[currentThemeIndex] 153 | const isMinimalMode = currentThemeIndex === 1 154 | 155 | const toggleTheme = () => { 156 | setCurrentThemeIndex((prev) => (prev === 0 ? 1 : 0)) 157 | } 158 | 159 | useEffect(() => { 160 | setColorPlateColors(themes[currentThemeIndex].colors) 161 | }, [currentThemeIndex]) 162 | 163 | const toggleTextareaColor = () => { 164 | setTextareaColor(prev => prev === 'transparent' ? '#66666682' : 'transparent') 165 | } 166 | 167 | const isInspecting = textareaColor !== 'transparent' 168 | const buttonText = isInspecting ? 'Matching' : 'Matched' 169 | 170 | const { defaultLiveCode, setDefaultLiveCode } = useDefaultLiveCode(defaultCode) 171 | const { 172 | text: liveCode, 173 | setText: setLiveCode, 174 | isTyping, 175 | } = useTextTypingAnimation(defaultLiveCode, 1000, enableTypingAnimation, () => { 176 | if (editorRef.current) { 177 | // focus needs to be delayed 178 | setTimeout(() => { 179 | editorRef.current.focus() 180 | }) 181 | } 182 | }) 183 | 184 | const [liveCodeTokens, setLiveCodeTokens] = useState([]) 185 | const debouncedTokenizeRef = useRef( 186 | debounce((c) => { 187 | const tokens = tokenize(c) 188 | setLiveCodeTokens(tokens) 189 | }) 190 | ) 191 | const debouncedTokenize = debouncedTokenizeRef.current 192 | 193 | const customizableColorsString = useMemo(() => { 194 | return customizableColors 195 | .map(([_tokenType, tokenTypeName]) => { 196 | return `--sh-${tokenTypeName}: ${colorPlateColors[tokenTypeName]};` 197 | }) 198 | .join('\n') 199 | }, [colorPlateColors]) 200 | 201 | return ( 202 |
203 | 220 | 221 |
222 | {process.env.NODE_ENV === 'development' && ( 223 |
224 | 230 |
231 | )} 232 | 239 |
240 |
241 | { 249 | setLiveCode(newCode) 250 | debouncedTokenize(newCode) 251 | if (!isTyping) setDefaultLiveCode(newCode) 252 | }} 253 | /> 254 | 255 |
    256 |
    257 | 264 | 265 |
    266 | {customizableColors.map(([tokenType, tokenTypeName]) => { 267 | const inputId = `live-editor-color__input--${tokenTypeName}` 268 | return ( 269 |
  • 270 | 278 | 279 | { 284 | setColorPlateColors({ 285 | ...colorPlateColors, 286 | [tokenTypeName]: e.target.value, 287 | }) 288 | }} 289 | /> 290 |
  • 291 | ) 292 | })} 293 |
294 |
295 | {/* show tokens */} 296 |
297 | {liveCodeTokens.map(([tokenType, token], index) => { 298 | const tokenTypeName = SugarHigh.TokenTypes[tokenType] 299 | if ( 300 | tokenTypeName === 'break' || 301 | tokenTypeName === 'space' || 302 | token === '\n' || 303 | token.trim() === '' 304 | ) return null 305 | return ( 306 | 307 | {token}{` `} 308 | 309 | ) 310 | })} 311 |
312 |
313 | ) 314 | } 315 | -------------------------------------------------------------------------------- /apps/docs/app/styles.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --codice-caret-color: #333; 3 | --codice-code-line-number-color: #a4a4a4; 4 | --codice-code-highlight-color: #555555; 5 | --codice-control-color: #8d8989; 6 | } 7 | 8 | ::selection { 9 | background-color: #333; 10 | color: #ddd; 11 | } 12 | 13 | .codice ::selection { 14 | background-color: #e2ffea; 15 | } 16 | 17 | * { 18 | box-sizing: border-box; 19 | } 20 | 21 | html { 22 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 23 | 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif; 24 | } 25 | body { 26 | margin: 0; 27 | overflow-x: hidden; 28 | } 29 | a { 30 | color: #888; 31 | text-decoration: underline solid currentColor; 32 | text-underline-position: from-font; 33 | text-decoration-thickness: from-font; 34 | } 35 | 36 | a:hover { 37 | color: #333; 38 | } 39 | 40 | .container-960 { 41 | max-width: 960px; 42 | padding: 0 10px 40px; 43 | margin: auto; 44 | } 45 | .container-720 { 46 | max-width: 720px; 47 | padding: 0 16px 24px; 48 | margin: auto; 49 | } 50 | input[type='radio'] { 51 | display: none; 52 | } 53 | .flex { 54 | display: flex; 55 | } 56 | .flex-1 { 57 | flex: 1; 58 | } 59 | .align-start { 60 | align-self: start; 61 | } 62 | .align-center { 63 | align-items: center; 64 | } 65 | .features { 66 | margin: 16px 0; 67 | } 68 | .features__control { 69 | display: flex; 70 | align-items: center; 71 | margin: 8px auto; 72 | } 73 | input[type='checkbox'] { 74 | cursor: pointer; 75 | appearance: none; 76 | background-color: transparent; 77 | font: inherit; 78 | color: #444; 79 | width: 1em; 80 | height: 1em; 81 | border: 0.15em solid currentColor; 82 | margin-right: 8px; 83 | } 84 | 85 | input[type='checkbox']:checked { 86 | background-color: currentColor; 87 | } 88 | .big-title { 89 | line-height: 1.2; 90 | transition: all 0.2s ease; 91 | } 92 | .header { 93 | margin: 1rem auto; 94 | text-align: center; 95 | padding: 2rem 0; 96 | color: #354150; 97 | max-height: 50vmin; 98 | } 99 | .header h1 span { 100 | font-size: 5rem; 101 | font-weight: 800; 102 | text-align: center; 103 | margin: 1rem; 104 | } 105 | .header p { 106 | font-size: 1.5rem; 107 | color: #808c97; 108 | } 109 | .cards .editor { 110 | position: relative; 111 | overflow-y: scroll; 112 | scrollbar-width: none; 113 | } 114 | .code-label { 115 | position: absolute; 116 | height: 100%; 117 | left: 0; 118 | right: 0; 119 | margin: auto; 120 | transition: box-shadow 0.3s ease, transform 0.3s ease; 121 | border-radius: 2px; 122 | overflow-x: hidden; 123 | box-shadow: -5px 12px 60px rgba(0, 0, 0, 0.5); 124 | } 125 | 126 | .card-indicator-dots { 127 | margin: auto; 128 | display: flex; 129 | gap: 12px; 130 | justify-content: center; 131 | padding: 8px; 132 | background-color: #ffffff1f; 133 | border-radius: 4px; 134 | } 135 | .card-indicator { 136 | width: 12px; 137 | height: 12px; 138 | border-radius: 50%; 139 | background-color: #bababa; 140 | transition: background-color 0.3s ease; 141 | cursor: pointer; 142 | } 143 | .card-indicator--selected { 144 | background-color: #354150; 145 | } 146 | .code-label { 147 | border-radius: 6px; 148 | } 149 | .code-frame { 150 | position: relative; 151 | } 152 | .code-copy-pic-button { 153 | position: absolute; 154 | top: 6px; 155 | right: 16px; 156 | padding: 4px 8px; 157 | cursor: pointer; 158 | background-color: #ffffff0f; 159 | border-radius: 4px; 160 | border: none; 161 | } 162 | /* svg change colors based on state */ 163 | .code-copy-pic-icon:hover { 164 | color: #333; 165 | } 166 | .code-copy-pic-icon.code-copy-pic-icon--pending { 167 | color: #666; 168 | } 169 | .code-copy-pic-icon.code-copy-pic-icon--success { 170 | color: #00a99a; 171 | } 172 | .code-copy-pic-icon.code-copy-pic-icon--error { 173 | color: #f47067; 174 | } 175 | .code-copy-pic-button:hover { 176 | background-color: #ffffff1f; 177 | } 178 | 179 | .code-copy-pic-button:active { 180 | background-color: #ffffff2f; 181 | } 182 | .code-copy-pic-button svg { 183 | color: #666; 184 | vertical-align: middle; 185 | } 186 | .carousel { 187 | margin: 8rem auto; 188 | transform-style: preserve-3d; 189 | display: flex; 190 | justify-content: center; 191 | flex-direction: column; 192 | align-items: center; 193 | } 194 | 195 | .cards { 196 | --sh-class: #2d5e9d; 197 | --sh-identifier: #354150; 198 | --sh-sign: #8996a3; 199 | --sh-property: #0550ae; 200 | --sh-entity: #249a97; 201 | --sh-jsxliterals: #6266d1; 202 | --sh-string: #00a99a; 203 | --sh-keyword: #f47067; 204 | --sh-comment: #a19595; 205 | --editor-background-color: transparent; 206 | 207 | position: relative; 208 | width: 600px; 209 | height: 480px; 210 | margin: 2rem auto; 211 | } 212 | .cards textarea { 213 | /* disable text area */ 214 | pointer-events: none; 215 | } 216 | 217 | code { 218 | counter-reset: sh-line-number; 219 | } 220 | 221 | .live-editor { 222 | cursor: default; 223 | margin: 2rem auto 6rem; 224 | max-width: 720px; 225 | border-radius: 6px; 226 | resize: both; 227 | overflow: auto; 228 | position: relative; 229 | box-shadow: -5px 12px 40px #8888887a; 230 | transition: box-shadow 0.3s ease; 231 | } 232 | 233 | .live-editor:focus, .live-editor:focus-visible, .live-editor:focus-within { 234 | outline: none; 235 | box-shadow: -5px 12px 60px #5d59597a; 236 | } 237 | 238 | .live-editor-section { 239 | position: relative; 240 | } 241 | 242 | /* Smooth color transitions for syntax highlighting */ 243 | .live-editor-section [class*="sh__token--"], 244 | .live-editor-section .live-editor__color__item__indicator, 245 | .live-editor-section .editor-token { 246 | transition: color 0.4s ease, background-color 0.4s ease; 247 | } 248 | 249 | .top-controls { 250 | display: flex; 251 | flex-direction: column; 252 | align-items: flex-end; 253 | gap: 8px; 254 | } 255 | 256 | .theme-mode-button--mobile { 257 | display: none; 258 | } 259 | 260 | .theme-mode-button { 261 | background: none; 262 | border: none; 263 | color: #68727f; 264 | font-size: 14px; 265 | padding: 2px 6px; 266 | margin: -2px -6px; 267 | cursor: pointer; 268 | transition: all 0.2s ease; 269 | font-family: inherit; 270 | font-weight: inherit; 271 | border-radius: 4px; 272 | } 273 | 274 | .theme-mode-button:hover { 275 | color: #333; 276 | background-color: #f5f5f5; 277 | } 278 | 279 | .live-editor__color { 280 | margin: 0; 281 | /* Give extra bottom padding to visually align with the top title */ 282 | padding: 8px 24px 16px; 283 | } 284 | 285 | .live-editor__color .color-theme-title { 286 | display: flex; 287 | justify-content: space-between; 288 | gap: 8px; 289 | color: #68727f; 290 | font-size: 14px; 291 | } 292 | 293 | .textarea-color-toggle { 294 | background: none; 295 | border: none; 296 | color: #a4a4a4; 297 | font-size: 12px; 298 | padding: 4px 8px; 299 | cursor: pointer; 300 | border-radius: 3px; 301 | transition: all 0.2s ease; 302 | border-radius: 8px; 303 | } 304 | 305 | .textarea-color-toggle:hover { 306 | background-color: #f8f8f8; 307 | color: #68727f; 308 | } 309 | 310 | .textarea-color-toggle--active { 311 | background-color: #f8f8f8; 312 | } 313 | 314 | .textarea-color-toggle--active:hover { 315 | background-color: #f8f8f8; 316 | } 317 | .textarea-color-toggle-container { 318 | display: flex; 319 | justify-content: flex-end; 320 | } 321 | .live-editor__color input[type='color'] { 322 | display: inline; 323 | width: 0; 324 | height: 0; 325 | border: none; 326 | padding: 0; 327 | margin: 0 0 0 8px; 328 | opacity: 0; 329 | cursor: none; 330 | } 331 | .live-editor__color__item { 332 | display: flex; 333 | align-items: center; 334 | color: #666; 335 | margin: 6px 0; 336 | width: 100%; 337 | } 338 | 339 | .live-editor__color__item .copy-button { 340 | visibility: hidden; 341 | } 342 | 343 | .live-editor__color__item:hover .copy-button { 344 | visibility: visible; 345 | } 346 | .live-editor__color__item:hover, 347 | .live-editor__color__item:hover label { 348 | cursor: pointer; 349 | } 350 | .live-editor__color__item__indicator { 351 | display: inline-block; 352 | width: 16px; 353 | height: 16px; 354 | margin-right: 8px; 355 | border-radius: 999px; 356 | } 357 | .live-editor__color__item__name { 358 | margin-right: 12px; 359 | color: #a4a4a4; 360 | } 361 | .live-editor__color__item__color { 362 | display: none; 363 | } 364 | .live-editor .sh__line { 365 | color: #333; 366 | } 367 | 368 | .live-editor-section .live-editor__color__item__indicator { 369 | background-color: currentColor; 370 | } 371 | 372 | @media screen and (max-width: 640px) { 373 | .cards { 374 | width: 100%; 375 | } 376 | .code-label--non-selected { 377 | width: 0 !important; 378 | height: 0 !important; 379 | transform: none !important; 380 | visibility: hidden; 381 | } 382 | .live-editor { 383 | margin: 1rem auto 3rem; 384 | } 385 | .live-editor__color { 386 | display: none; 387 | } 388 | .theme-mode-button--mobile { 389 | display: block; 390 | } 391 | .header h1 span { 392 | font-size: 200%; 393 | } 394 | /* Prevent zoom on mobile when clicking editor */ 395 | .codice[data-codice-code] textarea { 396 | font-size: 16px !important; 397 | } 398 | } 399 | 400 | [data-codice="editor"] code { 401 | width: 100%; 402 | } 403 | 404 | .editor, 405 | .code-frame { 406 | background-color: #f6f6f6; 407 | } 408 | 409 | .codice[data-codice-code] code, 410 | .codice[data-codice-code] textarea { 411 | font-family: Consolas, Monaco, monospace; 412 | border: none; 413 | font-size: 14px; 414 | line-height: 1.5em; 415 | caret-color: #333; 416 | outline: none; 417 | scrollbar-width: none; 418 | } 419 | 420 | .code-snippet [data-codice-code-content] { 421 | padding: 24px; 422 | padding-bottom: 36px; 423 | } 424 | .codice[data-codice-code] code::selection { 425 | color: transparent; 426 | } 427 | .codice[data-codice-code] textarea::selection { 428 | color: #2c7ea163; 429 | } 430 | 431 | .codice [data-codice-control] { 432 | background-color: rgba(0, 0, 0, 0.34); 433 | } 434 | .codice [data-codice-title] { 435 | color: rgba(0, 0, 0, 0.34); 436 | } 437 | 438 | .editor-token { 439 | display: inline-block; 440 | padding: 4px 8px; 441 | background-color: #e5e5e5; 442 | color: #524f4f; 443 | } 444 | .live-editor-section .editor-token--class { 445 | background-color: var(--sh-class); 446 | color: #fff; 447 | } 448 | .live-editor-section .editor-token--keyword { 449 | background-color: var(--sh-keyword); 450 | color: #fff; 451 | } 452 | .live-editor-section .editor-token--string { 453 | background-color: var(--sh-string); 454 | color: #fff; 455 | } 456 | .live-editor-section .editor-token--comment { 457 | background-color: var(--sh-comment); 458 | color: #fff; 459 | } 460 | .live-editor-section .editor-token--property { 461 | background-color: var(--sh-property); 462 | color: #fff; 463 | } 464 | .live-editor-section .editor-token--entity { 465 | background-color: var(--sh-entity); 466 | color: #fff; 467 | } 468 | .live-editor-section .editor-token--jsxliterals { 469 | background-color: var(--sh-jsxliterals); 470 | color: #fff; 471 | } 472 | .live-editor-section .editor-token--identifier { 473 | background-color: var(--sh-identifier); 474 | color: #fff; 475 | } 476 | .live-editor-section .editor-token--sign { 477 | background-color: var(--sh-sign); 478 | color: #fff; 479 | } 480 | 481 | .show-case-title h1 { 482 | color: #354150; 483 | } 484 | .show-case-title p { 485 | color: #5b626d; 486 | } 487 | 488 | .editor-tokens { 489 | font-size: 14px; 490 | } 491 | 492 | /* Remark page specific styles */ 493 | .code { 494 | margin-top: 1rem; 495 | margin-bottom: 1rem; 496 | width: 100%; 497 | } 498 | div[data-codice-editor-header="true"] { 499 | background-color: #d6e1eb; 500 | padding: 4px 6px; 501 | color: #507a99; 502 | margin: 0; 503 | } 504 | /* Override */ 505 | [data-codice-code] pre, 506 | [data-codice-code] code { 507 | width: 100%; 508 | } 509 | .code[data-codice-code="true"] pre { 510 | padding: 8px 0; 511 | } 512 | .sh__line { 513 | display: block; 514 | min-height: 17.5px; /* match line height */ 515 | } 516 | .live-editor [data-codice-editor] code { 517 | margin-left: 0 !important; 518 | } 519 | .sh__line [data-codice-code-line-number] { 520 | position: absolute; 521 | } 522 | .sh__line--highlighted { 523 | background-color: #dff1ff; 524 | } 525 | 526 | /* Navigation styles */ 527 | .navigation { 528 | background-color: #f8f9fa; 529 | border-bottom: 1px solid #e9ecef; 530 | padding: 1rem 0; 531 | } 532 | .navigation .container-960 { 533 | display: flex; 534 | gap: 2rem; 535 | } 536 | .nav-link { 537 | color: #495057; 538 | text-decoration: none; 539 | font-weight: 500; 540 | transition: color 0.2s ease; 541 | } 542 | .nav-link:hover { 543 | color: #212529; 544 | } -------------------------------------------------------------------------------- /packages/sugar-high/lib/index.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | const JSXBrackets = new Set(['<', '>', '{', '}', '[', ']']) 4 | const Keywords_Js = new Set([ 5 | 'for', 6 | 'do', 7 | 'while', 8 | 'if', 9 | 'else', 10 | 'return', 11 | 'function', 12 | 'var', 13 | 'let', 14 | 'const', 15 | 'true', 16 | 'false', 17 | 'undefined', 18 | 'this', 19 | 'new', 20 | 'delete', 21 | 'typeof', 22 | 'in', 23 | 'instanceof', 24 | 'void', 25 | 'break', 26 | 'continue', 27 | 'switch', 28 | 'case', 29 | 'default', 30 | 'throw', 31 | 'try', 32 | 'catch', 33 | 'finally', 34 | 'debugger', 35 | 'with', 36 | 'yield', 37 | 'async', 38 | 'await', 39 | 'class', 40 | 'extends', 41 | 'super', 42 | 'import', 43 | 'export', 44 | 'from', 45 | 'static', 46 | ]) 47 | 48 | const Signs = new Set([ 49 | '+', 50 | '-', 51 | '*', 52 | '/', 53 | '%', 54 | '=', 55 | '!', 56 | '&', 57 | '|', 58 | '^', 59 | '~', 60 | '!', 61 | '?', 62 | ':', 63 | '.', 64 | ',', 65 | ';', 66 | `'`, 67 | '"', 68 | '.', 69 | '(', 70 | ')', 71 | '[', 72 | ']', 73 | '#', 74 | '@', 75 | '\\', 76 | ...JSXBrackets, 77 | ]) 78 | 79 | const DefaultOptions = { 80 | keywords: Keywords_Js, 81 | onCommentStart: isCommentStart_Js, 82 | onCommentEnd: isCommentEnd_Js, 83 | } 84 | 85 | /** 86 | * 87 | * 0 - identifier 88 | * 1 - keyword 89 | * 2 - string 90 | * 3 - Class, number and null 91 | * 4 - property 92 | * 5 - entity 93 | * 6 - jsx literals 94 | * 7 - sign 95 | * 8 - comment 96 | * 9 - break 97 | * 10 - space 98 | * 99 | */ 100 | const TokenTypes = /** @type {const} */ ([ 101 | 'identifier', 102 | 'keyword', 103 | 'string', 104 | 'class', 105 | 'property', 106 | 'entity', 107 | 'jsxliterals', 108 | 'sign', 109 | 'comment', 110 | 'break', 111 | 'space', 112 | ]) 113 | const [ 114 | T_IDENTIFIER, 115 | T_KEYWORD, 116 | T_STRING, 117 | T_CLS_NUMBER, 118 | T_PROPERTY, 119 | T_ENTITY, 120 | T_JSX_LITERALS, 121 | T_SIGN, 122 | T_COMMENT, 123 | T_BREAK, 124 | T_SPACE, 125 | ] = /** @types {const} */ TokenTypes.map((_, i) => i) 126 | 127 | function isSpaces(str) { 128 | return /^[^\S\r\n]+$/g.test(str) 129 | } 130 | 131 | function isSign(ch) { 132 | return Signs.has(ch) 133 | } 134 | 135 | function encode(str) { 136 | return str 137 | .replace(/&/g, '&') 138 | .replace(//g, '>') 140 | .replace(/"/g, '"') 141 | .replace(/'/g, ''') 142 | } 143 | 144 | function isWord(chr) { 145 | return /^[\w_]+$/.test(chr) || hasUnicode(chr) 146 | } 147 | 148 | function isCls(str) { 149 | const chr0 = str[0] 150 | return isWord(chr0) && 151 | chr0 === chr0.toUpperCase() || 152 | str === 'null' 153 | } 154 | 155 | function hasUnicode(s) { 156 | return /[^\u0000-\u007f]/.test(s); 157 | } 158 | 159 | function isAlpha(chr) { 160 | return /^[a-zA-Z]$/.test(chr) 161 | } 162 | 163 | function isIdentifierChar(chr) { 164 | return isAlpha(chr) || hasUnicode(chr) 165 | } 166 | 167 | function isIdentifier(str) { 168 | return isIdentifierChar(str[0]) && (str.length === 1 || isWord(str.slice(1))) 169 | } 170 | 171 | function isStrTemplateChr(chr) { 172 | return chr === '`' 173 | } 174 | 175 | function isSingleQuotes(chr) { 176 | return chr === '"' || chr === "'" 177 | } 178 | 179 | function isStringQuotation(chr) { 180 | return isSingleQuotes(chr) || isStrTemplateChr(chr) 181 | } 182 | 183 | /** @returns {0|1|2} */ 184 | function isCommentStart_Js(curr, next) { 185 | const str = curr + next 186 | if (str === '/*') return 2 187 | return str === '//' ? 1 : 0 188 | } 189 | 190 | /** @returns {0|1|2} */ 191 | function isCommentEnd_Js(prev, curr) { 192 | return (prev + curr) === '*/' 193 | ? 2 194 | : curr === '\n' ? 1 : 0 195 | } 196 | 197 | function isRegexStart(str) { 198 | return str[0] === '/' && !isCommentStart_Js(str[0], str[1]) 199 | } 200 | 201 | /** 202 | * @param {string} code 203 | * @param {{ keywords: Set }} options 204 | * @return {Array<[number, string]>} 205 | */ 206 | function tokenize(code, options) { 207 | const { 208 | keywords, 209 | onCommentStart, 210 | onCommentEnd, 211 | } = { ...DefaultOptions, ...options } 212 | 213 | let current = '' 214 | let type = -1 215 | /** @type {[number, string]} */ 216 | let last = [-1, ''] 217 | /** @type {[number, string]} */ 218 | let beforeLast = [-2, ''] 219 | /** @type {Array<[number, string]>} */ 220 | const tokens = [] 221 | 222 | /** @type boolean if entered jsx tag, inside or */ 223 | let __jsxEnter = false 224 | /** 225 | * @type {0 | 1 | 2} 226 | * @example 227 | * 0 for not in jsx; 228 | * 1 for open jsx tag; 229 | * 2 for closing jsx tag; 230 | **/ 231 | let __jsxTag = 0 232 | let __jsxExpr = false 233 | 234 | // only match paired (open + close) tags, not self-closing tags 235 | let __jsxStack = 0 236 | const __jsxChild = () => __jsxEnter && !__jsxExpr && !__jsxTag 237 | // < __content__ > 238 | const inJsxTag = () => __jsxTag && !__jsxChild() 239 | // {'__content__'} 240 | const inJsxLiterals = () => !__jsxTag && __jsxChild() && !__jsxExpr && __jsxStack > 0 241 | 242 | /** @type {string | null} */ 243 | let __strQuote = null 244 | let __regexQuoteStart = false 245 | let __strTemplateExprStack = 0 246 | let __strTemplateQuoteStack = 0 247 | const inStringQuotes = () => __strQuote !== null 248 | const inRegexQuotes = () => __regexQuoteStart 249 | const inStrTemplateLiterals = () => (__strTemplateQuoteStack > __strTemplateExprStack) 250 | const inStrTemplateExpr = () => __strTemplateQuoteStack > 0 && (__strTemplateQuoteStack === __strTemplateExprStack) 251 | const inStringContent = () => inStringQuotes() || inStrTemplateLiterals() 252 | 253 | /** 254 | * 255 | * @param {string} token 256 | * @returns {number} 257 | */ 258 | function classify(token) { 259 | const isLineBreak = token === '\n' 260 | // First checking if they're attributes values 261 | if (inJsxTag()) { 262 | if (inStringQuotes()) { 263 | return T_STRING 264 | } 265 | 266 | const [, lastToken] = last 267 | if (isIdentifier(token)) { 268 | // classify jsx open tag 269 | if ((lastToken === '<' || lastToken === ' { 307 | if (token_) { 308 | current = token_ 309 | } 310 | if (current) { 311 | type = type_ || classify(current) 312 | /** @type [number, string] */ 313 | const pair = [type, current] 314 | if (type !== T_SPACE && type !== T_BREAK) { 315 | beforeLast = last 316 | last = pair 317 | } 318 | tokens.push(pair) 319 | } 320 | current = '' 321 | } 322 | for (let i = 0; i < code.length; i++) { 323 | const curr = code[i] 324 | const prev = code[i - 1] 325 | const next = code[i + 1] 326 | const p_c = prev + curr // previous and current 327 | const c_n = curr + next // current and next 328 | 329 | // Determine string quotation outside of jsx literals and template literals. 330 | // Inside jsx literals or template literals, string quotation is still part of it. 331 | if (isSingleQuotes(curr) && !inJsxLiterals() && !inStrTemplateLiterals()) { 332 | append() 333 | if (prev !== `\\`) { 334 | if (__strQuote && curr === __strQuote) { 335 | __strQuote = null 336 | } else if (!__strQuote) { 337 | __strQuote = curr 338 | } 339 | } 340 | 341 | append(T_STRING, curr) 342 | continue 343 | } 344 | 345 | if (!inStrTemplateLiterals()) { 346 | if (prev !== '\\n' && isStrTemplateChr(curr)) { 347 | append() 348 | append(T_STRING, curr) 349 | __strTemplateQuoteStack++ 350 | continue 351 | } 352 | } 353 | 354 | if (inStrTemplateLiterals()) { 355 | if (prev !== '\\n' && isStrTemplateChr(curr)) { 356 | if (__strTemplateQuoteStack > 0) { 357 | append() 358 | __strTemplateQuoteStack-- 359 | append(T_STRING, curr) 360 | continue 361 | } 362 | } 363 | 364 | if (c_n === '${') { 365 | __strTemplateExprStack++ 366 | append(T_STRING) 367 | append(T_SIGN, c_n) 368 | i++ 369 | continue 370 | } 371 | } 372 | 373 | if (inStrTemplateExpr() && curr === '}') { 374 | append() 375 | __strTemplateExprStack-- 376 | append(T_SIGN, curr) 377 | continue 378 | } 379 | 380 | if (__jsxChild()) { 381 | if (curr === '{') { 382 | append() 383 | append(T_SIGN, curr) 384 | __jsxExpr = true 385 | continue 386 | } 387 | } 388 | 389 | if (__jsxEnter) { 390 | // <: open tag sign 391 | // new '<' not inside jsx 392 | if (!__jsxTag && curr === '<') { 393 | append() 394 | if (next === '/') { 395 | // close tag 396 | __jsxTag = 2 397 | current = c_n 398 | i++ 399 | } else { 400 | // open tag 401 | __jsxTag = 1 402 | current = curr 403 | } 404 | append(T_SIGN) 405 | continue 406 | } 407 | if (__jsxTag) { 408 | // >: open tag close sign or closing tag closing sign 409 | // and it's not `=>` or `/>` 410 | // `curr` could be `>` or `/` 411 | if ((curr === '>' && !'/='.includes(prev))) { 412 | append() 413 | if (__jsxTag === 1) { 414 | __jsxTag = 0 415 | __jsxStack++ 416 | } else { 417 | __jsxTag = 0 418 | __jsxEnter = false 419 | } 420 | append(T_SIGN, curr) 421 | continue 422 | } 423 | 424 | // >: tag self close sign or close tag sign 425 | if (c_n === '/>' || c_n === '') { 432 | __jsxTag = 0 433 | } else { 434 | // is ' 465 | if (next === '=' && !inStringContent()) { 466 | // if current is not a space, ensure `prop` is a property 467 | if (!isSpaces(curr)) { 468 | // If there're leading spaces, append them first 469 | if (isSpaces(current)) { 470 | append() 471 | } 472 | 473 | // Now check if the accumulated token is a property 474 | const prop = current + curr 475 | if (isIdentifier(prop)) { 476 | append(T_PROPERTY, prop) 477 | continue 478 | } 479 | } 480 | } 481 | } 482 | } 483 | 484 | // if it's not in a jsx tag declaration or a string, close child if next is jsx close tag 485 | if (!__jsxTag && (curr === '<' && isIdentifierChar(next) || c_n === '/ expr: non comment start before `/` is not regex 514 | if ( 515 | isRegexChar && 516 | lastType !== -1 && 517 | !( 518 | (lastType === T_SIGN && ')' !== lastToken) || 519 | lastType === T_COMMENT 520 | ) 521 | ) { 522 | current = curr 523 | append() 524 | continue 525 | } 526 | 527 | __regexQuoteStart = true 528 | const start = i++ 529 | 530 | // end of line of end of file 531 | const isEof = () => i >= code.length 532 | const isEol = () => isEof() || code[i] === '\n' 533 | 534 | let foundClose = false 535 | 536 | // traverse to find closing regex slash 537 | for (; !isEol(); i++) { 538 | if (code[i] === '/' && code[i - 1] !== '\\') { 539 | foundClose = true 540 | // end of regex, append regex flags 541 | while (start !== i && /^[a-z]$/.test(code[i + 1]) && !isEol()) { 542 | i++ 543 | } 544 | break 545 | } 546 | } 547 | __regexQuoteStart = false 548 | 549 | if (start !== i && foundClose) { 550 | // If current line is fully closed with string quotes or regex slashes, 551 | // add them to tokens 552 | current = code.slice(start, i + 1) 553 | append(T_STRING) 554 | } else { 555 | // If it doesn't match any of the above, just leave it as operator and move on 556 | current = curr 557 | append() 558 | i = start 559 | } 560 | } else if (onCommentStart(curr, next)) { 561 | append() 562 | const start = i 563 | const startCommentType = onCommentStart(curr, next) 564 | 565 | // just match the comment, commentType === true 566 | // inline comment, commentType === 1 567 | // block comment, commentType === 2 568 | if (startCommentType) { 569 | for (; i < code.length; i++) { 570 | const endCommentType = onCommentEnd(code[i - 1], code[i]) 571 | if (endCommentType == startCommentType) break 572 | } 573 | } 574 | current = code.slice(start, i + 1) 575 | append(T_COMMENT) 576 | } else if (curr === ' ' || curr === '\n') { 577 | if ( 578 | curr === ' ' && 579 | ( 580 | (isSpaces(current) || !current) || 581 | isJsxLiterals 582 | ) 583 | ) { 584 | current += curr 585 | if (next === '<') { 586 | append() 587 | } 588 | } else { 589 | append() 590 | current = curr 591 | append() 592 | } 593 | } else { 594 | if (__jsxExpr && curr === '}') { 595 | append() 596 | current = curr 597 | append() 598 | __jsxExpr = false 599 | } else if ( 600 | // it's jsx literals and is not a jsx bracket 601 | (isJsxLiterals && !JSXBrackets.has(curr)) || 602 | // it's template literal content (including quotes) 603 | inStrTemplateLiterals() || 604 | // same type char as previous one in current token 605 | ((isWord(curr) === isWord(current[current.length - 1]) || __jsxChild()) && !Signs.has(curr)) 606 | ) { 607 | current += curr 608 | } else { 609 | if (p_c === '')) { 619 | current = c_n 620 | append() 621 | i++ 622 | } 623 | else if (JSXBrackets.has(curr)) append() 624 | } 625 | } 626 | } 627 | 628 | append() 629 | 630 | return tokens 631 | } 632 | 633 | /** 634 | * @param {Array<[number, string]>} tokens 635 | * @return {Array<{type: string, tagName: string, children: any[], properties: Record}>} 636 | */ 637 | function generate(tokens) { 638 | const lines = [] 639 | /** 640 | * @param {any} children 641 | * @return {{type: string, tagName: string, children: any[], properties: Record}} 642 | */ 643 | const createLine = (children) => 644 | ({ 645 | type: 'element', 646 | tagName: 'span', 647 | children, 648 | properties: { 649 | className: 'sh__line', 650 | }, 651 | }) 652 | 653 | /** 654 | * @param {Array<[number, string]>} tokens 655 | * @returns {void} 656 | */ 657 | function flushLine(tokens) { 658 | /** @type {Array} */ 659 | const lineTokens = ( 660 | tokens 661 | .map(([type, value]) => { 662 | const tokenType = TokenTypes[type] 663 | return { 664 | type: 'element', 665 | tagName: 'span', 666 | children: [{ 667 | type: 'text', // text node 668 | value, // to encode 669 | }], 670 | properties: { 671 | className: `sh__token--${tokenType}`, 672 | style: { color: `var(--sh-${tokenType})` }, 673 | }, 674 | } 675 | }) 676 | ) 677 | lines.push(createLine(lineTokens)) 678 | } 679 | /** @type {Array<[number, string]>} */ 680 | const lineTokens = [] 681 | let lastWasBreak = false 682 | 683 | for (let i = 0; i < tokens.length; i++) { 684 | const token = tokens[i] 685 | const [type, value] = token 686 | const isLastToken = i === tokens.length - 1 687 | 688 | if (type !== T_BREAK) { 689 | // Divide multi-line token into multi-line code 690 | if (value.includes('\n')) { 691 | const lines = value.split('\n') 692 | for (let j = 0; j < lines.length; j++) { 693 | lineTokens.push([type, lines[j]]) 694 | if (j < lines.length - 1) { 695 | flushLine(lineTokens) 696 | lineTokens.length = 0 697 | } 698 | } 699 | } else { 700 | lineTokens.push(token) 701 | } 702 | lastWasBreak = false 703 | } else { 704 | if (lastWasBreak) { 705 | // Consecutive break - create empty line 706 | flushLine([]) 707 | } else { 708 | // First break after content - flush current line 709 | flushLine(lineTokens) 710 | lineTokens.length = 0 711 | } 712 | 713 | // If this is the last token and it's a break, create an empty line 714 | if (isLastToken) { 715 | flushLine([]) 716 | } 717 | 718 | lastWasBreak = true 719 | } 720 | } 721 | 722 | // Flush remaining tokens if any 723 | if (lineTokens.length) { 724 | flushLine(lineTokens) 725 | } 726 | 727 | return lines 728 | } 729 | 730 | /** @param { className: string, style?: Record } */ 731 | const propsToString = (props) => { 732 | let str = `class="${props.className}"` 733 | 734 | if (props.style) { 735 | const style = Object.entries(props.style) 736 | .map(([key, value]) => `${key}:${value}`) 737 | .join(';') 738 | str += ` style="${style}"` 739 | } 740 | return str 741 | } 742 | 743 | function toHtml(lines) { 744 | return lines 745 | .map(line => { 746 | const { tagName: lineTag } = line 747 | const tokens = line.children 748 | .map(child => { 749 | const { tagName, children, properties } = child 750 | return `<${tagName} ${propsToString(properties)}>${encode(children[0].value)}` 751 | }) 752 | .join('') 753 | return `<${lineTag} class="${line.properties.className}">${tokens}` 754 | }) 755 | .join('\n') 756 | } 757 | 758 | /** 759 | * 760 | * @param {string} code 761 | * @param { 762 | * { 763 | * keywords: Set 764 | * onCommentStart?: (curr: string, next: string) => number | boolean 765 | * onCommentEnd?: (curr: string, prev: string) => number | boolean 766 | * } | undefined} options 767 | * @returns {string} 768 | */ 769 | function highlight(code, options) { 770 | const tokens = tokenize(code, options) 771 | const lines = generate(tokens) 772 | const output = toHtml(lines) 773 | return output 774 | } 775 | 776 | // namespace 777 | const SugarHigh = /** @type {const} */ { 778 | TokenTypes, 779 | TokenMap: new Map(TokenTypes.map((type, i) => [type, i])), 780 | } 781 | 782 | export { 783 | highlight, 784 | tokenize, 785 | generate, 786 | SugarHigh, 787 | } 788 | -------------------------------------------------------------------------------- /packages/sugar-high/test/ast.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from 'vitest' 2 | import { tokenize } from '../lib' 3 | import { 4 | getTokensAsString, 5 | } from './testing-utils' 6 | 7 | describe('function calls', () => { 8 | it('dot catch should not be determined as keyword', () => { 9 | const code = `promise.catch(log)` 10 | expect(getTokensAsString(tokenize(code))).toMatchInlineSnapshot(` 11 | [ 12 | "promise => identifier", 13 | ". => sign", 14 | "catch => identifier", 15 | "( => sign", 16 | "log => identifier", 17 | ") => sign", 18 | ] 19 | `) 20 | }) 21 | }) 22 | 23 | describe('calculation expression', () => { 24 | it('basic inline calculation expression', () => { 25 | const code = `123 - /555/ + 444;` 26 | expect(getTokensAsString(tokenize(code))).toMatchInlineSnapshot(` 27 | [ 28 | "123 => class", 29 | "- => sign", 30 | "/555/ => string", 31 | "+ => sign", 32 | "444 => class", 33 | "; => sign", 34 | ] 35 | `) 36 | }) 37 | 38 | it('calculation with comments', () => { 39 | const code = `/* evaluate */ (19) / 234 + 56 / 7;` 40 | expect(getTokensAsString(tokenize(code))).toMatchInlineSnapshot(` 41 | [ 42 | "/* evaluate */ => comment", 43 | "( => sign", 44 | "19 => class", 45 | ") => sign", 46 | "/ => sign", 47 | "234 => class", 48 | "+ => sign", 49 | "56 => class", 50 | "/ => sign", 51 | "7 => class", 52 | "; => sign", 53 | ] 54 | `) 55 | }) 56 | 57 | it('calculation with defs', () => { 58 | const code = `const _iu = (19) / 234 + 56 / 7;` 59 | expect(getTokensAsString(tokenize(code))).toMatchInlineSnapshot(` 60 | [ 61 | "const => keyword", 62 | "_iu => class", 63 | "= => sign", 64 | "( => sign", 65 | "19 => class", 66 | ") => sign", 67 | "/ => sign", 68 | "234 => class", 69 | "+ => sign", 70 | "56 => class", 71 | "/ => sign", 72 | "7 => class", 73 | "; => sign", 74 | ] 75 | `) 76 | }) 77 | 78 | it('complex expression with string concatenation and function calls', () => { 79 | const code = "`e's a` + a('.cls').oo, ); foo(arg)" 80 | expect(getTokensAsString(tokenize(code))).toMatchInlineSnapshot(` 81 | [ 82 | "\` => string", 83 | "e's a => string", 84 | "\` => string", 85 | "+ => sign", 86 | "a => identifier", 87 | "( => sign", 88 | "' => string", 89 | ".cls => string", 90 | "' => string", 91 | ") => sign", 92 | ". => sign", 93 | "oo => identifier", 94 | ", => sign", 95 | ") => sign", 96 | "; => sign", 97 | "foo => identifier", 98 | "( => sign", 99 | "arg => identifier", 100 | ") => sign", 101 | ] 102 | `) 103 | }) 104 | }) 105 | 106 | describe('jsx', () => { 107 | it('parse jsx compositions', () => { 108 | const code = `// jsx 109 | const element = ( 110 | <> 111 | 114 | }}> 115 | 116 | {/* jsx comment */} 117 |

118 | Read more{' '} 119 | 120 | this page! - {Date.now()} 121 | 122 |

123 | 124 | )` 125 | expect(getTokensAsString(tokenize(code))).toMatchInlineSnapshot(` 126 | [ 127 | "// jsx => comment", 128 | "const => keyword", 129 | "element => identifier", 130 | "= => sign", 131 | "( => sign", 132 | "< => sign", 133 | "> => sign", 134 | "< => sign", 135 | "Food => entity", 136 | "season => property", 137 | "= => sign", 138 | "{ => sign", 139 | "{ => sign", 140 | "sault => identifier", 141 | ": => sign", 142 | "< => sign", 143 | "p => entity", 144 | "a => property", 145 | "= => sign", 146 | "{ => sign", 147 | "[ => sign", 148 | "{ => sign", 149 | "} => sign", 150 | "] => sign", 151 | "} => sign", 152 | "/> => sign", 153 | "} => sign", 154 | "} => sign", 155 | "> => sign", 156 | " sign", 157 | "Food => entity", 158 | "> => sign", 159 | "{ => sign", 160 | "/* jsx comment */ => comment", 161 | "} => sign", 162 | "< => sign", 163 | "h1 => entity", 164 | "className => property", 165 | "= => sign", 166 | "" => string", 167 | "title => string", 168 | "" => string", 169 | "data- => property", 170 | "title => property", 171 | "= => sign", 172 | "" => string", 173 | "true => string", 174 | "" => string", 175 | "> => sign", 176 | " => jsxliterals", 177 | "Read more => jsxliterals", 178 | "{ => sign", 179 | "' => string", 180 | " => string", 181 | "' => string", 182 | "} => sign", 183 | " => jsxliterals", 184 | " => jsxliterals", 185 | "< => sign", 186 | "Link => entity", 187 | "href => property", 188 | "= => sign", 189 | "" => string", 190 | "/posts/first-post => string", 191 | "" => string", 192 | "> => sign", 193 | " => jsxliterals", 194 | " => jsxliterals", 195 | "< => sign", 196 | "a => entity", 197 | "> => sign", 198 | "this page! - => jsxliterals", 199 | "{ => sign", 200 | "Date => class", 201 | ". => sign", 202 | "now => property", 203 | "( => sign", 204 | ") => sign", 205 | "} => sign", 206 | " sign", 207 | "a => entity", 208 | "> => sign", 209 | " sign", 210 | "Link => entity", 211 | "> => sign", 212 | " sign", 213 | "h1 => entity", 214 | "> => sign", 215 | " sign", 216 | "> => sign", 217 | ") => sign", 218 | ] 219 | `) 220 | }) 221 | 222 | it('parse basic jsx with text without expression children', () => { 223 | const tokens = tokenize(`This is content`) 224 | expect(getTokensAsString(tokens)).toMatchInlineSnapshot(` 225 | [ 226 | "< => sign", 227 | "Foo => entity", 228 | "> => sign", 229 | "This is content => jsxliterals", 230 | " sign", 231 | "Foo => entity", 232 | "> => sign", 233 | ] 234 | `) 235 | }) 236 | 237 | it('parse basic jsx with expression children', () => { 238 | const tokens = tokenize(`{Class + variable}`) 239 | expect(getTokensAsString(tokens)).toMatchInlineSnapshot(` 240 | [ 241 | "< => sign", 242 | "Foo => entity", 243 | "> => sign", 244 | "{ => sign", 245 | "Class => class", 246 | "+ => sign", 247 | "variable => identifier", 248 | "} => sign", 249 | " sign", 250 | "Foo => entity", 251 | "> => sign", 252 | ] 253 | `) 254 | }) 255 | 256 | it('parse multi jsx definitions', () => { 257 | const tokens = tokenize( 258 | `x =
this
259 | y =
thi
260 | z =
this
261 | `) 262 | expect(getTokensAsString(tokens)).toMatchInlineSnapshot(` 263 | [ 264 | "x => identifier", 265 | "= => sign", 266 | "< => sign", 267 | "div => entity", 268 | "> => sign", 269 | "this => jsxliterals", 270 | " sign", 271 | "div => entity", 272 | "> => sign", 273 | "y => identifier", 274 | "= => sign", 275 | "< => sign", 276 | "div => entity", 277 | "> => sign", 278 | "thi => jsxliterals", 279 | " sign", 280 | "div => entity", 281 | "> => sign", 282 | "z => identifier", 283 | "= => sign", 284 | "< => sign", 285 | "div => entity", 286 | "> => sign", 287 | "this => jsxliterals", 288 | " sign", 289 | "div => entity", 290 | "> => sign", 291 | ] 292 | `) 293 | }) 294 | 295 | it('parse fold jsx', () => { 296 | const tokens = tokenize(`// jsx 297 | const element = ( 298 |
Hello World
299 | )`); 300 | 301 | expect(getTokensAsString(tokens)).toMatchInlineSnapshot(` 302 | [ 303 | "// jsx => comment", 304 | "const => keyword", 305 | "element => identifier", 306 | "= => sign", 307 | "( => sign", 308 | "< => sign", 309 | "div => entity", 310 | "> => sign", 311 | "Hello World => jsxliterals", 312 | "< => sign", 313 | "Food => entity", 314 | "/> => sign", 315 | " sign", 316 | "div => entity", 317 | "> => sign", 318 | ") => sign", 319 | ] 320 | `) 321 | }) 322 | 323 | it('parse keyword in jsx children literals as jsx literals', () => { 324 | const tokens = tokenize(`
Hello with {data}
`) 325 | expect(getTokensAsString(tokens)).toMatchInlineSnapshot(` 326 | [ 327 | "< => sign", 328 | "div => entity", 329 | "> => sign", 330 | "Hello => jsxliterals", 331 | "< => sign", 332 | "Name => entity", 333 | "/> => sign", 334 | "with => jsxliterals", 335 | "{ => sign", 336 | "data => identifier", 337 | "} => sign", 338 | " sign", 339 | "div => entity", 340 | "> => sign", 341 | ] 342 | `) 343 | }) 344 | 345 | it('parse svg with break lines', () => { 346 | const code = `\ 347 | 348 | 350 | ` 351 | const tokens = tokenize(code) 352 | expect(getTokensAsString(tokens)).toMatchInlineSnapshot(` 353 | [ 354 | "< => sign", 355 | "svg => entity", 356 | "> => sign", 357 | " => jsxliterals", 358 | " => jsxliterals", 359 | "< => sign", 360 | "path => entity", 361 | "d => property", 362 | "= => sign", 363 | "' => string", 364 | "M12 => string", 365 | "' => string", 366 | "/> => sign", 367 | " sign", 368 | "svg => entity", 369 | "> => sign", 370 | ] 371 | `) 372 | }) 373 | 374 | it('parse arrow function in jsx correctly', () => { 375 | const code = '' 376 | const tokens = tokenize(code) 377 | expect(getTokensAsString(tokens)).toMatchInlineSnapshot(` 378 | [ 379 | "< => sign", 380 | "button => entity", 381 | "onClick => property", 382 | "= => sign", 383 | "{ => sign", 384 | "( => sign", 385 | ") => sign", 386 | "= => sign", 387 | "> => sign", 388 | "{ => sign", 389 | "} => sign", 390 | "} => sign", 391 | "> => sign", 392 | "click => jsxliterals", 393 | " sign", 394 | "button => entity", 395 | "> => sign", 396 | ] 397 | `) 398 | }) 399 | 400 | it('preserve spaces in arrow function jsx prop correctly', () => { 401 | const code = ' 1)} />' 402 | 403 | const tokens = tokenize(code) 404 | expect(getTokensAsString(tokens, { filterSpaces: false })).toMatchInlineSnapshot(` 405 | [ 406 | "< => sign", 407 | "Foo => entity", 408 | " => space", 409 | "prop => property", 410 | "= => sign", 411 | "{ => sign", 412 | "( => sign", 413 | "v => identifier", 414 | ") => sign", 415 | " => space", 416 | "= => sign", 417 | "> => sign", 418 | " => space", 419 | "1 => identifier", 420 | ") => sign", 421 | "} => sign", 422 | " => space", 423 | "/> => sign", 424 | ] 425 | `) 426 | }) 427 | 428 | it('should render string for any jsx attribute values', () => { 429 | const code = '

' 430 | const tokens = tokenize(code) 431 | expect(getTokensAsString(tokens)).toMatchInlineSnapshot(` 432 | [ 433 | "< => sign", 434 | "h1 => entity", 435 | "data- => property", 436 | "title => property", 437 | "= => sign", 438 | "" => string", 439 | "true => string", 440 | "" => string", 441 | "/> => sign", 442 | ] 443 | `) 444 | 445 | const code2 = '' 446 | const tokens2 = tokenize(code2) 447 | expect(getTokensAsString(tokens2)).toMatchInlineSnapshot(` 448 | [ 449 | "< => sign", 450 | "svg => entity", 451 | "color => property", 452 | "= => sign", 453 | "" => string", 454 | "null => string", 455 | "" => string", 456 | "height => property", 457 | "= => sign", 458 | "" => string", 459 | "24 => string", 460 | "" => string", 461 | "/> => sign", 462 | ] 463 | `) 464 | }) 465 | 466 | it('should render single quote inside jsx literals as jsx literals', () => { 467 | const code = `

Let's get started!

` 468 | const tokens = tokenize(code) 469 | expect(getTokensAsString(tokens)).toMatchInlineSnapshot(` 470 | [ 471 | "< => sign", 472 | "p => entity", 473 | "> => sign", 474 | "Let's get started! => jsxliterals", 475 | " sign", 476 | "p => entity", 477 | "> => sign", 478 | ] 479 | `) 480 | }) 481 | 482 | it('should handle nested jsx literals correctly', async () => { 483 | const code = 484 | `<> 485 |
486 |

Text 1

487 |
488 |

Text 2

489 | ` 490 | const tokens = tokenize(code) 491 | expect(getTokensAsString(tokens)).toMatchInlineSnapshot(` 492 | [ 493 | "< => sign", 494 | "> => sign", 495 | "< => sign", 496 | "div => entity", 497 | "> => sign", 498 | " => jsxliterals", 499 | " => jsxliterals", 500 | "< => sign", 501 | "p => entity", 502 | "> => sign", 503 | "Text 1 => jsxliterals", 504 | " sign", 505 | "p => entity", 506 | "> => sign", 507 | " sign", 508 | "div => entity", 509 | "> => sign", 510 | "< => sign", 511 | "p => entity", 512 | "> => sign", 513 | "Text 2 => jsxliterals", 514 | " sign", 515 | "p => entity", 516 | "> => sign", 517 | " sign", 518 | "> => sign", 519 | ] 520 | `) 521 | }) 522 | 523 | it('should not affect the function param after closed jsx tag', () => { 524 | // issue: (str was treated as string 525 | const code = 526 | ` 527 | function p(str) {} 528 | ` 529 | const tokens = tokenize(code) 530 | expect(getTokensAsString(tokens)).toMatchInlineSnapshot(` 531 | [ 532 | "< => sign", 533 | "a => entity", 534 | "k => property", 535 | "= => sign", 536 | "{ => sign", 537 | "v => identifier", 538 | "} => sign", 539 | "/> => sign", 540 | "function => keyword", 541 | "p => identifier", 542 | "( => sign", 543 | "str => identifier", 544 | ") => sign", 545 | "{ => sign", 546 | "} => sign", 547 | ] 548 | `) 549 | }) 550 | 551 | it('should handle object spread correctly', () => { 552 | const code = `` 553 | const tokens = tokenize(code) 554 | expect(getTokensAsString(tokens)).toMatchInlineSnapshot(` 555 | [ 556 | "< => sign", 557 | "Component => entity", 558 | "{ => sign", 559 | ". => sign", 560 | ". => sign", 561 | ". => sign", 562 | "props => identifier", 563 | "} => sign", 564 | "/> => sign", 565 | ] 566 | `) 567 | }) 568 | 569 | it('should handle tailwind properties well', () => { 570 | const code = `
` 571 | const tokens = tokenize(code) 572 | expect(getTokensAsString(tokens)).toMatchInlineSnapshot(` 573 | [ 574 | "< => sign", 575 | "div => entity", 576 | "className => property", 577 | "= => sign", 578 | "" => string", 579 | "data-[layout=grid]:grid => string", 580 | "" => string", 581 | "/> => sign", 582 | ] 583 | `) 584 | }) 585 | }) 586 | 587 | describe('comments', () => { 588 | it('basic inline comments', () => { 589 | const code = `+ // This is a inline comment / <- a slash` 590 | const tokens = tokenize(code) 591 | expect(getTokensAsString(tokens)).toMatchInlineSnapshot(` 592 | [ 593 | "+ => sign", 594 | "// This is a inline comment / <- a slash => comment", 595 | ] 596 | `) 597 | }) 598 | 599 | it('multiple slashes started inline comments', () => { 600 | const code = `/// // reference comment` 601 | const tokens = tokenize(code) 602 | expect(getTokensAsString(tokens)).toMatchInlineSnapshot(` 603 | [ 604 | "/// // reference comment => comment", 605 | ] 606 | `) 607 | }) 608 | 609 | it('multi-line comments', () => { 610 | const code = `/* This is another comment */ alert('good') // <- alerts` 611 | const tokens = tokenize(code) 612 | expect(getTokensAsString(tokens)).toMatchInlineSnapshot(` 613 | [ 614 | "/* This is another comment */ => comment", 615 | "alert => identifier", 616 | "( => sign", 617 | "' => string", 618 | "good => string", 619 | "' => string", 620 | ") => sign", 621 | "// <- alerts => comment", 622 | ] 623 | `) 624 | }) 625 | 626 | it('multi-line comments with annotations', () => { 627 | const code = `/** 628 | * @param {string} names 629 | * @return {Promise} 630 | */` 631 | const tokens = tokenize(code) 632 | expect(getTokensAsString(tokens)).toMatchInlineSnapshot(` 633 | [ 634 | "/** 635 | * @param {string} names 636 | * @return {Promise} 637 | */ => comment", 638 | ] 639 | `) 640 | }) 641 | }) 642 | 643 | describe('regex', () => { 644 | it('basic regex', () => { 645 | const reg1 = '/^\\/[0-5]\\/$/' 646 | const reg2 = `/^\\w+[a-z0-9]/ig` 647 | 648 | expect(getTokensAsString(tokenize(reg1))).toMatchInlineSnapshot(` 649 | [ 650 | "/^\\/[0-5]\\/$/ => string", 651 | ] 652 | `) 653 | expect(getTokensAsString(tokenize(reg2))).toMatchInlineSnapshot(` 654 | [ 655 | "/^\\w+[a-z0-9]/ig => string", 656 | ] 657 | `) 658 | }) 659 | 660 | it('contain angle brackets', () => { 661 | const code = `/^\\w+\\/$/` 662 | const tokens = tokenize(code) 663 | expect(getTokensAsString(tokens)).toMatchInlineSnapshot(` 664 | [ 665 | "/^\\w+\\/$/ => string", 666 | ] 667 | `) 668 | }) 669 | 670 | it('regex plus operators', () => { 671 | const code = `/^\\/[0-5]\\/$/ + /^\\/\w+\\/$/gi` 672 | const tokens = tokenize(code) 673 | expect(getTokensAsString(tokens)).toMatchInlineSnapshot(` 674 | [ 675 | "/^\\/[0-5]\\/$/ => string", 676 | "+ => sign", 677 | "/^\\/w+\\/$/gi => string", 678 | ] 679 | `) 680 | }) 681 | 682 | it('regex with quotes inside', () => { 683 | const code = `replace(/'/, \`"\`)` 684 | const tokens = tokenize(code) 685 | expect(getTokensAsString(tokens)).toMatchInlineSnapshot(` 686 | [ 687 | "replace => identifier", 688 | "( => sign", 689 | "/'/ => string", 690 | ", => sign", 691 | "\` => string", 692 | "" => string", 693 | "\` => string", 694 | ") => sign", 695 | ] 696 | `) 697 | }) 698 | 699 | it('multi line regex tests', () => { 700 | const code1 = 701 | `/reg/.test('str')\n` + 702 | `[]\n` + 703 | `/reg/.test('str')` 704 | 705 | // '[]' consider as a end of the expression 706 | const tokens1 = tokenize(code1) 707 | expect(getTokensAsString(tokens1)).toMatchInlineSnapshot(` 708 | [ 709 | "/reg/ => string", 710 | ". => sign", 711 | "test => identifier", 712 | "( => sign", 713 | "' => string", 714 | "str => string", 715 | "' => string", 716 | ") => sign", 717 | "[ => sign", 718 | "] => sign", 719 | "/reg/ => string", 720 | ". => sign", 721 | "test => identifier", 722 | "( => sign", 723 | "' => string", 724 | "str => string", 725 | "' => string", 726 | ") => sign", 727 | ] 728 | `) 729 | 730 | const code2 = 731 | `/reg/.test('str')()\n` + 732 | `/reg/.test('str')` 733 | 734 | // what before '()' still considers as an expression 735 | const tokens2 = tokenize(code2) 736 | expect(getTokensAsString(tokens2)).toMatchInlineSnapshot(` 737 | [ 738 | "/reg/ => string", 739 | ". => sign", 740 | "test => identifier", 741 | "( => sign", 742 | "' => string", 743 | "str => string", 744 | "' => string", 745 | ") => sign", 746 | "( => sign", 747 | ") => sign", 748 | "/ => sign", 749 | "reg => identifier", 750 | "/ => sign", 751 | ". => sign", 752 | "test => identifier", 753 | "( => sign", 754 | "' => string", 755 | "str => string", 756 | "' => string", 757 | ") => sign", 758 | ] 759 | `) 760 | }) 761 | }) 762 | 763 | describe('strings', () => { 764 | it('import paths', () => { 765 | const code = `import mod from "../../mod"` 766 | const tokens = tokenize(code) 767 | expect(getTokensAsString(tokens)).toMatchInlineSnapshot(` 768 | [ 769 | "import => keyword", 770 | "mod => identifier", 771 | "from => keyword", 772 | "" => string", 773 | "../../mod => string", 774 | "" => string", 775 | ] 776 | `) 777 | }) 778 | 779 | it('contains curly brackets', () => { 780 | const code = `const str = 'hello {world}'` 781 | const tokens = tokenize(code) 782 | expect(getTokensAsString(tokens)).toMatchInlineSnapshot(` 783 | [ 784 | "const => keyword", 785 | "str => identifier", 786 | "= => sign", 787 | "' => string", 788 | "hello {world} => string", 789 | "' => string", 790 | ] 791 | `) 792 | }) 793 | 794 | it('contains angle brackets', () => { 795 | const code = `const str = 'hello '` 796 | const tokens = tokenize(code) 797 | expect(getTokensAsString(tokens)).toMatchInlineSnapshot(` 798 | [ 799 | "const => keyword", 800 | "str => identifier", 801 | "= => sign", 802 | "' => string", 803 | "hello => string", 804 | "' => string", 805 | ] 806 | `) 807 | }) 808 | 809 | it('multi quotes string', () => { 810 | const str1 = `"aa'bb'cc"` 811 | expect(getTokensAsString(tokenize(str1))).toMatchInlineSnapshot(` 812 | [ 813 | "" => string", 814 | "aa => string", 815 | "' => string", 816 | "bb => string", 817 | "' => string", 818 | "cc => string", 819 | "" => string", 820 | ] 821 | `) 822 | 823 | const str2 = `'aa"bb"cc'` 824 | expect(getTokensAsString(tokenize(str2))).toMatchInlineSnapshot(` 825 | [ 826 | "' => string", 827 | "aa => string", 828 | "" => string", 829 | "bb => string", 830 | "" => string", 831 | "cc => string", 832 | "' => string", 833 | ] 834 | `) 835 | 836 | const str3 = `\`\nabc\`` 837 | expect(getTokensAsString(tokenize(str3))).toMatchInlineSnapshot(` 838 | [ 839 | "\` => string", 840 | "abc => string", 841 | "\` => string", 842 | ] 843 | `) 844 | }) 845 | 846 | it('string template', () => { 847 | const code1 = ` 848 | \`hi \$\{ a \} world\` 849 | \`hello \$\{world\}\` 850 | ` 851 | expect(getTokensAsString(tokenize(code1))).toMatchInlineSnapshot(` 852 | [ 853 | "\` => string", 854 | "hi => string", 855 | "\${ => sign", 856 | "a => identifier", 857 | "} => sign", 858 | "world => string", 859 | "\` => string", 860 | "\` => string", 861 | "hello => string", 862 | "\${ => sign", 863 | "world => identifier", 864 | "} => sign", 865 | "\` => string", 866 | ] 867 | `) 868 | 869 | const code2 = ` 870 | \`hi \$\{ b \} plus \$\{ c + \`text\` \}\` 871 | \`nested \$\{ c + \`\$\{ no \}\` }\` 872 | ` 873 | expect(getTokensAsString(tokenize(code2))).toMatchInlineSnapshot(` 874 | [ 875 | "\` => string", 876 | "hi => string", 877 | "\${ => sign", 878 | "b => identifier", 879 | "} => sign", 880 | "plus => string", 881 | "\${ => sign", 882 | "c => identifier", 883 | "+ => sign", 884 | "\` => string", 885 | "text => string", 886 | "\` => string", 887 | "} => sign", 888 | "\` => string", 889 | "\` => string", 890 | "nested => string", 891 | "\${ => sign", 892 | "c => identifier", 893 | "+ => sign", 894 | "\` => string", 895 | "\${ => sign", 896 | "no => identifier", 897 | "} => sign", 898 | "\` => string", 899 | "} => sign", 900 | "\` => string", 901 | ] 902 | `) 903 | 904 | const code3 = ` 905 | \` 906 | hehehehe 907 | \` 908 | 'we' 909 | "no" 910 | \`hello\` 911 | ` 912 | expect(getTokensAsString(tokenize(code3))).toMatchInlineSnapshot(` 913 | [ 914 | "\` => string", 915 | "hehehehe => string", 916 | "\` => string", 917 | "' => string", 918 | "we => string", 919 | "' => string", 920 | "" => string", 921 | "no => string", 922 | "" => string", 923 | "\` => string", 924 | "hello => string", 925 | "\` => string", 926 | ] 927 | `) 928 | }) 929 | 930 | it('unicode token', () => { 931 | const code = `let hello你好 = 'hello你好'` 932 | expect(getTokensAsString(tokenize(code))).toMatchInlineSnapshot(` 933 | [ 934 | "let => keyword", 935 | "hello你好 => identifier", 936 | "= => sign", 937 | "' => string", 938 | "hello你好 => string", 939 | "' => string", 940 | ] 941 | `) 942 | }) 943 | 944 | it('number in string', () => { 945 | const code = `'123'\n'true'` 946 | expect(getTokensAsString(tokenize(code))).toMatchInlineSnapshot(` 947 | [ 948 | "' => string", 949 | "123 => string", 950 | "' => string", 951 | "' => string", 952 | "true => string", 953 | "' => string", 954 | ] 955 | `) 956 | }) 957 | }) 958 | 959 | describe('class', () => { 960 | it('determine class name', () => { 961 | const code = `class Bar extends Array {}` 962 | expect(getTokensAsString(tokenize(code))).toMatchInlineSnapshot(` 963 | [ 964 | "class => keyword", 965 | "Bar => class", 966 | "extends => keyword", 967 | "Array => class", 968 | "{ => sign", 969 | "} => sign", 970 | ] 971 | `) 972 | }) 973 | }) 974 | -------------------------------------------------------------------------------- /packages/sugar-high/pnpm-lock.yaml: -------------------------------------------------------------------------------- 1 | lockfileVersion: '6.0' 2 | 3 | settings: 4 | autoInstallPeers: true 5 | excludeLinksFromLockfile: false 6 | 7 | overrides: 8 | sugar-high: link:./ 9 | 10 | devDependencies: 11 | '@types/node': 12 | specifier: 22.10.7 13 | version: 22.10.7 14 | '@types/react': 15 | specifier: 19.0.7 16 | version: 19.0.7 17 | codice: 18 | specifier: ^1.3.1 19 | version: 1.3.1(react@19.0.0) 20 | dom-to-image: 21 | specifier: ^2.6.0 22 | version: 2.6.0 23 | next: 24 | specifier: 15.1.5 25 | version: 15.1.5(react-dom@19.0.0)(react@19.0.0) 26 | react: 27 | specifier: ^19.0.0 28 | version: 19.0.0 29 | react-dom: 30 | specifier: ^19.0.0 31 | version: 19.0.0(react@19.0.0) 32 | sugar-high: 33 | specifier: 'link:' 34 | version: 'link:' 35 | typescript: 36 | specifier: 5.7.3 37 | version: 5.7.3 38 | vitest: 39 | specifier: ^3.0.2 40 | version: 3.0.2(@types/node@22.10.7) 41 | 42 | packages: 43 | 44 | /@emnapi/runtime@1.3.1: 45 | resolution: {integrity: sha512-kEBmG8KyqtxJZv+ygbEim+KCGtIq1fC22Ms3S4ziXmYKm8uyoLX0MHONVKwp+9opg390VaKRNt4a7A9NwmpNhw==} 46 | requiresBuild: true 47 | dependencies: 48 | tslib: 2.8.1 49 | dev: true 50 | optional: true 51 | 52 | /@esbuild/android-arm64@0.19.8: 53 | resolution: {integrity: sha512-B8JbS61bEunhfx8kasogFENgQfr/dIp+ggYXwTqdbMAgGDhRa3AaPpQMuQU0rNxDLECj6FhDzk1cF9WHMVwrtA==} 54 | engines: {node: '>=12'} 55 | cpu: [arm64] 56 | os: [android] 57 | requiresBuild: true 58 | dev: true 59 | optional: true 60 | 61 | /@esbuild/android-arm@0.19.8: 62 | resolution: {integrity: sha512-31E2lxlGM1KEfivQl8Yf5aYU/mflz9g06H6S15ITUFQueMFtFjESRMoDSkvMo8thYvLBax+VKTPlpnx+sPicOA==} 63 | engines: {node: '>=12'} 64 | cpu: [arm] 65 | os: [android] 66 | requiresBuild: true 67 | dev: true 68 | optional: true 69 | 70 | /@esbuild/android-x64@0.19.8: 71 | resolution: {integrity: sha512-rdqqYfRIn4jWOp+lzQttYMa2Xar3OK9Yt2fhOhzFXqg0rVWEfSclJvZq5fZslnz6ypHvVf3CT7qyf0A5pM682A==} 72 | engines: {node: '>=12'} 73 | cpu: [x64] 74 | os: [android] 75 | requiresBuild: true 76 | dev: true 77 | optional: true 78 | 79 | /@esbuild/darwin-arm64@0.19.8: 80 | resolution: {integrity: sha512-RQw9DemMbIq35Bprbboyf8SmOr4UXsRVxJ97LgB55VKKeJOOdvsIPy0nFyF2l8U+h4PtBx/1kRf0BelOYCiQcw==} 81 | engines: {node: '>=12'} 82 | cpu: [arm64] 83 | os: [darwin] 84 | requiresBuild: true 85 | dev: true 86 | optional: true 87 | 88 | /@esbuild/darwin-x64@0.19.8: 89 | resolution: {integrity: sha512-3sur80OT9YdeZwIVgERAysAbwncom7b4bCI2XKLjMfPymTud7e/oY4y+ci1XVp5TfQp/bppn7xLw1n/oSQY3/Q==} 90 | engines: {node: '>=12'} 91 | cpu: [x64] 92 | os: [darwin] 93 | requiresBuild: true 94 | dev: true 95 | optional: true 96 | 97 | /@esbuild/freebsd-arm64@0.19.8: 98 | resolution: {integrity: sha512-WAnPJSDattvS/XtPCTj1tPoTxERjcTpH6HsMr6ujTT+X6rylVe8ggxk8pVxzf5U1wh5sPODpawNicF5ta/9Tmw==} 99 | engines: {node: '>=12'} 100 | cpu: [arm64] 101 | os: [freebsd] 102 | requiresBuild: true 103 | dev: true 104 | optional: true 105 | 106 | /@esbuild/freebsd-x64@0.19.8: 107 | resolution: {integrity: sha512-ICvZyOplIjmmhjd6mxi+zxSdpPTKFfyPPQMQTK/w+8eNK6WV01AjIztJALDtwNNfFhfZLux0tZLC+U9nSyA5Zg==} 108 | engines: {node: '>=12'} 109 | cpu: [x64] 110 | os: [freebsd] 111 | requiresBuild: true 112 | dev: true 113 | optional: true 114 | 115 | /@esbuild/linux-arm64@0.19.8: 116 | resolution: {integrity: sha512-z1zMZivxDLHWnyGOctT9JP70h0beY54xDDDJt4VpTX+iwA77IFsE1vCXWmprajJGa+ZYSqkSbRQ4eyLCpCmiCQ==} 117 | engines: {node: '>=12'} 118 | cpu: [arm64] 119 | os: [linux] 120 | requiresBuild: true 121 | dev: true 122 | optional: true 123 | 124 | /@esbuild/linux-arm@0.19.8: 125 | resolution: {integrity: sha512-H4vmI5PYqSvosPaTJuEppU9oz1dq2A7Mr2vyg5TF9Ga+3+MGgBdGzcyBP7qK9MrwFQZlvNyJrvz6GuCaj3OukQ==} 126 | engines: {node: '>=12'} 127 | cpu: [arm] 128 | os: [linux] 129 | requiresBuild: true 130 | dev: true 131 | optional: true 132 | 133 | /@esbuild/linux-ia32@0.19.8: 134 | resolution: {integrity: sha512-1a8suQiFJmZz1khm/rDglOc8lavtzEMRo0v6WhPgxkrjcU0LkHj+TwBrALwoz/OtMExvsqbbMI0ChyelKabSvQ==} 135 | engines: {node: '>=12'} 136 | cpu: [ia32] 137 | os: [linux] 138 | requiresBuild: true 139 | dev: true 140 | optional: true 141 | 142 | /@esbuild/linux-loong64@0.19.8: 143 | resolution: {integrity: sha512-fHZWS2JJxnXt1uYJsDv9+b60WCc2RlvVAy1F76qOLtXRO+H4mjt3Tr6MJ5l7Q78X8KgCFudnTuiQRBhULUyBKQ==} 144 | engines: {node: '>=12'} 145 | cpu: [loong64] 146 | os: [linux] 147 | requiresBuild: true 148 | dev: true 149 | optional: true 150 | 151 | /@esbuild/linux-mips64el@0.19.8: 152 | resolution: {integrity: sha512-Wy/z0EL5qZYLX66dVnEg9riiwls5IYnziwuju2oUiuxVc+/edvqXa04qNtbrs0Ukatg5HEzqT94Zs7J207dN5Q==} 153 | engines: {node: '>=12'} 154 | cpu: [mips64el] 155 | os: [linux] 156 | requiresBuild: true 157 | dev: true 158 | optional: true 159 | 160 | /@esbuild/linux-ppc64@0.19.8: 161 | resolution: {integrity: sha512-ETaW6245wK23YIEufhMQ3HSeHO7NgsLx8gygBVldRHKhOlD1oNeNy/P67mIh1zPn2Hr2HLieQrt6tWrVwuqrxg==} 162 | engines: {node: '>=12'} 163 | cpu: [ppc64] 164 | os: [linux] 165 | requiresBuild: true 166 | dev: true 167 | optional: true 168 | 169 | /@esbuild/linux-riscv64@0.19.8: 170 | resolution: {integrity: sha512-T2DRQk55SgoleTP+DtPlMrxi/5r9AeFgkhkZ/B0ap99zmxtxdOixOMI570VjdRCs9pE4Wdkz7JYrsPvsl7eESg==} 171 | engines: {node: '>=12'} 172 | cpu: [riscv64] 173 | os: [linux] 174 | requiresBuild: true 175 | dev: true 176 | optional: true 177 | 178 | /@esbuild/linux-s390x@0.19.8: 179 | resolution: {integrity: sha512-NPxbdmmo3Bk7mbNeHmcCd7R7fptJaczPYBaELk6NcXxy7HLNyWwCyDJ/Xx+/YcNH7Im5dHdx9gZ5xIwyliQCbg==} 180 | engines: {node: '>=12'} 181 | cpu: [s390x] 182 | os: [linux] 183 | requiresBuild: true 184 | dev: true 185 | optional: true 186 | 187 | /@esbuild/linux-x64@0.19.8: 188 | resolution: {integrity: sha512-lytMAVOM3b1gPypL2TRmZ5rnXl7+6IIk8uB3eLsV1JwcizuolblXRrc5ShPrO9ls/b+RTp+E6gbsuLWHWi2zGg==} 189 | engines: {node: '>=12'} 190 | cpu: [x64] 191 | os: [linux] 192 | requiresBuild: true 193 | dev: true 194 | optional: true 195 | 196 | /@esbuild/netbsd-x64@0.19.8: 197 | resolution: {integrity: sha512-hvWVo2VsXz/8NVt1UhLzxwAfo5sioj92uo0bCfLibB0xlOmimU/DeAEsQILlBQvkhrGjamP0/el5HU76HAitGw==} 198 | engines: {node: '>=12'} 199 | cpu: [x64] 200 | os: [netbsd] 201 | requiresBuild: true 202 | dev: true 203 | optional: true 204 | 205 | /@esbuild/openbsd-x64@0.19.8: 206 | resolution: {integrity: sha512-/7Y7u77rdvmGTxR83PgaSvSBJCC2L3Kb1M/+dmSIvRvQPXXCuC97QAwMugBNG0yGcbEGfFBH7ojPzAOxfGNkwQ==} 207 | engines: {node: '>=12'} 208 | cpu: [x64] 209 | os: [openbsd] 210 | requiresBuild: true 211 | dev: true 212 | optional: true 213 | 214 | /@esbuild/sunos-x64@0.19.8: 215 | resolution: {integrity: sha512-9Lc4s7Oi98GqFA4HzA/W2JHIYfnXbUYgekUP/Sm4BG9sfLjyv6GKKHKKVs83SMicBF2JwAX6A1PuOLMqpD001w==} 216 | engines: {node: '>=12'} 217 | cpu: [x64] 218 | os: [sunos] 219 | requiresBuild: true 220 | dev: true 221 | optional: true 222 | 223 | /@esbuild/win32-arm64@0.19.8: 224 | resolution: {integrity: sha512-rq6WzBGjSzihI9deW3fC2Gqiak68+b7qo5/3kmB6Gvbh/NYPA0sJhrnp7wgV4bNwjqM+R2AApXGxMO7ZoGhIJg==} 225 | engines: {node: '>=12'} 226 | cpu: [arm64] 227 | os: [win32] 228 | requiresBuild: true 229 | dev: true 230 | optional: true 231 | 232 | /@esbuild/win32-ia32@0.19.8: 233 | resolution: {integrity: sha512-AIAbverbg5jMvJznYiGhrd3sumfwWs8572mIJL5NQjJa06P8KfCPWZQ0NwZbPQnbQi9OWSZhFVSUWjjIrn4hSw==} 234 | engines: {node: '>=12'} 235 | cpu: [ia32] 236 | os: [win32] 237 | requiresBuild: true 238 | dev: true 239 | optional: true 240 | 241 | /@esbuild/win32-x64@0.19.8: 242 | resolution: {integrity: sha512-bfZ0cQ1uZs2PqpulNL5j/3w+GDhP36k1K5c38QdQg+Swy51jFZWWeIkteNsufkQxp986wnqRRsb/bHbY1WQ7TA==} 243 | engines: {node: '>=12'} 244 | cpu: [x64] 245 | os: [win32] 246 | requiresBuild: true 247 | dev: true 248 | optional: true 249 | 250 | /@img/sharp-darwin-arm64@0.33.5: 251 | resolution: {integrity: sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ==} 252 | engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} 253 | cpu: [arm64] 254 | os: [darwin] 255 | requiresBuild: true 256 | optionalDependencies: 257 | '@img/sharp-libvips-darwin-arm64': 1.0.4 258 | dev: true 259 | optional: true 260 | 261 | /@img/sharp-darwin-x64@0.33.5: 262 | resolution: {integrity: sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q==} 263 | engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} 264 | cpu: [x64] 265 | os: [darwin] 266 | requiresBuild: true 267 | optionalDependencies: 268 | '@img/sharp-libvips-darwin-x64': 1.0.4 269 | dev: true 270 | optional: true 271 | 272 | /@img/sharp-libvips-darwin-arm64@1.0.4: 273 | resolution: {integrity: sha512-XblONe153h0O2zuFfTAbQYAX2JhYmDHeWikp1LM9Hul9gVPjFY427k6dFEcOL72O01QxQsWi761svJ/ev9xEDg==} 274 | cpu: [arm64] 275 | os: [darwin] 276 | requiresBuild: true 277 | dev: true 278 | optional: true 279 | 280 | /@img/sharp-libvips-darwin-x64@1.0.4: 281 | resolution: {integrity: sha512-xnGR8YuZYfJGmWPvmlunFaWJsb9T/AO2ykoP3Fz/0X5XV2aoYBPkX6xqCQvUTKKiLddarLaxpzNe+b1hjeWHAQ==} 282 | cpu: [x64] 283 | os: [darwin] 284 | requiresBuild: true 285 | dev: true 286 | optional: true 287 | 288 | /@img/sharp-libvips-linux-arm64@1.0.4: 289 | resolution: {integrity: sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA==} 290 | cpu: [arm64] 291 | os: [linux] 292 | requiresBuild: true 293 | dev: true 294 | optional: true 295 | 296 | /@img/sharp-libvips-linux-arm@1.0.5: 297 | resolution: {integrity: sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g==} 298 | cpu: [arm] 299 | os: [linux] 300 | requiresBuild: true 301 | dev: true 302 | optional: true 303 | 304 | /@img/sharp-libvips-linux-s390x@1.0.4: 305 | resolution: {integrity: sha512-u7Wz6ntiSSgGSGcjZ55im6uvTrOxSIS8/dgoVMoiGE9I6JAfU50yH5BoDlYA1tcuGS7g/QNtetJnxA6QEsCVTA==} 306 | cpu: [s390x] 307 | os: [linux] 308 | requiresBuild: true 309 | dev: true 310 | optional: true 311 | 312 | /@img/sharp-libvips-linux-x64@1.0.4: 313 | resolution: {integrity: sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw==} 314 | cpu: [x64] 315 | os: [linux] 316 | requiresBuild: true 317 | dev: true 318 | optional: true 319 | 320 | /@img/sharp-libvips-linuxmusl-arm64@1.0.4: 321 | resolution: {integrity: sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA==} 322 | cpu: [arm64] 323 | os: [linux] 324 | requiresBuild: true 325 | dev: true 326 | optional: true 327 | 328 | /@img/sharp-libvips-linuxmusl-x64@1.0.4: 329 | resolution: {integrity: sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw==} 330 | cpu: [x64] 331 | os: [linux] 332 | requiresBuild: true 333 | dev: true 334 | optional: true 335 | 336 | /@img/sharp-linux-arm64@0.33.5: 337 | resolution: {integrity: sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA==} 338 | engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} 339 | cpu: [arm64] 340 | os: [linux] 341 | requiresBuild: true 342 | optionalDependencies: 343 | '@img/sharp-libvips-linux-arm64': 1.0.4 344 | dev: true 345 | optional: true 346 | 347 | /@img/sharp-linux-arm@0.33.5: 348 | resolution: {integrity: sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ==} 349 | engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} 350 | cpu: [arm] 351 | os: [linux] 352 | requiresBuild: true 353 | optionalDependencies: 354 | '@img/sharp-libvips-linux-arm': 1.0.5 355 | dev: true 356 | optional: true 357 | 358 | /@img/sharp-linux-s390x@0.33.5: 359 | resolution: {integrity: sha512-y/5PCd+mP4CA/sPDKl2961b+C9d+vPAveS33s6Z3zfASk2j5upL6fXVPZi7ztePZ5CuH+1kW8JtvxgbuXHRa4Q==} 360 | engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} 361 | cpu: [s390x] 362 | os: [linux] 363 | requiresBuild: true 364 | optionalDependencies: 365 | '@img/sharp-libvips-linux-s390x': 1.0.4 366 | dev: true 367 | optional: true 368 | 369 | /@img/sharp-linux-x64@0.33.5: 370 | resolution: {integrity: sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA==} 371 | engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} 372 | cpu: [x64] 373 | os: [linux] 374 | requiresBuild: true 375 | optionalDependencies: 376 | '@img/sharp-libvips-linux-x64': 1.0.4 377 | dev: true 378 | optional: true 379 | 380 | /@img/sharp-linuxmusl-arm64@0.33.5: 381 | resolution: {integrity: sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g==} 382 | engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} 383 | cpu: [arm64] 384 | os: [linux] 385 | requiresBuild: true 386 | optionalDependencies: 387 | '@img/sharp-libvips-linuxmusl-arm64': 1.0.4 388 | dev: true 389 | optional: true 390 | 391 | /@img/sharp-linuxmusl-x64@0.33.5: 392 | resolution: {integrity: sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw==} 393 | engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} 394 | cpu: [x64] 395 | os: [linux] 396 | requiresBuild: true 397 | optionalDependencies: 398 | '@img/sharp-libvips-linuxmusl-x64': 1.0.4 399 | dev: true 400 | optional: true 401 | 402 | /@img/sharp-wasm32@0.33.5: 403 | resolution: {integrity: sha512-ykUW4LVGaMcU9lu9thv85CbRMAwfeadCJHRsg2GmeRa/cJxsVY9Rbd57JcMxBkKHag5U/x7TSBpScF4U8ElVzg==} 404 | engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} 405 | cpu: [wasm32] 406 | requiresBuild: true 407 | dependencies: 408 | '@emnapi/runtime': 1.3.1 409 | dev: true 410 | optional: true 411 | 412 | /@img/sharp-win32-ia32@0.33.5: 413 | resolution: {integrity: sha512-T36PblLaTwuVJ/zw/LaH0PdZkRz5rd3SmMHX8GSmR7vtNSP5Z6bQkExdSK7xGWyxLw4sUknBuugTelgw2faBbQ==} 414 | engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} 415 | cpu: [ia32] 416 | os: [win32] 417 | requiresBuild: true 418 | dev: true 419 | optional: true 420 | 421 | /@img/sharp-win32-x64@0.33.5: 422 | resolution: {integrity: sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg==} 423 | engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} 424 | cpu: [x64] 425 | os: [win32] 426 | requiresBuild: true 427 | dev: true 428 | optional: true 429 | 430 | /@jridgewell/sourcemap-codec@1.5.0: 431 | resolution: {integrity: sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==} 432 | dev: true 433 | 434 | /@next/env@15.1.5: 435 | resolution: {integrity: sha512-jg8ygVq99W3/XXb9Y6UQsritwhjc+qeiO7QrGZRYOfviyr/HcdnhdBQu4gbp2rBIh2ZyBYTBMWbPw3JSCb0GHw==} 436 | dev: true 437 | 438 | /@next/swc-darwin-arm64@15.1.5: 439 | resolution: {integrity: sha512-5ttHGE75Nw9/l5S8zR2xEwR8OHEqcpPym3idIMAZ2yo+Edk0W/Vf46jGqPOZDk+m/SJ+vYZDSuztzhVha8rcdA==} 440 | engines: {node: '>= 10'} 441 | cpu: [arm64] 442 | os: [darwin] 443 | requiresBuild: true 444 | dev: true 445 | optional: true 446 | 447 | /@next/swc-darwin-x64@15.1.5: 448 | resolution: {integrity: sha512-8YnZn7vDURUUTInfOcU5l0UWplZGBqUlzvqKKUFceM11SzfNEz7E28E1Arn4/FsOf90b1Nopboy7i7ufc4jXag==} 449 | engines: {node: '>= 10'} 450 | cpu: [x64] 451 | os: [darwin] 452 | requiresBuild: true 453 | dev: true 454 | optional: true 455 | 456 | /@next/swc-linux-arm64-gnu@15.1.5: 457 | resolution: {integrity: sha512-rDJC4ctlYbK27tCyFUhgIv8o7miHNlpCjb2XXfTLQszwAUOSbcMN9q2y3urSrrRCyGVOd9ZR9a4S45dRh6JF3A==} 458 | engines: {node: '>= 10'} 459 | cpu: [arm64] 460 | os: [linux] 461 | requiresBuild: true 462 | dev: true 463 | optional: true 464 | 465 | /@next/swc-linux-arm64-musl@15.1.5: 466 | resolution: {integrity: sha512-FG5RApf4Gu+J+pHUQxXPM81oORZrKBYKUaBTylEIQ6Lz17hKVDsLbSXInfXM0giclvXbyiLXjTv42sQMATmZ0A==} 467 | engines: {node: '>= 10'} 468 | cpu: [arm64] 469 | os: [linux] 470 | requiresBuild: true 471 | dev: true 472 | optional: true 473 | 474 | /@next/swc-linux-x64-gnu@15.1.5: 475 | resolution: {integrity: sha512-NX2Ar3BCquAOYpnoYNcKz14eH03XuF7SmSlPzTSSU4PJe7+gelAjxo3Y7F2m8+hLT8ZkkqElawBp7SWBdzwqQw==} 476 | engines: {node: '>= 10'} 477 | cpu: [x64] 478 | os: [linux] 479 | requiresBuild: true 480 | dev: true 481 | optional: true 482 | 483 | /@next/swc-linux-x64-musl@15.1.5: 484 | resolution: {integrity: sha512-EQgqMiNu3mrV5eQHOIgeuh6GB5UU57tu17iFnLfBEhYfiOfyK+vleYKh2dkRVkV6ayx3eSqbIYgE7J7na4hhcA==} 485 | engines: {node: '>= 10'} 486 | cpu: [x64] 487 | os: [linux] 488 | requiresBuild: true 489 | dev: true 490 | optional: true 491 | 492 | /@next/swc-win32-arm64-msvc@15.1.5: 493 | resolution: {integrity: sha512-HPULzqR/VqryQZbZME8HJE3jNFmTGcp+uRMHabFbQl63TtDPm+oCXAz3q8XyGv2AoihwNApVlur9Up7rXWRcjg==} 494 | engines: {node: '>= 10'} 495 | cpu: [arm64] 496 | os: [win32] 497 | requiresBuild: true 498 | dev: true 499 | optional: true 500 | 501 | /@next/swc-win32-x64-msvc@15.1.5: 502 | resolution: {integrity: sha512-n74fUb/Ka1dZSVYfjwQ+nSJ+ifUff7jGurFcTuJNKZmI62FFOxQXUYit/uZXPTj2cirm1rvGWHG2GhbSol5Ikw==} 503 | engines: {node: '>= 10'} 504 | cpu: [x64] 505 | os: [win32] 506 | requiresBuild: true 507 | dev: true 508 | optional: true 509 | 510 | /@rollup/rollup-android-arm-eabi@4.6.1: 511 | resolution: {integrity: sha512-0WQ0ouLejaUCRsL93GD4uft3rOmB8qoQMU05Kb8CmMtMBe7XUDLAltxVZI1q6byNqEtU7N1ZX1Vw5lIpgulLQA==} 512 | cpu: [arm] 513 | os: [android] 514 | requiresBuild: true 515 | dev: true 516 | optional: true 517 | 518 | /@rollup/rollup-android-arm64@4.6.1: 519 | resolution: {integrity: sha512-1TKm25Rn20vr5aTGGZqo6E4mzPicCUD79k17EgTLAsXc1zysyi4xXKACfUbwyANEPAEIxkzwue6JZ+stYzWUTA==} 520 | cpu: [arm64] 521 | os: [android] 522 | requiresBuild: true 523 | dev: true 524 | optional: true 525 | 526 | /@rollup/rollup-darwin-arm64@4.6.1: 527 | resolution: {integrity: sha512-cEXJQY/ZqMACb+nxzDeX9IPLAg7S94xouJJCNVE5BJM8JUEP4HeTF+ti3cmxWeSJo+5D+o8Tc0UAWUkfENdeyw==} 528 | cpu: [arm64] 529 | os: [darwin] 530 | requiresBuild: true 531 | dev: true 532 | optional: true 533 | 534 | /@rollup/rollup-darwin-x64@4.6.1: 535 | resolution: {integrity: sha512-LoSU9Xu56isrkV2jLldcKspJ7sSXmZWkAxg7sW/RfF7GS4F5/v4EiqKSMCFbZtDu2Nc1gxxFdQdKwkKS4rwxNg==} 536 | cpu: [x64] 537 | os: [darwin] 538 | requiresBuild: true 539 | dev: true 540 | optional: true 541 | 542 | /@rollup/rollup-linux-arm-gnueabihf@4.6.1: 543 | resolution: {integrity: sha512-EfI3hzYAy5vFNDqpXsNxXcgRDcFHUWSx5nnRSCKwXuQlI5J9dD84g2Usw81n3FLBNsGCegKGwwTVsSKK9cooSQ==} 544 | cpu: [arm] 545 | os: [linux] 546 | requiresBuild: true 547 | dev: true 548 | optional: true 549 | 550 | /@rollup/rollup-linux-arm64-gnu@4.6.1: 551 | resolution: {integrity: sha512-9lhc4UZstsegbNLhH0Zu6TqvDfmhGzuCWtcTFXY10VjLLUe4Mr0Ye2L3rrtHaDd/J5+tFMEuo5LTCSCMXWfUKw==} 552 | cpu: [arm64] 553 | os: [linux] 554 | requiresBuild: true 555 | dev: true 556 | optional: true 557 | 558 | /@rollup/rollup-linux-arm64-musl@4.6.1: 559 | resolution: {integrity: sha512-FfoOK1yP5ksX3wwZ4Zk1NgyGHZyuRhf99j64I5oEmirV8EFT7+OhUZEnP+x17lcP/QHJNWGsoJwrz4PJ9fBEXw==} 560 | cpu: [arm64] 561 | os: [linux] 562 | requiresBuild: true 563 | dev: true 564 | optional: true 565 | 566 | /@rollup/rollup-linux-x64-gnu@4.6.1: 567 | resolution: {integrity: sha512-DNGZvZDO5YF7jN5fX8ZqmGLjZEXIJRdJEdTFMhiyXqyXubBa0WVLDWSNlQ5JR2PNgDbEV1VQowhVRUh+74D+RA==} 568 | cpu: [x64] 569 | os: [linux] 570 | requiresBuild: true 571 | dev: true 572 | optional: true 573 | 574 | /@rollup/rollup-linux-x64-musl@4.6.1: 575 | resolution: {integrity: sha512-RkJVNVRM+piYy87HrKmhbexCHg3A6Z6MU0W9GHnJwBQNBeyhCJG9KDce4SAMdicQnpURggSvtbGo9xAWOfSvIQ==} 576 | cpu: [x64] 577 | os: [linux] 578 | requiresBuild: true 579 | dev: true 580 | optional: true 581 | 582 | /@rollup/rollup-win32-arm64-msvc@4.6.1: 583 | resolution: {integrity: sha512-v2FVT6xfnnmTe3W9bJXl6r5KwJglMK/iRlkKiIFfO6ysKs0rDgz7Cwwf3tjldxQUrHL9INT/1r4VA0n9L/F1vQ==} 584 | cpu: [arm64] 585 | os: [win32] 586 | requiresBuild: true 587 | dev: true 588 | optional: true 589 | 590 | /@rollup/rollup-win32-ia32-msvc@4.6.1: 591 | resolution: {integrity: sha512-YEeOjxRyEjqcWphH9dyLbzgkF8wZSKAKUkldRY6dgNR5oKs2LZazqGB41cWJ4Iqqcy9/zqYgmzBkRoVz3Q9MLw==} 592 | cpu: [ia32] 593 | os: [win32] 594 | requiresBuild: true 595 | dev: true 596 | optional: true 597 | 598 | /@rollup/rollup-win32-x64-msvc@4.6.1: 599 | resolution: {integrity: sha512-0zfTlFAIhgz8V2G8STq8toAjsYYA6eci1hnXuyOTUFnymrtJwnS6uGKiv3v5UrPZkBlamLvrLV2iiaeqCKzb0A==} 600 | cpu: [x64] 601 | os: [win32] 602 | requiresBuild: true 603 | dev: true 604 | optional: true 605 | 606 | /@swc/counter@0.1.3: 607 | resolution: {integrity: sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==} 608 | dev: true 609 | 610 | /@swc/helpers@0.5.15: 611 | resolution: {integrity: sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==} 612 | dependencies: 613 | tslib: 2.8.1 614 | dev: true 615 | 616 | /@types/estree@1.0.5: 617 | resolution: {integrity: sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==} 618 | dev: true 619 | 620 | /@types/node@22.10.7: 621 | resolution: {integrity: sha512-V09KvXxFiutGp6B7XkpaDXlNadZxrzajcY50EuoLIpQ6WWYCSvf19lVIazzfIzQvhUN2HjX12spLojTnhuKlGg==} 622 | dependencies: 623 | undici-types: 6.20.0 624 | dev: true 625 | 626 | /@types/react@19.0.7: 627 | resolution: {integrity: sha512-MoFsEJKkAtZCrC1r6CM8U22GzhG7u2Wir8ons/aCKH6MBdD1ibV24zOSSkdZVUKqN5i396zG5VKLYZ3yaUZdLA==} 628 | dependencies: 629 | csstype: 3.1.3 630 | dev: true 631 | 632 | /@vitest/expect@3.0.2: 633 | resolution: {integrity: sha512-dKSHLBcoZI+3pmP5hiZ7I5grNru2HRtEW8Z5Zp4IXog8QYcxhlox7JUPyIIFWfN53+3HW3KPLIl6nSzUGgKSuQ==} 634 | dependencies: 635 | '@vitest/spy': 3.0.2 636 | '@vitest/utils': 3.0.2 637 | chai: 5.1.2 638 | tinyrainbow: 2.0.0 639 | dev: true 640 | 641 | /@vitest/mocker@3.0.2(vite@5.0.5): 642 | resolution: {integrity: sha512-Hr09FoBf0jlwwSyzIF4Xw31OntpO3XtZjkccpcBf8FeVW3tpiyKlkeUzxS/txzHqpUCNIX157NaTySxedyZLvA==} 643 | peerDependencies: 644 | msw: ^2.4.9 645 | vite: ^5.0.0 || ^6.0.0 646 | peerDependenciesMeta: 647 | msw: 648 | optional: true 649 | vite: 650 | optional: true 651 | dependencies: 652 | '@vitest/spy': 3.0.2 653 | estree-walker: 3.0.3 654 | magic-string: 0.30.17 655 | vite: 5.0.5(@types/node@22.10.7) 656 | dev: true 657 | 658 | /@vitest/pretty-format@3.0.2: 659 | resolution: {integrity: sha512-yBohcBw/T/p0/JRgYD+IYcjCmuHzjC3WLAKsVE4/LwiubzZkE8N49/xIQ/KGQwDRA8PaviF8IRO8JMWMngdVVQ==} 660 | dependencies: 661 | tinyrainbow: 2.0.0 662 | dev: true 663 | 664 | /@vitest/runner@3.0.2: 665 | resolution: {integrity: sha512-GHEsWoncrGxWuW8s405fVoDfSLk6RF2LCXp6XhevbtDjdDme1WV/eNmUueDfpY1IX3MJaCRelVCEXsT9cArfEg==} 666 | dependencies: 667 | '@vitest/utils': 3.0.2 668 | pathe: 2.0.2 669 | dev: true 670 | 671 | /@vitest/snapshot@3.0.2: 672 | resolution: {integrity: sha512-h9s67yD4+g+JoYG0zPCo/cLTabpDqzqNdzMawmNPzDStTiwxwkyYM1v5lWE8gmGv3SVJ2DcxA2NpQJZJv9ym3g==} 673 | dependencies: 674 | '@vitest/pretty-format': 3.0.2 675 | magic-string: 0.30.17 676 | pathe: 2.0.2 677 | dev: true 678 | 679 | /@vitest/spy@3.0.2: 680 | resolution: {integrity: sha512-8mI2iUn+PJFMT44e3ISA1R+K6ALVs47W6eriDTfXe6lFqlflID05MB4+rIFhmDSLBj8iBsZkzBYlgSkinxLzSQ==} 681 | dependencies: 682 | tinyspy: 3.0.2 683 | dev: true 684 | 685 | /@vitest/utils@3.0.2: 686 | resolution: {integrity: sha512-Qu01ZYZlgHvDP02JnMBRpX43nRaZtNpIzw3C1clDXmn8eakgX6iQVGzTQ/NjkIr64WD8ioqOjkaYRVvHQI5qiw==} 687 | dependencies: 688 | '@vitest/pretty-format': 3.0.2 689 | loupe: 3.1.2 690 | tinyrainbow: 2.0.0 691 | dev: true 692 | 693 | /assertion-error@2.0.1: 694 | resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} 695 | engines: {node: '>=12'} 696 | dev: true 697 | 698 | /busboy@1.6.0: 699 | resolution: {integrity: sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==} 700 | engines: {node: '>=10.16.0'} 701 | dependencies: 702 | streamsearch: 1.1.0 703 | dev: true 704 | 705 | /cac@6.7.14: 706 | resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} 707 | engines: {node: '>=8'} 708 | dev: true 709 | 710 | /caniuse-lite@1.0.30001583: 711 | resolution: {integrity: sha512-acWTYaha8xfhA/Du/z4sNZjHUWjkiuoAi2LM+T/aL+kemKQgPT1xBb/YKjlQ0Qo8gvbHsGNplrEJ+9G3gL7i4Q==} 712 | dev: true 713 | 714 | /chai@5.1.2: 715 | resolution: {integrity: sha512-aGtmf24DW6MLHHG5gCx4zaI3uBq3KRtxeVs0DjFH6Z0rDNbsvTxFASFvdj79pxjxZ8/5u3PIiN3IwEIQkiiuPw==} 716 | engines: {node: '>=12'} 717 | dependencies: 718 | assertion-error: 2.0.1 719 | check-error: 2.1.1 720 | deep-eql: 5.0.2 721 | loupe: 3.1.2 722 | pathval: 2.0.0 723 | dev: true 724 | 725 | /check-error@2.1.1: 726 | resolution: {integrity: sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==} 727 | engines: {node: '>= 16'} 728 | dev: true 729 | 730 | /client-only@0.0.1: 731 | resolution: {integrity: sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==} 732 | dev: true 733 | 734 | /codice@1.3.1(react@19.0.0): 735 | resolution: {integrity: sha512-ckY384EemMDYkNuB+TBQPyo8TRjFSegxOoqTfThNQSb/mLOGwFZHupBLbOED+MlmuC93UXWQDrI6+cwNHzCr5w==} 736 | peerDependencies: 737 | react: ^18.0.0 || ^19.0.0 738 | dependencies: 739 | react: 19.0.0 740 | sugar-high: 'link:' 741 | dev: true 742 | 743 | /color-convert@2.0.1: 744 | resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} 745 | engines: {node: '>=7.0.0'} 746 | requiresBuild: true 747 | dependencies: 748 | color-name: 1.1.4 749 | dev: true 750 | optional: true 751 | 752 | /color-name@1.1.4: 753 | resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} 754 | requiresBuild: true 755 | dev: true 756 | optional: true 757 | 758 | /color-string@1.9.1: 759 | resolution: {integrity: sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==} 760 | requiresBuild: true 761 | dependencies: 762 | color-name: 1.1.4 763 | simple-swizzle: 0.2.2 764 | dev: true 765 | optional: true 766 | 767 | /color@4.2.3: 768 | resolution: {integrity: sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==} 769 | engines: {node: '>=12.5.0'} 770 | requiresBuild: true 771 | dependencies: 772 | color-convert: 2.0.1 773 | color-string: 1.9.1 774 | dev: true 775 | optional: true 776 | 777 | /csstype@3.1.3: 778 | resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==} 779 | dev: true 780 | 781 | /debug@4.4.0: 782 | resolution: {integrity: sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==} 783 | engines: {node: '>=6.0'} 784 | peerDependencies: 785 | supports-color: '*' 786 | peerDependenciesMeta: 787 | supports-color: 788 | optional: true 789 | dependencies: 790 | ms: 2.1.3 791 | dev: true 792 | 793 | /deep-eql@5.0.2: 794 | resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==} 795 | engines: {node: '>=6'} 796 | dev: true 797 | 798 | /detect-libc@2.0.3: 799 | resolution: {integrity: sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==} 800 | engines: {node: '>=8'} 801 | requiresBuild: true 802 | dev: true 803 | optional: true 804 | 805 | /dom-to-image@2.6.0: 806 | resolution: {integrity: sha512-Dt0QdaHmLpjURjU7Tnu3AgYSF2LuOmksSGsUcE6ItvJoCWTBEmiMXcqBdNSAm9+QbbwD7JMoVsuuKX6ZVQv1qA==} 807 | dev: true 808 | 809 | /es-module-lexer@1.6.0: 810 | resolution: {integrity: sha512-qqnD1yMU6tk/jnaMosogGySTZP8YtUgAffA9nMN+E/rjxcfRQ6IEk7IiozUjgxKoFHBGjTLnrHB/YC45r/59EQ==} 811 | dev: true 812 | 813 | /esbuild@0.19.8: 814 | resolution: {integrity: sha512-l7iffQpT2OrZfH2rXIp7/FkmaeZM0vxbxN9KfiCwGYuZqzMg/JdvX26R31Zxn/Pxvsrg3Y9N6XTcnknqDyyv4w==} 815 | engines: {node: '>=12'} 816 | hasBin: true 817 | requiresBuild: true 818 | optionalDependencies: 819 | '@esbuild/android-arm': 0.19.8 820 | '@esbuild/android-arm64': 0.19.8 821 | '@esbuild/android-x64': 0.19.8 822 | '@esbuild/darwin-arm64': 0.19.8 823 | '@esbuild/darwin-x64': 0.19.8 824 | '@esbuild/freebsd-arm64': 0.19.8 825 | '@esbuild/freebsd-x64': 0.19.8 826 | '@esbuild/linux-arm': 0.19.8 827 | '@esbuild/linux-arm64': 0.19.8 828 | '@esbuild/linux-ia32': 0.19.8 829 | '@esbuild/linux-loong64': 0.19.8 830 | '@esbuild/linux-mips64el': 0.19.8 831 | '@esbuild/linux-ppc64': 0.19.8 832 | '@esbuild/linux-riscv64': 0.19.8 833 | '@esbuild/linux-s390x': 0.19.8 834 | '@esbuild/linux-x64': 0.19.8 835 | '@esbuild/netbsd-x64': 0.19.8 836 | '@esbuild/openbsd-x64': 0.19.8 837 | '@esbuild/sunos-x64': 0.19.8 838 | '@esbuild/win32-arm64': 0.19.8 839 | '@esbuild/win32-ia32': 0.19.8 840 | '@esbuild/win32-x64': 0.19.8 841 | dev: true 842 | 843 | /estree-walker@3.0.3: 844 | resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} 845 | dependencies: 846 | '@types/estree': 1.0.5 847 | dev: true 848 | 849 | /expect-type@1.1.0: 850 | resolution: {integrity: sha512-bFi65yM+xZgk+u/KRIpekdSYkTB5W1pEf0Lt8Q8Msh7b+eQ7LXVtIB1Bkm4fvclDEL1b2CZkMhv2mOeF8tMdkA==} 851 | engines: {node: '>=12.0.0'} 852 | dev: true 853 | 854 | /fsevents@2.3.3: 855 | resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} 856 | engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} 857 | os: [darwin] 858 | requiresBuild: true 859 | dev: true 860 | optional: true 861 | 862 | /is-arrayish@0.3.2: 863 | resolution: {integrity: sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==} 864 | requiresBuild: true 865 | dev: true 866 | optional: true 867 | 868 | /loupe@3.1.2: 869 | resolution: {integrity: sha512-23I4pFZHmAemUnz8WZXbYRSKYj801VDaNv9ETuMh7IrMc7VuVVSo+Z9iLE3ni30+U48iDWfi30d3twAXBYmnCg==} 870 | dev: true 871 | 872 | /magic-string@0.30.17: 873 | resolution: {integrity: sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==} 874 | dependencies: 875 | '@jridgewell/sourcemap-codec': 1.5.0 876 | dev: true 877 | 878 | /ms@2.1.3: 879 | resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} 880 | dev: true 881 | 882 | /nanoid@3.3.7: 883 | resolution: {integrity: sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==} 884 | engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} 885 | hasBin: true 886 | dev: true 887 | 888 | /next@15.1.5(react-dom@19.0.0)(react@19.0.0): 889 | resolution: {integrity: sha512-Cf/TEegnt01hn3Hoywh6N8fvkhbOuChO4wFje24+a86wKOubgVaWkDqxGVgoWlz2Hp9luMJ9zw3epftujdnUOg==} 890 | engines: {node: ^18.18.0 || ^19.8.0 || >= 20.0.0} 891 | hasBin: true 892 | peerDependencies: 893 | '@opentelemetry/api': ^1.1.0 894 | '@playwright/test': ^1.41.2 895 | babel-plugin-react-compiler: '*' 896 | react: ^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0 897 | react-dom: ^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0 898 | sass: ^1.3.0 899 | peerDependenciesMeta: 900 | '@opentelemetry/api': 901 | optional: true 902 | '@playwright/test': 903 | optional: true 904 | babel-plugin-react-compiler: 905 | optional: true 906 | sass: 907 | optional: true 908 | dependencies: 909 | '@next/env': 15.1.5 910 | '@swc/counter': 0.1.3 911 | '@swc/helpers': 0.5.15 912 | busboy: 1.6.0 913 | caniuse-lite: 1.0.30001583 914 | postcss: 8.4.31 915 | react: 19.0.0 916 | react-dom: 19.0.0(react@19.0.0) 917 | styled-jsx: 5.1.6(react@19.0.0) 918 | optionalDependencies: 919 | '@next/swc-darwin-arm64': 15.1.5 920 | '@next/swc-darwin-x64': 15.1.5 921 | '@next/swc-linux-arm64-gnu': 15.1.5 922 | '@next/swc-linux-arm64-musl': 15.1.5 923 | '@next/swc-linux-x64-gnu': 15.1.5 924 | '@next/swc-linux-x64-musl': 15.1.5 925 | '@next/swc-win32-arm64-msvc': 15.1.5 926 | '@next/swc-win32-x64-msvc': 15.1.5 927 | sharp: 0.33.5 928 | transitivePeerDependencies: 929 | - '@babel/core' 930 | - babel-plugin-macros 931 | dev: true 932 | 933 | /pathe@2.0.2: 934 | resolution: {integrity: sha512-15Ztpk+nov8DR524R4BF7uEuzESgzUEAV4Ah7CUMNGXdE5ELuvxElxGXndBl32vMSsWa1jpNf22Z+Er3sKwq+w==} 935 | dev: true 936 | 937 | /pathval@2.0.0: 938 | resolution: {integrity: sha512-vE7JKRyES09KiunauX7nd2Q9/L7lhok4smP9RZTDeD4MVs72Dp2qNFVz39Nz5a0FVEW0BJR6C0DYrq6unoziZA==} 939 | engines: {node: '>= 14.16'} 940 | dev: true 941 | 942 | /picocolors@1.0.0: 943 | resolution: {integrity: sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==} 944 | dev: true 945 | 946 | /postcss@8.4.31: 947 | resolution: {integrity: sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==} 948 | engines: {node: ^10 || ^12 || >=14} 949 | dependencies: 950 | nanoid: 3.3.7 951 | picocolors: 1.0.0 952 | source-map-js: 1.0.2 953 | dev: true 954 | 955 | /postcss@8.4.32: 956 | resolution: {integrity: sha512-D/kj5JNu6oo2EIy+XL/26JEDTlIbB8hw85G8StOE6L74RQAVVP5rej6wxCNqyMbR4RkPfqvezVbPw81Ngd6Kcw==} 957 | engines: {node: ^10 || ^12 || >=14} 958 | dependencies: 959 | nanoid: 3.3.7 960 | picocolors: 1.0.0 961 | source-map-js: 1.0.2 962 | dev: true 963 | 964 | /react-dom@19.0.0(react@19.0.0): 965 | resolution: {integrity: sha512-4GV5sHFG0e/0AD4X+ySy6UJd3jVl1iNsNHdpad0qhABJ11twS3TTBnseqsKurKcsNqCEFeGL3uLpVChpIO3QfQ==} 966 | peerDependencies: 967 | react: ^19.0.0 968 | dependencies: 969 | react: 19.0.0 970 | scheduler: 0.25.0 971 | dev: true 972 | 973 | /react@19.0.0: 974 | resolution: {integrity: sha512-V8AVnmPIICiWpGfm6GLzCR/W5FXLchHop40W4nXBmdlEceh16rCN8O8LNWm5bh5XUX91fh7KpA+W0TgMKmgTpQ==} 975 | engines: {node: '>=0.10.0'} 976 | dev: true 977 | 978 | /rollup@4.6.1: 979 | resolution: {integrity: sha512-jZHaZotEHQaHLgKr8JnQiDT1rmatjgKlMekyksz+yk9jt/8z9quNjnKNRoaM0wd9DC2QKXjmWWuDYtM3jfF8pQ==} 980 | engines: {node: '>=18.0.0', npm: '>=8.0.0'} 981 | hasBin: true 982 | optionalDependencies: 983 | '@rollup/rollup-android-arm-eabi': 4.6.1 984 | '@rollup/rollup-android-arm64': 4.6.1 985 | '@rollup/rollup-darwin-arm64': 4.6.1 986 | '@rollup/rollup-darwin-x64': 4.6.1 987 | '@rollup/rollup-linux-arm-gnueabihf': 4.6.1 988 | '@rollup/rollup-linux-arm64-gnu': 4.6.1 989 | '@rollup/rollup-linux-arm64-musl': 4.6.1 990 | '@rollup/rollup-linux-x64-gnu': 4.6.1 991 | '@rollup/rollup-linux-x64-musl': 4.6.1 992 | '@rollup/rollup-win32-arm64-msvc': 4.6.1 993 | '@rollup/rollup-win32-ia32-msvc': 4.6.1 994 | '@rollup/rollup-win32-x64-msvc': 4.6.1 995 | fsevents: 2.3.3 996 | dev: true 997 | 998 | /scheduler@0.25.0: 999 | resolution: {integrity: sha512-xFVuu11jh+xcO7JOAGJNOXld8/TcEHK/4CituBUeUb5hqxJLj9YuemAEuvm9gQ/+pgXYfbQuqAkiYu+u7YEsNA==} 1000 | dev: true 1001 | 1002 | /semver@7.6.3: 1003 | resolution: {integrity: sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==} 1004 | engines: {node: '>=10'} 1005 | hasBin: true 1006 | requiresBuild: true 1007 | dev: true 1008 | optional: true 1009 | 1010 | /sharp@0.33.5: 1011 | resolution: {integrity: sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw==} 1012 | engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} 1013 | requiresBuild: true 1014 | dependencies: 1015 | color: 4.2.3 1016 | detect-libc: 2.0.3 1017 | semver: 7.6.3 1018 | optionalDependencies: 1019 | '@img/sharp-darwin-arm64': 0.33.5 1020 | '@img/sharp-darwin-x64': 0.33.5 1021 | '@img/sharp-libvips-darwin-arm64': 1.0.4 1022 | '@img/sharp-libvips-darwin-x64': 1.0.4 1023 | '@img/sharp-libvips-linux-arm': 1.0.5 1024 | '@img/sharp-libvips-linux-arm64': 1.0.4 1025 | '@img/sharp-libvips-linux-s390x': 1.0.4 1026 | '@img/sharp-libvips-linux-x64': 1.0.4 1027 | '@img/sharp-libvips-linuxmusl-arm64': 1.0.4 1028 | '@img/sharp-libvips-linuxmusl-x64': 1.0.4 1029 | '@img/sharp-linux-arm': 0.33.5 1030 | '@img/sharp-linux-arm64': 0.33.5 1031 | '@img/sharp-linux-s390x': 0.33.5 1032 | '@img/sharp-linux-x64': 0.33.5 1033 | '@img/sharp-linuxmusl-arm64': 0.33.5 1034 | '@img/sharp-linuxmusl-x64': 0.33.5 1035 | '@img/sharp-wasm32': 0.33.5 1036 | '@img/sharp-win32-ia32': 0.33.5 1037 | '@img/sharp-win32-x64': 0.33.5 1038 | dev: true 1039 | optional: true 1040 | 1041 | /siginfo@2.0.0: 1042 | resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} 1043 | dev: true 1044 | 1045 | /simple-swizzle@0.2.2: 1046 | resolution: {integrity: sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==} 1047 | requiresBuild: true 1048 | dependencies: 1049 | is-arrayish: 0.3.2 1050 | dev: true 1051 | optional: true 1052 | 1053 | /source-map-js@1.0.2: 1054 | resolution: {integrity: sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==} 1055 | engines: {node: '>=0.10.0'} 1056 | dev: true 1057 | 1058 | /stackback@0.0.2: 1059 | resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} 1060 | dev: true 1061 | 1062 | /std-env@3.8.0: 1063 | resolution: {integrity: sha512-Bc3YwwCB+OzldMxOXJIIvC6cPRWr/LxOp48CdQTOkPyk/t4JWWJbrilwBd7RJzKV8QW7tJkcgAmeuLLJugl5/w==} 1064 | dev: true 1065 | 1066 | /streamsearch@1.1.0: 1067 | resolution: {integrity: sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==} 1068 | engines: {node: '>=10.0.0'} 1069 | dev: true 1070 | 1071 | /styled-jsx@5.1.6(react@19.0.0): 1072 | resolution: {integrity: sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==} 1073 | engines: {node: '>= 12.0.0'} 1074 | peerDependencies: 1075 | '@babel/core': '*' 1076 | babel-plugin-macros: '*' 1077 | react: '>= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0' 1078 | peerDependenciesMeta: 1079 | '@babel/core': 1080 | optional: true 1081 | babel-plugin-macros: 1082 | optional: true 1083 | dependencies: 1084 | client-only: 0.0.1 1085 | react: 19.0.0 1086 | dev: true 1087 | 1088 | /tinybench@2.9.0: 1089 | resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} 1090 | dev: true 1091 | 1092 | /tinyexec@0.3.2: 1093 | resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==} 1094 | dev: true 1095 | 1096 | /tinypool@1.0.2: 1097 | resolution: {integrity: sha512-al6n+QEANGFOMf/dmUMsuS5/r9B06uwlyNjZZql/zv8J7ybHCgoihBNORZCY2mzUuAnomQa2JdhyHKzZxPCrFA==} 1098 | engines: {node: ^18.0.0 || >=20.0.0} 1099 | dev: true 1100 | 1101 | /tinyrainbow@2.0.0: 1102 | resolution: {integrity: sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==} 1103 | engines: {node: '>=14.0.0'} 1104 | dev: true 1105 | 1106 | /tinyspy@3.0.2: 1107 | resolution: {integrity: sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==} 1108 | engines: {node: '>=14.0.0'} 1109 | dev: true 1110 | 1111 | /tslib@2.8.1: 1112 | resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} 1113 | dev: true 1114 | 1115 | /typescript@5.7.3: 1116 | resolution: {integrity: sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==} 1117 | engines: {node: '>=14.17'} 1118 | hasBin: true 1119 | dev: true 1120 | 1121 | /undici-types@6.20.0: 1122 | resolution: {integrity: sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==} 1123 | dev: true 1124 | 1125 | /vite-node@3.0.2(@types/node@22.10.7): 1126 | resolution: {integrity: sha512-hsEQerBAHvVAbv40m3TFQe/lTEbOp7yDpyqMJqr2Tnd+W58+DEYOt+fluQgekOePcsNBmR77lpVAnIU2Xu4SvQ==} 1127 | engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} 1128 | hasBin: true 1129 | dependencies: 1130 | cac: 6.7.14 1131 | debug: 4.4.0 1132 | es-module-lexer: 1.6.0 1133 | pathe: 2.0.2 1134 | vite: 5.0.5(@types/node@22.10.7) 1135 | transitivePeerDependencies: 1136 | - '@types/node' 1137 | - less 1138 | - lightningcss 1139 | - sass 1140 | - stylus 1141 | - sugarss 1142 | - supports-color 1143 | - terser 1144 | dev: true 1145 | 1146 | /vite@5.0.5(@types/node@22.10.7): 1147 | resolution: {integrity: sha512-OekeWqR9Ls56f3zd4CaxzbbS11gqYkEiBtnWFFgYR2WV8oPJRRKq0mpskYy/XaoCL3L7VINDhqqOMNDiYdGvGg==} 1148 | engines: {node: ^18.0.0 || >=20.0.0} 1149 | hasBin: true 1150 | peerDependencies: 1151 | '@types/node': ^18.0.0 || >=20.0.0 1152 | less: '*' 1153 | lightningcss: ^1.21.0 1154 | sass: '*' 1155 | stylus: '*' 1156 | sugarss: '*' 1157 | terser: ^5.4.0 1158 | peerDependenciesMeta: 1159 | '@types/node': 1160 | optional: true 1161 | less: 1162 | optional: true 1163 | lightningcss: 1164 | optional: true 1165 | sass: 1166 | optional: true 1167 | stylus: 1168 | optional: true 1169 | sugarss: 1170 | optional: true 1171 | terser: 1172 | optional: true 1173 | dependencies: 1174 | '@types/node': 22.10.7 1175 | esbuild: 0.19.8 1176 | postcss: 8.4.32 1177 | rollup: 4.6.1 1178 | optionalDependencies: 1179 | fsevents: 2.3.3 1180 | dev: true 1181 | 1182 | /vitest@3.0.2(@types/node@22.10.7): 1183 | resolution: {integrity: sha512-5bzaHakQ0hmVVKLhfh/jXf6oETDBtgPo8tQCHYB+wftNgFJ+Hah67IsWc8ivx4vFL025Ow8UiuTf4W57z4izvQ==} 1184 | engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} 1185 | hasBin: true 1186 | peerDependencies: 1187 | '@edge-runtime/vm': '*' 1188 | '@types/node': ^18.0.0 || ^20.0.0 || >=22.0.0 1189 | '@vitest/browser': 3.0.2 1190 | '@vitest/ui': 3.0.2 1191 | happy-dom: '*' 1192 | jsdom: '*' 1193 | peerDependenciesMeta: 1194 | '@edge-runtime/vm': 1195 | optional: true 1196 | '@types/node': 1197 | optional: true 1198 | '@vitest/browser': 1199 | optional: true 1200 | '@vitest/ui': 1201 | optional: true 1202 | happy-dom: 1203 | optional: true 1204 | jsdom: 1205 | optional: true 1206 | dependencies: 1207 | '@types/node': 22.10.7 1208 | '@vitest/expect': 3.0.2 1209 | '@vitest/mocker': 3.0.2(vite@5.0.5) 1210 | '@vitest/pretty-format': 3.0.2 1211 | '@vitest/runner': 3.0.2 1212 | '@vitest/snapshot': 3.0.2 1213 | '@vitest/spy': 3.0.2 1214 | '@vitest/utils': 3.0.2 1215 | chai: 5.1.2 1216 | debug: 4.4.0 1217 | expect-type: 1.1.0 1218 | magic-string: 0.30.17 1219 | pathe: 2.0.2 1220 | std-env: 3.8.0 1221 | tinybench: 2.9.0 1222 | tinyexec: 0.3.2 1223 | tinypool: 1.0.2 1224 | tinyrainbow: 2.0.0 1225 | vite: 5.0.5(@types/node@22.10.7) 1226 | vite-node: 3.0.2(@types/node@22.10.7) 1227 | why-is-node-running: 2.3.0 1228 | transitivePeerDependencies: 1229 | - less 1230 | - lightningcss 1231 | - msw 1232 | - sass 1233 | - stylus 1234 | - sugarss 1235 | - supports-color 1236 | - terser 1237 | dev: true 1238 | 1239 | /why-is-node-running@2.3.0: 1240 | resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} 1241 | engines: {node: '>=8'} 1242 | hasBin: true 1243 | dependencies: 1244 | siginfo: 2.0.0 1245 | stackback: 0.0.2 1246 | dev: true 1247 | --------------------------------------------------------------------------------