sugar-high 0.9.5 → 1.1.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,19 +1,19 @@
1
1
  # Sugar High
2
2
 
3
- [![Build][build-badge]][build]
3
+ [![npm][npm-badge]][npm]
4
4
 
5
- ### Introduction
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
6
 
7
- Super lightweight JSX syntax highlighter, around 1KB after minified and gzipped
7
+ ![Sugar High preview](https://repository-images.githubusercontent.com/453236442/9aa2144a-3a4c-4a93-a87f-92ca6a37ded6)
8
8
 
9
- ![img](https://repository-images.githubusercontent.com/453236442/aa0db684-bad3-4cd3-a420-f4e53b8c6757)
10
-
11
- ### Usage
9
+ ## Install
12
10
 
13
11
  ```sh
14
- npm install --save sugar-high
12
+ npm install sugar-high
15
13
  ```
16
14
 
15
+ ## Usage
16
+
17
17
  ```js
18
18
  import { highlight } from 'sugar-high'
19
19
 
@@ -22,26 +22,26 @@ const codeHTML = highlight(code)
22
22
  document.querySelector('pre > code').innerHTML = codeHTML
23
23
  ```
24
24
 
25
- ### Highlight with CSS
25
+ ### Language presets
26
+
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`:
28
+
29
+ ```js
30
+ import { highlight } from 'sugar-high'
31
+ import { rust } from 'sugar-high/presets'
32
+
33
+ const html = highlight(source, { ...rust })
34
+ ```
35
+
36
+ For more language presets and syntax color themes, see **[sugar-high.vercel.app](https://sugar-high.vercel.app/)**.
26
37
 
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.
38
+ ## Styling
39
+
40
+ 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.
41
+
42
+ Example theme:
28
43
 
29
44
  ```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
45
  :root {
46
46
  --sh-class: #2d5e9d;
47
47
  --sh-identifier: #354150;
@@ -55,11 +55,9 @@ Then make your own theme with customized colors by token type and put in global
55
55
  }
56
56
  ```
57
57
 
58
- ### Features
59
-
60
- #### Line number
58
+ ### Line numbers
61
59
 
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.
60
+ Use a `::before` counter on `.sh__line` for gutter numbers:
63
61
 
64
62
  ```css
65
63
  pre code {
@@ -75,9 +73,9 @@ pre code {
75
73
  }
76
74
  ```
77
75
 
78
- ### Line Highlight
76
+ ### Highlighting a line
79
77
 
80
- Use `.sh__line:nth-child(<line number>)` to highlight specific line.
78
+ Target a line with `.sh__line:nth-child(<n>)` (1-based):
81
79
 
82
80
  ```css
83
81
  .sh__line:nth-child(5) {
@@ -85,36 +83,16 @@ Use `.sh__line:nth-child(<line number>)` to highlight specific line.
85
83
  }
86
84
  ```
87
85
 
88
- #### CSS Class Names
86
+ ## Remark
89
87
 
90
- You can use `.sh__token--<token type>` to customize the output node of each token.
88
+ 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).
91
89
 
92
- ```css
93
- .sh__token--keyword {
94
- background: #f47067;
95
- }
96
- ```
97
-
98
- ### Use With Remark.js
99
-
100
- [Remark.js](https://remark.js.org/) is a powerful markdown processor, you can use the [sugar-high remark plugin](https://sugar-high.vercel.app/remark) with remark.js to highlight code blocks in markdown.
101
-
102
- Check out the [readme](https://github.com/huozhi/sugar-high/tree/main/packages/remark-sugar-high) for more details.
103
-
104
- ### LICENSE
90
+ ## License
105
91
 
106
92
  MIT
107
93
 
108
94
  <!-- Definitions -->
109
95
 
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
96
+ [npm-badge]: https://img.shields.io/npm/v/sugar-high.svg
119
97
 
120
- [downloads]: https://www.npmjs.com/package/sugar-high
98
+ [npm]: https://www.npmjs.com/package/sugar-high
package/lib/index.d.ts CHANGED
@@ -1,7 +1,16 @@
1
1
  type HighlightOptions = {
2
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>
3
7
  onCommentStart?: (curr: string, next: string) => number | boolean
4
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
5
14
  }
6
15
 
7
16
  export function highlight(code: string, options?: HighlightOptions): string
package/lib/index.js CHANGED
@@ -45,6 +45,39 @@ const Keywords_Js = new Set([
45
45
  'static',
46
46
  ])
47
47
 
48
+ const Keywords_Ts = new Set([
49
+ ...Keywords_Js,
50
+ 'type',
51
+ 'interface',
52
+ 'enum',
53
+ 'implements',
54
+ 'readonly',
55
+ 'abstract',
56
+ 'declare',
57
+ 'namespace',
58
+ 'module',
59
+ 'private',
60
+ 'protected',
61
+ 'public',
62
+ 'override',
63
+ 'keyof',
64
+ 'infer',
65
+ 'is',
66
+ 'asserts',
67
+ 'satisfies',
68
+ 'as',
69
+ 'unknown',
70
+ 'never',
71
+ 'any',
72
+ // Built-in type names (annotations / type positions)
73
+ 'number',
74
+ 'string',
75
+ 'boolean',
76
+ 'bigint',
77
+ 'symbol',
78
+ 'object',
79
+ ])
80
+
48
81
  const Signs = new Set([
49
82
  '+',
50
83
  '-',
@@ -82,6 +115,76 @@ const DefaultOptions = {
82
115
  onCommentEnd: isCommentEnd_Js,
83
116
  }
84
117
 
118
+ /**
119
+ * Fast, heuristic TS detection. It intentionally prefers speed over full parsing.
120
+ * @param {string} code
121
+ * @returns {boolean}
122
+ */
123
+ function isLikelyTypeScript(code) {
124
+ let tsScore = 0
125
+
126
+ // TS-only declarations and operators.
127
+ if (/\binterface\s+[A-Za-z_$][\w$]*/.test(code)) tsScore += 2
128
+ if (/\btype\s+[A-Za-z_$][\w$]*\s*=/.test(code)) tsScore += 2
129
+ if (/\benum\s+[A-Za-z_$][\w$]*/.test(code)) tsScore += 2
130
+ if (/\b(?:implements|readonly|declare|namespace|satisfies|infer|keyof|asserts)\b/.test(code)) tsScore += 2
131
+
132
+ // Common TS annotations/signatures.
133
+ if (/:\s*[A-Za-z_$][\w$]*(?:<[^>\n]+>)?(?:\[\])?(?=\s*[,)=;{])/m.test(code)) tsScore += 1
134
+ if (/\b(?:const|let|var)\s+[A-Za-z_$][\w$]*\s*:\s*/.test(code)) tsScore += 1
135
+ if (/\)\s*:\s*[A-Za-z_$][\w$]*(?:<[^>\n]+>)?(?:\[\])?\s*(?:=>|\{)/.test(code)) tsScore += 1
136
+
137
+ return tsScore >= 2
138
+ }
139
+
140
+ /**
141
+ * Detects `<T, U = ...>(` style generic parameter lists so they are not
142
+ * treated as JSX tags.
143
+ * @param {string} code
144
+ * @param {number} startIndex
145
+ * @returns {boolean}
146
+ */
147
+ function isTypeParameterListStart(code, startIndex) {
148
+ if (code[startIndex] !== '<') return false
149
+
150
+ let depth = 0
151
+ let sawIdentifierStart = false
152
+
153
+ for (let i = startIndex; i < code.length; i++) {
154
+ const ch = code[i]
155
+
156
+ if (ch === '<') {
157
+ depth++
158
+ continue
159
+ }
160
+ if (ch === '>') {
161
+ depth--
162
+ if (depth === 0) {
163
+ let next = i + 1
164
+ while (next < code.length && /\s/.test(code[next])) next++
165
+ if (!(sawIdentifierStart && code[next] === '(')) return false
166
+
167
+ // Focus this heuristic on generic arrow functions:
168
+ // const fn = <T>(arg) => ...
169
+ const tail = code.slice(next, next + 320)
170
+ return /\)\s*(?::[\s\S]{0,120}?)?=>/.test(tail)
171
+ }
172
+ continue
173
+ }
174
+ if (depth === 0) continue
175
+
176
+ if (/[$A-Za-z_]/.test(ch)) {
177
+ sawIdentifierStart = true
178
+ continue
179
+ }
180
+ if (/[\s,\.\=\?\:\|\&\[\]]/.test(ch)) continue
181
+
182
+ return false
183
+ }
184
+
185
+ return false
186
+ }
187
+
85
188
  /**
86
189
  *
87
190
  * 0 - identifier
@@ -200,16 +303,33 @@ function isRegexStart(str) {
200
303
 
201
304
  /**
202
305
  * @param {string} code
203
- * @param {{ keywords: Set<string> }} options
306
+ * @param {{
307
+ * keywords?: Set<string>
308
+ * typeKeywords?: Set<string>
309
+ * onCommentStart?: (curr: string, next: string) => number | boolean
310
+ * onCommentEnd?: (prev: string, curr: string) => number | boolean
311
+ * onQuote?: (curr: string, i: number, code: string) => number | null | undefined
312
+ * } | undefined} options
313
+ * Optional `onQuote(curr, i, code)` at `code[i] === "'"`: return length to consume from `i` (>= 1),
314
+ * or null/undefined/below 1 for default JS single-quoted strings. No substring allocation.
204
315
  * @return {Array<[number, string]>}
205
316
  */
206
317
  function tokenize(code, options) {
318
+ const mergedOptions = { ...DefaultOptions, ...options }
319
+ const hasCustomKeywords = !!(options && options.keywords instanceof Set)
320
+ const isTs = isLikelyTypeScript(code)
321
+ const resolvedKeywords = hasCustomKeywords
322
+ ? mergedOptions.keywords
323
+ : (isTs ? Keywords_Ts : Keywords_Js)
324
+
207
325
  const {
208
- keywords,
209
326
  onCommentStart,
210
327
  onCommentEnd,
211
- } = { ...DefaultOptions, ...options }
212
-
328
+ } = mergedOptions
329
+
330
+ const resolvedTypeKeywords =
331
+ mergedOptions.typeKeywords instanceof Set ? mergedOptions.typeKeywords : null
332
+
213
333
  let current = ''
214
334
  let type = -1
215
335
  /** @type {[number, string]} */
@@ -219,20 +339,22 @@ function tokenize(code, options) {
219
339
  /** @type {Array<[number, string]>} */
220
340
  const tokens = []
221
341
 
222
- /** @type boolean if entered jsx tag, inside <open tag> or </close tag> */
223
- let __jsxEnter = false
224
342
  /**
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
- **/
343
+ * TS generics (`Map<string>`) and JSX (`<div>`) share the same `<Name …>` lexical shape. We use
344
+ * one tag-lexer mode (__jsxTag + __jsxStack) for both when we enter it; isTsTypeArgStart and
345
+ * isTypeParameterListStart only decide when *not* to enter (e.g. `foo<T>`, `<T>(x)=>`).
346
+ * __jsxEnter gates that mode so a latched __jsxTag (from `<` in `"a<b"`) does not run the tag
347
+ * lexer until we are in real JSX/TSX surface (not inside strings).
348
+ * @type {boolean}
349
+ */
350
+ let __jsxEnter = false
351
+ /** @type {0 | 1 | 2} 0 = none; 1 = inside `<open`; 2 = inside `</close` */
231
352
  let __jsxTag = 0
232
353
  let __jsxExpr = false
233
354
 
234
- // only match paired (open + close) tags, not self-closing tags
355
+ /** Nested `<open>…</open>` depth (content between tags, including nested elements). */
235
356
  let __jsxStack = 0
357
+
236
358
  const __jsxChild = () => __jsxEnter && !__jsxExpr && !__jsxTag
237
359
  // < __content__ >
238
360
  const inJsxTag = () => __jsxTag && !__jsxChild()
@@ -266,7 +388,7 @@ function tokenize(code, options) {
266
388
  const [, lastToken] = last
267
389
  if (isIdentifier(token)) {
268
390
  // classify jsx open tag
269
- if ((lastToken === '<' || lastToken === '</'))
391
+ if ((lastToken === '<' || lastToken === '</'))
270
392
  return T_ENTITY
271
393
  }
272
394
  }
@@ -277,7 +399,9 @@ function tokenize(code, options) {
277
399
  // Determine strings first before other types
278
400
  if (inStringQuotes() || inStrTemplateLiterals()) {
279
401
  return T_STRING
280
- } else if (keywords.has(token)) {
402
+ } else if (resolvedTypeKeywords && resolvedTypeKeywords.has(token)) {
403
+ return last[1] === '.' ? T_IDENTIFIER : T_CLS_NUMBER
404
+ } else if (resolvedKeywords.has(token)) {
281
405
  return last[1] === '.' ? T_IDENTIFIER : T_KEYWORD
282
406
  } else if (isLineBreak) {
283
407
  return T_BREAK
@@ -299,16 +423,16 @@ function tokenize(code, options) {
299
423
  }
300
424
 
301
425
  /**
302
- *
303
- * @param {number} type_
304
- * @param {string} token_
426
+ *
427
+ * @param {number | undefined} [type_]
428
+ * @param {string | undefined} [token_]
305
429
  */
306
430
  const append = (type_, token_) => {
307
431
  if (token_) {
308
432
  current = token_
309
433
  }
310
434
  if (current) {
311
- type = type_ || classify(current)
435
+ type = typeof type_ === 'number' ? type_ : classify(current)
312
436
  /** @type [number, string] */
313
437
  const pair = [type, current]
314
438
  if (type !== T_SPACE && type !== T_BREAK) {
@@ -326,6 +450,30 @@ function tokenize(code, options) {
326
450
  const p_c = prev + curr // previous and current
327
451
  const c_n = curr + next // current and next
328
452
 
453
+ // onQuote(curr, i, code): length from i; end = i + len (capped).
454
+ if (
455
+ typeof mergedOptions.onQuote === 'function' &&
456
+ curr === "'" &&
457
+ !inStringQuotes() &&
458
+ !inJsxLiterals() &&
459
+ !inStrTemplateLiterals()
460
+ ) {
461
+ const rawLen = mergedOptions.onQuote(curr, i, code)
462
+ if (
463
+ typeof rawLen === 'number' &&
464
+ rawLen >= 1 &&
465
+ !Number.isNaN(rawLen)
466
+ ) {
467
+ const len = Math.min(rawLen, code.length - i)
468
+ const end = i + len
469
+ append()
470
+ current = code.slice(i, end)
471
+ append(T_IDENTIFIER)
472
+ i = end - 1
473
+ continue
474
+ }
475
+ }
476
+
329
477
  // Determine string quotation outside of jsx literals and template literals.
330
478
  // Inside jsx literals or template literals, string quotation is still part of it.
331
479
  if (isSingleQuotes(curr) && !inJsxLiterals() && !inStrTemplateLiterals()) {
@@ -469,7 +617,7 @@ function tokenize(code, options) {
469
617
  if (isSpaces(current)) {
470
618
  append()
471
619
  }
472
-
620
+
473
621
  // Now check if the accumulated token is a property
474
622
  const prop = current + curr
475
623
  if (isIdentifier(prop)) {
@@ -483,12 +631,45 @@ function tokenize(code, options) {
483
631
 
484
632
  // if it's not in a jsx tag declaration or a string, close child if next is jsx close tag
485
633
  if (!__jsxTag && (curr === '<' && isIdentifierChar(next) || c_n === '</')) {
486
- __jsxTag = next === '/' ? 2 : 1
634
+ let prevNonSpace = i - 1
635
+ while (prevNonSpace >= 0 && /\s/.test(code[prevNonSpace])) prevNonSpace--
636
+ const prevChar = prevNonSpace >= 0 ? code[prevNonSpace] : ''
637
+
638
+ const [lastType, lastTok] = last
639
+ // Without a space before `<`, the LHS is often still in `current` (not flushed), so `last` is stale.
640
+ let typeArgFromPending = false
641
+ let jsxFromPending = false
642
+ if (current && !isSpaces(current)) {
643
+ const w = current
644
+ // Unflushed LHS before `<`: numbers/null/Upper (isCls), and booleans, behave like `foo<` (type
645
+ // args / `<`). Any other keyword (`return`, `void`, …) is JSX-friendly — no keyword allowlist.
646
+ if (isCls(w) || w === 'true' || w === 'false') {
647
+ typeArgFromPending = true
648
+ } else if (resolvedKeywords.has(w) && isIdentifier(w)) {
649
+ jsxFromPending = true
650
+ } else if (isIdentifier(w)) {
651
+ typeArgFromPending = true
652
+ }
653
+ }
654
+
655
+ const isTsTypeArgStart =
656
+ curr === '<' &&
657
+ /[$\w\]\)]/.test(prevChar) &&
658
+ (typeArgFromPending ||
659
+ (!jsxFromPending &&
660
+ (lastType === T_IDENTIFIER ||
661
+ lastType === T_CLS_NUMBER ||
662
+ (lastType === T_SIGN && (lastTok === ')' || lastTok === ']')))))
663
+ const isTsGenericStart = curr === '<' && isTypeParameterListStart(code, i)
664
+ if (!isTsTypeArgStart && !isTsGenericStart) {
665
+ __jsxTag = next === '/' ? 2 : 1
666
+ }
487
667
 
488
- // current and next char can form a jsx open or close tag
489
668
  if (curr === '<' && (next === '/' || isAlpha(next))) {
490
669
  if (
491
- !inStringContent() &&
670
+ !isTsTypeArgStart &&
671
+ !isTsGenericStart &&
672
+ !inStringContent() &&
492
673
  !inJsxLiterals() &&
493
674
  !inRegexQuotes()
494
675
  ) {
@@ -532,7 +713,7 @@ function tokenize(code, options) {
532
713
  const isEol = () => isEof() || code[i] === '\n'
533
714
 
534
715
  let foundClose = false
535
-
716
+
536
717
  // traverse to find closing regex slash
537
718
  for (; !isEol(); i++) {
538
719
  if (code[i] === '/' && code[i - 1] !== '\\') {
@@ -640,7 +821,7 @@ function generate(tokens) {
640
821
  * @param {any} children
641
822
  * @return {{type: string, tagName: string, children: any[], properties: Record<string, string>}}
642
823
  */
643
- const createLine = (children) =>
824
+ const createLine = (children) =>
644
825
  ({
645
826
  type: 'element',
646
827
  tagName: 'span',
@@ -679,12 +860,12 @@ function generate(tokens) {
679
860
  /** @type {Array<[number, string]>} */
680
861
  const lineTokens = []
681
862
  let lastWasBreak = false
682
-
863
+
683
864
  for (let i = 0; i < tokens.length; i++) {
684
865
  const token = tokens[i]
685
866
  const [type, value] = token
686
867
  const isLastToken = i === tokens.length - 1
687
-
868
+
688
869
  if (type !== T_BREAK) {
689
870
  // Divide multi-line token into multi-line code
690
871
  if (value.includes('\n')) {
@@ -709,12 +890,12 @@ function generate(tokens) {
709
890
  flushLine(lineTokens)
710
891
  lineTokens.length = 0
711
892
  }
712
-
893
+
713
894
  // If this is the last token and it's a break, create an empty line
714
895
  if (isLastToken) {
715
896
  flushLine([])
716
897
  }
717
-
898
+
718
899
  lastWasBreak = true
719
900
  }
720
901
  }
@@ -727,7 +908,7 @@ function generate(tokens) {
727
908
  return lines
728
909
  }
729
910
 
730
- /** @param { className: string, style?: Record<string, string> } */
911
+ /** @param {{ className: string, style?: Record<string, string> }} props */
731
912
  const propsToString = (props) => {
732
913
  let str = `class="${props.className}"`
733
914
 
@@ -758,12 +939,14 @@ function toHtml(lines) {
758
939
  /**
759
940
  *
760
941
  * @param {string} code
761
- * @param {
762
- * {
763
- * keywords: Set<string>
942
+ * @param {{
943
+ * keywords?: Set<string>
944
+ * typeKeywords?: Set<string>
764
945
  * onCommentStart?: (curr: string, next: string) => number | boolean
765
946
  * onCommentEnd?: (curr: string, prev: string) => number | boolean
947
+ * onQuote?: (curr: string, i: number, code: string) => number | null | undefined
766
948
  * } | undefined} options
949
+ * `onQuote` same as `tokenize`.
767
950
  * @returns {string}
768
951
  */
769
952
  function highlight(code, options) {
@@ -1,9 +1,14 @@
1
1
  type LanguageConfig = {
2
2
  keywords: Set<string>
3
- onCommentStart(curr: string, next: string): 0 | 1 | 2
4
- onCommentEnd(prev: string, curr: string): 0 | 1 | 2
3
+ typeKeywords?: Set<string>
4
+ onCommentStart?(curr: string, next: string): 0 | 1 | 2
5
+ onCommentEnd?(prev: string, curr: string): 0 | 1 | 2
6
+ onQuote?(curr: string, i: number, code: string): number | null | undefined
5
7
  }
6
8
 
7
9
  export const css: LanguageConfig
8
10
  export const rust: LanguageConfig
9
11
  export const python: LanguageConfig
12
+ export const c: LanguageConfig
13
+ export const go: LanguageConfig
14
+ export const java: LanguageConfig
@@ -1,3 +1,6 @@
1
1
  export * as css from './lang/css.js'
2
2
  export * as rust from './lang/rust.js'
3
3
  export * as python from './lang/python.js'
4
+ export * as c from './lang/c.js'
5
+ export * as go from './lang/go.js'
6
+ export * as java from './lang/java.js'
@@ -0,0 +1,17 @@
1
+ // @ts-check
2
+ import { onCommentStart, onCommentEnd } from './clike-base.js'
3
+
4
+ export const typeKeywords = new Set([
5
+ 'void', 'char', 'short', 'int', 'long', 'float', 'double', 'signed', 'unsigned',
6
+ '_Bool', '_Complex', '_Imaginary',
7
+ ])
8
+
9
+ export const keywords = new Set([
10
+ 'auto', 'break', 'case', 'const', 'continue', 'default', 'do', 'else', 'enum',
11
+ 'extern', 'for', 'goto', 'if', 'inline', 'register', 'restrict', 'return',
12
+ 'sizeof', 'static', 'struct', 'switch', 'typedef', 'union', 'volatile', 'while',
13
+ '_Alignas', '_Alignof', '_Atomic', '_Generic', '_Noreturn', '_Static_assert',
14
+ '_Thread_local',
15
+ ])
16
+
17
+ export { onCommentStart, onCommentEnd }
@@ -0,0 +1,12 @@
1
+ // @ts-check
2
+ export const onCommentStart = (currentChar, nextChar) => {
3
+ const pair = currentChar + nextChar
4
+ if (pair === '//') return 1
5
+ if (pair === '/*') return 1
6
+ return 0
7
+ }
8
+
9
+ export const onCommentEnd = (prevChar, currChar) => {
10
+ if (currChar === '\n') return 1
11
+ return prevChar + currChar === '*/' ? 1 : 0
12
+ }
@@ -0,0 +1,16 @@
1
+ // @ts-check
2
+ import { onCommentStart, onCommentEnd } from './clike-base.js'
3
+
4
+ export const typeKeywords = new Set([
5
+ 'bool', 'byte', 'complex64', 'complex128', 'error', 'float32', 'float64', 'int',
6
+ 'int8', 'int16', 'int32', 'int64', 'rune', 'string', 'uint', 'uint8', 'uint16',
7
+ 'uint32', 'uint64', 'uintptr',
8
+ ])
9
+
10
+ export const keywords = new Set([
11
+ 'break', 'case', 'chan', 'const', 'continue', 'default', 'defer', 'else',
12
+ 'fallthrough', 'for', 'func', 'go', 'goto', 'if', 'import', 'interface', 'map',
13
+ 'package', 'range', 'return', 'select', 'struct', 'switch', 'type', 'var',
14
+ ])
15
+
16
+ export { onCommentStart, onCommentEnd }
@@ -0,0 +1,17 @@
1
+ // @ts-check
2
+ import { onCommentStart, onCommentEnd } from './clike-base.js'
3
+
4
+ export const typeKeywords = new Set([
5
+ 'boolean', 'byte', 'char', 'double', 'float', 'int', 'long', 'short', 'void',
6
+ ])
7
+
8
+ export const keywords = new Set([
9
+ 'abstract', 'assert', 'break', 'case', 'catch', 'class', 'const', 'continue',
10
+ 'default', 'do', 'else', 'enum', 'extends', 'final', 'finally', 'for', 'goto',
11
+ 'if', 'implements', 'import', 'instanceof', 'interface', 'native', 'new',
12
+ 'package', 'private', 'protected', 'public', 'return', 'static', 'strictfp',
13
+ 'super', 'switch', 'synchronized', 'this', 'throw', 'throws', 'transient',
14
+ 'try', 'volatile', 'while',
15
+ ])
16
+
17
+ export { onCommentStart, onCommentEnd }
@@ -6,3 +6,38 @@ export const keywords = new Set([
6
6
  'async', 'await', 'dyn', 'abstract', 'become', 'box', 'do', 'final', 'macro', 'override',
7
7
  'priv', 'typeof', 'unsized', 'virtual', 'yield', 'try',
8
8
  ])
9
+
10
+ /**
11
+ * @param {string} curr `code[i]` (apostrophe)
12
+ * @param {number} i
13
+ * @param {string} code
14
+ * @returns {number} code units to consume from `i`
15
+ */
16
+ export function onQuote(curr, i, code) {
17
+ if (curr !== "'" || i + 1 >= code.length) return 1
18
+ const n1 = code[i + 1]
19
+ if (n1 === '\\') {
20
+ let j = i + 2
21
+ while (j < code.length) {
22
+ if (code[j] === '\\') {
23
+ j += 2
24
+ continue
25
+ }
26
+ if (code[j] === "'") return j - i + 1
27
+ j++
28
+ }
29
+ return code.length - i
30
+ }
31
+ if (n1 === '_') {
32
+ return 2
33
+ }
34
+ if (/[a-zA-Z]/.test(n1)) {
35
+ let j = i + 2
36
+ while (j < code.length && /[a-zA-Z0-9_]/.test(code[j])) j++
37
+ if (j < code.length && code[j] === "'") {
38
+ return j - i + 1
39
+ }
40
+ return j - i
41
+ }
42
+ return 1
43
+ }
package/package.json CHANGED
@@ -1,6 +1,11 @@
1
1
  {
2
2
  "name": "sugar-high",
3
- "version": "0.9.5",
3
+ "version": "1.1.0",
4
+ "repository": {
5
+ "type": "git",
6
+ "url": "git+https://github.com/huozhi/sugar-high.git",
7
+ "directory": "packages/sugar-high"
8
+ },
4
9
  "type": "module",
5
10
  "types": "./lib/index.d.ts",
6
11
  "main": "./lib/index.js",
@@ -19,14 +24,13 @@
19
24
  "lib"
20
25
  ],
21
26
  "license": "MIT",
22
- "devDependencies": {
23
- "@types/node": "22.12.0",
24
- "typescript": "5.7.3",
25
- "vitest": "^3.0.2"
26
- },
27
- "packageManager": "pnpm@8.7.1",
28
27
  "scripts": {
29
28
  "test": "vitest",
30
29
  "build": "echo 'package requires no build'"
30
+ },
31
+ "devDependencies": {
32
+ "@types/node": "22.12.0",
33
+ "typescript": "6.0.2",
34
+ "vitest": "^3.0.2"
31
35
  }
32
- }
36
+ }