@sveltia/ui 0.62.1 → 0.63.1
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/dist/components/button/button.svelte +0 -5
- package/dist/components/listbox/option.svelte +8 -0
- package/dist/components/select/combobox.svelte +53 -0
- package/dist/components/text-editor/constants.d.ts +0 -1
- package/dist/components/text-editor/constants.js +1 -36
- package/dist/components/text-editor/core.js +24 -31
- package/dist/components/text-editor/lexical-root.svelte +0 -68
- package/dist/components/text-editor/shiki/cache.d.ts +3 -0
- package/dist/components/text-editor/shiki/cache.js +121 -0
- package/dist/components/text-editor/shiki/engine-entry.d.ts +2 -0
- package/dist/components/text-editor/shiki/engine-entry.js +20 -0
- package/dist/components/text-editor/shiki/facade.d.ts +16 -0
- package/dist/components/text-editor/shiki/facade.js +452 -0
- package/dist/components/text-editor/shiki/generated.d.ts +28 -0
- package/dist/components/text-editor/shiki/generated.js +25 -0
- package/dist/components/text-editor/shiki/highlighter.d.ts +11 -0
- package/dist/components/text-editor/shiki/highlighter.js +477 -0
- package/dist/components/text-editor/shiki/loader.d.ts +6 -0
- package/dist/components/text-editor/shiki/loader.js +102 -0
- package/dist/components/text-editor/shiki/theme.d.ts +12 -0
- package/dist/components/text-editor/shiki/theme.js +86 -0
- package/dist/components/text-editor/toolbar/code-language-switcher.svelte +24 -34
- package/dist/components/text-editor/toolbar/toggle-block-menu-item.svelte +1 -5
- package/dist/components/util/popup.svelte +5 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +4 -0
- package/dist/shiki-engine.js +152 -0
- package/dist/typedefs.d.ts +52 -0
- package/dist/typedefs.js +29 -0
- package/package.json +11 -9
|
@@ -220,9 +220,7 @@ button.ghost[aria-pressed=true] {
|
|
|
220
220
|
background-color: var(--sui-button-ghost-background-color-pressed, var(--sui-selected-background-color));
|
|
221
221
|
}
|
|
222
222
|
button.link {
|
|
223
|
-
outline: 0;
|
|
224
223
|
margin: 0;
|
|
225
|
-
border-radius: 0 !important;
|
|
226
224
|
padding: 0 !important;
|
|
227
225
|
height: auto !important;
|
|
228
226
|
color: var(--sui-button-link-foreground-color, var(--sui-primary-accent-color-text));
|
|
@@ -237,9 +235,6 @@ button.link .label {
|
|
|
237
235
|
:global(:is(:root, :host)[data-underline-links='true']) button.link .label {
|
|
238
236
|
text-decoration: underline;
|
|
239
237
|
}
|
|
240
|
-
button.link:is(:hover, :focus, :active) .label {
|
|
241
|
-
text-decoration: var(--sui-button-link-text-decoration-focus, underline);
|
|
242
|
-
}
|
|
243
238
|
button.small {
|
|
244
239
|
border-radius: var(--sui-button-small-border-radius);
|
|
245
240
|
padding: var(--sui-button-small-padding);
|
|
@@ -50,6 +50,13 @@
|
|
|
50
50
|
/* eslint-enable prefer-const */
|
|
51
51
|
} = $props();
|
|
52
52
|
|
|
53
|
+
/**
|
|
54
|
+
* Fallback `id`, so the option can always be referenced by `aria-activedescendant`. `<Group>`
|
|
55
|
+
* assigns one to each member as it activates, but within a `<Combobox>` that happens while the
|
|
56
|
+
* dropdown is still collapsed and no option has rendered yet.
|
|
57
|
+
*/
|
|
58
|
+
const fallbackId = $props.id();
|
|
59
|
+
|
|
53
60
|
/**
|
|
54
61
|
* The registry provided by an ancestor `<Combobox>`. This is `undefined` when the option is used
|
|
55
62
|
* standalone within a `<Listbox>`, in which case it always renders itself.
|
|
@@ -126,6 +133,7 @@
|
|
|
126
133
|
<Button
|
|
127
134
|
{...restProps}
|
|
128
135
|
role="option"
|
|
136
|
+
id={restProps.id ?? fallbackId}
|
|
129
137
|
tabindex="-1"
|
|
130
138
|
aria-selected={selected}
|
|
131
139
|
{label}
|
|
@@ -124,11 +124,63 @@
|
|
|
124
124
|
onChange?.(new CustomEvent('Change', { detail }));
|
|
125
125
|
};
|
|
126
126
|
|
|
127
|
+
/**
|
|
128
|
+
* The `<Listbox>` rendered for this combobox. It’s reached through {@link optionHost}, which owns
|
|
129
|
+
* it wherever the options currently live, rather than through the popup, which only holds it
|
|
130
|
+
* while the dropdown is expanded.
|
|
131
|
+
* @returns {HTMLElement | null | undefined} Listbox element.
|
|
132
|
+
*/
|
|
133
|
+
const getListbox = () => optionHost?.querySelector('[role="listbox"]');
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Make the selected option the starting point of the expanded dropdown: scroll it into view, and
|
|
137
|
+
* mark it as the focused member so the arrow keys carry on from there. A long option list is
|
|
138
|
+
* scrollable, and the dropdown would otherwise open at the top with the current selection nowhere
|
|
139
|
+
* to be seen — and the first arrow key would jump back up to the first option.
|
|
140
|
+
*/
|
|
141
|
+
const revealSelectedOption = () => {
|
|
142
|
+
const listbox = getListbox();
|
|
143
|
+
|
|
144
|
+
const option = /** @type {HTMLElement | null | undefined} */ (
|
|
145
|
+
listbox?.querySelector('[role="option"][aria-selected="true"]')
|
|
146
|
+
);
|
|
147
|
+
|
|
148
|
+
if (!listbox || !option) {
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// `<Group>` tracks the focused member with this class, and moves it on from there
|
|
153
|
+
option.classList.add('focused');
|
|
154
|
+
listbox.setAttribute('aria-activedescendant', option.id);
|
|
155
|
+
|
|
156
|
+
option.scrollIntoView(true);
|
|
157
|
+
};
|
|
158
|
+
|
|
127
159
|
// Let the options know whether they should render themselves
|
|
128
160
|
$effect(() => {
|
|
129
161
|
registry.expanded = isPopupOpen;
|
|
130
162
|
});
|
|
131
163
|
|
|
164
|
+
// Reveal the selected option once the dropdown is expanded. The options are rendered and moved
|
|
165
|
+
// into the popup by the effects around this one, and the popup is then measured against the space
|
|
166
|
+
// below the anchor to get its height, so there is nothing to scroll until all of that has
|
|
167
|
+
// settled. The popup moves focus onto its first tab stop 100ms in, which scrolls that element
|
|
168
|
+
// into view in turn, so this has to come after it rather than be undone by it.
|
|
169
|
+
$effect(() => {
|
|
170
|
+
if (!isPopupOpen) {
|
|
171
|
+
return undefined;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const timer = globalThis.setTimeout(revealSelectedOption, 150);
|
|
175
|
+
|
|
176
|
+
return () => {
|
|
177
|
+
globalThis.clearTimeout(timer);
|
|
178
|
+
// The options are unmounted along with the popup, so the listbox is about to be pointing at
|
|
179
|
+
// an element that no longer exists
|
|
180
|
+
getListbox()?.removeAttribute('aria-activedescendant');
|
|
181
|
+
};
|
|
182
|
+
});
|
|
183
|
+
|
|
132
184
|
// Move the options into the popup while it’s expanded, and back out before it’s unmounted. Only
|
|
133
185
|
// the wrapper is moved, never its children, so Svelte keeps full ownership of the subtree.
|
|
134
186
|
$effect(() => {
|
|
@@ -414,6 +466,7 @@
|
|
|
414
466
|
}
|
|
415
467
|
|
|
416
468
|
.combobox-inner {
|
|
469
|
+
flex: auto;
|
|
417
470
|
display: flex;
|
|
418
471
|
flex-direction: column;
|
|
419
472
|
overflow: hidden;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { CodeHighlightNode, CodeNode } from '@lexical/code';
|
|
1
|
+
import { CodeHighlightNode, CodeNode } from '@lexical/code-core';
|
|
2
2
|
import { LinkNode } from '@lexical/link';
|
|
3
3
|
import { ListItemNode, ListNode } from '@lexical/list';
|
|
4
4
|
import {
|
|
@@ -30,8 +30,6 @@ import { HeadingNode, QuoteNode } from '@lexical/rich-text';
|
|
|
30
30
|
* } from '../../typedefs';
|
|
31
31
|
*/
|
|
32
32
|
|
|
33
|
-
export const PRISM_BASE_URL = `https://unpkg.com/prismjs@1.30.0`;
|
|
34
|
-
|
|
35
33
|
/**
|
|
36
34
|
* @type {EditorThemeClasses}
|
|
37
35
|
*/
|
|
@@ -50,39 +48,6 @@ export const EDITOR_THEME = {
|
|
|
50
48
|
},
|
|
51
49
|
},
|
|
52
50
|
code: 'code-block',
|
|
53
|
-
// https://github.com/facebook/lexical/blob/main/packages/lexical-website/docs/getting-started/theming.md
|
|
54
|
-
codeHighlight: {
|
|
55
|
-
atrule: 'token atrule',
|
|
56
|
-
attr: 'token attr',
|
|
57
|
-
boolean: 'token boolean',
|
|
58
|
-
builtin: 'token builtin',
|
|
59
|
-
cdata: 'token cdata',
|
|
60
|
-
char: 'token char',
|
|
61
|
-
class: 'token class',
|
|
62
|
-
'class-name': 'token class-name',
|
|
63
|
-
comment: 'token comment',
|
|
64
|
-
constant: 'token constant',
|
|
65
|
-
deleted: 'token deleted',
|
|
66
|
-
doctype: 'token doctype',
|
|
67
|
-
entity: 'token entity',
|
|
68
|
-
function: 'token function',
|
|
69
|
-
important: 'token important',
|
|
70
|
-
inserted: 'token inserted',
|
|
71
|
-
keyword: 'token keyword',
|
|
72
|
-
namespace: 'token namespace',
|
|
73
|
-
number: 'token number',
|
|
74
|
-
operator: 'token operator',
|
|
75
|
-
prolog: 'token prolog',
|
|
76
|
-
property: 'token property',
|
|
77
|
-
punctuation: 'token punctuation',
|
|
78
|
-
regex: 'token regex',
|
|
79
|
-
selector: 'token selector',
|
|
80
|
-
string: 'token string',
|
|
81
|
-
symbol: 'token symbol',
|
|
82
|
-
tag: 'token tag',
|
|
83
|
-
url: 'token url',
|
|
84
|
-
variable: 'token variable',
|
|
85
|
-
},
|
|
86
51
|
};
|
|
87
52
|
|
|
88
53
|
/**
|
|
@@ -1,15 +1,10 @@
|
|
|
1
|
-
// Work around the “Prism is not defined” error in consumers
|
|
2
|
-
// @see https://github.com/remix-run/remix/discussions/8182
|
|
3
|
-
import 'prismjs';
|
|
4
|
-
|
|
5
1
|
import {
|
|
6
2
|
CodeHighlightNode,
|
|
7
3
|
CodeNode,
|
|
8
4
|
$createCodeNode as createCodeNode,
|
|
9
5
|
$isCodeHighlightNode as isCodeHighlightNode,
|
|
10
6
|
$isCodeNode as isCodeNode,
|
|
11
|
-
} from '@lexical/code';
|
|
12
|
-
import { PrismTokenizer, registerCodeHighlighting } from '@lexical/code-prism';
|
|
7
|
+
} from '@lexical/code-core';
|
|
13
8
|
import { registerDragonSupport } from '@lexical/dragon';
|
|
14
9
|
import { HorizontalRuleNode } from '@lexical/extension';
|
|
15
10
|
import { createEmptyHistoryState, registerHistory } from '@lexical/history';
|
|
@@ -59,17 +54,24 @@ import {
|
|
|
59
54
|
OUTDENT_CONTENT_COMMAND,
|
|
60
55
|
PASTE_COMMAND,
|
|
61
56
|
} from 'lexical';
|
|
62
|
-
import prismComponents from 'prismjs/components';
|
|
63
57
|
import {
|
|
64
58
|
BLOCK_BUTTON_TYPES,
|
|
65
59
|
DISABLED_MARKDOWN_TAGS,
|
|
66
60
|
EDITOR_THEME,
|
|
67
61
|
NODE_MAP,
|
|
68
|
-
PRISM_BASE_URL,
|
|
69
62
|
TEXT_FORMAT_BUTTON_TYPES,
|
|
70
63
|
TRANSFORMER_MAP,
|
|
71
64
|
} from './constants.js';
|
|
72
65
|
import { increaseListIndentation, splitMultilineFormatting } from './markdown.js';
|
|
66
|
+
import {
|
|
67
|
+
isPlainLanguage,
|
|
68
|
+
loadCodeLanguage,
|
|
69
|
+
loadCodeTheme,
|
|
70
|
+
loadEngine,
|
|
71
|
+
normalizeCodeLanguage,
|
|
72
|
+
} from './shiki/facade.js';
|
|
73
|
+
import { registerCodeHighlighting, shikiTokenizer } from './shiki/highlighter.js';
|
|
74
|
+
import { getCodeTheme, observeCodeTheme } from './shiki/theme.js';
|
|
73
75
|
import { HR } from './transformers/hr.js';
|
|
74
76
|
import { TABLE } from './transformers/table.js';
|
|
75
77
|
|
|
@@ -267,13 +269,13 @@ export const initEditor = ({
|
|
|
267
269
|
if (enabledButtons.includes('code-block') || isCodeEditor) {
|
|
268
270
|
addUnregister(
|
|
269
271
|
registerCodeHighlighting(editor, {
|
|
272
|
+
...shikiTokenizer,
|
|
270
273
|
defaultLanguage,
|
|
271
|
-
|
|
272
|
-
tokenize: (code, lang = 'plain') =>
|
|
273
|
-
window.Prism.tokenize(code, window.Prism.languages[lang] ?? window.Prism.languages.plain),
|
|
274
|
-
$tokenize: PrismTokenizer.$tokenize,
|
|
274
|
+
defaultTheme: getCodeTheme(),
|
|
275
275
|
}),
|
|
276
276
|
);
|
|
277
|
+
|
|
278
|
+
addUnregister(observeCodeTheme(editor));
|
|
277
279
|
}
|
|
278
280
|
|
|
279
281
|
// https://github.com/facebook/lexical/blob/main/packages/lexical-link/src/LexicalLinkExtension.ts
|
|
@@ -466,31 +468,21 @@ export const initEditor = ({
|
|
|
466
468
|
};
|
|
467
469
|
|
|
468
470
|
/**
|
|
469
|
-
*
|
|
471
|
+
* Preload the syntax highlighter for the given programming language.
|
|
472
|
+
*
|
|
473
|
+
* Highlighting also works without this — the transform loads whatever it needs and re-highlights
|
|
474
|
+
* once it arrives — but preloading avoids a visible flash of unhighlighted code.
|
|
470
475
|
* @param {string} lang Language name, like scss.
|
|
471
476
|
*/
|
|
472
477
|
export const loadCodeHighlighter = async (lang) => {
|
|
473
|
-
if (lang
|
|
478
|
+
if (isPlainLanguage(lang)) {
|
|
474
479
|
return;
|
|
475
480
|
}
|
|
476
481
|
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
([key, { alias }]) =>
|
|
480
|
-
key === lang ||
|
|
481
|
-
(Array.isArray(alias) ? alias.includes(lang) : /* v8 ignore next */ alias === lang),
|
|
482
|
-
)?.[0];
|
|
482
|
+
// The grammar and theme loaders are no-ops until the engine is in place
|
|
483
|
+
await loadEngine();
|
|
483
484
|
|
|
484
|
-
|
|
485
|
-
return;
|
|
486
|
-
}
|
|
487
|
-
|
|
488
|
-
try {
|
|
489
|
-
// eslint-disable-next-line jsdoc/no-bad-blocks
|
|
490
|
-
await import(/* @vite-ignore */ `${PRISM_BASE_URL}/components/prism-${canonicalLang}.min.js`);
|
|
491
|
-
} catch {
|
|
492
|
-
//
|
|
493
|
-
}
|
|
485
|
+
await Promise.all([loadCodeLanguage(normalizeCodeLanguage(lang)), loadCodeTheme(getCodeTheme())]);
|
|
494
486
|
};
|
|
495
487
|
|
|
496
488
|
/**
|
|
@@ -502,7 +494,8 @@ export const loadCodeHighlighter = async (lang) => {
|
|
|
502
494
|
* @throws {Error} Failed to convert the value to Lexical nodes.
|
|
503
495
|
*/
|
|
504
496
|
export const convertMarkdownToLexical = async (editor, value, enabledTransformers) => {
|
|
505
|
-
//
|
|
497
|
+
// Preload the highlighter for every language used in the document, so code blocks are highlighted
|
|
498
|
+
// as soon as they appear rather than a moment later
|
|
506
499
|
await Promise.all(
|
|
507
500
|
[...value.matchAll(/^```(?<lang>.+?)\n/gm)].map(async ({ groups: { lang = 'plain' } = {} }) =>
|
|
508
501
|
loadCodeHighlighter(lang),
|
|
@@ -222,72 +222,4 @@
|
|
|
222
222
|
background-color: var(--sui-control-border-color);
|
|
223
223
|
line-height: 2px;
|
|
224
224
|
content: "";
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
:root[data-theme=light] .lexical-root :global(.token:is(.comment, .prolog, .doctype, .cdata)) {
|
|
228
|
-
color: slategray;
|
|
229
|
-
}
|
|
230
|
-
:root[data-theme=light] .lexical-root :global(.token.punctuation) {
|
|
231
|
-
color: #999;
|
|
232
|
-
}
|
|
233
|
-
:root[data-theme=light] .lexical-root :global(.token.namespace) {
|
|
234
|
-
opacity: 0.7;
|
|
235
|
-
}
|
|
236
|
-
:root[data-theme=light] .lexical-root :global(.token:is(.property, .tag, .boolean, .number, .constant, .symbol, .deleted)) {
|
|
237
|
-
color: #905;
|
|
238
|
-
}
|
|
239
|
-
:root[data-theme=light] .lexical-root :global(.token:is(.selector, .attr-name, .string, .char, .builtin, .inserted)) {
|
|
240
|
-
color: #690;
|
|
241
|
-
}
|
|
242
|
-
:root[data-theme=light] .lexical-root :global(.token:is(.operator, .entity, .url)), :root[data-theme=light] .lexical-root :global(.language-css) :global(.token.string), :root[data-theme=light] .lexical-root :global(.style) :global(.token.string) {
|
|
243
|
-
color: #9a6e3a;
|
|
244
|
-
}
|
|
245
|
-
:root[data-theme=light] .lexical-root :global(.token:is(.atrule, .attr-value, .keyword)) {
|
|
246
|
-
color: #07a;
|
|
247
|
-
}
|
|
248
|
-
:root[data-theme=light] .lexical-root :global(.token:is(.function, .class-name)) {
|
|
249
|
-
color: #dd4a68;
|
|
250
|
-
}
|
|
251
|
-
:root[data-theme=light] .lexical-root :global(.token:is(.regex, .important, .variable)) {
|
|
252
|
-
color: #e90;
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
:root[data-theme=dark] .lexical-root :global(.token:is(.comment, .block-comment, .prolog, .doctype, .cdata)) {
|
|
256
|
-
color: #999;
|
|
257
|
-
}
|
|
258
|
-
:root[data-theme=dark] .lexical-root :global(.token.punctuation) {
|
|
259
|
-
color: #ccc;
|
|
260
|
-
}
|
|
261
|
-
:root[data-theme=dark] .lexical-root :global(.token:is(.tag, .attr-name, .namespace, .deleted)) {
|
|
262
|
-
color: #e2777a;
|
|
263
|
-
}
|
|
264
|
-
:root[data-theme=dark] .lexical-root :global(.token.function-name) {
|
|
265
|
-
color: #6196cc;
|
|
266
|
-
}
|
|
267
|
-
:root[data-theme=dark] .lexical-root :global(.token:is(.boolean, .number, .function)) {
|
|
268
|
-
color: #f08d49;
|
|
269
|
-
}
|
|
270
|
-
:root[data-theme=dark] .lexical-root :global(.token:is(.property, .class-name, .constant, .symbol)) {
|
|
271
|
-
color: #f8c555;
|
|
272
|
-
}
|
|
273
|
-
:root[data-theme=dark] .lexical-root :global(.token:is(.selector, .important, .atrule, .keyword, .builtin)) {
|
|
274
|
-
color: #cc99cd;
|
|
275
|
-
}
|
|
276
|
-
:root[data-theme=dark] .lexical-root :global(.token:is(.string, .char, .attr-value, .regex, .variable)) {
|
|
277
|
-
color: #7ec699;
|
|
278
|
-
}
|
|
279
|
-
:root[data-theme=dark] .lexical-root :global(.token:is(.operator, .entity, .url)) {
|
|
280
|
-
color: #67cdcc;
|
|
281
|
-
}
|
|
282
|
-
:root[data-theme=dark] .lexical-root :global(.token:is(.important, .bold)) {
|
|
283
|
-
font-weight: bold;
|
|
284
|
-
}
|
|
285
|
-
:root[data-theme=dark] .lexical-root :global(.token.italic) {
|
|
286
|
-
font-style: italic;
|
|
287
|
-
}
|
|
288
|
-
:root[data-theme=dark] .lexical-root :global(.token.entity) {
|
|
289
|
-
cursor: help;
|
|
290
|
-
}
|
|
291
|
-
:root[data-theme=dark] .lexical-root :global(.token.inserted) {
|
|
292
|
-
color: green;
|
|
293
225
|
}</style>
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { IndexedDB } from '@sveltia/utils/storage';
|
|
2
|
+
import { SHIKI_VERSION } from './generated.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Client-side cache for Shiki grammars and themes.
|
|
6
|
+
*
|
|
7
|
+
* Grammars and themes resolve to plain data, so they can be stored and replayed without going back
|
|
8
|
+
* to the network. That saves more than bytes: a grammar references its embedded languages by
|
|
9
|
+
* relative path, so loading Vue or Svelte fans out to several requests, and a cache hit collapses
|
|
10
|
+
* all of them into one read.
|
|
11
|
+
*
|
|
12
|
+
* The engine is deliberately not cached here. It is executable code, so replaying it from storage
|
|
13
|
+
* would mean importing a `blob:` URL, which needs `script-src blob:` in the page’s CSP. It also
|
|
14
|
+
* gains nothing, because it is fetched from a version-immutable URL that the browser’s HTTP cache
|
|
15
|
+
* already keeps.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
const DATABASE_NAME = 'sveltia-ui';
|
|
19
|
+
const STORE_NAME = 'shiki';
|
|
20
|
+
/** @type {IndexedDB | undefined | null} `null` once the store is known to be unusable. */
|
|
21
|
+
let database;
|
|
22
|
+
/** @type {Promise<void> | undefined} */
|
|
23
|
+
let purgePromise;
|
|
24
|
+
let enabled = true;
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Enable or disable the cache.
|
|
28
|
+
*
|
|
29
|
+
* Worth turning off when the grammars are bundled with the app rather than fetched, since reading
|
|
30
|
+
* them back from storage is then slower than the bundled import it replaces.
|
|
31
|
+
* @param {boolean} value Whether to use the cache.
|
|
32
|
+
*/
|
|
33
|
+
export const setCodeHighlighterCacheEnabled = (value) => {
|
|
34
|
+
enabled = value;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Build the cache key for a payload.
|
|
39
|
+
* @param {string} kind Payload kind, either `lang` or `theme`.
|
|
40
|
+
* @param {string} id Language or theme ID.
|
|
41
|
+
* @returns {string} Key, scoped to the Shiki version the payload came from.
|
|
42
|
+
*/
|
|
43
|
+
const getKey = (kind, id) => `${SHIKI_VERSION}/${kind}/${id}`;
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Get the store, or `null` when IndexedDB is unavailable, as in a server-side render or with
|
|
47
|
+
* storage blocked.
|
|
48
|
+
* @returns {IndexedDB | null} Store.
|
|
49
|
+
*/
|
|
50
|
+
const getDatabase = () => {
|
|
51
|
+
if (database === undefined) {
|
|
52
|
+
database = typeof indexedDB === 'undefined' ? null : new IndexedDB(DATABASE_NAME, STORE_NAME);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return database;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Drop anything cached for a different Shiki version, once per session. A version bump changes
|
|
60
|
+
* every key, so the old payloads would otherwise linger forever.
|
|
61
|
+
* @param {IndexedDB} db Store.
|
|
62
|
+
* @returns {Promise<void>} Nothing.
|
|
63
|
+
*/
|
|
64
|
+
const purgeStaleVersions = async (db) => {
|
|
65
|
+
purgePromise ??= (async () => {
|
|
66
|
+
const stale = (await db.keys()).filter(
|
|
67
|
+
(key) => typeof key === 'string' && !key.startsWith(`${SHIKI_VERSION}/`),
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
if (stale.length) {
|
|
71
|
+
await db.deleteEntries(stale);
|
|
72
|
+
}
|
|
73
|
+
})();
|
|
74
|
+
|
|
75
|
+
return purgePromise;
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Read a cached grammar or theme.
|
|
80
|
+
* @param {string} kind Payload kind, either `lang` or `theme`.
|
|
81
|
+
* @param {string} id Language or theme ID.
|
|
82
|
+
* @returns {Promise<any>} Cached payload, or `undefined` when not cached or unreadable.
|
|
83
|
+
*/
|
|
84
|
+
export const getCachedPayload = async (kind, id) => {
|
|
85
|
+
const db = enabled ? getDatabase() : null;
|
|
86
|
+
|
|
87
|
+
if (!db) {
|
|
88
|
+
return undefined;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
try {
|
|
92
|
+
await purgeStaleVersions(db);
|
|
93
|
+
|
|
94
|
+
return await db.get(getKey(kind, id));
|
|
95
|
+
} catch {
|
|
96
|
+
// A failed cache read must never stop a code block from highlighting
|
|
97
|
+
return undefined;
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Store a grammar or theme.
|
|
103
|
+
* @param {string} kind Payload kind, either `lang` or `theme`.
|
|
104
|
+
* @param {string} id Language or theme ID.
|
|
105
|
+
* @param {any} payload Payload to store. Must be structured-cloneable, which grammars and themes
|
|
106
|
+
* are, being plain data.
|
|
107
|
+
* @returns {Promise<void>} Nothing.
|
|
108
|
+
*/
|
|
109
|
+
export const cachePayload = async (kind, id, payload) => {
|
|
110
|
+
const db = enabled ? getDatabase() : null;
|
|
111
|
+
|
|
112
|
+
if (!db) {
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
try {
|
|
117
|
+
await db.set(getKey(kind, id), payload);
|
|
118
|
+
} catch {
|
|
119
|
+
// Out of quota, storage blocked, or a payload that won’t clone: not worth failing over
|
|
120
|
+
}
|
|
121
|
+
};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Entry point for the standalone Shiki engine chunk built by `scripts/build-shiki-engine.js`.
|
|
3
|
+
*
|
|
4
|
+
* This is the complete set of Shiki APIs the vendored highlighter needs. It is never imported
|
|
5
|
+
* directly by the library — only bundled into `dist/shiki-engine.js` and loaded at runtime — so
|
|
6
|
+
* Shiki stays a development dependency and never reaches a consumer’s bundle.
|
|
7
|
+
*
|
|
8
|
+
* Everything is imported through the `shiki` package rather than from `@shikijs/*` directly,
|
|
9
|
+
* because `shiki` pins those to exact versions. That keeps the engine bundled here and the
|
|
10
|
+
* grammars fetched at runtime on the same version, which `generated.js` records.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
export {
|
|
14
|
+
createHighlighterCoreSync,
|
|
15
|
+
getTokenStyleObject,
|
|
16
|
+
isSpecialLang,
|
|
17
|
+
isSpecialTheme,
|
|
18
|
+
stringifyTokenStyle,
|
|
19
|
+
} from 'shiki/core';
|
|
20
|
+
export { createJavaScriptRegexEngine } from 'shiki/engine/javascript';
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export function isPlainLanguage(language: string | null | undefined): boolean;
|
|
2
|
+
export function isEngineLoaded(): boolean;
|
|
3
|
+
export function isEngineUnavailable(): boolean;
|
|
4
|
+
export function isCodeLanguageLoaded(language: string): boolean;
|
|
5
|
+
export function loadEngine(editor?: LexicalEditor | undefined, codeNodeKey?: string | undefined): Promise<void> | undefined;
|
|
6
|
+
export function normalizeCodeLanguage(language: string): string;
|
|
7
|
+
export function loadCodeLanguage(language: string, editor?: LexicalEditor | undefined, codeNodeKey?: string | undefined): Promise<void> | undefined;
|
|
8
|
+
export function isCodeThemeLoaded(theme: string): boolean;
|
|
9
|
+
export function loadCodeTheme(theme: string, editor?: LexicalEditor | undefined, codeNodeKey?: string | undefined): Promise<void> | undefined;
|
|
10
|
+
export function getHighlightNodes(codeNode: CodeNode, language: string): LexicalNode[];
|
|
11
|
+
export function highlightCodeToHTML(code: string, language: string, { theme }?: {
|
|
12
|
+
theme?: string | undefined;
|
|
13
|
+
} | undefined): string | undefined;
|
|
14
|
+
import type { LexicalEditor } from 'lexical';
|
|
15
|
+
import type { CodeNode } from '@lexical/code-core';
|
|
16
|
+
import type { LexicalNode } from 'lexical';
|