@blcklab/yuirinx 0.1.1 → 0.1.2

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.
Files changed (85) hide show
  1. package/README.md +47 -64
  2. package/dist/cjs/core/index.cjs +36 -7
  3. package/dist/cjs/index.cjs +37 -8
  4. package/dist/cjs/languages/c.cjs +1 -1
  5. package/dist/cjs/languages/cpp.cjs +1 -1
  6. package/dist/cjs/languages/csharp.cjs +1 -1
  7. package/dist/cjs/languages/css.cjs +2 -2
  8. package/dist/cjs/languages/dart.cjs +1 -1
  9. package/dist/cjs/languages/go.cjs +1 -1
  10. package/dist/cjs/languages/java.cjs +1 -1
  11. package/dist/cjs/languages/javascript.cjs +65 -6
  12. package/dist/cjs/languages/jsx.cjs +65 -6
  13. package/dist/cjs/languages/kotlin.cjs +1 -1
  14. package/dist/cjs/languages/markdown.cjs +30 -6
  15. package/dist/cjs/languages/php.cjs +1 -1
  16. package/dist/cjs/languages/python.cjs +1 -1
  17. package/dist/cjs/languages/ruby.cjs +1 -1
  18. package/dist/cjs/languages/rust.cjs +1 -1
  19. package/dist/cjs/languages/scss.cjs +2 -2
  20. package/dist/cjs/languages/sql.cjs +1 -1
  21. package/dist/cjs/languages/swift.cjs +1 -1
  22. package/dist/cjs/languages/tsx.cjs +65 -6
  23. package/dist/cjs/languages/typescript.cjs +65 -6
  24. package/dist/esm/core/create-highlighter.js +132 -0
  25. package/dist/esm/core/errors.js +10 -0
  26. package/dist/esm/core/escape.js +17 -0
  27. package/dist/esm/core/grammar.js +103 -0
  28. package/dist/esm/core/index.js +4 -536
  29. package/dist/esm/core/renderer.js +215 -0
  30. package/dist/esm/core/tokenizer.js +107 -0
  31. package/dist/esm/core/types.js +0 -0
  32. package/dist/esm/index.js +8 -558
  33. package/dist/esm/languages/bash.js +9 -21
  34. package/dist/esm/languages/c.js +2 -82
  35. package/dist/esm/languages/cpp.js +2 -82
  36. package/dist/esm/languages/csharp.js +2 -82
  37. package/dist/esm/languages/css.js +2 -64
  38. package/dist/esm/languages/dart.js +2 -82
  39. package/dist/esm/languages/dockerfile.js +3 -12
  40. package/dist/esm/languages/go.js +2 -82
  41. package/dist/esm/languages/graphql.js +8 -20
  42. package/dist/esm/languages/html.js +2 -44
  43. package/dist/esm/languages/ini.js +2 -5
  44. package/dist/esm/languages/java.js +2 -82
  45. package/dist/esm/languages/javascript.js +2 -229
  46. package/dist/esm/languages/json.js +2 -5
  47. package/dist/esm/languages/jsx.js +2 -229
  48. package/dist/esm/languages/kotlin.js +2 -82
  49. package/dist/esm/languages/markdown.js +32 -11
  50. package/dist/esm/languages/nginx.js +2 -5
  51. package/dist/esm/languages/php.js +3 -83
  52. package/dist/esm/languages/plaintext.js +1 -2
  53. package/dist/esm/languages/python.js +10 -25
  54. package/dist/esm/languages/ruby.js +9 -24
  55. package/dist/esm/languages/rust.js +2 -82
  56. package/dist/esm/languages/scss.js +2 -64
  57. package/dist/esm/languages/shared/c-family.js +55 -0
  58. package/dist/esm/languages/shared/css.js +48 -0
  59. package/dist/esm/languages/shared/ecmascript.js +261 -0
  60. package/dist/esm/languages/shared/helpers.js +52 -0
  61. package/dist/esm/languages/shared/markup.js +41 -0
  62. package/dist/esm/languages/shell.js +1 -115
  63. package/dist/esm/languages/sql.js +11 -32
  64. package/dist/esm/languages/svelte.js +2 -44
  65. package/dist/esm/languages/swift.js +2 -82
  66. package/dist/esm/languages/toml.js +2 -5
  67. package/dist/esm/languages/tsx.js +2 -229
  68. package/dist/esm/languages/typescript.js +2 -229
  69. package/dist/esm/languages/vue.js +2 -44
  70. package/dist/esm/languages/xml.js +2 -44
  71. package/dist/esm/languages/yaml.js +2 -5
  72. package/dist/esm/singleton.js +24 -0
  73. package/dist/esm/themes/index.js +3 -188
  74. package/dist/esm/themes/yuirinx-aurora.js +1 -2
  75. package/dist/esm/themes/yuirinx-noir.js +1 -2
  76. package/dist/esm/themes/yuirinx-pearl.js +1 -2
  77. package/dist/types/core/errors.d.cts +1 -1
  78. package/dist/types/core/errors.d.ts +1 -1
  79. package/dist/types/core/index.d.cts +1 -1
  80. package/dist/types/core/index.d.ts +1 -1
  81. package/dist/types/core/types.d.cts +5 -1
  82. package/dist/types/core/types.d.ts +5 -1
  83. package/dist/types/singleton.d.cts +1 -1
  84. package/dist/types/singleton.d.ts +1 -1
  85. package/package.json +4 -4
package/README.md CHANGED
@@ -5,8 +5,6 @@
5
5
  <img src="https://img.shields.io/github/license/blcklab/yuirinx?style=flat-square" alt="license" />
6
6
  </p>
7
7
 
8
-
9
-
10
8
  # Yuirinx
11
9
 
12
10
  A lightweight, synchronous syntax highlighter for modern JavaScript and TypeScript applications.
@@ -56,13 +54,7 @@ Add your preferred layout styles:
56
54
  overflow-x: auto;
57
55
  padding: 1.25rem;
58
56
  border-radius: 1rem;
59
- font-family:
60
- ui-monospace,
61
- SFMono-Regular,
62
- Menlo,
63
- Monaco,
64
- Consolas,
65
- monospace;
57
+ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
66
58
  font-size: 0.875rem;
67
59
  line-height: 1.7;
68
60
  tab-size: 2;
@@ -71,16 +63,16 @@ Add your preferred layout styles:
71
63
 
72
64
  ## Features
73
65
 
74
- * Zero runtime dependencies
75
- * Synchronous API with no setup step
76
- * Safe HTML escaping
77
- * Individually importable languages and themes
78
- * ESM and CommonJS support
79
- * Inline-style and CSS-class rendering
80
- * Token API for custom renderers
81
- * Custom language and theme registration
82
- * Works in browsers, Node.js, workers, serverless, and edge runtimes
83
- * Safe plaintext fallback for unsupported languages
66
+ - Zero runtime dependencies
67
+ - Synchronous API with no setup step
68
+ - Safe HTML escaping
69
+ - Individually importable languages and themes
70
+ - ESM and CommonJS support
71
+ - Inline-style and CSS-class rendering
72
+ - Token API for custom renderers
73
+ - Custom language and theme registration
74
+ - Works in browsers, Node.js, workers, serverless, and edge runtimes
75
+ - Safe plaintext fallback for unsupported languages
84
76
 
85
77
  ## Tree-Shakable by Design
86
78
 
@@ -107,19 +99,16 @@ import { yuirinxPearl } from "@blcklab/yuirinx/theme/yuirinx-pearl";
107
99
  import { yuirinxAurora } from "@blcklab/yuirinx/theme/yuirinx-aurora";
108
100
  ```
109
101
 
110
- * **Yuirinx Noir** — a refined dark theme for documentation and long code samples
111
- * **Yuirinx Pearl** — a warm light theme with comfortable contrast
112
- * **Yuirinx Aurora** — an expressive dark theme for showcases and landing pages
102
+ - **Yuirinx Noir** — a refined dark theme for documentation and long code samples
103
+ - **Yuirinx Pearl** — a warm light theme with comfortable contrast
104
+ - **Yuirinx Aurora** — an expressive dark theme for showcases and landing pages
113
105
 
114
106
  Register more than one theme when your app supports theme switching:
115
107
 
116
108
  ```ts
117
109
  const highlighter = createHighlighter({
118
110
  languages: [typescript],
119
- themes: [
120
- yuirinxNoir,
121
- yuirinxPearl,
122
- ],
111
+ themes: [yuirinxNoir, yuirinxPearl],
123
112
  });
124
113
  ```
125
114
 
@@ -151,10 +140,7 @@ The colors are deterministic, theme-aware, and stable across server and browser
151
140
  Inline styles work without extra setup. If you want full control through your own stylesheet, use class mode:
152
141
 
153
142
  ```ts
154
- import {
155
- createHighlighter,
156
- themeToCss,
157
- } from "@blcklab/yuirinx";
143
+ import { createHighlighter, themeToCss } from "@blcklab/yuirinx";
158
144
 
159
145
  const html = highlighter.highlight(code, {
160
146
  lang: "typescript",
@@ -167,20 +153,17 @@ const css = themeToCss(yuirinxNoir);
167
153
 
168
154
  Class mode is useful for:
169
155
 
170
- * Content Security Policies that restrict inline styles
171
- * Automatic light and dark mode
172
- * Shared application stylesheets
173
- * Custom token styling
156
+ - Content Security Policies that restrict inline styles
157
+ - Automatic light and dark mode
158
+ - Shared application stylesheets
159
+ - Custom token styling
174
160
 
175
161
  ## Token API
176
162
 
177
163
  Use the lower-level token API when rendering code through Vue, React, a terminal, or another custom interface:
178
164
 
179
165
  ```ts
180
- const tokens = highlighter.tokenize(
181
- `const enabled = true;`,
182
- "typescript",
183
- );
166
+ const tokens = highlighter.tokenize(`const enabled = true;`, "typescript");
184
167
 
185
168
  for (const token of tokens) {
186
169
  console.log(token.type, token.value);
@@ -193,16 +176,16 @@ Each token includes its type, source value, and original character offsets.
193
176
 
194
177
  Yuirinx includes independently importable grammars for popular web, systems, application, and configuration languages, including:
195
178
 
196
- * JavaScript, TypeScript, JSX, and TSX
197
- * HTML, XML, CSS, and SCSS
198
- * JSON, YAML, TOML, and INI
199
- * Markdown
200
- * Bash and shell scripts
201
- * Python, PHP, Ruby, and SQL
202
- * C, C++, C#, Java, Kotlin, Swift, Dart, Go, and Rust
203
- * Vue and Svelte
204
- * Dockerfile, Nginx, and GraphQL
205
- * Plaintext
179
+ - JavaScript, TypeScript, JSX, and TSX
180
+ - HTML, XML, CSS, and SCSS
181
+ - JSON, YAML, TOML, and INI
182
+ - Markdown
183
+ - Bash and shell scripts
184
+ - Python, PHP, Ruby, and SQL
185
+ - C, C++, C#, Java, Kotlin, Swift, Dart, Go, and Rust
186
+ - Vue and Svelte
187
+ - Dockerfile, Nginx, and GraphQL
188
+ - Plaintext
206
189
 
207
190
  Each grammar also registers common aliases such as `js`, `ts`, `py`, `md`, `yml`, `sh`, and `c++`.
208
191
 
@@ -212,12 +195,12 @@ Yuirinx treats source code as untrusted text.
212
195
 
213
196
  It:
214
197
 
215
- * Escapes HTML-sensitive characters
216
- * Never evaluates or executes source code
217
- * Does not use `eval` or `new Function`
218
- * Does not require DOM access
219
- * Preserves malformed and unfinished source safely
220
- * Falls back to escaped plaintext when a grammar is unavailable
198
+ - Escapes HTML-sensitive characters
199
+ - Never evaluates or executes source code
200
+ - Does not use `eval` or `new Function`
201
+ - Does not require DOM access
202
+ - Preserves malformed and unfinished source safely
203
+ - Falls back to escaped plaintext when a grammar is unavailable
221
204
 
222
205
  The returned HTML is intended for normal HTML content contexts. Do not place it directly inside JavaScript, CSS, or URL contexts.
223
206
 
@@ -225,16 +208,16 @@ The returned HTML is intended for normal HTML content contexts. Do not place it
225
208
 
226
209
  Complete guides and API documentation are available in the project repository:
227
210
 
228
- * [Getting started](https://github.com/blcklab/yuirinx/blob/main/docs/getting-started.md)
229
- * [API reference](https://github.com/blcklab/yuirinx/blob/main/docs/api.md)
230
- * [Rendering and styling](https://github.com/blcklab/yuirinx/blob/main/docs/rendering.md)
231
- * [Languages and aliases](https://github.com/blcklab/yuirinx/blob/main/docs/languages.md)
232
- * [Themes](https://github.com/blcklab/yuirinx/blob/main/docs/themes.md)
233
- * [Custom languages](https://github.com/blcklab/yuirinx/blob/main/docs/custom-languages.md)
234
- * [Custom themes](https://github.com/blcklab/yuirinx/blob/main/docs/custom-themes.md)
235
- * [Browser and framework usage](https://github.com/blcklab/yuirinx/blob/main/docs/browser-and-frameworks.md)
236
- * [Security](https://github.com/blcklab/yuirinx/blob/main/docs/security.md)
237
- * [Limitations](https://github.com/blcklab/yuirinx/blob/main/docs/limitations.md)
211
+ - [Getting started](https://github.com/blcklab/yuirinx/blob/main/docs/getting-started.md)
212
+ - [API reference](https://github.com/blcklab/yuirinx/blob/main/docs/api.md)
213
+ - [Rendering and styling](https://github.com/blcklab/yuirinx/blob/main/docs/rendering.md)
214
+ - [Languages and aliases](https://github.com/blcklab/yuirinx/blob/main/docs/languages.md)
215
+ - [Themes](https://github.com/blcklab/yuirinx/blob/main/docs/themes.md)
216
+ - [Custom languages](https://github.com/blcklab/yuirinx/blob/main/docs/custom-languages.md)
217
+ - [Custom themes](https://github.com/blcklab/yuirinx/blob/main/docs/custom-themes.md)
218
+ - [Browser and framework usage](https://github.com/blcklab/yuirinx/blob/main/docs/browser-and-frameworks.md)
219
+ - [Security](https://github.com/blcklab/yuirinx/blob/main/docs/security.md)
220
+ - [Limitations](https://github.com/blcklab/yuirinx/blob/main/docs/limitations.md)
238
221
 
239
222
  ## Project Status
240
223
 
@@ -171,6 +171,13 @@ function classForToken(type, prefix) {
171
171
  const safeType = sanitizeClassPart(type.replace(/\./g, "-")) || "plain";
172
172
  return `${safeClassPrefix(prefix)}${safeType}`;
173
173
  }
174
+ function classesForToken(type, prefix) {
175
+ const parts = type.split(".").filter(Boolean);
176
+ if (parts.length === 0) return [classForToken("plain", prefix)];
177
+ return parts.map(
178
+ (_, index) => classForToken(parts.slice(0, index + 1).join("."), prefix)
179
+ );
180
+ }
174
181
  function classForFallbackLine(index, prefix) {
175
182
  return `${safeClassPrefix(prefix)}fallback-line-${index}`;
176
183
  }
@@ -305,7 +312,8 @@ function renderTokens(tokens, options, theme) {
305
312
  const escaped = escapeHtml(token.value);
306
313
  if (token.type === "plain") return escaped;
307
314
  if (mode === "classes") {
308
- return `<span class="${escapeHtml(classForToken(token.type, prefix))}">${escaped}</span>`;
315
+ const classes = classesForToken(token.type, prefix);
316
+ return `<span class="${escapeHtml(classes.join(" "))}">${escaped}</span>`;
309
317
  }
310
318
  const declarations = styleToDeclarations(
311
319
  resolveTokenStyle(theme, token.type)
@@ -335,7 +343,10 @@ function themeToCss(theme, options = {}) {
335
343
  ].filter(Boolean);
336
344
  lines.push(`${selector}{${declarations.join(";")}}`);
337
345
  }
338
- for (const [type, style] of Object.entries(theme.tokens)) {
346
+ const tokenStyles = Object.entries(theme.tokens).sort(
347
+ ([left], [right]) => left.split(".").length - right.split(".").length
348
+ );
349
+ for (const [type, style] of tokenStyles) {
339
350
  const declarations = styleToDeclarations(style);
340
351
  if (declarations)
341
352
  lines.push(
@@ -424,11 +435,13 @@ function tokenizeCompiled(code, grammar, maxStateDepth) {
424
435
  );
425
436
  }
426
437
  const context = {
438
+ source: code,
427
439
  language: grammar.id,
428
440
  state: stateName,
429
441
  stack: publicStack(grammar.id, stack),
430
442
  offset
431
443
  };
444
+ if (rule.when && !rule.when(match, context)) continue;
432
445
  const tokenType = resolveValue(
433
446
  rule.type,
434
447
  match,
@@ -470,7 +483,14 @@ function createHighlighter(options = {}) {
470
483
  const grammars = /* @__PURE__ */ new Map();
471
484
  const canonicalIds = /* @__PURE__ */ new Set();
472
485
  const themes = /* @__PURE__ */ new Map();
473
- const maxStateDepth = Math.max(2, options.maxStateDepth ?? 64);
486
+ const requestedMaxStateDepth = options.maxStateDepth ?? 64;
487
+ if (!Number.isFinite(requestedMaxStateDepth) || !Number.isInteger(requestedMaxStateDepth) || requestedMaxStateDepth < 2) {
488
+ throw new YuirinxError(
489
+ "YUIRINX_INVALID_OPTION",
490
+ "maxStateDepth must be a finite integer greater than or equal to 2."
491
+ );
492
+ }
493
+ const maxStateDepth = requestedMaxStateDepth;
474
494
  const compiledPlaintext = compileGrammar(plaintextGrammar);
475
495
  const plaintextIds = /* @__PURE__ */ new Set([
476
496
  normalizeId(plaintextGrammar.id),
@@ -534,7 +554,7 @@ function createHighlighter(options = {}) {
534
554
  };
535
555
  const tokenize = (code, language) => tokenizeCompiled(code, resolveGrammar(language).grammar, maxStateDepth);
536
556
  const render = (tokens, renderOptions = {}) => renderTokens(tokens, renderOptions, resolveTheme(renderOptions.theme));
537
- const highlight = (code, highlightOptions) => {
557
+ const highlight = (code, highlightOptions = {}) => {
538
558
  const language = highlightOptions.lang ?? options.defaultLanguage ?? "plaintext";
539
559
  const resolved = resolveGrammar(language);
540
560
  const theme = resolveTheme(highlightOptions.theme);
@@ -555,11 +575,20 @@ function createHighlighter(options = {}) {
555
575
  render,
556
576
  registerLanguage,
557
577
  registerTheme,
558
- hasLanguage: (language) => grammars.has(normalizeId(language)),
578
+ hasLanguage: (language) => {
579
+ const id = normalizeId(language);
580
+ return plaintextIds.has(id) || grammars.has(id);
581
+ },
559
582
  hasTheme: (theme) => themes.has(normalizeId(theme)),
560
- getLanguage: (language) => grammars.get(normalizeId(language))?.source,
583
+ getLanguage: (language) => {
584
+ const id = normalizeId(language);
585
+ return grammars.get(id)?.source ?? (plaintextIds.has(id) ? plaintextGrammar : void 0);
586
+ },
561
587
  getTheme: (theme) => themes.get(normalizeId(theme)),
562
- listLanguages: () => Object.freeze([...canonicalIds]),
588
+ listLanguages: () => Object.freeze([
589
+ "plaintext",
590
+ ...[...canonicalIds].filter((id) => id !== "plaintext")
591
+ ]),
563
592
  listThemes: () => Object.freeze([...themes.keys()])
564
593
  };
565
594
  }
@@ -176,6 +176,13 @@ function classForToken(type, prefix) {
176
176
  const safeType = sanitizeClassPart(type.replace(/\./g, "-")) || "plain";
177
177
  return `${safeClassPrefix(prefix)}${safeType}`;
178
178
  }
179
+ function classesForToken(type, prefix) {
180
+ const parts = type.split(".").filter(Boolean);
181
+ if (parts.length === 0) return [classForToken("plain", prefix)];
182
+ return parts.map(
183
+ (_, index) => classForToken(parts.slice(0, index + 1).join("."), prefix)
184
+ );
185
+ }
179
186
  function classForFallbackLine(index, prefix) {
180
187
  return `${safeClassPrefix(prefix)}fallback-line-${index}`;
181
188
  }
@@ -310,7 +317,8 @@ function renderTokens(tokens, options, theme) {
310
317
  const escaped = escapeHtml(token.value);
311
318
  if (token.type === "plain") return escaped;
312
319
  if (mode === "classes") {
313
- return `<span class="${escapeHtml(classForToken(token.type, prefix))}">${escaped}</span>`;
320
+ const classes = classesForToken(token.type, prefix);
321
+ return `<span class="${escapeHtml(classes.join(" "))}">${escaped}</span>`;
314
322
  }
315
323
  const declarations = styleToDeclarations(
316
324
  resolveTokenStyle(theme, token.type)
@@ -340,7 +348,10 @@ function themeToCss(theme, options = {}) {
340
348
  ].filter(Boolean);
341
349
  lines.push(`${selector}{${declarations.join(";")}}`);
342
350
  }
343
- for (const [type, style] of Object.entries(theme.tokens)) {
351
+ const tokenStyles = Object.entries(theme.tokens).sort(
352
+ ([left], [right]) => left.split(".").length - right.split(".").length
353
+ );
354
+ for (const [type, style] of tokenStyles) {
344
355
  const declarations = styleToDeclarations(style);
345
356
  if (declarations)
346
357
  lines.push(
@@ -429,11 +440,13 @@ function tokenizeCompiled(code, grammar, maxStateDepth) {
429
440
  );
430
441
  }
431
442
  const context = {
443
+ source: code,
432
444
  language: grammar.id,
433
445
  state: stateName,
434
446
  stack: publicStack(grammar.id, stack),
435
447
  offset
436
448
  };
449
+ if (rule.when && !rule.when(match, context)) continue;
437
450
  const tokenType = resolveValue(
438
451
  rule.type,
439
452
  match,
@@ -475,7 +488,14 @@ function createHighlighter(options = {}) {
475
488
  const grammars = /* @__PURE__ */ new Map();
476
489
  const canonicalIds = /* @__PURE__ */ new Set();
477
490
  const themes = /* @__PURE__ */ new Map();
478
- const maxStateDepth = Math.max(2, options.maxStateDepth ?? 64);
491
+ const requestedMaxStateDepth = options.maxStateDepth ?? 64;
492
+ if (!Number.isFinite(requestedMaxStateDepth) || !Number.isInteger(requestedMaxStateDepth) || requestedMaxStateDepth < 2) {
493
+ throw new YuirinxError(
494
+ "YUIRINX_INVALID_OPTION",
495
+ "maxStateDepth must be a finite integer greater than or equal to 2."
496
+ );
497
+ }
498
+ const maxStateDepth = requestedMaxStateDepth;
479
499
  const compiledPlaintext = compileGrammar(plaintextGrammar);
480
500
  const plaintextIds = /* @__PURE__ */ new Set([
481
501
  normalizeId(plaintextGrammar.id),
@@ -539,7 +559,7 @@ function createHighlighter(options = {}) {
539
559
  };
540
560
  const tokenize2 = (code, language) => tokenizeCompiled(code, resolveGrammar(language).grammar, maxStateDepth);
541
561
  const render2 = (tokens, renderOptions = {}) => renderTokens(tokens, renderOptions, resolveTheme(renderOptions.theme));
542
- const highlight2 = (code, highlightOptions) => {
562
+ const highlight2 = (code, highlightOptions = {}) => {
543
563
  const language = highlightOptions.lang ?? options.defaultLanguage ?? "plaintext";
544
564
  const resolved = resolveGrammar(language);
545
565
  const theme = resolveTheme(highlightOptions.theme);
@@ -560,18 +580,27 @@ function createHighlighter(options = {}) {
560
580
  render: render2,
561
581
  registerLanguage: registerLanguage2,
562
582
  registerTheme: registerTheme2,
563
- hasLanguage: (language) => grammars.has(normalizeId(language)),
583
+ hasLanguage: (language) => {
584
+ const id = normalizeId(language);
585
+ return plaintextIds.has(id) || grammars.has(id);
586
+ },
564
587
  hasTheme: (theme) => themes.has(normalizeId(theme)),
565
- getLanguage: (language) => grammars.get(normalizeId(language))?.source,
588
+ getLanguage: (language) => {
589
+ const id = normalizeId(language);
590
+ return grammars.get(id)?.source ?? (plaintextIds.has(id) ? plaintextGrammar : void 0);
591
+ },
566
592
  getTheme: (theme) => themes.get(normalizeId(theme)),
567
- listLanguages: () => Object.freeze([...canonicalIds]),
593
+ listLanguages: () => Object.freeze([
594
+ "plaintext",
595
+ ...[...canonicalIds].filter((id) => id !== "plaintext")
596
+ ]),
568
597
  listThemes: () => Object.freeze([...themes.keys()])
569
598
  };
570
599
  }
571
600
 
572
601
  // src/singleton.ts
573
602
  var defaultHighlighter = createHighlighter();
574
- function highlight(code, options) {
603
+ function highlight(code, options = {}) {
575
604
  return defaultHighlighter.highlight(code, options);
576
605
  }
577
606
  function tokenize(code, language) {
@@ -38,7 +38,7 @@ var punctuationRule = {
38
38
  };
39
39
  var operatorRule = {
40
40
  type: "operator",
41
- pattern: /(?:===|!==|>>>|<<=|>>=|=>|==|!=|<=|>=|&&|\|\||\?\?|\?\.|\+\+|--|\+=|-=|\*=|\/=|%=|\*\*|<<|>>|[+\-*/%=&|!<>^~?])+/
41
+ pattern: /(?:===|!==|>>>|<<=|>>=|=>|==|!=|<=|>=|&&|\|\||\?\?|\?\.|\+\+|--|\+=|-=|\*=|\/=|%=|\*\*|<<|>>|[+\-*/%=&|!<>^~?])/
42
42
  };
43
43
  var numberRule = {
44
44
  type: "number",
@@ -38,7 +38,7 @@ var punctuationRule = {
38
38
  };
39
39
  var operatorRule = {
40
40
  type: "operator",
41
- pattern: /(?:===|!==|>>>|<<=|>>=|=>|==|!=|<=|>=|&&|\|\||\?\?|\?\.|\+\+|--|\+=|-=|\*=|\/=|%=|\*\*|<<|>>|[+\-*/%=&|!<>^~?])+/
41
+ pattern: /(?:===|!==|>>>|<<=|>>=|=>|==|!=|<=|>=|&&|\|\||\?\?|\?\.|\+\+|--|\+=|-=|\*=|\/=|%=|\*\*|<<|>>|[+\-*/%=&|!<>^~?])/
42
42
  };
43
43
  var numberRule = {
44
44
  type: "number",
@@ -38,7 +38,7 @@ var punctuationRule = {
38
38
  };
39
39
  var operatorRule = {
40
40
  type: "operator",
41
- pattern: /(?:===|!==|>>>|<<=|>>=|=>|==|!=|<=|>=|&&|\|\||\?\?|\?\.|\+\+|--|\+=|-=|\*=|\/=|%=|\*\*|<<|>>|[+\-*/%=&|!<>^~?])+/
41
+ pattern: /(?:===|!==|>>>|<<=|>>=|=>|==|!=|<=|>=|&&|\|\||\?\?|\?\.|\+\+|--|\+=|-=|\*=|\/=|%=|\*\*|<<|>>|[+\-*/%=&|!<>^~?])/
42
42
  };
43
43
  var numberRule = {
44
44
  type: "number",
@@ -32,7 +32,7 @@ var punctuationRule = {
32
32
  };
33
33
  var operatorRule = {
34
34
  type: "operator",
35
- pattern: /(?:===|!==|>>>|<<=|>>=|=>|==|!=|<=|>=|&&|\|\||\?\?|\?\.|\+\+|--|\+=|-=|\*=|\/=|%=|\*\*|<<|>>|[+\-*/%=&|!<>^~?])+/
35
+ pattern: /(?:===|!==|>>>|<<=|>>=|=>|==|!=|<=|>=|&&|\|\||\?\?|\?\.|\+\+|--|\+=|-=|\*=|\/=|%=|\*\*|<<|>>|[+\-*/%=&|!<>^~?])/
36
36
  };
37
37
  var doubleStringRule = {
38
38
  type: "string",
@@ -61,7 +61,7 @@ function createCssGrammar(options) {
61
61
  { type: "number", pattern: /(?:\b\d*\.\d+|\b\d+)(?:e[+-]?\d+)?/i },
62
62
  {
63
63
  type: "unit",
64
- pattern: /(?:%|px|r?em|vh|vw|vmin|vmax|ch|ex|cm|mm|in|pt|pc|s|ms|deg|rad|turn)\b/i
64
+ pattern: /(?:%(?![\w-])|(?:px|r?em|vh|vw|vmin|vmax|ch|ex|cm|mm|in|pt|pc|s|ms|deg|rad|turn)\b)/i
65
65
  },
66
66
  {
67
67
  type: "selector",
@@ -38,7 +38,7 @@ var punctuationRule = {
38
38
  };
39
39
  var operatorRule = {
40
40
  type: "operator",
41
- pattern: /(?:===|!==|>>>|<<=|>>=|=>|==|!=|<=|>=|&&|\|\||\?\?|\?\.|\+\+|--|\+=|-=|\*=|\/=|%=|\*\*|<<|>>|[+\-*/%=&|!<>^~?])+/
41
+ pattern: /(?:===|!==|>>>|<<=|>>=|=>|==|!=|<=|>=|&&|\|\||\?\?|\?\.|\+\+|--|\+=|-=|\*=|\/=|%=|\*\*|<<|>>|[+\-*/%=&|!<>^~?])/
42
42
  };
43
43
  var numberRule = {
44
44
  type: "number",
@@ -38,7 +38,7 @@ var punctuationRule = {
38
38
  };
39
39
  var operatorRule = {
40
40
  type: "operator",
41
- pattern: /(?:===|!==|>>>|<<=|>>=|=>|==|!=|<=|>=|&&|\|\||\?\?|\?\.|\+\+|--|\+=|-=|\*=|\/=|%=|\*\*|<<|>>|[+\-*/%=&|!<>^~?])+/
41
+ pattern: /(?:===|!==|>>>|<<=|>>=|=>|==|!=|<=|>=|&&|\|\||\?\?|\?\.|\+\+|--|\+=|-=|\*=|\/=|%=|\*\*|<<|>>|[+\-*/%=&|!<>^~?])/
42
42
  };
43
43
  var numberRule = {
44
44
  type: "number",
@@ -38,7 +38,7 @@ var punctuationRule = {
38
38
  };
39
39
  var operatorRule = {
40
40
  type: "operator",
41
- pattern: /(?:===|!==|>>>|<<=|>>=|=>|==|!=|<=|>=|&&|\|\||\?\?|\?\.|\+\+|--|\+=|-=|\*=|\/=|%=|\*\*|<<|>>|[+\-*/%=&|!<>^~?])+/
41
+ pattern: /(?:===|!==|>>>|<<=|>>=|=>|==|!=|<=|>=|&&|\|\||\?\?|\?\.|\+\+|--|\+=|-=|\*=|\/=|%=|\*\*|<<|>>|[+\-*/%=&|!<>^~?])/
42
42
  };
43
43
  var numberRule = {
44
44
  type: "number",
@@ -38,7 +38,7 @@ var punctuationRule = {
38
38
  };
39
39
  var operatorRule = {
40
40
  type: "operator",
41
- pattern: /(?:===|!==|>>>|<<=|>>=|=>|==|!=|<=|>=|&&|\|\||\?\?|\?\.|\+\+|--|\+=|-=|\*=|\/=|%=|\*\*|<<|>>|[+\-*/%=&|!<>^~?])+/
41
+ pattern: /(?:===|!==|>>>|<<=|>>=|=>|==|!=|<=|>=|&&|\|\||\?\?|\?\.|\+\+|--|\+=|-=|\*=|\/=|%=|\*\*|<<|>>|[+\-*/%=&|!<>^~?])/
42
42
  };
43
43
  var numberRule = {
44
44
  type: "number",
@@ -163,6 +163,18 @@ var BUILTINS = [
163
163
  "process",
164
164
  "window"
165
165
  ];
166
+ function canStartRegex(source, offset) {
167
+ const before = source.slice(0, offset).trimEnd();
168
+ if (!before) return true;
169
+ const previous = before[before.length - 1] ?? "";
170
+ if (/[([{,:;=!?&|+\-*%^~<>]/.test(previous)) return true;
171
+ const word = /[A-Za-z_$][\w$]*$/.exec(before)?.[0];
172
+ return Boolean(
173
+ word && /^(?:await|case|delete|do|else|in|instanceof|new|of|return|throw|typeof|void|yield)$/.test(
174
+ word
175
+ )
176
+ );
177
+ }
166
178
  function createEcmaGrammar(options) {
167
179
  const keywordValues = options.typescript ? [...JS_KEYWORDS, ...TS_KEYWORDS] : JS_KEYWORDS;
168
180
  const rootRules = [
@@ -173,16 +185,23 @@ function createEcmaGrammar(options) {
173
185
  { type: "string", pattern: /`/, push: "template" }
174
186
  ];
175
187
  if (options.jsx) {
188
+ if (options.typescript) {
189
+ rootRules.push({
190
+ type: "operator",
191
+ pattern: /<(?=[A-Za-z_$][\w$]*(?:\s+extends\s+[^>]+)?,?>\s*\()/
192
+ });
193
+ }
176
194
  rootRules.push({
177
195
  type: "tag.punctuation",
178
- pattern: /<\/?/,
179
- push: "jsxTag"
196
+ pattern: /<(?=[A-Za-z_$>])/,
197
+ push: "jsxTagName"
180
198
  });
181
199
  }
182
200
  rootRules.push(
183
201
  {
184
202
  type: "regex",
185
- pattern: /\/(?![/*\s])(?:\\.|\[(?:\\.|[^\]\\])*\]|[^/\\\r\n])+\/[dgimsuvy]*/
203
+ pattern: /\/(?![/*\s])(?:\\.|\[(?:\\.|[^\]\\])*\]|[^/\\\r\n])+\/[dgimsuvy]*/,
204
+ when: (_match, context) => canStartRegex(context.source, context.offset)
186
205
  },
187
206
  numberRule,
188
207
  { type: "boolean", pattern: /\b(?:true|false)\b/ },
@@ -228,14 +247,54 @@ function createEcmaGrammar(options) {
228
247
  { include: "root" }
229
248
  ]
230
249
  },
231
- jsxTag: {
250
+ jsxTagName: {
251
+ rules: [
252
+ { type: "tag.punctuation", pattern: />/, next: "jsxChildren" },
253
+ { type: "tag.punctuation", pattern: /\/>/, pop: true },
254
+ whitespaceRule,
255
+ {
256
+ type: "tag",
257
+ pattern: /[A-Za-z_$][\w$.-]*/,
258
+ next: "jsxAttributes"
259
+ },
260
+ punctuationRule
261
+ ]
262
+ },
263
+ jsxAttributes: {
232
264
  rules: [
233
- { type: "tag.punctuation", pattern: /\/?>/, pop: true },
265
+ { type: "tag.punctuation", pattern: /\/>/, pop: true },
266
+ { type: "tag.punctuation", pattern: />/, next: "jsxChildren" },
234
267
  whitespaceRule,
235
268
  { type: "attribute.value", pattern: /"(?:\\.|[^"\\])*"?/ },
236
269
  { type: "attribute.value", pattern: /'(?:\\.|[^'\\])*'?/ },
237
270
  { type: "punctuation", pattern: /{/, push: "jsxExpression" },
238
271
  { type: "operator", pattern: /=/ },
272
+ { type: "attribute", pattern: /[A-Za-z_$][\w$.-]*/ },
273
+ punctuationRule
274
+ ]
275
+ },
276
+ jsxChildren: {
277
+ fallbackType: "plain",
278
+ rules: [
279
+ {
280
+ type: "tag.punctuation",
281
+ pattern: /<\/(?=[A-Za-z_$>])/,
282
+ push: "jsxClosingTag"
283
+ },
284
+ {
285
+ type: "tag.punctuation",
286
+ pattern: /<(?=[A-Za-z_$>])/,
287
+ push: "jsxTagName"
288
+ },
289
+ { type: "punctuation", pattern: /{/, push: "jsxExpression" },
290
+ { type: "plain", pattern: /[^<{]+/ },
291
+ { type: "plain", pattern: /[<{]/ }
292
+ ]
293
+ },
294
+ jsxClosingTag: {
295
+ rules: [
296
+ { type: "tag.punctuation", pattern: />/, pop: 2 },
297
+ whitespaceRule,
239
298
  { type: "tag", pattern: /[A-Za-z_$][\w$.-]*/ },
240
299
  punctuationRule
241
300
  ]