sugar-high 1.3.0 → 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
@@ -3,7 +3,9 @@
3
3
  [![version][npm-version-badge]][npm]
4
4
  [![downloads][npm-downloads-badge]][npm]
5
5
 
6
- 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.
7
9
 
8
10
  ![Sugar High preview](https://repository-images.githubusercontent.com/453236442/9aa2144a-3a4c-4a93-a87f-92ca6a37ded6)
9
11
 
@@ -13,47 +15,128 @@ Super lightweight syntax highlighter for JavaScript and JSX—about **1 kB** min
13
15
  npm install sugar-high
14
16
  ```
15
17
 
16
- ## Usage
18
+ ## Highlight code
17
19
 
18
20
  ```js
19
21
  import { highlight } from 'sugar-high'
20
22
 
21
- 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:
22
27
 
23
- 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' })
24
32
  ```
25
33
 
26
- ### Language presets
34
+ The `lang` option is typed and accepts canonical names only.
27
35
 
28
- 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`:
36
+ ## Advanced: normalize extensions and aliases
37
+
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`:
29
41
 
30
42
  ```js
31
- import { highlight } from 'sugar-high'
32
- import { rust } from 'sugar-high/presets'
43
+ import { lang } from 'sugar-high/lang'
33
44
 
34
- const html = highlight(source, { ...rust })
45
+ lang('py') // 'python'
46
+ lang('bash') // 'shell'
47
+ lang('jsonc') // 'json'
48
+ lang('.yml') // 'yaml'
35
49
  ```
36
50
 
37
- Available presets: `c`, `css`, `diff`, `go`, `java`, `json`, `python`, and `rust`.
51
+ If you already have a canonical name, pass it directly—`lang()` is not required.
52
+
53
+ See [the API reference](../../docs/API.md#lang-mapping) for the complete mapping and normalization
54
+ behavior.
38
55
 
39
- The `json` preset highlights quoted object keys as `property` tokens, keeping them distinct from string values:
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:
40
69
 
41
70
  ```js
42
- import { json } from 'sugar-high/presets'
71
+ import { parse, render } from 'sugar-high/core'
72
+
73
+ const parsed = parse('select * from users', {
74
+ keywords: new Set(['select', 'from', 'where']),
75
+ })
43
76
 
44
- const html = highlight('{"name": "Alice"}', json)
77
+ const html = render(parsed, {
78
+ cx: { keyword: 'font-bold' },
79
+ })
45
80
  ```
46
81
 
47
- For more language presets and syntax color themes, see **[sugar-high.vercel.app](https://sugar-high.vercel.app/)**.
82
+ Use the default `sugar-high` export when you want built-in languages and one-step `highlight()`.
48
83
 
49
- ## Styling
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.
114
+
115
+ ## Highlight lines
116
+
117
+ Use `markLine` to customize generated lines. Its index is zero-based:
50
118
 
51
- 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.
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}`.
52
132
 
53
- Example theme:
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:
54
137
 
55
138
  ```css
56
- :root {
139
+ .code {
57
140
  --sh-class: #2d5e9d;
58
141
  --sh-identifier: #354150;
59
142
  --sh-sign: #8996a3;
@@ -66,50 +149,52 @@ Example theme:
66
149
  }
67
150
  ```
68
151
 
69
- ### Line numbers
70
-
71
- Use a `::before` counter on `.sh__line` for gutter numbers:
152
+ Lines can be styled or numbered with ordinary CSS:
72
153
 
73
154
  ```css
74
155
  pre code {
75
- counter-reset: sh-line-number;
156
+ counter-reset: line;
76
157
  }
77
158
 
78
- .sh__line::before {
79
- counter-increment: sh-line-number 1;
80
- content: counter(sh-line-number);
81
- margin-right: 24px;
82
- text-align: right;
83
- color: #a4a4a4;
84
- }
85
- ```
86
-
87
- ### Highlighting a line
88
-
89
- Target a line with `.sh__line:nth-child(<n>)` (1-based):
90
-
91
- ```css
92
159
  .sh__line {
93
160
  display: block;
94
- padding: 0 12px;
95
- margin: 0 -12px;
96
- min-height: 1rem;
97
161
  }
98
162
 
99
- .sh__line:nth-child(5) {
100
- background: #fff8c5;
163
+ .sh__line::before {
164
+ counter-increment: line;
165
+ content: counter(line);
166
+ margin-right: 1.5rem;
167
+ color: #a4a4a4;
101
168
  }
102
169
 
170
+ .sh__line:nth-child(5),
103
171
  .sh__line--highlighted {
104
172
  background: #fff8c5;
105
173
  }
106
174
  ```
107
175
 
108
- 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
+ ```
109
187
 
110
188
  ## Remark
111
189
 
112
- 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).
113
198
 
114
199
  ## License
115
200
 
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,27 +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
- /** Highlight quoted object keys followed by `:` as `property` tokens. */
15
- quotedKeys?: boolean
16
- 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
17
40
  }
18
41
 
19
42
  export function highlight(code: string, options?: HighlightOptions): string
20
- export function tokenize(code: string, options?: HighlightOptions): Array<[number, string]>
21
- export function generate(tokens: Array<[number, string]>, options?: Pick<HighlightOptions, 'lineClassName'>): Array<any>
22
- export const SugarHigh: {
23
- TokenTypes: {
24
- [key: number]: string
25
- }
26
- TokenMap: Map<string, number>
27
- }