├── .github └── workflows │ └── test.yml ├── .gitignore ├── README.md ├── docs ├── app │ ├── carousel.tsx │ ├── components │ │ ├── copy-button.css │ │ ├── copy-button.tsx │ │ ├── install-banner.css │ │ └── install-banner.tsx │ ├── editor │ │ └── page.tsx │ ├── icon.svg │ ├── layout.tsx │ ├── lib │ │ └── copy-image.ts │ ├── live-editor.tsx │ ├── page.tsx │ └── styles.css ├── next-env.d.ts └── tsconfig.json ├── lib ├── index.d.ts ├── index.js └── presets │ ├── index.d.ts │ ├── index.js │ └── lang │ ├── css.js │ ├── python.js │ └── rust.js ├── package.json ├── pnpm-lock.yaml └── test ├── ast.test.ts ├── testing-utils.ts └── tokenize.test.ts /.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 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | .next 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Sugar High 2 | 3 | [![Build][build-badge]][build] 4 | 5 | ### Introduction 6 | 7 | Super lightweight JSX syntax highlighter, around 1KB after minified and gzipped 8 | 9 | ![img](https://repository-images.githubusercontent.com/453236442/aa0db684-bad3-4cd3-a420-f4e53b8c6757) 10 | 11 | ### Usage 12 | 13 | ```sh 14 | npm install --save sugar-high 15 | ``` 16 | 17 | ```js 18 | import { highlight } from 'sugar-high' 19 | 20 | const codeHTML = highlight(code) 21 | 22 | document.querySelector('pre > code').innerHTML = codeHTML 23 | ``` 24 | 25 | ### Highlight with CSS 26 | 27 | Then make your own theme with customized colors by token type and put in global CSS. The corresponding class names start with `--sh-` prefix. 28 | 29 | ```css 30 | /** 31 | * Types that sugar-high have: 32 | * 33 | * identifier 34 | * keyword 35 | * string 36 | * Class, number and null 37 | * property 38 | * entity 39 | * jsx literals 40 | * sign 41 | * comment 42 | * break 43 | * space 44 | */ 45 | :root { 46 | --sh-class: #2d5e9d; 47 | --sh-identifier: #354150; 48 | --sh-sign: #8996a3; 49 | --sh-property: #0550ae; 50 | --sh-entity: #249a97; 51 | --sh-jsxliterals: #6266d1; 52 | --sh-string: #00a99a; 53 | --sh-keyword: #f47067; 54 | --sh-comment: #a19595; 55 | } 56 | ``` 57 | 58 | ### Features 59 | 60 | #### Line number 61 | 62 | Sugar high provide `.sh_line` class name for each line. To display line number, define the `.sh_line::before` element with CSS will enable line numbers automatically. 63 | 64 | ```css 65 | pre code { 66 | counter-reset: sh-line-number; 67 | } 68 | 69 | .sh__line::before { 70 | counter-increment: sh-line-number 1; 71 | content: counter(sh-line-number); 72 | margin-right: 24px; 73 | text-align: right; 74 | color: #a4a4a4; 75 | } 76 | ``` 77 | 78 | ### Line Highlight 79 | 80 | Use `.sh__line:nth-child()` to highlight specific line. 81 | 82 | ```css 83 | .sh__line:nth-child(5) { 84 | background: #f5f5f5; 85 | } 86 | ``` 87 | 88 | #### CSS Class Names 89 | 90 | You can use `.sh__token--` to customize the output node of each token. 91 | 92 | ```css 93 | .sh__token--keyword { 94 | background: #f47067; 95 | } 96 | ``` 97 | 98 | ### Use With Remark.js 99 | 100 | [Remark.js](https://remark.js.org/) is a powerful markdown processor, you can use the [sugar-high remark plugin](https://remark-sugar-high.vercel.app/) with remark.js to highlight code blocks in markdown. 101 | 102 | Check out the [documentation](https://remark-sugar-high.vercel.app/) 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 | -------------------------------------------------------------------------------- /docs/app/carousel.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import { startTransition, useActionState, useEffect, useState } from 'react' 4 | import domToImage from 'dom-to-image' 5 | import { Code } from 'codice' 6 | import { copyImageDataUrl } from './lib/copy-image' 7 | 8 | const EXAMPLE_PAIRS = [ 9 | [ 10 | 'install.js', 11 | `\ 12 | // npm i -S sugar-high 13 | 14 | import { highlight } from 'sugar-high' 15 | 16 | const html = highlight(code) 17 | 18 | document.querySelector('pre > code').innerHTML = html 19 | `, 20 | { 21 | highlightedLines: [5] 22 | }, 23 | ], 24 | 25 | [ 26 | `app.jsx`, 27 | `\ 28 | const element = ( 29 | <> 30 | 33 | }}> 34 | 35 | {/* jsx comment */} 36 |

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

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

Showcase

226 |

Code highlight examples built with sugar-high

227 |
228 |
229 | {examples.map((_, i) => ( 230 |
237 |
238 | {examples.map(([name, code, config], i) => { 239 | function handleCopyImage() { 240 | const domNode = document.querySelector(`#code-frame-${i}`) 241 | return domToImage.toPng(domNode).then(dataUrl => { 242 | return copyImageDataUrl(dataUrl).then( 243 | () => { 244 | return true 245 | }, () => { 246 | return false 247 | } 248 | ) 249 | }) 250 | } 251 | 252 | return ( 253 | 267 | )} 268 | )} 269 |
270 |
271 | ) 272 | } 273 | 274 | function CameraIcon({ ...props }: React.SVGProps) { 275 | return ( 276 | 277 | 278 | 279 | 280 | ) 281 | } 282 | 283 | function cx(...args: any[]) { 284 | return args.filter(Boolean).join(' ') 285 | } 286 | 287 | 288 | function CopyImageButton({ onCopy } : { onCopy: () => Promise }) { 289 | function handleActionState(state, action) { 290 | if (action === 'copy') { 291 | return onCopy().then( 292 | result => result ? 1 : 2, 293 | ) 294 | } else if (action === 'reset') { 295 | return 0 296 | } 297 | return state 298 | } 299 | // 0: idle, 1: success, 2: error 300 | const [copyState, dispatch, isPending] = useActionState(handleActionState, 0) 301 | function copy() { 302 | startTransition(() => { 303 | dispatch('copy') 304 | }) 305 | } 306 | const reset = () => dispatch('reset') 307 | 308 | useEffect(() => { 309 | if (copyState === 1) { 310 | const timer = setTimeout(() => { 311 | reset() 312 | }, 2000) 313 | return () => clearTimeout(timer) 314 | } 315 | }) 316 | 317 | return ( 318 | 344 | ) 345 | } -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /docs/app/components/copy-button.tsx: -------------------------------------------------------------------------------- 1 | import "./copy-button.css" 2 | 3 | export function CopyButton({ codeSnippet, ...props }) { 4 | return ( 5 | 14 | ) 15 | } 16 | -------------------------------------------------------------------------------- /docs/app/components/install-banner.css: -------------------------------------------------------------------------------- 1 | .install-banner { 2 | display: flex; 3 | flex-direction: column; 4 | justify-content: center; 5 | 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 | } -------------------------------------------------------------------------------- /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 | 7 | const cssCode = `\ 8 | /* styles.css */ 9 | :root { 10 | --sh-class: #2d5e9d; 11 | --sh-identifier: #354150; 12 | --sh-sign: #8996a3; 13 | --sh-property: #0550ae; 14 | --sh-entity: #249a97; 15 | --sh-jsxliterals: #6266d1; 16 | --sh-string: #00a99a; 17 | --sh-keyword: #f47067; 18 | --sh-comment: #a19595; 19 | } 20 | ` 21 | 22 | const usageCode = `\ 23 | import { highlight } from 'sugar-high' 24 | 25 | const html = highlight(code) 26 | ` 27 | 28 | export default function InstallBanner() { 29 | return ( 30 |
31 | 44 |

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

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

Usage with remark.js

67 |

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

71 |
72 |
73 |
74 | ) 75 | } 76 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /docs/app/icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /docs/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import './styles.css' 2 | 3 | const imgUrl = 'https://repository-images.githubusercontent.com/453236442/aa0db684-bad3-4cd3-a420-f4e53b8c6757' 4 | 5 | export default function Layout({ children }) { 6 | return ( 7 | 8 | 9 | {children} 10 | 11 | 12 | ) 13 | } 14 | 15 | export const metadata = { 16 | metadataBase: new URL('https://sugar-high.vercel.app'), 17 | title: 'Sugar High', 18 | authors: [{ name: '@huozhi' }], 19 | description: 'Super lightweight JSX syntax highlighter, around 1KB after minified and gzipped', 20 | twitter: { 21 | card: 'summary_large_image', 22 | images: imgUrl, 23 | title: 'Sugar High', 24 | description: 'Super lightweight JSX syntax highlighter, around 1KB after minified and gzipped', 25 | }, 26 | openGraph: { 27 | images: imgUrl, 28 | title: 'Sugar High', 29 | description: 'Super lightweight JSX syntax highlighter, around 1KB after minified and gzipped', 30 | }, 31 | } 32 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | const defaultColorPlateColors = { 9 | class: '#8d85ff', 10 | identifier: '#354150', 11 | sign: '#8996a3', 12 | entity: '#6eafad', 13 | property: '#4e8fdf', 14 | jsxliterals: '#bf7db6', 15 | string: '#00a99a', 16 | keyword: '#f47067', 17 | comment: '#a19595', 18 | break: '#ffffff', 19 | space: '#ffffff', 20 | } 21 | 22 | function debounce(func, timeout = 200) { 23 | let timer 24 | return (...args) => { 25 | clearTimeout(timer) 26 | timer = setTimeout(() => { 27 | func.apply(this, args) 28 | }, timeout) 29 | } 30 | } 31 | 32 | const customizableColors = Object.entries(SugarHigh.TokenTypes) 33 | .filter(([, tokenTypeName]) => tokenTypeName !== 'break' && tokenTypeName !== 'space') 34 | .sort((a, b) => Number(a) - Number(b)) 35 | 36 | const DEFAULT_LIVE_CODE = `\ 37 | export default function App() { 38 | return ( 39 | <> 40 |

41 | Hello 42 | world 43 |

44 |
45 | 46 | ) 47 | } 48 | 49 | ` 50 | 51 | function useTextTypingAnimation(targetText, delay, enableTypingAnimation, onReady) { 52 | if (!enableTypingAnimation) { 53 | return { 54 | text: targetText, 55 | isTyping: false, 56 | setText: () => {}, 57 | } 58 | } 59 | const [text, setText] = useState('') 60 | const [isTyping, setIsTyping] = useState(true) 61 | const animationDuration = delay / targetText.length 62 | let timeoutId = useRef(null) 63 | 64 | useEffect(() => { 65 | if (isTyping && targetText.length) { 66 | if (text.length < targetText.length) { 67 | const nextText = targetText.substring(0, text.length + 1) 68 | if (timeoutId.current) { 69 | clearTimeout(timeoutId.current) 70 | timeoutId.current = null 71 | } 72 | timeoutId.current = setTimeout(() => { 73 | setText(nextText) 74 | }, animationDuration) 75 | } else if (text.length === targetText.length) { 76 | setIsTyping(false) 77 | onReady() 78 | } 79 | } 80 | return () => { 81 | if (timeoutId.current) { 82 | clearTimeout(timeoutId.current) 83 | timeoutId.current = null 84 | } 85 | } 86 | }, [targetText, text, timeoutId.current]) 87 | 88 | return { text, isTyping, setText } 89 | } 90 | 91 | const DEFAULT_LIVE_CODE_KEY = '$saved-live-code' 92 | function useDefaultLiveCode(defaultCodeText) { 93 | const [defaultCode, setCode] = useState(defaultCodeText || '') 94 | 95 | useEffect(() => { 96 | if (defaultCode) return 97 | 98 | setCode(window.localStorage.getItem(DEFAULT_LIVE_CODE_KEY) || DEFAULT_LIVE_CODE) 99 | }, [defaultCode]) 100 | 101 | const setDefaultLiveCode = (code) => window.localStorage.setItem(DEFAULT_LIVE_CODE_KEY, code) 102 | 103 | return { 104 | defaultLiveCode: defaultCode, 105 | setDefaultLiveCode, 106 | } 107 | } 108 | 109 | export default function LiveEditor({ 110 | enableTypingAnimation = true, 111 | defaultCode = DEFAULT_LIVE_CODE, 112 | }) { 113 | const editorRef = useRef(null) 114 | const [colorPlateColors, setColorPlateColors] = useState(defaultColorPlateColors) 115 | const { defaultLiveCode, setDefaultLiveCode } = useDefaultLiveCode(defaultCode) 116 | const { 117 | text: liveCode, 118 | setText: setLiveCode, 119 | isTyping, 120 | } = useTextTypingAnimation(defaultLiveCode, 1000, enableTypingAnimation, () => { 121 | if (editorRef.current) { 122 | // focus needs to be delayed 123 | setTimeout(() => { 124 | editorRef.current.focus() 125 | }) 126 | } 127 | }) 128 | 129 | const [liveCodeTokens, setLiveCodeTokens] = useState([]) 130 | const debouncedTokenizeRef = useRef( 131 | debounce((c) => { 132 | const tokens = tokenize(c) 133 | setLiveCodeTokens(tokens) 134 | }) 135 | ) 136 | const debouncedTokenize = debouncedTokenizeRef.current 137 | 138 | const customizableColorsString = useMemo(() => { 139 | return customizableColors 140 | .map(([_tokenType, tokenTypeName]) => { 141 | return `--sh-${tokenTypeName}: ${colorPlateColors[tokenTypeName]};` 142 | }) 143 | .join('\n') 144 | }, [colorPlateColors]) 145 | 146 | return ( 147 |
148 | 162 | 163 |
164 | { 172 | setLiveCode(newCode) 173 | debouncedTokenize(newCode) 174 | if (!isTyping) setDefaultLiveCode(newCode) 175 | }} 176 | /> 177 | 178 |
    179 |

    180 | Color palette 181 |

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

91 | Read more{' '} 92 | 93 | this page! - {Date.now()} 94 | 95 |

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

' 403 | const tokens = tokenize(code) 404 | expect(getTokensAsString(tokens)).toMatchInlineSnapshot(` 405 | [ 406 | "< => sign", 407 | "h1 => entity", 408 | "data- => property", 409 | "title => property", 410 | "= => sign", 411 | "" => string", 412 | "true => string", 413 | "" => string", 414 | "/> => sign", 415 | ] 416 | `) 417 | 418 | const code2 = '' 419 | const tokens2 = tokenize(code2) 420 | expect(getTokensAsString(tokens2)).toMatchInlineSnapshot(` 421 | [ 422 | "< => sign", 423 | "svg => entity", 424 | "color => property", 425 | "= => sign", 426 | "" => string", 427 | "null => string", 428 | "" => string", 429 | "height => property", 430 | "= => sign", 431 | "" => string", 432 | "24 => string", 433 | "" => string", 434 | "/> => sign", 435 | ] 436 | `) 437 | }) 438 | 439 | it('should render single quote inside jsx literals as jsx literals', () => { 440 | const code = `

Let's get started!

` 441 | const tokens = tokenize(code) 442 | expect(getTokensAsString(tokens)).toMatchInlineSnapshot(` 443 | [ 444 | "< => sign", 445 | "p => entity", 446 | "> => sign", 447 | "Let's get started! => jsxliterals", 448 | " sign", 449 | "p => entity", 450 | "> => sign", 451 | ] 452 | `) 453 | }) 454 | 455 | it('should handle nested jsx literals correctly', async () => { 456 | const code = 457 | `<> 458 |
459 |

Text 1

460 |
461 |

Text 2

462 | ` 463 | const tokens = tokenize(code) 464 | expect(getTokensAsString(tokens)).toMatchInlineSnapshot(` 465 | [ 466 | "< => sign", 467 | "> => sign", 468 | "< => sign", 469 | "div => entity", 470 | "> => sign", 471 | " => jsxliterals", 472 | " => jsxliterals", 473 | "< => sign", 474 | "p => entity", 475 | "> => sign", 476 | "Text 1 => jsxliterals", 477 | " sign", 478 | "p => entity", 479 | "> => sign", 480 | " sign", 481 | "div => entity", 482 | "> => sign", 483 | "< => sign", 484 | "p => entity", 485 | "> => sign", 486 | "Text 2 => jsxliterals", 487 | " sign", 488 | "p => entity", 489 | "> => sign", 490 | " sign", 491 | "> => sign", 492 | ] 493 | `) 494 | }) 495 | 496 | it('should not affect the function param after closed jsx tag', () => { 497 | // issue: (str was treated as string 498 | const code = 499 | ` 500 | function p(str) {} 501 | ` 502 | const tokens = tokenize(code) 503 | expect(getTokensAsString(tokens)).toMatchInlineSnapshot(` 504 | [ 505 | "< => sign", 506 | "a => entity", 507 | "k => property", 508 | "= => sign", 509 | "{ => sign", 510 | "v => identifier", 511 | "} => sign", 512 | "/> => sign", 513 | "function => keyword", 514 | "p => identifier", 515 | "( => sign", 516 | "str => identifier", 517 | ") => sign", 518 | "{ => sign", 519 | "} => sign", 520 | ] 521 | `) 522 | }) 523 | 524 | it('should handle object spread correctly', () => { 525 | const code = `` 526 | const tokens = tokenize(code) 527 | expect(getTokensAsString(tokens)).toMatchInlineSnapshot(` 528 | [ 529 | "< => sign", 530 | "Component => entity", 531 | "{ => sign", 532 | ". => sign", 533 | ". => sign", 534 | ". => sign", 535 | "props => identifier", 536 | "} => sign", 537 | "/> => sign", 538 | ] 539 | `) 540 | }) 541 | 542 | it('should handle tailwind properties well', () => { 543 | const code = `
` 544 | const tokens = tokenize(code) 545 | expect(getTokensAsString(tokens)).toMatchInlineSnapshot(` 546 | [ 547 | "< => sign", 548 | "div => entity", 549 | "className => property", 550 | "= => sign", 551 | "" => string", 552 | "data-[layout=grid]:grid => string", 553 | "" => string", 554 | "/> => sign", 555 | ] 556 | `) 557 | }) 558 | }) 559 | 560 | describe('comments', () => { 561 | it('basic inline comments', () => { 562 | const code = `+ // This is a inline comment / <- a slash` 563 | const tokens = tokenize(code) 564 | expect(getTokensAsString(tokens)).toMatchInlineSnapshot(` 565 | [ 566 | "+ => sign", 567 | "// This is a inline comment / <- a slash => comment", 568 | ] 569 | `) 570 | }) 571 | 572 | it('multiple slashes started inline comments', () => { 573 | const code = `/// // reference comment` 574 | const tokens = tokenize(code) 575 | expect(getTokensAsString(tokens)).toMatchInlineSnapshot(` 576 | [ 577 | "/// // reference comment => comment", 578 | ] 579 | `) 580 | }) 581 | 582 | it('multi-line comments', () => { 583 | const code = `/* This is another comment */ alert('good') // <- alerts` 584 | const tokens = tokenize(code) 585 | expect(getTokensAsString(tokens)).toMatchInlineSnapshot(` 586 | [ 587 | "/* This is another comment */ => comment", 588 | "alert => identifier", 589 | "( => sign", 590 | "' => string", 591 | "good => string", 592 | "' => string", 593 | ") => sign", 594 | "// <- alerts => comment", 595 | ] 596 | `) 597 | }) 598 | 599 | it('multi-line comments with annotations', () => { 600 | const code = `/** 601 | * @param {string} names 602 | * @return {Promise} 603 | */` 604 | const tokens = tokenize(code) 605 | expect(getTokensAsString(tokens)).toMatchInlineSnapshot(` 606 | [ 607 | "/** 608 | * @param {string} names 609 | * @return {Promise} 610 | */ => comment", 611 | ] 612 | `) 613 | }) 614 | }) 615 | 616 | describe('regex', () => { 617 | it('basic regex', () => { 618 | const reg1 = '/^\\/[0-5]\\/$/' 619 | const reg2 = `/^\\w+[a-z0-9]/ig` 620 | 621 | expect(getTokensAsString(tokenize(reg1))).toMatchInlineSnapshot(` 622 | [ 623 | "/^\\/[0-5]\\/$/ => string", 624 | ] 625 | `) 626 | expect(getTokensAsString(tokenize(reg2))).toMatchInlineSnapshot(` 627 | [ 628 | "/^\\w+[a-z0-9]/ig => string", 629 | ] 630 | `) 631 | }) 632 | 633 | it('contain angle brackets', () => { 634 | const code = `/^\\w+\\/$/` 635 | const tokens = tokenize(code) 636 | expect(getTokensAsString(tokens)).toMatchInlineSnapshot(` 637 | [ 638 | "/^\\w+\\/$/ => string", 639 | ] 640 | `) 641 | }) 642 | 643 | it('regex plus operators', () => { 644 | const code = `/^\\/[0-5]\\/$/ + /^\\/\w+\\/$/gi` 645 | const tokens = tokenize(code) 646 | expect(getTokensAsString(tokens)).toMatchInlineSnapshot(` 647 | [ 648 | "/^\\/[0-5]\\/$/ => string", 649 | "+ => sign", 650 | "/^\\/w+\\/$/gi => string", 651 | ] 652 | `) 653 | }) 654 | 655 | it('regex with quotes inside', () => { 656 | const code = `replace(/'/, \`"\`)` 657 | const tokens = tokenize(code) 658 | expect(getTokensAsString(tokens)).toMatchInlineSnapshot(` 659 | [ 660 | "replace => identifier", 661 | "( => sign", 662 | "/'/ => string", 663 | ", => sign", 664 | "\` => string", 665 | "" => string", 666 | "\` => string", 667 | ") => string", 668 | ] 669 | `) 670 | }) 671 | 672 | it('multi line regex tests', () => { 673 | const code1 = 674 | `/reg/.test('str')\n` + 675 | `[]\n` + 676 | `/reg/.test('str')` 677 | 678 | // '[]' consider as a end of the expression 679 | const tokens1 = tokenize(code1) 680 | expect(getTokensAsString(tokens1)).toMatchInlineSnapshot(` 681 | [ 682 | "/reg/ => string", 683 | ". => sign", 684 | "test => identifier", 685 | "( => sign", 686 | "' => string", 687 | "str => string", 688 | "' => string", 689 | ") => sign", 690 | "[ => sign", 691 | "] => sign", 692 | "/reg/ => string", 693 | ". => sign", 694 | "test => identifier", 695 | "( => sign", 696 | "' => string", 697 | "str => string", 698 | "' => string", 699 | ") => sign", 700 | ] 701 | `) 702 | 703 | const code2 = 704 | `/reg/.test('str')()\n` + 705 | `/reg/.test('str')` 706 | 707 | // what before '()' still considers as an expression 708 | const tokens2 = tokenize(code2) 709 | expect(getTokensAsString(tokens2)).toMatchInlineSnapshot(` 710 | [ 711 | "/reg/ => string", 712 | ". => sign", 713 | "test => identifier", 714 | "( => sign", 715 | "' => string", 716 | "str => string", 717 | "' => string", 718 | ") => sign", 719 | "( => sign", 720 | ") => sign", 721 | "/ => sign", 722 | "reg => identifier", 723 | "/ => sign", 724 | ". => sign", 725 | "test => identifier", 726 | "( => sign", 727 | "' => string", 728 | "str => string", 729 | "' => string", 730 | ") => sign", 731 | ] 732 | `) 733 | }) 734 | }) 735 | 736 | describe('strings', () => { 737 | it('import paths', () => { 738 | const code = `import mod from "../../mod"` 739 | const tokens = tokenize(code) 740 | expect(getTokensAsString(tokens)).toMatchInlineSnapshot(` 741 | [ 742 | "import => keyword", 743 | "mod => identifier", 744 | "from => keyword", 745 | "" => string", 746 | "../../mod => string", 747 | "" => string", 748 | ] 749 | `) 750 | }) 751 | 752 | it('contains curly brackets', () => { 753 | const code = `const str = 'hello {world}'` 754 | const tokens = tokenize(code) 755 | expect(getTokensAsString(tokens)).toMatchInlineSnapshot(` 756 | [ 757 | "const => keyword", 758 | "str => identifier", 759 | "= => sign", 760 | "' => string", 761 | "hello {world} => string", 762 | "' => string", 763 | ] 764 | `) 765 | }) 766 | 767 | it('contains angle brackets', () => { 768 | const code = `const str = 'hello '` 769 | const tokens = tokenize(code) 770 | expect(getTokensAsString(tokens)).toMatchInlineSnapshot(` 771 | [ 772 | "const => keyword", 773 | "str => identifier", 774 | "= => sign", 775 | "' => string", 776 | "hello => string", 777 | "' => string", 778 | ] 779 | `) 780 | }) 781 | 782 | it('multi quotes string', () => { 783 | const str1 = `"aa'bb'cc"` 784 | expect(getTokensAsString(tokenize(str1))).toMatchInlineSnapshot(` 785 | [ 786 | "" => string", 787 | "aa => string", 788 | "' => string", 789 | "bb => string", 790 | "' => string", 791 | "cc => string", 792 | "" => string", 793 | ] 794 | `) 795 | 796 | const str2 = `'aa"bb"cc'` 797 | expect(getTokensAsString(tokenize(str2))).toMatchInlineSnapshot(` 798 | [ 799 | "' => string", 800 | "aa => string", 801 | "" => string", 802 | "bb => string", 803 | "" => string", 804 | "cc => string", 805 | "' => string", 806 | ] 807 | `) 808 | 809 | const str3 = `\`\nabc\`` 810 | expect(getTokensAsString(tokenize(str3))).toMatchInlineSnapshot(` 811 | [ 812 | "\` => string", 813 | "abc => string", 814 | "\` => string", 815 | ] 816 | `) 817 | }) 818 | 819 | it('string template', () => { 820 | const code1 = ` 821 | \`hi \$\{ a \} world\` 822 | \`hello \$\{world\}\` 823 | ` 824 | expect(getTokensAsString(tokenize(code1))).toMatchInlineSnapshot(` 825 | [ 826 | "\` => string", 827 | "hi => string", 828 | "\${ => sign", 829 | "a => identifier", 830 | "} => sign", 831 | "world => string", 832 | "\` => string", 833 | "\` => string", 834 | "hello => string", 835 | "\${ => sign", 836 | "world => identifier", 837 | "} => sign", 838 | "\` => string", 839 | ] 840 | `) 841 | 842 | const code2 = ` 843 | \`hi \$\{ b \} plus \$\{ c + \`text\` \}\` 844 | \`nested \$\{ c + \`\$\{ no \}\` }\` 845 | ` 846 | expect(getTokensAsString(tokenize(code2))).toMatchInlineSnapshot(` 847 | [ 848 | "\` => string", 849 | "hi => string", 850 | "\${ => sign", 851 | "b => identifier", 852 | "} => sign", 853 | "plus => string", 854 | "\${ => sign", 855 | "c => identifier", 856 | "+ => sign", 857 | "\` => string", 858 | "text => string", 859 | "\` => string", 860 | "} => sign", 861 | "\` => string", 862 | "\` => string", 863 | "nested => string", 864 | "\${ => sign", 865 | "c => identifier", 866 | "+ => sign", 867 | "\` => string", 868 | "\${ => sign", 869 | "no => identifier", 870 | "} => sign", 871 | "\` => string", 872 | "} => sign", 873 | "\` => string", 874 | ] 875 | `) 876 | 877 | const code3 = ` 878 | \` 879 | hehehehe 880 | \` 881 | 'we' 882 | "no" 883 | \`hello\` 884 | ` 885 | expect(getTokensAsString(tokenize(code3))).toMatchInlineSnapshot(` 886 | [ 887 | "\` => string", 888 | "hehehehe => string", 889 | "\` => string", 890 | "' => string", 891 | "we => string", 892 | "' => string", 893 | "" => string", 894 | "no => string", 895 | "" => string", 896 | "\` => string", 897 | "hello => string", 898 | "\` => string", 899 | ] 900 | `) 901 | }) 902 | 903 | it('unicode token', () => { 904 | const code = `let hello你好 = 'hello你好'` 905 | expect(getTokensAsString(tokenize(code))).toMatchInlineSnapshot(` 906 | [ 907 | "let => keyword", 908 | "hello你好 => identifier", 909 | "= => sign", 910 | "' => string", 911 | "hello你好 => string", 912 | "' => string", 913 | ] 914 | `) 915 | }) 916 | 917 | it('number in string', () => { 918 | const code = `'123'\n'true'` 919 | expect(getTokensAsString(tokenize(code))).toMatchInlineSnapshot(` 920 | [ 921 | "' => string", 922 | "123 => string", 923 | "' => string", 924 | "' => string", 925 | "true => string", 926 | "' => string", 927 | ] 928 | `) 929 | }) 930 | }) 931 | 932 | describe('class', () => { 933 | it('determine class name', () => { 934 | const code = `class Bar extends Array {}` 935 | expect(getTokensAsString(tokenize(code))).toMatchInlineSnapshot(` 936 | [ 937 | "class => keyword", 938 | "Bar => class", 939 | "extends => keyword", 940 | "Array => class", 941 | "{ => sign", 942 | "} => sign", 943 | ] 944 | `) 945 | }) 946 | }) 947 | -------------------------------------------------------------------------------- /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 | } -------------------------------------------------------------------------------- /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 | }) --------------------------------------------------------------------------------