@aleph-alpha/chat-kit 5.12.0 → 6.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{CkHistory.vue_vue_type_script_setup_true_lang-CImSzLYs.js → CkHistory.vue_vue_type_script_setup_true_lang-D9Ph9eYp.js} +115 -31
- package/dist/adapters/responses-api.d.ts +1 -1
- package/dist/adapters/responses-api.d.ts.map +1 -1
- package/dist/adapters/responses-api.js +9 -7
- package/dist/chat-kit.css +12 -12
- package/dist/components/base/CkCitation/CkCitation.stories.d.ts +1 -4
- package/dist/components/base/CkCitation/CkCitation.stories.d.ts.map +1 -1
- package/dist/components/base/CkCitation/CkCitation.vue.d.ts.map +1 -1
- package/dist/components/base/CkCitation/types.d.ts +6 -1
- package/dist/components/base/CkCitation/types.d.ts.map +1 -1
- package/dist/components/base/CkCitationCard/CkCitationCard.stories.d.ts +13 -2
- package/dist/components/base/CkCitationCard/CkCitationCard.stories.d.ts.map +1 -1
- package/dist/components/base/CkCitationCard/CkCitationCard.vue.d.ts +1 -0
- package/dist/components/base/CkCitationCard/CkCitationCard.vue.d.ts.map +1 -1
- package/dist/components/base/CkCitationCard/types.d.ts +6 -1
- package/dist/components/base/CkCitationCard/types.d.ts.map +1 -1
- package/dist/components/base/CkCitationLabel/CkCitationLabel.vue.d.ts.map +1 -1
- package/dist/components/base/CkHistoryItem/CkHistoryItem.vue.d.ts.map +1 -1
- package/dist/components/base/CkHistoryWrapper/CkHistoryWrapper.vue.d.ts.map +1 -1
- package/dist/components/base/CkMarkdownRenderer/CkMarkdownRenderer.stories.d.ts.map +1 -1
- package/dist/components/index.js +1 -1
- package/dist/composables/fileUpload/useFileUpload.d.ts.map +1 -1
- package/dist/composables/index.js +1 -1
- package/dist/composables/useChat.d.ts.map +1 -1
- package/dist/composables/useConversations.d.ts.map +1 -1
- package/dist/composables/useIsTruncated.d.ts +22 -0
- package/dist/composables/useIsTruncated.d.ts.map +1 -0
- package/dist/handlers/index.js +1 -1
- package/dist/handlers/useChatKitLabels.d.ts +35 -1
- package/dist/handlers/useChatKitLabels.d.ts.map +1 -1
- package/dist/{index-Dn4ctxk8.js → index-C72e57-5.js} +48 -24
- package/dist/index.js +3 -3
- package/dist/test-utils/createTextPlugin.d.ts +7 -1
- package/dist/test-utils/createTextPlugin.d.ts.map +1 -1
- package/dist/{useChatKitLabels-CMOr3Kjq.js → useChatKitLabels-368lodS5.js} +1 -1
- package/package.json +4 -4
- package/src/adapters/responses-api.spec.ts +86 -0
- package/src/adapters/responses-api.ts +13 -9
- package/src/components/base/CkCitation/CkCitation.spec.ts +3 -3
- package/src/components/base/CkCitation/CkCitation.stories.ts +85 -3
- package/src/components/base/CkCitation/CkCitation.vue +5 -1
- package/src/components/base/CkCitation/types.ts +6 -1
- package/src/components/base/CkCitationCard/CkCitationCard.spec.ts +37 -0
- package/src/components/base/CkCitationCard/CkCitationCard.stories.ts +30 -3
- package/src/components/base/CkCitationCard/CkCitationCard.vue +54 -12
- package/src/components/base/CkCitationCard/types.ts +6 -1
- package/src/components/base/CkCitationLabel/CkCitationLabel.spec.ts +3 -3
- package/src/components/base/CkCitationLabel/CkCitationLabel.vue +7 -6
- package/src/components/base/CkConversationLayout/CkConversationLayout.spec.ts +1 -5
- package/src/components/base/CkCopyMessageButton/CkCopyMessageButton.spec.ts +1 -1
- package/src/components/base/CkFeedbackButton/CkFeedbackButton.vue +3 -3
- package/src/components/base/CkFeedbackDialog/CkFeedbackDialog.spec.ts +1 -1
- package/src/components/base/CkFeedbackDialog/CkFeedbackDialog.vue +9 -9
- package/src/components/base/CkHistoryItem/CkHistoryItem.vue +5 -7
- package/src/components/base/CkHistoryWrapper/CkHistoryWrapper.vue +2 -1
- package/src/components/base/CkMarkdownRenderer/CkMarkdownRenderer.stories.ts +9 -4
- package/src/components/base/CkMarkdownRenderer/CkMarkdownRenderer.vue +1 -1
- package/src/components/base/CkModelSelector/CkModelSelector.vue +3 -3
- package/src/composables/fileUpload/useFileUpload.ts +45 -30
- package/src/composables/fileUpload/useFileUploadWrapper.spec.ts +1 -1
- package/src/composables/fileUpload/useFileUploadWrapper.ts +3 -3
- package/src/composables/useChat.ts +3 -1
- package/src/composables/useConversations.ts +8 -5
- package/src/composables/useIsTruncated.ts +82 -0
- package/src/handlers/useChatKitLabels.spec.ts +0 -1
- package/src/handlers/useChatKitLabels.ts +2 -2
- package/src/handlers/useMarkdownParser.ts +1 -1
- package/src/test-setup.ts +3 -3
- package/src/test-utils/createTextPlugin.ts +11 -3
|
@@ -24,7 +24,8 @@ const rootClass = computed(() => {
|
|
|
24
24
|
});
|
|
25
25
|
|
|
26
26
|
const passthroughAttrs = computed(() => {
|
|
27
|
-
const
|
|
27
|
+
const rest = { ...(attrs as Record<string, unknown>) };
|
|
28
|
+
delete rest.class;
|
|
28
29
|
return rest;
|
|
29
30
|
});
|
|
30
31
|
</script>
|
|
@@ -394,16 +394,21 @@ export const MathExpressions: Story = {
|
|
|
394
394
|
parameters: { docs: { source: { code: mathTemplateSource } } },
|
|
395
395
|
};
|
|
396
396
|
|
|
397
|
-
|
|
398
|
-
|
|
397
|
+
type MdTextNode = { type: string; value?: string; children?: MdTextNode[] };
|
|
398
|
+
|
|
399
|
+
function visitTextNodes(
|
|
400
|
+
node: MdTextNode,
|
|
401
|
+
transform: (value: string) => string,
|
|
402
|
+
): void {
|
|
403
|
+
if (node.type === 'text' && node.value !== undefined) {
|
|
399
404
|
node.value = transform(node.value);
|
|
400
405
|
}
|
|
401
406
|
if (node.children) {
|
|
402
|
-
node.children.forEach((child
|
|
407
|
+
node.children.forEach((child) => visitTextNodes(child, transform));
|
|
403
408
|
}
|
|
404
409
|
}
|
|
405
410
|
|
|
406
|
-
const uppercasePlugin = () => (tree:
|
|
411
|
+
const uppercasePlugin = () => (tree: MdTextNode) => {
|
|
407
412
|
visitTextNodes(tree, (value) => value.toUpperCase());
|
|
408
413
|
};
|
|
409
414
|
|
|
@@ -146,7 +146,7 @@ export default defineComponent({
|
|
|
146
146
|
classes: mergedClasses.value,
|
|
147
147
|
tableLabel:
|
|
148
148
|
labels.value.markdownTableLabel ??
|
|
149
|
-
defaultChatKitLabels.markdownTableLabel
|
|
149
|
+
defaultChatKitLabels.markdownTableLabel,
|
|
150
150
|
renderCitation: (el) => renderCitationNode(el, slots.citation),
|
|
151
151
|
};
|
|
152
152
|
const children = parsedNodes.value
|
|
@@ -32,17 +32,17 @@ const labels = useChatKitLabels();
|
|
|
32
32
|
const triggerAriaLabel = computed(
|
|
33
33
|
() =>
|
|
34
34
|
labels.value.modelSelectorAriaLabel ??
|
|
35
|
-
defaultChatKitLabels.modelSelectorAriaLabel
|
|
35
|
+
defaultChatKitLabels.modelSelectorAriaLabel,
|
|
36
36
|
);
|
|
37
37
|
const listAriaLabel = computed(
|
|
38
38
|
() =>
|
|
39
39
|
labels.value.modelSelectorListAriaLabel ??
|
|
40
|
-
defaultChatKitLabels.modelSelectorListAriaLabel
|
|
40
|
+
defaultChatKitLabels.modelSelectorListAriaLabel,
|
|
41
41
|
);
|
|
42
42
|
const placeholderLabel = computed(
|
|
43
43
|
() =>
|
|
44
44
|
labels.value.modelSelectorPlaceholder ??
|
|
45
|
-
defaultChatKitLabels.modelSelectorPlaceholder
|
|
45
|
+
defaultChatKitLabels.modelSelectorPlaceholder,
|
|
46
46
|
);
|
|
47
47
|
|
|
48
48
|
const open = ref(false);
|
|
@@ -137,8 +137,9 @@ class Semaphore {
|
|
|
137
137
|
}
|
|
138
138
|
|
|
139
139
|
release(): void {
|
|
140
|
-
|
|
141
|
-
|
|
140
|
+
const next = this.queue.shift();
|
|
141
|
+
if (next) {
|
|
142
|
+
next();
|
|
142
143
|
} else {
|
|
143
144
|
this.permits++;
|
|
144
145
|
}
|
|
@@ -184,7 +185,12 @@ function generateCanvasThumbnail(
|
|
|
184
185
|
const canvas = document.createElement('canvas');
|
|
185
186
|
canvas.width = w;
|
|
186
187
|
canvas.height = h;
|
|
187
|
-
const ctx = canvas.getContext('2d')
|
|
188
|
+
const ctx = canvas.getContext('2d');
|
|
189
|
+
if (!ctx) {
|
|
190
|
+
URL.revokeObjectURL(url);
|
|
191
|
+
reject(new Error('Canvas 2D context is not available'));
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
188
194
|
ctx.drawImage(img, 0, 0, w, h);
|
|
189
195
|
|
|
190
196
|
const dataUrl = canvas.toDataURL('image/png');
|
|
@@ -284,12 +290,18 @@ async function runCustomValidators(
|
|
|
284
290
|
// Retry
|
|
285
291
|
// ---------------------------------------------------------------------------
|
|
286
292
|
|
|
293
|
+
function extractHttpStatus(err: unknown): number | undefined {
|
|
294
|
+
const e = err as {
|
|
295
|
+
httpStatus?: number;
|
|
296
|
+
status?: number;
|
|
297
|
+
response?: { status?: number };
|
|
298
|
+
} | null;
|
|
299
|
+
return e?.httpStatus ?? e?.status ?? e?.response?.status;
|
|
300
|
+
}
|
|
301
|
+
|
|
287
302
|
function isRetryable(err: unknown, policy: RetryPolicy): boolean {
|
|
288
303
|
if (!policy.retryableStatusCodes?.length) return true;
|
|
289
|
-
const status =
|
|
290
|
-
(err as any)?.httpStatus ??
|
|
291
|
-
(err as any)?.status ??
|
|
292
|
-
(err as any)?.response?.status;
|
|
304
|
+
const status = extractHttpStatus(err);
|
|
293
305
|
return status != null && policy.retryableStatusCodes.includes(status);
|
|
294
306
|
}
|
|
295
307
|
|
|
@@ -608,10 +620,11 @@ export function useFileUpload(
|
|
|
608
620
|
|
|
609
621
|
for (let i = 0; i < uploadReady.length; i++) {
|
|
610
622
|
const { stateKey } = uploadReady[i] as { file: File; stateKey: string };
|
|
611
|
-
const entry = files.value.get(stateKey)
|
|
623
|
+
const entry = files.value.get(stateKey);
|
|
624
|
+
if (!entry) continue;
|
|
625
|
+
const startedAt = entry.uploadStartedAt ?? completedAt;
|
|
612
626
|
const durationMs =
|
|
613
|
-
new Date(completedAt).getTime() -
|
|
614
|
-
new Date(entry.uploadStartedAt!).getTime();
|
|
627
|
+
new Date(completedAt).getTime() - new Date(startedAt).getTime();
|
|
615
628
|
|
|
616
629
|
const remoteResult = results[i] as
|
|
617
630
|
| { id?: string; documentId?: string }
|
|
@@ -682,12 +695,13 @@ export function useFileUpload(
|
|
|
682
695
|
const uploadErr: FileUploadError = {
|
|
683
696
|
code: 'UPLOAD_FAILED',
|
|
684
697
|
message: (err as Error).message ?? 'Upload failed.',
|
|
685
|
-
httpStatus: (err
|
|
698
|
+
httpStatus: extractHttpStatus(err),
|
|
686
699
|
occurredAt: completedAt,
|
|
687
700
|
};
|
|
688
701
|
|
|
689
702
|
for (const { stateKey } of uploadReady) {
|
|
690
|
-
const entry = files.value.get(stateKey)
|
|
703
|
+
const entry = files.value.get(stateKey);
|
|
704
|
+
if (!entry) continue;
|
|
691
705
|
const durationMs = entry.uploadStartedAt
|
|
692
706
|
? new Date(completedAt).getTime() -
|
|
693
707
|
new Date(entry.uploadStartedAt).getTime()
|
|
@@ -764,16 +778,17 @@ export function useFileUpload(
|
|
|
764
778
|
|
|
765
779
|
const uploadStarted = now();
|
|
766
780
|
for (const { stateKey } of uploadReady) {
|
|
781
|
+
const attemptCount = (files.value.get(stateKey)?.attemptCount ?? 0) + 1;
|
|
767
782
|
updateEntry(stateKey, {
|
|
768
783
|
status: 'uploading',
|
|
769
784
|
uploadStartedAt: uploadStarted,
|
|
770
|
-
attemptCount
|
|
785
|
+
attemptCount,
|
|
771
786
|
lastAttemptAt: uploadStarted,
|
|
772
787
|
});
|
|
773
788
|
emit({
|
|
774
789
|
type: 'file:uploading',
|
|
775
790
|
fileId: stateKey,
|
|
776
|
-
attemptCount
|
|
791
|
+
attemptCount,
|
|
777
792
|
});
|
|
778
793
|
}
|
|
779
794
|
|
|
@@ -863,17 +878,20 @@ export function useFileUpload(
|
|
|
863
878
|
): UploadResult {
|
|
864
879
|
return {
|
|
865
880
|
id: groupId,
|
|
866
|
-
results: batch.
|
|
867
|
-
const entry = files.value.get(stateKey)
|
|
868
|
-
return
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
881
|
+
results: batch.flatMap(({ stateKey }) => {
|
|
882
|
+
const entry = files.value.get(stateKey);
|
|
883
|
+
if (!entry) return [];
|
|
884
|
+
return [
|
|
885
|
+
{
|
|
886
|
+
fileId: stateKey,
|
|
887
|
+
success: entry.status === 'success',
|
|
888
|
+
status: entry.status,
|
|
889
|
+
remoteFileId: entry.remoteFileId ?? undefined,
|
|
890
|
+
error: entry.error ?? undefined,
|
|
891
|
+
uploadDate: entry.uploadCompletedAt ?? entry.queuedAt,
|
|
892
|
+
metadata: entry.metadata,
|
|
893
|
+
} satisfies UploadFileResult,
|
|
894
|
+
];
|
|
877
895
|
}),
|
|
878
896
|
};
|
|
879
897
|
}
|
|
@@ -927,10 +945,7 @@ export function useFileUpload(
|
|
|
927
945
|
try {
|
|
928
946
|
await client.delete(endpoints.file(storeId, entry.remoteFileId));
|
|
929
947
|
} catch (err) {
|
|
930
|
-
const status =
|
|
931
|
-
(err as any)?.httpStatus ??
|
|
932
|
-
(err as any)?.status ??
|
|
933
|
-
(err as any)?.response?.status;
|
|
948
|
+
const status = extractHttpStatus(err);
|
|
934
949
|
|
|
935
950
|
if (status === 404) {
|
|
936
951
|
// File no longer exists on the server — proceed with local removal.
|
|
@@ -1022,7 +1037,7 @@ export function useFileUpload(
|
|
|
1022
1037
|
files.value.delete(entry.id);
|
|
1023
1038
|
if (db) {
|
|
1024
1039
|
deletePersistedEntry(db, persistStoreName, entry.id).catch(
|
|
1025
|
-
() =>
|
|
1040
|
+
() => undefined,
|
|
1026
1041
|
);
|
|
1027
1042
|
}
|
|
1028
1043
|
}
|
|
@@ -206,7 +206,7 @@ describe('useFileUploadWrapper handleFilesSelected', () => {
|
|
|
206
206
|
const { handleFilesSelected } = provider.useDocumentUpload('conv-1');
|
|
207
207
|
|
|
208
208
|
const event = makeInputEvent([createFile('a.pdf')]);
|
|
209
|
-
const input =
|
|
209
|
+
const input = event.target as unknown as HTMLInputElement;
|
|
210
210
|
|
|
211
211
|
const valueSetter = vi.spyOn(input, 'value', 'set');
|
|
212
212
|
|
|
@@ -401,9 +401,9 @@ export function createDocumentUploadProvider(
|
|
|
401
401
|
}
|
|
402
402
|
}
|
|
403
403
|
|
|
404
|
-
const successFileIds = result.results
|
|
405
|
-
.
|
|
406
|
-
|
|
404
|
+
const successFileIds = result.results.flatMap((r) =>
|
|
405
|
+
r.success && r.remoteFileId ? [r.remoteFileId] : [],
|
|
406
|
+
);
|
|
407
407
|
|
|
408
408
|
if (successFileIds.length > 0) {
|
|
409
409
|
await upload.runWorkflow(successFileIds);
|
|
@@ -216,11 +216,14 @@ export function useConversations(
|
|
|
216
216
|
const raw = await http.post<RawConversation>(path, body);
|
|
217
217
|
const created = mapConversation(raw);
|
|
218
218
|
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
219
|
+
if (placeholder) {
|
|
220
|
+
const placeholderId = placeholder.id;
|
|
221
|
+
conversations.value = conversations.value.map((c) =>
|
|
222
|
+
c.id === placeholderId ? created : c,
|
|
223
|
+
);
|
|
224
|
+
} else {
|
|
225
|
+
conversations.value = [created, ...conversations.value];
|
|
226
|
+
}
|
|
224
227
|
|
|
225
228
|
return created;
|
|
226
229
|
} catch (e) {
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Local copy of ui-library's internal probe of the same name. chat-kit's
|
|
3
|
+
* supported ui-library range (^5.3.0) does not export it, and it is too small
|
|
4
|
+
* to justify widening that package's public API. Drop this file once the range
|
|
5
|
+
* moves up to a version that exports it.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { onScopeDispose, ref, watch, type Ref, type ShallowRef } from 'vue';
|
|
9
|
+
|
|
10
|
+
/** An element ref as produced by `useTemplateRef`. */
|
|
11
|
+
type ElementRef = Readonly<ShallowRef<HTMLElement | null>>;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* One observer for every caller. `ResizeObserver` is built to watch many
|
|
15
|
+
* targets and delivers them in a single batched callback, so a list of rows
|
|
16
|
+
* costs one callback and one layout pass rather than one per row. Created
|
|
17
|
+
* lazily so importing this module never touches the DOM.
|
|
18
|
+
*/
|
|
19
|
+
const measurers = new WeakMap<Element, () => void>();
|
|
20
|
+
let sharedObserver: ResizeObserver | undefined;
|
|
21
|
+
|
|
22
|
+
function getSharedObserver(): ResizeObserver | undefined {
|
|
23
|
+
if (typeof ResizeObserver === 'undefined') return undefined;
|
|
24
|
+
sharedObserver ??= new ResizeObserver((entries) => {
|
|
25
|
+
for (const entry of entries) measurers.get(entry.target)?.();
|
|
26
|
+
});
|
|
27
|
+
return sharedObserver;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function watchElement(el: Element, measure: () => void): void {
|
|
31
|
+
measurers.set(el, measure);
|
|
32
|
+
getSharedObserver()?.observe(el);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function unwatchElement(el: Element): void {
|
|
36
|
+
measurers.delete(el);
|
|
37
|
+
getSharedObserver()?.unobserve(el);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Whether an element's text is currently clipped by `text-overflow: ellipsis`.
|
|
42
|
+
*
|
|
43
|
+
* Re-measures when the element resizes and whenever `watchSource` changes, so
|
|
44
|
+
* it stays correct as the container narrows or the text is swapped out. Use it
|
|
45
|
+
* to show a tooltip only for names that are actually cut off.
|
|
46
|
+
* @param elementRef - The element whose text may be clipped
|
|
47
|
+
* @param watchSource - Value to re-measure on, e.g. the rendered text
|
|
48
|
+
* @returns Reactive truncation state
|
|
49
|
+
*/
|
|
50
|
+
export function useIsTruncated(
|
|
51
|
+
elementRef: ElementRef,
|
|
52
|
+
watchSource?: () => unknown,
|
|
53
|
+
): Ref<boolean> {
|
|
54
|
+
const isTruncated = ref(false);
|
|
55
|
+
|
|
56
|
+
function measure(): void {
|
|
57
|
+
const el = elementRef.value;
|
|
58
|
+
// Sub-pixel widths round differently per browser, so require a whole
|
|
59
|
+
// pixel of overflow before calling it truncated.
|
|
60
|
+
isTruncated.value = el ? el.scrollWidth - el.clientWidth >= 1 : false;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// A resize alone doesn't cover a text swap that keeps the box the same size.
|
|
64
|
+
if (watchSource) watch(watchSource, measure, { flush: 'post' });
|
|
65
|
+
|
|
66
|
+
// Runs on mount too, when the template ref goes null -> element.
|
|
67
|
+
watch(
|
|
68
|
+
elementRef,
|
|
69
|
+
(el, previous) => {
|
|
70
|
+
if (previous) unwatchElement(previous);
|
|
71
|
+
if (el) watchElement(el, measure);
|
|
72
|
+
measure();
|
|
73
|
+
},
|
|
74
|
+
{ immediate: true, flush: 'post' },
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
onScopeDispose(() => {
|
|
78
|
+
if (elementRef.value) unwatchElement(elementRef.value);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
return isTruncated;
|
|
82
|
+
}
|
|
@@ -114,7 +114,7 @@ export type ChatKitLabels = {
|
|
|
114
114
|
};
|
|
115
115
|
|
|
116
116
|
/** English defaults used when no labels are provided by the consumer. */
|
|
117
|
-
export const defaultChatKitLabels
|
|
117
|
+
export const defaultChatKitLabels = {
|
|
118
118
|
agentAvatarAlt: 'Agent avatar',
|
|
119
119
|
agentLoadingAvatarAlt: 'Agent loading',
|
|
120
120
|
inputPlaceholder: 'Type a message...',
|
|
@@ -150,7 +150,7 @@ export const defaultChatKitLabels: ChatKitLabels = {
|
|
|
150
150
|
historyItemEditAction: 'Edit',
|
|
151
151
|
historyItemDeleteAction: 'Delete',
|
|
152
152
|
historyShowAll: 'Show all',
|
|
153
|
-
}
|
|
153
|
+
} satisfies Required<ChatKitLabels>;
|
|
154
154
|
|
|
155
155
|
/** Vue injection key for ChatKit labels. */
|
|
156
156
|
export const CHAT_KIT_LABELS: InjectionKey<ComputedRef<ChatKitLabels>> =
|
|
@@ -100,7 +100,7 @@ export function useMarkdownParser(
|
|
|
100
100
|
let cachedProcessor: SyncProcessor | undefined;
|
|
101
101
|
|
|
102
102
|
function getProcessor(list: PluggableList): SyncProcessor {
|
|
103
|
-
if (list === cachedPlugins) return cachedProcessor
|
|
103
|
+
if (list === cachedPlugins && cachedProcessor) return cachedProcessor;
|
|
104
104
|
cachedPlugins = list;
|
|
105
105
|
cachedProcessor = buildProcessor(list);
|
|
106
106
|
return cachedProcessor;
|
package/src/test-setup.ts
CHANGED
|
@@ -6,7 +6,7 @@ import '@testing-library/jest-dom/vitest';
|
|
|
6
6
|
|
|
7
7
|
// ResizeObserver is not available in JSDOM
|
|
8
8
|
global.ResizeObserver = class ResizeObserver {
|
|
9
|
-
observe(): void
|
|
10
|
-
unobserve(): void
|
|
11
|
-
disconnect(): void
|
|
9
|
+
observe = (): void => undefined;
|
|
10
|
+
unobserve = (): void => undefined;
|
|
11
|
+
disconnect = (): void => undefined;
|
|
12
12
|
};
|
|
@@ -1,7 +1,15 @@
|
|
|
1
|
+
type TextPluginNode = {
|
|
2
|
+
type: string;
|
|
3
|
+
value?: string;
|
|
4
|
+
children?: TextPluginNode[];
|
|
5
|
+
};
|
|
6
|
+
|
|
1
7
|
export function createTextPlugin(transform: (text: string) => string) {
|
|
2
|
-
return () => (tree:
|
|
3
|
-
const visit = (node:
|
|
4
|
-
if (node.type === 'text'
|
|
8
|
+
return () => (tree: TextPluginNode) => {
|
|
9
|
+
const visit = (node: TextPluginNode) => {
|
|
10
|
+
if (node.type === 'text' && node.value !== undefined) {
|
|
11
|
+
node.value = transform(node.value);
|
|
12
|
+
}
|
|
5
13
|
if (node.children) node.children.forEach(visit);
|
|
6
14
|
};
|
|
7
15
|
visit(tree);
|