├── 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 | {
8 | navigator.clipboard.writeText(codeSnippet)
9 | }}
10 | className="copy-button"
11 | >
12 |
13 |
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 | 
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 |
49 | async function hello ( ) {
50 | console . log ( ' Hello, world from JavaScript! ' )
51 | return 123
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 |
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 |
227 | ))}
228 |
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 |
251 |
257 |
258 |
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 | {
313 | copy()
314 | }}
315 | title={
316 | isPending ? 'Copying...' :
317 | copyState === 1 ? 'Copied!' :
318 | copyState === 2 ? 'Copy failed' : 'Copy image'
319 | }
320 | >
321 |
335 |
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 |
228 | {buttonText}
229 |
230 |
231 | )}
232 |
237 | {currentTheme.name}
238 |
239 |
240 |
241 |
{
249 | setLiveCode(newCode)
250 | debouncedTokenize(newCode)
251 | if (!isTyping) setDefaultLiveCode(newCode)
252 | }}
253 | />
254 |
255 |
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 === ''))
270 | return T_ENTITY
271 | }
272 | }
273 | // Then determine if they're jsx literals
274 | const isJsxLiterals = inJsxLiterals()
275 | if (isJsxLiterals) return T_JSX_LITERALS
276 |
277 | // Determine strings first before other types
278 | if (inStringQuotes() || inStrTemplateLiterals()) {
279 | return T_STRING
280 | } else if (keywords.has(token)) {
281 | return last[1] === '.' ? T_IDENTIFIER : T_KEYWORD
282 | } else if (isLineBreak) {
283 | return T_BREAK
284 | } else if (isSpaces(token)) {
285 | return T_SPACE
286 | } else if (token.split('').every(isSign)) {
287 | return T_SIGN
288 | } else if (isCls(token)) {
289 | return inJsxTag() ? T_IDENTIFIER : T_CLS_NUMBER
290 | } else {
291 | if (isIdentifier(token)) {
292 | const isLastPropDot = last[1] === '.' && isIdentifier(beforeLast[1])
293 |
294 | if (!inStringContent() && !isLastPropDot) return T_IDENTIFIER
295 | if (isLastPropDot) return T_PROPERTY
296 | }
297 | return T_STRING
298 | }
299 | }
300 |
301 | /**
302 | *
303 | * @param {number} type_
304 | * @param {string} token_
305 | */
306 | const append = (type_, token_) => {
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 === '') {
426 | // if current token is not part of close tag sign, push it first
427 | if (current !== '<' && current !== '/') {
428 | append()
429 | }
430 |
431 | if (c_n === '/>') {
432 | __jsxTag = 0
433 | } else {
434 | // is ''
435 | __jsxStack--
436 | }
437 |
438 | if (!__jsxStack)
439 | __jsxEnter = false
440 |
441 | current = c_n
442 | i++
443 | append(T_SIGN)
444 | continue
445 | }
446 |
447 | // <: open tag sign
448 | if (curr === '<') {
449 | append()
450 | current = curr
451 | append(T_SIGN)
452 | continue
453 | }
454 |
455 | // jsx property
456 | // `-` in data-prop
457 | if (next === '-' && !inStringContent() && !inJsxLiterals()) {
458 | if (current) {
459 | append(T_PROPERTY, current + curr + next)
460 | i++
461 | continue
462 | }
463 | }
464 | // `=` in property=
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 === '')) {
486 | __jsxTag = next === '/' ? 2 : 1
487 |
488 | // current and next char can form a jsx open or close tag
489 | if (curr === '<' && (next === '/' || isAlpha(next))) {
490 | if (
491 | !inStringContent() &&
492 | !inJsxLiterals() &&
493 | !inRegexQuotes()
494 | ) {
495 | __jsxEnter = true
496 | }
497 | }
498 | }
499 |
500 | const isQuotationChar = isStringQuotation(curr)
501 | const isStringTemplateLiterals = inStrTemplateLiterals()
502 | const isRegexChar = !__jsxEnter && isRegexStart(c_n)
503 | const isJsxLiterals = inJsxLiterals()
504 |
505 | // string quotation
506 | if (isQuotationChar || isStringTemplateLiterals || isSingleQuotes(__strQuote)) {
507 | current += curr
508 | } else if (isRegexChar) {
509 | append()
510 | const [lastType, lastToken] = last
511 | // Special cases that are not considered as regex:
512 | // * (expr1) / expr2: `)` before `/` operator is still in expression
513 | // * / 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 === '') {
610 | current = p_c
611 | }
612 | append()
613 |
614 | if (p_c !== '') {
615 | current = curr
616 |
617 | }
618 | if ((c_n === '' || c_n === '/>')) {
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)}${tagName}>`
751 | })
752 | .join('')
753 | return `<${lineTag} class="${line.properties.className}">${tokens}${lineTag}>`
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 |
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 = ' {}}>click '
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 |
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 |
--------------------------------------------------------------------------------