sugar-high 1.2.1 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,8 +1,11 @@
1
1
  # Sugar High
2
2
 
3
- [![npm][npm-badge]][npm]
3
+ [![version][npm-version-badge]][npm]
4
+ [![downloads][npm-downloads-badge]][npm]
4
5
 
5
- Super lightweight syntax highlighter for JavaScript and JSX—about **1 kB** minified and gzipped. Works in the browser or any JS runtime that can set HTML strings.
6
+ Lightweight, zero-dependency syntax highlighting for JavaScript, popular programming languages,
7
+ and formats commonly generated by coding agents. It runs in browsers and JavaScript runtimes and
8
+ returns HTML without requiring a DOM.
6
9
 
7
10
  ![Sugar High preview](https://repository-images.githubusercontent.com/453236442/9aa2144a-3a4c-4a93-a87f-92ca6a37ded6)
8
11
 
@@ -12,39 +15,128 @@ Super lightweight syntax highlighter for JavaScript and JSX—about **1 kB** min
12
15
  npm install sugar-high
13
16
  ```
14
17
 
15
- ## Usage
18
+ ## Highlight code
16
19
 
17
20
  ```js
18
21
  import { highlight } from 'sugar-high'
19
22
 
20
- const codeHTML = highlight(code)
23
+ const html = highlight('const ready = true')
24
+ ```
25
+
26
+ JavaScript, including JSX, is the default. Pass a canonical name for another built-in language:
21
27
 
22
- document.querySelector('pre > code').innerHTML = codeHTML
28
+ ```js
29
+ highlight('print("hi")', { lang: 'python' })
30
+ highlight('{"ready": true}', { lang: 'json' })
31
+ highlight('+ added', { lang: 'diff' })
23
32
  ```
24
33
 
25
- ### Language presets
34
+ The `lang` option is typed and accepts canonical names only.
35
+
36
+ ## Advanced: normalize extensions and aliases
26
37
 
27
- The core highlighter targets JavaScript and JSX. For CSS, Rust, Python, and similar, import a preset from [`sugar-high/presets`](https://github.com/huozhi/sugar-high/tree/main/packages/sugar-high/lib/presets) and pass it into `highlight`:
38
+ You do not need `lang()` when the language is already known. Use it only when input comes from a
39
+ filename extension, Markdown fence, or another integration. It converts aliases to the canonical
40
+ name expected by `highlight`:
28
41
 
29
42
  ```js
30
- import { highlight } from 'sugar-high'
31
- import { rust } from 'sugar-high/presets'
43
+ import { lang } from 'sugar-high/lang'
32
44
 
33
- const html = highlight(source, { ...rust })
45
+ lang('py') // 'python'
46
+ lang('bash') // 'shell'
47
+ lang('jsonc') // 'json'
48
+ lang('.yml') // 'yaml'
34
49
  ```
35
50
 
36
- Available presets: `c`, `css`, `diff`, `go`, `java`, `python`, and `rust`.
51
+ If you already have a canonical name, pass it directly—`lang()` is not required.
37
52
 
38
- For more language presets and syntax color themes, see **[sugar-high.vercel.app](https://sugar-high.vercel.app/)**.
53
+ See [the API reference](../../docs/API.md#lang-mapping) for the complete mapping and normalization
54
+ behavior.
39
55
 
40
- ## Styling
56
+ ## Built-in languages
57
+
58
+ `javascript`, `typescript`, `css`, `python`, `c`, `go`, `java`, `rust`, `json`, `diff`, `shell`,
59
+ `cpp`, `csharp`, `sql`, `html`, `yaml`, `markdown`, `kotlin`, `swift`, `php`, `toml`, `powershell`,
60
+ `dockerfile`, `graphql`, and `hcl`.
61
+
62
+ Related dialects share one implementation: JavaScript includes JSX, TypeScript includes TSX, JSON
63
+ includes JSONC comments, Shell includes sh/Bash/Zsh, and HCL includes Terraform.
64
+
65
+ ## Composable core
66
+
67
+ Use `sugar-high/core` to separate configurable syntax parsing from HTML rendering. It does not
68
+ include the built-in language registry:
69
+
70
+ ```js
71
+ import { parse, render } from 'sugar-high/core'
72
+
73
+ const parsed = parse('select * from users', {
74
+ keywords: new Set(['select', 'from', 'where']),
75
+ })
76
+
77
+ const html = render(parsed, {
78
+ cx: { keyword: 'font-bold' },
79
+ })
80
+ ```
81
+
82
+ Use the default `sugar-high` export when you want built-in languages and one-step `highlight()`.
83
+
84
+ ## Customize tokens
85
+
86
+ Use `cx` for a class map. It works well with utility CSS, CSS Modules, and global styles while
87
+ preserving Sugar High's semantic token classes.
88
+
89
+ ```js
90
+ highlight(source, {
91
+ lang: 'typescript',
92
+ cx: {
93
+ keyword: 'font-bold',
94
+ comment: 'italic opacity-60',
95
+ },
96
+ })
97
+ ```
98
+
99
+ Use `mark(token)` for conditional classes, inline styles, or custom attributes. It mutates the
100
+ token and returns nothing:
101
+
102
+ ```js
103
+ highlight(source, {
104
+ mark(token) {
105
+ if (token.type === 'comment' && token.value.includes('TODO')) {
106
+ token.className += ' text-orange-500'
107
+ token.properties['data-todo'] = true
108
+ }
109
+ },
110
+ })
111
+ ```
112
+
113
+ `cx` runs before `mark`, so `mark` receives the composed class name.
41
114
 
42
- Each line is wrapped in `sh__line`. Set **CSS custom properties** `--sh-*` on an ancestor (for example `:root`) to pick colors—inspect the output or the example below for the variable names you need.
115
+ ## Highlight lines
43
116
 
44
- Example theme:
117
+ Use `markLine` to customize generated lines. Its index is zero-based:
118
+
119
+ ```js
120
+ highlight(source, {
121
+ markLine(line) {
122
+ if (line.index === 1) {
123
+ line.className += ' sh__line--highlighted'
124
+ }
125
+ },
126
+ })
127
+ ```
128
+
129
+ Style the class with CSS. The React package also provides the one-based
130
+ `highlightLines={[1, [4, 7]]}` prop, while the Remark plugin reads ranges from fence metadata such
131
+ as `{2,5-7}`.
132
+
133
+ ## Styling
134
+
135
+ Each line uses `.sh__line`. Token colors use CSS custom properties, so a theme can be embedded in
136
+ your own stylesheet or scoped to any ancestor:
45
137
 
46
138
  ```css
47
- :root {
139
+ .code {
48
140
  --sh-class: #2d5e9d;
49
141
  --sh-identifier: #354150;
50
142
  --sh-sign: #8996a3;
@@ -57,50 +149,52 @@ Example theme:
57
149
  }
58
150
  ```
59
151
 
60
- ### Line numbers
61
-
62
- Use a `::before` counter on `.sh__line` for gutter numbers:
152
+ Lines can be styled or numbered with ordinary CSS:
63
153
 
64
154
  ```css
65
155
  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;
156
+ counter-reset: line;
75
157
  }
76
- ```
77
158
 
78
- ### Highlighting a line
79
-
80
- Target a line with `.sh__line:nth-child(<n>)` (1-based):
81
-
82
- ```css
83
159
  .sh__line {
84
160
  display: block;
85
- padding: 0 12px;
86
- margin: 0 -12px;
87
- min-height: 1rem;
88
161
  }
89
162
 
90
- .sh__line:nth-child(5) {
91
- background: #fff8c5;
163
+ .sh__line::before {
164
+ counter-increment: line;
165
+ content: counter(line);
166
+ margin-right: 1.5rem;
167
+ color: #a4a4a4;
92
168
  }
93
169
 
170
+ .sh__line:nth-child(5),
94
171
  .sh__line--highlighted {
95
172
  background: #fff8c5;
96
173
  }
97
174
  ```
98
175
 
99
- Use `.sh__line--highlighted` when you add highlight classes yourself (for example with `lineClassName`).
176
+ ## React
177
+
178
+ [`@sugar-high/react`](https://sugar-high.vercel.app/react) provides a highlighted `<Code />` block
179
+ and textarea-overlay `<Editor />` as separate composable exports.
180
+
181
+ ```tsx
182
+ import { Code, Editor } from '@sugar-high/react'
183
+
184
+ <Editor lang="typescript" value={source} onChange={setSource} />
185
+ <Code lang="typescript" lineNumbers>{source}</Code>
186
+ ```
100
187
 
101
188
  ## Remark
102
189
 
103
- Use the [remark plugin](https://sugar-high.vercel.app/remark) to highlight fenced code blocks when processing Markdown with [remark](https://remark.js.org/). Details: [`packages/remark-sugar-high`](https://github.com/huozhi/sugar-high/tree/main/packages/remark-sugar-high).
190
+ [`@sugar-high/remark`](https://sugar-high.vercel.app/remark) highlights fenced code blocks while
191
+ processing Markdown. Fence aliases are normalized through the same `lang()` mapping.
192
+
193
+ ## API
194
+
195
+ See [`docs/API.md`](https://github.com/huozhi/sugar-high/blob/main/docs/API.md) for package exports, the full language mapping, highlighting
196
+ options, and lower-level functions. Upgrading from v1? Read the
197
+ [v2 migration guide](https://github.com/huozhi/sugar-high/blob/main/docs/MIGRATION.md).
104
198
 
105
199
  ## License
106
200
 
@@ -108,6 +202,7 @@ MIT
108
202
 
109
203
  <!-- Definitions -->
110
204
 
111
- [npm-badge]: https://img.shields.io/npm/v/sugar-high.svg
205
+ [npm-version-badge]: https://img.shields.io/npm/v/sugar-high?style=flat&colorA=000000&colorB=000000
206
+ [npm-downloads-badge]: https://img.shields.io/npm/dt/sugar-high.svg?style=flat&colorA=000000&colorB=000000
112
207
 
113
208
  [npm]: https://www.npmjs.com/package/sugar-high
package/lib/core.d.ts ADDED
@@ -0,0 +1,117 @@
1
+ export type TokenType =
2
+ | 'identifier'
3
+ | 'keyword'
4
+ | 'string'
5
+ | 'class'
6
+ | 'property'
7
+ | 'entity'
8
+ | 'jsxliterals'
9
+ | 'sign'
10
+ | 'comment'
11
+ | 'break'
12
+ | 'space'
13
+
14
+ export type ParsedToken = {
15
+ type: TokenType
16
+ value: string
17
+ }
18
+
19
+ export type MarkToken = {
20
+ type: TokenType
21
+ value: string
22
+ className: string
23
+ style: Record<string, string | number>
24
+ properties: Record<string, string | number | boolean>
25
+ }
26
+
27
+ export type ParsedLine = {
28
+ readonly index: number
29
+ readonly value: string
30
+ readonly tokens: readonly ParsedToken[]
31
+ readonly annotations: readonly string[]
32
+ }
33
+
34
+ export type AnnotateLine = {
35
+ readonly index: number
36
+ readonly value: string
37
+ readonly tokens: readonly ParsedToken[]
38
+ annotations: string[]
39
+ }
40
+
41
+ export type MarkLine = {
42
+ readonly index: number
43
+ readonly value: string
44
+ readonly tokens: readonly ParsedToken[]
45
+ readonly annotations: readonly string[]
46
+ className: string
47
+ style: Record<string, string | number>
48
+ properties: Record<string, string | number | boolean>
49
+ }
50
+
51
+ export type ParsedCode = {
52
+ readonly value: string
53
+ readonly lines: readonly ParsedLine[]
54
+ }
55
+
56
+ export type GeneratedProperties = {
57
+ className: string
58
+ style: Record<string, string | number>
59
+ [name: string]: unknown
60
+ }
61
+
62
+ export type GeneratedText = {
63
+ type: 'text'
64
+ value: string
65
+ }
66
+
67
+ export type GeneratedToken = {
68
+ type: 'element'
69
+ tokenType: TokenType
70
+ tagName: 'span'
71
+ children: GeneratedText[]
72
+ properties: GeneratedProperties
73
+ }
74
+
75
+ export type GeneratedLine = {
76
+ type: 'element'
77
+ tagName: 'span'
78
+ children: GeneratedToken[]
79
+ properties: GeneratedProperties
80
+ }
81
+
82
+ export type DisplayOptions = {
83
+ cx?: Partial<Record<TokenType, string>>
84
+ mark?: (token: MarkToken) => void
85
+ markLine?: (line: MarkLine) => void
86
+ }
87
+
88
+ export type ParseOptions = {
89
+ keywords?: Set<string>
90
+ typeKeywords?: Set<string>
91
+ onCommentStart?: (curr: string, next: string, index: number, code: string) => number | boolean
92
+ onCommentEnd?: (prev: string, curr: string, index: number, code: string) => number | boolean
93
+ onLiteral?: (curr: string, index: number, code: string) => number | null | undefined
94
+ onQuote?: (curr: string, index: number, code: string) => number | null | undefined
95
+ quotedKeys?: boolean
96
+ jsx?: boolean
97
+ regex?: boolean
98
+ templateStrings?: boolean
99
+ caseInsensitive?: boolean
100
+ typescript?: boolean
101
+ tokenize?: (code: string, options: ParseOptions) => Array<[number, string]>
102
+ /** Apply syntax-specific semantic annotations while parsing. */
103
+ annotateLine?: (line: AnnotateLine) => void
104
+ }
105
+
106
+ export function parse(code: string, options?: ParseOptions): ParsedCode
107
+ export function render(parsed: ParsedCode, options?: DisplayOptions): string
108
+
109
+ /** Low-level token API used by language presets and integrations. */
110
+ export function tokenize(code: string, options?: ParseOptions): Array<[number, string]>
111
+ /** Build renderable line nodes for HTML, React, and syntax-tree integrations. */
112
+ export function generate(parsed: ParsedCode, options?: DisplayOptions): GeneratedLine[]
113
+
114
+ export const SugarHigh: {
115
+ TokenTypes: { [key: number]: string }
116
+ TokenMap: Map<string, number>
117
+ }
package/lib/core.js ADDED
@@ -0,0 +1,202 @@
1
+ // @ts-check
2
+
3
+ import {
4
+ assemble, generate, SugarHigh, toHtml, T_BREAK, T_CLASS, T_COMMENT, T_IDENTIFIER,
5
+ T_KEYWORD, T_PROPERTY, T_SIGN, T_SPACE, T_STRING,
6
+ } from './shared.js'
7
+
8
+ const signs = new Set('+-*/%=!&|^~?:.,;()[]{}<>#@\\'.split(''))
9
+ const noComment = () => 0
10
+
11
+ /** @param {string} value */
12
+ const isWord = (value) => value === '_' || value === '$' || /[\p{L}\p{N}]/u.test(value)
13
+
14
+ /** @param {string} code @param {number} index */
15
+ function isQuotedKey(code, index) {
16
+ while (index < code.length && /\s/.test(code[index])) index++
17
+ return code[index] === ':'
18
+ }
19
+
20
+ /**
21
+ * General-purpose lexer for keyword, string, comment, and punctuation based languages.
22
+ * Complex language presets may supply `tokenize` to replace this stage.
23
+ * @param {string} code
24
+ * @param {HighlightOptions | undefined} options
25
+ * @returns {Array<[number, string]>}
26
+ */
27
+ function tokenize(code, options) {
28
+ if (typeof options?.tokenize === 'function') return options.tokenize(code, options)
29
+
30
+ const keywords = options?.keywords || new Set()
31
+ const typeKeywords = options?.typeKeywords || new Set()
32
+ const onCommentStart = options?.onCommentStart || noComment
33
+ const onCommentEnd = options?.onCommentEnd || noComment
34
+ const normalize = options?.caseInsensitive
35
+ ? (value) => value.toLowerCase()
36
+ : (value) => value
37
+ /** @type {Array<[number, string]>} */
38
+ const tokens = []
39
+ let lastSignificant = ''
40
+
41
+ /** @param {number} type @param {string} value */
42
+ function append(type, value) {
43
+ if (!value) return
44
+ tokens.push([type, value])
45
+ if (type !== T_SPACE && type !== T_BREAK) lastSignificant = value
46
+ }
47
+
48
+ for (let i = 0; i < code.length;) {
49
+ const curr = code[i]
50
+ const next = code[i + 1]
51
+
52
+ const commentType = onCommentStart(curr, next, i, code)
53
+ if (commentType) {
54
+ const start = i++
55
+ while (i < code.length) {
56
+ if (onCommentEnd(code[i - 1], code[i], i, code) == commentType) {
57
+ i++
58
+ break
59
+ }
60
+ i++
61
+ }
62
+ append(T_COMMENT, code.slice(start, i))
63
+ continue
64
+ }
65
+
66
+ const literalLength = options?.onLiteral?.(curr, i, code)
67
+ if (literalLength) {
68
+ append(T_STRING, code.slice(i, i + literalLength))
69
+ i += literalLength
70
+ continue
71
+ }
72
+
73
+ if (typeof options?.onQuote === 'function' && curr === "'") {
74
+ const length = options.onQuote(curr, i, code)
75
+ if (typeof length === 'number' && length >= 1) {
76
+ append(T_IDENTIFIER, code.slice(i, i + length))
77
+ i += length
78
+ continue
79
+ }
80
+ }
81
+
82
+ if (curr === '"' || curr === "'" || (options?.templateStrings && curr === '`')) {
83
+ const quote = curr
84
+ const start = i++
85
+ while (i < code.length) {
86
+ if (code[i] === quote && code[i - 1] !== '\\') {
87
+ i++
88
+ break
89
+ }
90
+ i++
91
+ }
92
+ const value = code.slice(start, i)
93
+ append(options?.quotedKeys && isQuotedKey(code, i) ? T_PROPERTY : T_STRING, value)
94
+ continue
95
+ }
96
+
97
+ if (curr === '\n') {
98
+ append(T_BREAK, curr)
99
+ i++
100
+ continue
101
+ }
102
+
103
+ if (/[^\S\r\n]/.test(curr)) {
104
+ const start = i++
105
+ while (i < code.length && /[^\S\r\n]/.test(code[i])) i++
106
+ append(T_SPACE, code.slice(start, i))
107
+ continue
108
+ }
109
+
110
+ if (isWord(curr)) {
111
+ const start = i++
112
+ while (i < code.length && isWord(code[i])) i++
113
+ const value = code.slice(start, i)
114
+ const normalized = normalize(value)
115
+ const type = typeKeywords.has(normalized)
116
+ ? T_CLASS
117
+ : keywords.has(normalized)
118
+ ? T_KEYWORD
119
+ : lastSignificant === '.'
120
+ ? T_PROPERTY
121
+ : (/^\d/.test(value) || value === 'null' || /^\p{Lu}/u.test(value))
122
+ ? T_CLASS
123
+ : T_IDENTIFIER
124
+ append(type, value)
125
+ continue
126
+ }
127
+
128
+ if (signs.has(curr)) {
129
+ append(T_SIGN, curr)
130
+ i++
131
+ continue
132
+ }
133
+
134
+ append(T_STRING, curr)
135
+ i++
136
+ }
137
+
138
+ return tokens
139
+ }
140
+
141
+ /** @param {string} code @param {ParseOptions | undefined} options */
142
+ function parse(code, options) {
143
+ const parsed = assemble(code, tokenize(code, options))
144
+ if (options?.annotateLine) {
145
+ for (const line of parsed.lines) options.annotateLine(line)
146
+ }
147
+ return parsed
148
+ }
149
+
150
+ /** @param {ParsedCode} parsed @param {DisplayOptions | undefined} options */
151
+ function render(parsed, options) {
152
+ return toHtml(generate(parsed, options))
153
+ }
154
+
155
+ export { generate, parse, render, SugarHigh, tokenize }
156
+
157
+ /**
158
+ * @typedef {Object} ParseOptions
159
+ * @property {Set<string>} [keywords]
160
+ * @property {Set<string>} [typeKeywords]
161
+ * @property {(curr: string, next: string, index: number, code: string) => number | boolean} [onCommentStart]
162
+ * @property {(prev: string, curr: string, index: number, code: string) => number | boolean} [onCommentEnd]
163
+ * @property {(curr: string, index: number, code: string) => number | null | undefined} [onLiteral]
164
+ * @property {(curr: string, index: number, code: string) => number | null | undefined} [onQuote]
165
+ * @property {boolean} [quotedKeys]
166
+ * @property {boolean} [caseInsensitive]
167
+ * @property {boolean} [templateStrings]
168
+ * @property {(code: string, options: ParseOptions) => Array<[number, string]>} [tokenize]
169
+ * @property {(line: AnnotateLine) => void} [annotateLine]
170
+ */
171
+
172
+ /**
173
+ * @typedef {Object} DisplayOptions
174
+ * @property {Partial<Record<TokenType, string>>} [cx]
175
+ * @property {(token: MarkToken) => void} [mark]
176
+ * @property {(line: MarkLine) => void} [markLine]
177
+ */
178
+
179
+ /**
180
+ * @typedef {'identifier' | 'keyword' | 'string' | 'class' | 'property' | 'entity' | 'jsxliterals' | 'sign' | 'comment' | 'break' | 'space'} TokenType
181
+ * @typedef {{
182
+ * type: TokenType
183
+ * value: string
184
+ * className: string
185
+ * style: Record<string, string | number>
186
+ * properties: Record<string, string | number | boolean>
187
+ * }} MarkToken
188
+ * @typedef {{ type: TokenType, value: string }} ParsedToken
189
+ * @typedef {{
190
+ * index: number
191
+ * value: string
192
+ * tokens: ParsedToken[]
193
+ * annotations: string[]
194
+ * }} ParsedLine
195
+ * @typedef {ParsedLine} AnnotateLine
196
+ * @typedef {ParsedLine & {
197
+ * className: string
198
+ * style: Record<string, string | number>
199
+ * properties: Record<string, string | number | boolean>
200
+ * }} MarkLine
201
+ * @typedef {{ value: string, lines: ParsedLine[] }} ParsedCode
202
+ */
package/lib/index.d.ts CHANGED
@@ -1,25 +1,42 @@
1
- type HighlightOptions = {
2
- keywords?: Set<string>
3
- /**
4
- * Highlighted as the `class` token type (e.g. built-in types). Checked before `keywords`.
5
- */
6
- typeKeywords?: Set<string>
7
- onCommentStart?: (curr: string, next: string) => number | boolean
8
- onCommentEnd?: (curr: string, prev: string) => number | boolean
9
- /**
10
- * At `code[i] === "'"`: return how many code units to consume from `i` as one token,
11
- * or null/undefined or a number below 1 for default JS single-quoted string rules.
12
- */
13
- onQuote?: (curr: string, i: number, code: string) => number | null | undefined
14
- lineClassName?: (line: string, index: number) => string | null | undefined
1
+ import type {
2
+ DisplayOptions,
3
+ MarkLine,
4
+ MarkToken,
5
+ TokenType,
6
+ } from './core.js'
7
+
8
+ export type { DisplayOptions, MarkLine, MarkToken, TokenType }
9
+
10
+ export type LanguageName =
11
+ | 'javascript'
12
+ | 'typescript'
13
+ | 'css'
14
+ | 'python'
15
+ | 'c'
16
+ | 'go'
17
+ | 'java'
18
+ | 'rust'
19
+ | 'json'
20
+ | 'diff'
21
+ | 'shell'
22
+ | 'cpp'
23
+ | 'csharp'
24
+ | 'sql'
25
+ | 'html'
26
+ | 'yaml'
27
+ | 'markdown'
28
+ | 'kotlin'
29
+ | 'swift'
30
+ | 'php'
31
+ | 'toml'
32
+ | 'powershell'
33
+ | 'dockerfile'
34
+ | 'graphql'
35
+ | 'hcl'
36
+
37
+ export type HighlightOptions = DisplayOptions & {
38
+ /** Canonical language name. Fence and extension aliases must be normalized first. */
39
+ lang?: LanguageName
15
40
  }
16
41
 
17
42
  export function highlight(code: string, options?: HighlightOptions): string
18
- export function tokenize(code: string, options?: HighlightOptions): Array<[number, string]>
19
- export function generate(tokens: Array<[number, string]>, options?: Pick<HighlightOptions, 'lineClassName'>): Array<any>
20
- export const SugarHigh: {
21
- TokenTypes: {
22
- [key: number]: string
23
- }
24
- TokenMap: Map<string, number>
25
- }