@mindlogic-ai/logician-ui 5.0.0-alpha.20 → 5.0.0-alpha.21
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/Code/shikiAdapter.d.ts +3 -1
- package/dist/components/Code/shikiAdapter.d.ts.map +1 -1
- package/dist/components/Code/shikiAdapter.js +29 -8
- package/dist/components/Code/shikiAdapter.js.map +1 -1
- package/dist/components/Code/shikiAdapter.mjs +29 -8
- package/dist/components/Code/shikiAdapter.mjs.map +1 -1
- package/package.json +1 -1
- package/src/components/Code/Code.test.tsx +116 -0
- package/src/components/Code/shikiAdapter.ts +38 -9
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import type { BundledLanguage } from 'shiki';
|
|
2
2
|
export declare const BUNDLED_LANGUAGES: BundledLanguage[];
|
|
3
|
-
|
|
3
|
+
declare const baseAdapter: import("@chakra-ui/react").CodeBlockAdapter;
|
|
4
|
+
export declare const shikiAdapter: typeof baseAdapter;
|
|
5
|
+
export {};
|
|
4
6
|
//# sourceMappingURL=shikiAdapter.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"shikiAdapter.d.ts","sourceRoot":"","sources":["../../../src/components/Code/shikiAdapter.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,eAAe,EAAoC,MAAM,OAAO,CAAC;AAE/E,eAAO,MAAM,iBAAiB,EAAE,eAAe,EA8B9C,CAAC;
|
|
1
|
+
{"version":3,"file":"shikiAdapter.d.ts","sourceRoot":"","sources":["../../../src/components/Code/shikiAdapter.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,eAAe,EAAoC,MAAM,OAAO,CAAC;AAE/E,eAAO,MAAM,iBAAiB,EAAE,eAAe,EA8B9C,CAAC;AA+BF,QAAA,MAAM,WAAW,6CAGf,CAAC;AAKH,eAAO,MAAM,YAAY,EAAE,OAAO,WAGjC,CAAC"}
|
|
@@ -34,16 +34,37 @@ const BUNDLED_LANGUAGES = [
|
|
|
34
34
|
'xml',
|
|
35
35
|
'yaml',
|
|
36
36
|
];
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
37
|
+
// One highlighter for the whole app. Every <Code> mounts its own
|
|
38
|
+
// AdapterProvider, and Chakra calls `load()` once per provider. Without this
|
|
39
|
+
// cache a chat with 136 code blocks built 136 highlighters, each compiling all
|
|
40
|
+
// BUNDLED_LANGUAGES, and froze the tab for tens of seconds.
|
|
41
|
+
let highlighterPromise = null;
|
|
42
|
+
const loadHighlighter = () => {
|
|
43
|
+
highlighterPromise ?? (highlighterPromise = Promise.resolve().then(function () { return require('../../node_modules/shiki/dist/index.js'); })
|
|
44
|
+
.then(({ createHighlighter }) => createHighlighter({
|
|
45
|
+
langs: BUNDLED_LANGUAGES,
|
|
46
|
+
themes: ['github-dark'],
|
|
47
|
+
}))
|
|
48
|
+
.catch((error) => {
|
|
49
|
+
// Chakra does not catch a rejected load, so resolve to null instead: the
|
|
50
|
+
// block renders as plain text. Clear the cache so the next mount retries.
|
|
51
|
+
console.error('[logician-ui] Failed to load the Shiki highlighter', error);
|
|
52
|
+
highlighterPromise = null;
|
|
53
|
+
return null;
|
|
54
|
+
}));
|
|
55
|
+
return highlighterPromise;
|
|
56
|
+
};
|
|
57
|
+
const baseAdapter = react.createShikiAdapter({
|
|
58
|
+
load: loadHighlighter,
|
|
45
59
|
theme: 'github-dark',
|
|
46
60
|
});
|
|
61
|
+
// Chakra's default `unloadContext` calls `dispose()` when a provider unmounts.
|
|
62
|
+
// The highlighter is shared, so one block unmounting would break highlighting
|
|
63
|
+
// in every other block. Keep it alive for the life of the page.
|
|
64
|
+
const shikiAdapter = {
|
|
65
|
+
...baseAdapter,
|
|
66
|
+
unloadContext: () => { },
|
|
67
|
+
};
|
|
47
68
|
|
|
48
69
|
exports.BUNDLED_LANGUAGES = BUNDLED_LANGUAGES;
|
|
49
70
|
exports.shikiAdapter = shikiAdapter;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"shikiAdapter.js","sources":["../../../src/components/Code/shikiAdapter.ts"],"sourcesContent":[null],"names":["createShikiAdapter"],"mappings":";;;;;AAGO,MAAM,iBAAiB,GAAsB;IAClD,MAAM;IACN,GAAG;IACH,QAAQ;IACR,KAAK;IACL,MAAM;IACN,YAAY;IACZ,IAAI;IACJ,MAAM;IACN,MAAM;IACN,YAAY;IACZ,MAAM;IACN,KAAK;IACL,QAAQ;IACR,KAAK;IACL,UAAU;IACV,QAAQ;IACR,SAAS;IACT,QAAQ;IACR,GAAG;IACH,MAAM;IACN,MAAM;IACN,OAAO;IACP,KAAK;IACL,OAAO;IACP,MAAM;IACN,KAAK;IACL,YAAY;IACZ,KAAK;IACL,MAAM;;
|
|
1
|
+
{"version":3,"file":"shikiAdapter.js","sources":["../../../src/components/Code/shikiAdapter.ts"],"sourcesContent":[null],"names":["createShikiAdapter"],"mappings":";;;;;AAGO,MAAM,iBAAiB,GAAsB;IAClD,MAAM;IACN,GAAG;IACH,QAAQ;IACR,KAAK;IACL,MAAM;IACN,YAAY;IACZ,IAAI;IACJ,MAAM;IACN,MAAM;IACN,YAAY;IACZ,MAAM;IACN,KAAK;IACL,QAAQ;IACR,KAAK;IACL,UAAU;IACV,QAAQ;IACR,SAAS;IACT,QAAQ;IACR,GAAG;IACH,MAAM;IACN,MAAM;IACN,OAAO;IACP,KAAK;IACL,OAAO;IACP,MAAM;IACN,KAAK;IACL,YAAY;IACZ,KAAK;IACL,MAAM;;AAKR;AACA;AACA;AACA;AACA,IAAI,kBAAkB,GAAuC,IAAI;AAEjE,MAAM,eAAe,GAAG,MAAkC;AACxD,IAAA,kBAAkB,KAAlB,kBAAkB,GAAK,oDAAO,wCAAO;SAClC,IAAI,CAAC,CAAC,EAAE,iBAAiB,EAAE,KAC1B,iBAAiB,CAAC;AAChB,QAAA,KAAK,EAAE,iBAAiB;QACxB,MAAM,EAAE,CAAC,aAAa,CAAC;AACxB,KAAA,CAAC;AAEH,SAAA,KAAK,CAAC,CAAC,KAAc,KAAI;;;AAGxB,QAAA,OAAO,CAAC,KAAK,CACX,oDAAoD,EACpD,KAAK,CACN;QACD,kBAAkB,GAAG,IAAI;AACzB,QAAA,OAAO,IAAI;AACb,IAAA,CAAC,CAAC,CAAA;AACJ,IAAA,OAAO,kBAAkB;AAC3B,CAAC;AAED,MAAM,WAAW,GAAGA,wBAAkB,CAAqB;AACzD,IAAA,IAAI,EAAE,eAAe;AACrB,IAAA,KAAK,EAAE,aAAa;AACrB,CAAA,CAAC;AAEF;AACA;AACA;AACO,MAAM,YAAY,GAAuB;AAC9C,IAAA,GAAG,WAAW;AACd,IAAA,aAAa,EAAE,MAAK,EAAE,CAAC;;;;;;"}
|
|
@@ -32,16 +32,37 @@ const BUNDLED_LANGUAGES = [
|
|
|
32
32
|
'xml',
|
|
33
33
|
'yaml',
|
|
34
34
|
];
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
35
|
+
// One highlighter for the whole app. Every <Code> mounts its own
|
|
36
|
+
// AdapterProvider, and Chakra calls `load()` once per provider. Without this
|
|
37
|
+
// cache a chat with 136 code blocks built 136 highlighters, each compiling all
|
|
38
|
+
// BUNDLED_LANGUAGES, and froze the tab for tens of seconds.
|
|
39
|
+
let highlighterPromise = null;
|
|
40
|
+
const loadHighlighter = () => {
|
|
41
|
+
highlighterPromise ?? (highlighterPromise = import('../../node_modules/shiki/dist/index.mjs')
|
|
42
|
+
.then(({ createHighlighter }) => createHighlighter({
|
|
43
|
+
langs: BUNDLED_LANGUAGES,
|
|
44
|
+
themes: ['github-dark'],
|
|
45
|
+
}))
|
|
46
|
+
.catch((error) => {
|
|
47
|
+
// Chakra does not catch a rejected load, so resolve to null instead: the
|
|
48
|
+
// block renders as plain text. Clear the cache so the next mount retries.
|
|
49
|
+
console.error('[logician-ui] Failed to load the Shiki highlighter', error);
|
|
50
|
+
highlighterPromise = null;
|
|
51
|
+
return null;
|
|
52
|
+
}));
|
|
53
|
+
return highlighterPromise;
|
|
54
|
+
};
|
|
55
|
+
const baseAdapter = createShikiAdapter({
|
|
56
|
+
load: loadHighlighter,
|
|
43
57
|
theme: 'github-dark',
|
|
44
58
|
});
|
|
59
|
+
// Chakra's default `unloadContext` calls `dispose()` when a provider unmounts.
|
|
60
|
+
// The highlighter is shared, so one block unmounting would break highlighting
|
|
61
|
+
// in every other block. Keep it alive for the life of the page.
|
|
62
|
+
const shikiAdapter = {
|
|
63
|
+
...baseAdapter,
|
|
64
|
+
unloadContext: () => { },
|
|
65
|
+
};
|
|
45
66
|
|
|
46
67
|
export { BUNDLED_LANGUAGES, shikiAdapter };
|
|
47
68
|
//# sourceMappingURL=shikiAdapter.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"shikiAdapter.mjs","sources":["../../../src/components/Code/shikiAdapter.ts"],"sourcesContent":[null],"names":[],"mappings":";;;AAGO,MAAM,iBAAiB,GAAsB;IAClD,MAAM;IACN,GAAG;IACH,QAAQ;IACR,KAAK;IACL,MAAM;IACN,YAAY;IACZ,IAAI;IACJ,MAAM;IACN,MAAM;IACN,YAAY;IACZ,MAAM;IACN,KAAK;IACL,QAAQ;IACR,KAAK;IACL,UAAU;IACV,QAAQ;IACR,SAAS;IACT,QAAQ;IACR,GAAG;IACH,MAAM;IACN,MAAM;IACN,OAAO;IACP,KAAK;IACL,OAAO;IACP,MAAM;IACN,KAAK;IACL,YAAY;IACZ,KAAK;IACL,MAAM;;
|
|
1
|
+
{"version":3,"file":"shikiAdapter.mjs","sources":["../../../src/components/Code/shikiAdapter.ts"],"sourcesContent":[null],"names":[],"mappings":";;;AAGO,MAAM,iBAAiB,GAAsB;IAClD,MAAM;IACN,GAAG;IACH,QAAQ;IACR,KAAK;IACL,MAAM;IACN,YAAY;IACZ,IAAI;IACJ,MAAM;IACN,MAAM;IACN,YAAY;IACZ,MAAM;IACN,KAAK;IACL,QAAQ;IACR,KAAK;IACL,UAAU;IACV,QAAQ;IACR,SAAS;IACT,QAAQ;IACR,GAAG;IACH,MAAM;IACN,MAAM;IACN,OAAO;IACP,KAAK;IACL,OAAO;IACP,MAAM;IACN,KAAK;IACL,YAAY;IACZ,KAAK;IACL,MAAM;;AAKR;AACA;AACA;AACA;AACA,IAAI,kBAAkB,GAAuC,IAAI;AAEjE,MAAM,eAAe,GAAG,MAAkC;AACxD,IAAA,kBAAkB,KAAlB,kBAAkB,GAAK,OAAO,yCAAO;SAClC,IAAI,CAAC,CAAC,EAAE,iBAAiB,EAAE,KAC1B,iBAAiB,CAAC;AAChB,QAAA,KAAK,EAAE,iBAAiB;QACxB,MAAM,EAAE,CAAC,aAAa,CAAC;AACxB,KAAA,CAAC;AAEH,SAAA,KAAK,CAAC,CAAC,KAAc,KAAI;;;AAGxB,QAAA,OAAO,CAAC,KAAK,CACX,oDAAoD,EACpD,KAAK,CACN;QACD,kBAAkB,GAAG,IAAI;AACzB,QAAA,OAAO,IAAI;AACb,IAAA,CAAC,CAAC,CAAA;AACJ,IAAA,OAAO,kBAAkB;AAC3B,CAAC;AAED,MAAM,WAAW,GAAG,kBAAkB,CAAqB;AACzD,IAAA,IAAI,EAAE,eAAe;AACrB,IAAA,KAAK,EAAE,aAAa;AACrB,CAAA,CAAC;AAEF;AACA;AACA;AACO,MAAM,YAAY,GAAuB;AAC9C,IAAA,GAAG,WAAW;AACd,IAAA,aAAa,EAAE,MAAK,EAAE,CAAC;;;;;"}
|
package/package.json
CHANGED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { ChakraProvider } from '@chakra-ui/react';
|
|
2
|
+
import { render, waitFor } from '@testing-library/react';
|
|
3
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
4
|
+
|
|
5
|
+
import { system } from '../../theme';
|
|
6
|
+
|
|
7
|
+
const shiki = vi.hoisted(() => ({
|
|
8
|
+
createHighlighter: vi.fn(),
|
|
9
|
+
dispose: vi.fn(),
|
|
10
|
+
}));
|
|
11
|
+
|
|
12
|
+
// The real Shiki compiles grammars through WASM, which is slow in jsdom and
|
|
13
|
+
// not what these tests are about. The fake marks highlighted output with a
|
|
14
|
+
// class so a test can tell highlighted text from the plain-text fallback.
|
|
15
|
+
vi.mock('shiki', () => ({
|
|
16
|
+
createHighlighter: shiki.createHighlighter,
|
|
17
|
+
}));
|
|
18
|
+
|
|
19
|
+
const fakeHighlighter = {
|
|
20
|
+
codeToHtml: (code: string) =>
|
|
21
|
+
`<pre><code><span class="test-highlighted">${code}</span></code></pre>`,
|
|
22
|
+
dispose: shiki.dispose,
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
// shikiAdapter caches the highlighter at module level. Re-import Code per test
|
|
26
|
+
// so each one starts with an empty cache.
|
|
27
|
+
const loadCode = async () => (await import('./Code')).Code;
|
|
28
|
+
|
|
29
|
+
const wrap = (ui: React.ReactNode) =>
|
|
30
|
+
render(<ChakraProvider value={system}>{ui}</ChakraProvider>);
|
|
31
|
+
|
|
32
|
+
beforeEach(() => {
|
|
33
|
+
vi.resetModules();
|
|
34
|
+
shiki.createHighlighter.mockReset();
|
|
35
|
+
shiki.createHighlighter.mockResolvedValue(fakeHighlighter);
|
|
36
|
+
shiki.dispose.mockReset();
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
describe('Code highlighter', () => {
|
|
40
|
+
it('creates one highlighter for many code blocks', async () => {
|
|
41
|
+
const Code = await loadCode();
|
|
42
|
+
const { container } = wrap(
|
|
43
|
+
<>
|
|
44
|
+
{Array.from({ length: 20 }, (_, i) => (
|
|
45
|
+
<Code key={i} language="javascript">{`const n = ${i};`}</Code>
|
|
46
|
+
))}
|
|
47
|
+
</>
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
await waitFor(() =>
|
|
51
|
+
expect(container.querySelectorAll('.test-highlighted')).toHaveLength(20)
|
|
52
|
+
);
|
|
53
|
+
expect(shiki.createHighlighter).toHaveBeenCalledTimes(1);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('keeps highlighting the other blocks when one block unmounts', async () => {
|
|
57
|
+
const Code = await loadCode();
|
|
58
|
+
const Blocks = ({ showFirst }: { showFirst: boolean }) => (
|
|
59
|
+
<>
|
|
60
|
+
{showFirst && <Code language="javascript">first</Code>}
|
|
61
|
+
<Code language="javascript">second</Code>
|
|
62
|
+
</>
|
|
63
|
+
);
|
|
64
|
+
const { container, rerender } = wrap(<Blocks showFirst />);
|
|
65
|
+
await waitFor(() =>
|
|
66
|
+
expect(container.querySelectorAll('.test-highlighted')).toHaveLength(2)
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
rerender(
|
|
70
|
+
<ChakraProvider value={system}>
|
|
71
|
+
<Blocks showFirst={false} />
|
|
72
|
+
</ChakraProvider>
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
expect(shiki.dispose).not.toHaveBeenCalled();
|
|
76
|
+
expect(container.querySelectorAll('.test-highlighted')).toHaveLength(1);
|
|
77
|
+
|
|
78
|
+
// A block mounted later reuses the same highlighter.
|
|
79
|
+
rerender(
|
|
80
|
+
<ChakraProvider value={system}>
|
|
81
|
+
<Blocks showFirst />
|
|
82
|
+
</ChakraProvider>
|
|
83
|
+
);
|
|
84
|
+
await waitFor(() =>
|
|
85
|
+
expect(container.querySelectorAll('.test-highlighted')).toHaveLength(2)
|
|
86
|
+
);
|
|
87
|
+
expect(shiki.createHighlighter).toHaveBeenCalledTimes(1);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('retries on the next mount when loading fails', async () => {
|
|
91
|
+
const consoleError = vi
|
|
92
|
+
.spyOn(console, 'error')
|
|
93
|
+
.mockImplementation(() => {});
|
|
94
|
+
shiki.createHighlighter.mockRejectedValueOnce(new Error('load failed'));
|
|
95
|
+
const Code = await loadCode();
|
|
96
|
+
|
|
97
|
+
const first = wrap(<Code language="javascript">a</Code>);
|
|
98
|
+
await waitFor(() =>
|
|
99
|
+
expect(consoleError).toHaveBeenCalledWith(
|
|
100
|
+
expect.stringContaining('Failed to load the Shiki highlighter'),
|
|
101
|
+
expect.any(Error)
|
|
102
|
+
)
|
|
103
|
+
);
|
|
104
|
+
// The failed block falls back to plain text instead of throwing.
|
|
105
|
+
expect(first.container).toHaveTextContent('a');
|
|
106
|
+
expect(first.container.querySelector('.test-highlighted')).toBeNull();
|
|
107
|
+
first.unmount();
|
|
108
|
+
|
|
109
|
+
const { container } = wrap(<Code language="javascript">b</Code>);
|
|
110
|
+
await waitFor(() =>
|
|
111
|
+
expect(container.querySelectorAll('.test-highlighted')).toHaveLength(1)
|
|
112
|
+
);
|
|
113
|
+
expect(shiki.createHighlighter).toHaveBeenCalledTimes(2);
|
|
114
|
+
consoleError.mockRestore();
|
|
115
|
+
});
|
|
116
|
+
});
|
|
@@ -33,15 +33,44 @@ export const BUNDLED_LANGUAGES: BundledLanguage[] = [
|
|
|
33
33
|
'yaml',
|
|
34
34
|
];
|
|
35
35
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
>
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
36
|
+
type Highlighter = HighlighterGeneric<BundledLanguage, BundledTheme>;
|
|
37
|
+
|
|
38
|
+
// One highlighter for the whole app. Every <Code> mounts its own
|
|
39
|
+
// AdapterProvider, and Chakra calls `load()` once per provider. Without this
|
|
40
|
+
// cache a chat with 136 code blocks built 136 highlighters, each compiling all
|
|
41
|
+
// BUNDLED_LANGUAGES, and froze the tab for tens of seconds.
|
|
42
|
+
let highlighterPromise: Promise<Highlighter | null> | null = null;
|
|
43
|
+
|
|
44
|
+
const loadHighlighter = (): Promise<Highlighter | null> => {
|
|
45
|
+
highlighterPromise ??= import('shiki')
|
|
46
|
+
.then(({ createHighlighter }) =>
|
|
47
|
+
createHighlighter({
|
|
48
|
+
langs: BUNDLED_LANGUAGES,
|
|
49
|
+
themes: ['github-dark'],
|
|
50
|
+
})
|
|
51
|
+
)
|
|
52
|
+
.catch((error: unknown) => {
|
|
53
|
+
// Chakra does not catch a rejected load, so resolve to null instead: the
|
|
54
|
+
// block renders as plain text. Clear the cache so the next mount retries.
|
|
55
|
+
console.error(
|
|
56
|
+
'[logician-ui] Failed to load the Shiki highlighter',
|
|
57
|
+
error
|
|
58
|
+
);
|
|
59
|
+
highlighterPromise = null;
|
|
60
|
+
return null;
|
|
44
61
|
});
|
|
45
|
-
|
|
62
|
+
return highlighterPromise;
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
const baseAdapter = createShikiAdapter<Highlighter | null>({
|
|
66
|
+
load: loadHighlighter,
|
|
46
67
|
theme: 'github-dark',
|
|
47
68
|
});
|
|
69
|
+
|
|
70
|
+
// Chakra's default `unloadContext` calls `dispose()` when a provider unmounts.
|
|
71
|
+
// The highlighter is shared, so one block unmounting would break highlighting
|
|
72
|
+
// in every other block. Keep it alive for the life of the page.
|
|
73
|
+
export const shikiAdapter: typeof baseAdapter = {
|
|
74
|
+
...baseAdapter,
|
|
75
|
+
unloadContext: () => {},
|
|
76
|
+
};
|