@particle-academy/react-fancy 5.67.0 → 5.68.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/CHANGELOG.md +39 -0
- package/dist/ContentRenderer.types-RO_l7_6N.d.cts +137 -0
- package/dist/ContentRenderer.types-RO_l7_6N.d.ts +137 -0
- package/dist/{chunk-FXOT4P4P.js → chunk-2LQR4EAN.js} +131 -55
- package/dist/chunk-2LQR4EAN.js.map +1 -0
- package/dist/{chunk-E5UU666U.cjs → chunk-464QSZ4A.cjs} +131 -55
- package/dist/chunk-464QSZ4A.cjs.map +1 -0
- package/dist/{chunk-4S5DFN5N.cjs → chunk-E4DXICU3.cjs} +17 -11
- package/dist/chunk-E4DXICU3.cjs.map +1 -0
- package/dist/{chunk-HEY4SMS2.js → chunk-TQ77ZWBD.js} +10 -4
- package/dist/chunk-TQ77ZWBD.js.map +1 -0
- package/dist/content-renderer.cjs +4 -4
- package/dist/content-renderer.d.cts +3 -72
- package/dist/content-renderer.d.ts +3 -72
- package/dist/content-renderer.js +1 -1
- package/dist/editor.cjs +4 -4
- package/dist/editor.d.cts +19 -2
- package/dist/editor.d.ts +19 -2
- package/dist/editor.js +2 -2
- package/dist/index.cjs +8 -8
- package/dist/index.d.cts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -2
- package/docs/ContentRenderer.md +59 -5
- package/docs/Editor.md +38 -0
- package/package.json +1 -1
- package/dist/chunk-4S5DFN5N.cjs.map +0 -1
- package/dist/chunk-E5UU666U.cjs.map +0 -1
- package/dist/chunk-FXOT4P4P.js.map +0 -1
- package/dist/chunk-HEY4SMS2.js.map +0 -1
- package/dist/extensions-DiByZIpD.d.cts +0 -61
- package/dist/extensions-DiByZIpD.d.ts +0 -61
package/CHANGELOG.md
CHANGED
|
@@ -11,6 +11,45 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
11
11
|
|
|
12
12
|
## [Unreleased]
|
|
13
13
|
|
|
14
|
+
## [5.68.0] - 2026-10-27
|
|
15
|
+
|
|
16
|
+
### Added
|
|
17
|
+
|
|
18
|
+
- **`Editor` forwards `copyButtons`, `clipboardWriter` and `onCopy` to the
|
|
19
|
+
`ContentRenderer` its view mode renders through.** 5.67.0 turned copy buttons
|
|
20
|
+
on in view mode but gave a host no way to route those copies (a sandboxed
|
|
21
|
+
Electron window), observe them, or turn them off from `Editor`. Same types and
|
|
22
|
+
same defaults as on `ContentRenderer`; with the props absent nothing changes.
|
|
23
|
+
They have no effect in edit mode. Editor's docs now also document `mode`.
|
|
24
|
+
- **`ContentRenderer streaming="progressive"`: an open block shows as it
|
|
25
|
+
fills.** `streaming={true}` still hides an open block until it closes. In the
|
|
26
|
+
new mode:
|
|
27
|
+
- an open tag of a registered extension whose opening tag is complete renders
|
|
28
|
+
its component with the body so far (a `markdown` extension gets it as
|
|
29
|
+
`content` and rendered, sanitised, as `children`);
|
|
30
|
+
- an open code fence renders as a code block of what has arrived, with **no
|
|
31
|
+
copy button until it closes** (hidden rather than disabled: half a block is
|
|
32
|
+
never what a copy means). Finished blocks before it keep theirs;
|
|
33
|
+
- a partial opening tag (`<result title="Do`), a partial closing tag, a fence
|
|
34
|
+
marker still arriving and a fence opener line with no newline yet are still
|
|
35
|
+
held back, so none of it flashes raw. Inside an open fence `<` is code;
|
|
36
|
+
- it closes the open fence and open tags itself and renders normally, so the
|
|
37
|
+
body is the one the final render shows once the real closes arrive.
|
|
38
|
+
- The root carries `data-streaming-mode="progressive"` as well as
|
|
39
|
+
`data-streaming="true"`.
|
|
40
|
+
|
|
41
|
+
It is a value of `streaming` rather than a new prop because it is a way of
|
|
42
|
+
streaming: a separate flag would add a combination that means nothing.
|
|
43
|
+
`streaming` is now typed `boolean | "progressive"`. Code that passes a
|
|
44
|
+
boolean is unaffected; code that READS the prop's type and assumes `boolean`
|
|
45
|
+
needs to allow the string.
|
|
46
|
+
|
|
47
|
+
### Changed
|
|
48
|
+
|
|
49
|
+
- **Docs: `markdown` tags are lifted only at line start and only when closed,
|
|
50
|
+
and that is now stated as deliberate.** The behaviour is unchanged.
|
|
51
|
+
Progressive streaming does not relax either rule.
|
|
52
|
+
|
|
14
53
|
## [5.67.0] - 2026-10-27
|
|
15
54
|
|
|
16
55
|
### Added
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { ComponentType, ReactNode } from 'react';
|
|
2
|
+
|
|
3
|
+
/** Props passed to every render-extension component. */
|
|
4
|
+
interface RenderExtensionProps {
|
|
5
|
+
/**
|
|
6
|
+
* The inner content of the custom tag (raw string). For an extension with
|
|
7
|
+
* `markdown: true` in markdown content this is the raw MARKDOWN body, before
|
|
8
|
+
* conversion. Either way it is untrusted: render `children` rather than
|
|
9
|
+
* injecting it.
|
|
10
|
+
*/
|
|
11
|
+
content: string;
|
|
12
|
+
/**
|
|
13
|
+
* Parsed attributes from the opening tag. Event handlers (`on*`), `srcdoc`,
|
|
14
|
+
* and URL attributes with an unsafe scheme are removed (unless the renderer
|
|
15
|
+
* is `unsafe`).
|
|
16
|
+
*/
|
|
17
|
+
attributes: Record<string, string>;
|
|
18
|
+
/**
|
|
19
|
+
* Only for extensions with `markdown: true`: the body, rendered by the same
|
|
20
|
+
* pipeline as the rest of the content (sanitised, nested extensions, copy
|
|
21
|
+
* buttons).
|
|
22
|
+
*/
|
|
23
|
+
children?: ReactNode;
|
|
24
|
+
}
|
|
25
|
+
/** Registers a custom tag with a React component for rendering. */
|
|
26
|
+
interface RenderExtension {
|
|
27
|
+
/** Tag name to match, e.g. "questions", "thinking". Case-insensitive. */
|
|
28
|
+
tag: string;
|
|
29
|
+
/** React component that renders the matched tag. */
|
|
30
|
+
component: ComponentType<RenderExtensionProps>;
|
|
31
|
+
/**
|
|
32
|
+
* Whether this is a block-level element.
|
|
33
|
+
* Block extensions are wrapped in a `<div>`, inline in a `<span>`.
|
|
34
|
+
* @default true
|
|
35
|
+
*/
|
|
36
|
+
block?: boolean;
|
|
37
|
+
/**
|
|
38
|
+
* The tag's body is MARKDOWN. In markdown content, a tag that starts a line
|
|
39
|
+
* is lifted out of the source before conversion, so the component gets the
|
|
40
|
+
* raw markdown as `content` and the rendered, sanitised body as `children`.
|
|
41
|
+
* In HTML content the body is HTML and `children` renders it.
|
|
42
|
+
* @default false
|
|
43
|
+
*/
|
|
44
|
+
markdown?: boolean;
|
|
45
|
+
}
|
|
46
|
+
/** A parsed segment — either raw HTML or a matched extension tag. */
|
|
47
|
+
type ContentSegment = {
|
|
48
|
+
type: "html";
|
|
49
|
+
content: string;
|
|
50
|
+
} | {
|
|
51
|
+
type: "extension";
|
|
52
|
+
tag: string;
|
|
53
|
+
content: string;
|
|
54
|
+
attributes: Record<string, string>;
|
|
55
|
+
};
|
|
56
|
+
/** Register a single render extension globally (available to all ContentRenderer and Editor instances). */
|
|
57
|
+
declare function registerExtension(extension: RenderExtension): void;
|
|
58
|
+
/** Register multiple render extensions globally. */
|
|
59
|
+
declare function registerExtensions(extensions: RenderExtension[]): void;
|
|
60
|
+
|
|
61
|
+
/** What a copy button copies. */
|
|
62
|
+
type ContentCopyKind = "code" | "table";
|
|
63
|
+
/** What is handed to a clipboard writer. */
|
|
64
|
+
interface ContentClipboardPayload {
|
|
65
|
+
kind: ContentCopyKind;
|
|
66
|
+
/**
|
|
67
|
+
* Plain text. For code: exactly the code between the fences, with no
|
|
68
|
+
* language label and no added trailing newline. For a table: TSV (tab
|
|
69
|
+
* between cells, newline between rows, no trailing newline).
|
|
70
|
+
*/
|
|
71
|
+
text: string;
|
|
72
|
+
/** The block's sanitised HTML (`<pre>…</pre>` or `<table>…</table>`), for rich paste. */
|
|
73
|
+
html: string;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Writes one copy to the clipboard. Throwing, rejecting, or returning `false`
|
|
77
|
+
* means the copy failed; the failure is announced, never swallowed.
|
|
78
|
+
*/
|
|
79
|
+
type ContentClipboardWriter = (payload: ContentClipboardPayload) => void | boolean | Promise<void | boolean>;
|
|
80
|
+
/** Reported through `onCopy` after every copy attempt. */
|
|
81
|
+
interface ContentCopyEvent extends ContentClipboardPayload {
|
|
82
|
+
/** Position of the button among this renderer's copy buttons, in document order. */
|
|
83
|
+
index: number;
|
|
84
|
+
/** Whether the clipboard write succeeded. */
|
|
85
|
+
ok: boolean;
|
|
86
|
+
}
|
|
87
|
+
interface ContentRendererProps {
|
|
88
|
+
value: string;
|
|
89
|
+
format?: "html" | "markdown" | "auto";
|
|
90
|
+
lineSpacing?: number;
|
|
91
|
+
className?: string;
|
|
92
|
+
/** Per-instance render extensions. Merged with any globally-registered extensions. */
|
|
93
|
+
extensions?: RenderExtension[];
|
|
94
|
+
/**
|
|
95
|
+
* Skip HTML sanitization. By default, rendered output is sanitized to remove
|
|
96
|
+
* `<script>`, `<iframe>`, event handlers, and `javascript:` URIs. Pass
|
|
97
|
+
* `unsafe` only when the input is fully trusted (e.g. server-rendered
|
|
98
|
+
* markdown from your own CMS).
|
|
99
|
+
* @default false
|
|
100
|
+
*/
|
|
101
|
+
unsafe?: boolean;
|
|
102
|
+
/**
|
|
103
|
+
* Copy buttons on code blocks and tables. `false` turns them all off; an
|
|
104
|
+
* object turns kinds off individually (`{ table: false }`).
|
|
105
|
+
* @default true
|
|
106
|
+
*/
|
|
107
|
+
copyButtons?: boolean | {
|
|
108
|
+
code?: boolean;
|
|
109
|
+
table?: boolean;
|
|
110
|
+
};
|
|
111
|
+
/** Called after every copy attempt, successful or not. */
|
|
112
|
+
onCopy?: (event: ContentCopyEvent) => void;
|
|
113
|
+
/**
|
|
114
|
+
* Host-supplied clipboard writer, for environments where
|
|
115
|
+
* `navigator.clipboard` is unreliable (e.g. a sandboxed Electron window:
|
|
116
|
+
* pass one that calls Electron's `clipboard`). The default writes
|
|
117
|
+
* text/html + text/plain via `ClipboardItem`, falling back to `writeText`.
|
|
118
|
+
*/
|
|
119
|
+
clipboardWriter?: ContentClipboardWriter;
|
|
120
|
+
/**
|
|
121
|
+
* The value is still arriving. Set it back to `false` when the stream ends
|
|
122
|
+
* and everything renders.
|
|
123
|
+
*
|
|
124
|
+
* - `true` holds back an unfinished trailing custom tag (of a registered
|
|
125
|
+
* extension) or code fence until it closes, so a partial `<result` or half
|
|
126
|
+
* a code block never flashes as raw text.
|
|
127
|
+
* - `"progressive"` shows an open block as it fills: an open registered tag
|
|
128
|
+
* whose opening tag is complete renders its component with the body so
|
|
129
|
+
* far, and an open code fence renders as a code block of what has arrived
|
|
130
|
+
* (with no copy button until it closes). A partial opening tag
|
|
131
|
+
* (`<result title="Do`) is still held back.
|
|
132
|
+
* @default false
|
|
133
|
+
*/
|
|
134
|
+
streaming?: boolean | "progressive";
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export { type ContentClipboardPayload as C, type RenderExtension as R, type ContentClipboardWriter as a, type ContentCopyEvent as b, type ContentCopyKind as c, type ContentRendererProps as d, type RenderExtensionProps as e, registerExtensions as f, type ContentSegment as g, registerExtension as r };
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { ComponentType, ReactNode } from 'react';
|
|
2
|
+
|
|
3
|
+
/** Props passed to every render-extension component. */
|
|
4
|
+
interface RenderExtensionProps {
|
|
5
|
+
/**
|
|
6
|
+
* The inner content of the custom tag (raw string). For an extension with
|
|
7
|
+
* `markdown: true` in markdown content this is the raw MARKDOWN body, before
|
|
8
|
+
* conversion. Either way it is untrusted: render `children` rather than
|
|
9
|
+
* injecting it.
|
|
10
|
+
*/
|
|
11
|
+
content: string;
|
|
12
|
+
/**
|
|
13
|
+
* Parsed attributes from the opening tag. Event handlers (`on*`), `srcdoc`,
|
|
14
|
+
* and URL attributes with an unsafe scheme are removed (unless the renderer
|
|
15
|
+
* is `unsafe`).
|
|
16
|
+
*/
|
|
17
|
+
attributes: Record<string, string>;
|
|
18
|
+
/**
|
|
19
|
+
* Only for extensions with `markdown: true`: the body, rendered by the same
|
|
20
|
+
* pipeline as the rest of the content (sanitised, nested extensions, copy
|
|
21
|
+
* buttons).
|
|
22
|
+
*/
|
|
23
|
+
children?: ReactNode;
|
|
24
|
+
}
|
|
25
|
+
/** Registers a custom tag with a React component for rendering. */
|
|
26
|
+
interface RenderExtension {
|
|
27
|
+
/** Tag name to match, e.g. "questions", "thinking". Case-insensitive. */
|
|
28
|
+
tag: string;
|
|
29
|
+
/** React component that renders the matched tag. */
|
|
30
|
+
component: ComponentType<RenderExtensionProps>;
|
|
31
|
+
/**
|
|
32
|
+
* Whether this is a block-level element.
|
|
33
|
+
* Block extensions are wrapped in a `<div>`, inline in a `<span>`.
|
|
34
|
+
* @default true
|
|
35
|
+
*/
|
|
36
|
+
block?: boolean;
|
|
37
|
+
/**
|
|
38
|
+
* The tag's body is MARKDOWN. In markdown content, a tag that starts a line
|
|
39
|
+
* is lifted out of the source before conversion, so the component gets the
|
|
40
|
+
* raw markdown as `content` and the rendered, sanitised body as `children`.
|
|
41
|
+
* In HTML content the body is HTML and `children` renders it.
|
|
42
|
+
* @default false
|
|
43
|
+
*/
|
|
44
|
+
markdown?: boolean;
|
|
45
|
+
}
|
|
46
|
+
/** A parsed segment — either raw HTML or a matched extension tag. */
|
|
47
|
+
type ContentSegment = {
|
|
48
|
+
type: "html";
|
|
49
|
+
content: string;
|
|
50
|
+
} | {
|
|
51
|
+
type: "extension";
|
|
52
|
+
tag: string;
|
|
53
|
+
content: string;
|
|
54
|
+
attributes: Record<string, string>;
|
|
55
|
+
};
|
|
56
|
+
/** Register a single render extension globally (available to all ContentRenderer and Editor instances). */
|
|
57
|
+
declare function registerExtension(extension: RenderExtension): void;
|
|
58
|
+
/** Register multiple render extensions globally. */
|
|
59
|
+
declare function registerExtensions(extensions: RenderExtension[]): void;
|
|
60
|
+
|
|
61
|
+
/** What a copy button copies. */
|
|
62
|
+
type ContentCopyKind = "code" | "table";
|
|
63
|
+
/** What is handed to a clipboard writer. */
|
|
64
|
+
interface ContentClipboardPayload {
|
|
65
|
+
kind: ContentCopyKind;
|
|
66
|
+
/**
|
|
67
|
+
* Plain text. For code: exactly the code between the fences, with no
|
|
68
|
+
* language label and no added trailing newline. For a table: TSV (tab
|
|
69
|
+
* between cells, newline between rows, no trailing newline).
|
|
70
|
+
*/
|
|
71
|
+
text: string;
|
|
72
|
+
/** The block's sanitised HTML (`<pre>…</pre>` or `<table>…</table>`), for rich paste. */
|
|
73
|
+
html: string;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Writes one copy to the clipboard. Throwing, rejecting, or returning `false`
|
|
77
|
+
* means the copy failed; the failure is announced, never swallowed.
|
|
78
|
+
*/
|
|
79
|
+
type ContentClipboardWriter = (payload: ContentClipboardPayload) => void | boolean | Promise<void | boolean>;
|
|
80
|
+
/** Reported through `onCopy` after every copy attempt. */
|
|
81
|
+
interface ContentCopyEvent extends ContentClipboardPayload {
|
|
82
|
+
/** Position of the button among this renderer's copy buttons, in document order. */
|
|
83
|
+
index: number;
|
|
84
|
+
/** Whether the clipboard write succeeded. */
|
|
85
|
+
ok: boolean;
|
|
86
|
+
}
|
|
87
|
+
interface ContentRendererProps {
|
|
88
|
+
value: string;
|
|
89
|
+
format?: "html" | "markdown" | "auto";
|
|
90
|
+
lineSpacing?: number;
|
|
91
|
+
className?: string;
|
|
92
|
+
/** Per-instance render extensions. Merged with any globally-registered extensions. */
|
|
93
|
+
extensions?: RenderExtension[];
|
|
94
|
+
/**
|
|
95
|
+
* Skip HTML sanitization. By default, rendered output is sanitized to remove
|
|
96
|
+
* `<script>`, `<iframe>`, event handlers, and `javascript:` URIs. Pass
|
|
97
|
+
* `unsafe` only when the input is fully trusted (e.g. server-rendered
|
|
98
|
+
* markdown from your own CMS).
|
|
99
|
+
* @default false
|
|
100
|
+
*/
|
|
101
|
+
unsafe?: boolean;
|
|
102
|
+
/**
|
|
103
|
+
* Copy buttons on code blocks and tables. `false` turns them all off; an
|
|
104
|
+
* object turns kinds off individually (`{ table: false }`).
|
|
105
|
+
* @default true
|
|
106
|
+
*/
|
|
107
|
+
copyButtons?: boolean | {
|
|
108
|
+
code?: boolean;
|
|
109
|
+
table?: boolean;
|
|
110
|
+
};
|
|
111
|
+
/** Called after every copy attempt, successful or not. */
|
|
112
|
+
onCopy?: (event: ContentCopyEvent) => void;
|
|
113
|
+
/**
|
|
114
|
+
* Host-supplied clipboard writer, for environments where
|
|
115
|
+
* `navigator.clipboard` is unreliable (e.g. a sandboxed Electron window:
|
|
116
|
+
* pass one that calls Electron's `clipboard`). The default writes
|
|
117
|
+
* text/html + text/plain via `ClipboardItem`, falling back to `writeText`.
|
|
118
|
+
*/
|
|
119
|
+
clipboardWriter?: ContentClipboardWriter;
|
|
120
|
+
/**
|
|
121
|
+
* The value is still arriving. Set it back to `false` when the stream ends
|
|
122
|
+
* and everything renders.
|
|
123
|
+
*
|
|
124
|
+
* - `true` holds back an unfinished trailing custom tag (of a registered
|
|
125
|
+
* extension) or code fence until it closes, so a partial `<result` or half
|
|
126
|
+
* a code block never flashes as raw text.
|
|
127
|
+
* - `"progressive"` shows an open block as it fills: an open registered tag
|
|
128
|
+
* whose opening tag is complete renders its component with the body so
|
|
129
|
+
* far, and an open code fence renders as a code block of what has arrived
|
|
130
|
+
* (with no copy button until it closes). A partial opening tag
|
|
131
|
+
* (`<result title="Do`) is still held back.
|
|
132
|
+
* @default false
|
|
133
|
+
*/
|
|
134
|
+
streaming?: boolean | "progressive";
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export { type ContentClipboardPayload as C, type RenderExtension as R, type ContentClipboardWriter as a, type ContentCopyEvent as b, type ContentCopyKind as c, type ContentRendererProps as d, type RenderExtensionProps as e, registerExtensions as f, type ContentSegment as g, registerExtension as r };
|
|
@@ -876,7 +876,7 @@ function maskInlineCode(line) {
|
|
|
876
876
|
function scan(src, names, trackFences) {
|
|
877
877
|
const tokens = [];
|
|
878
878
|
let openFenceStart = -1;
|
|
879
|
-
if (names.length === 0 && !trackFences) return { tokens, openFenceStart };
|
|
879
|
+
if (names.length === 0 && !trackFences) return { tokens, openFenceStart, openFence: null };
|
|
880
880
|
const tagRe = names.length ? new RegExp(`<(\\/?)(${names.map(escapeRegExp).join("|")})(?=[\\s>/])([^>]*)>`, "gi") : null;
|
|
881
881
|
let fence = null;
|
|
882
882
|
let pos = 0;
|
|
@@ -927,7 +927,39 @@ function scan(src, names, trackFences) {
|
|
|
927
927
|
if (nl < 0) break;
|
|
928
928
|
pos = lineEnd + 1;
|
|
929
929
|
}
|
|
930
|
-
return { tokens, openFenceStart };
|
|
930
|
+
return { tokens, openFenceStart, openFence: fence };
|
|
931
|
+
}
|
|
932
|
+
function partialTagAt(lastLine, names, markdown) {
|
|
933
|
+
if (!names.length) return -1;
|
|
934
|
+
const masked = markdown ? maskInlineCode(lastLine) : lastLine;
|
|
935
|
+
const lt = masked.lastIndexOf("<");
|
|
936
|
+
if (lt < 0) return -1;
|
|
937
|
+
const m = /^<(\/?)([A-Za-z][\w-]*)?([^>]*)$/.exec(masked.slice(lt));
|
|
938
|
+
if (!m) return -1;
|
|
939
|
+
const name = (m[2] ?? "").toLowerCase();
|
|
940
|
+
const rest = m[3];
|
|
941
|
+
let partial = false;
|
|
942
|
+
if (!name) partial = rest === "";
|
|
943
|
+
else if (names.includes(name)) partial = rest === "" || /^\s/.test(rest);
|
|
944
|
+
else partial = rest === "" && names.some((n) => n.startsWith(name));
|
|
945
|
+
return partial ? lt : -1;
|
|
946
|
+
}
|
|
947
|
+
function openTagsBefore(tokens, cut) {
|
|
948
|
+
const stack = [];
|
|
949
|
+
for (const t of tokens) {
|
|
950
|
+
if (t.start >= cut) break;
|
|
951
|
+
if (t.kind === "open") {
|
|
952
|
+
stack.push(t);
|
|
953
|
+
continue;
|
|
954
|
+
}
|
|
955
|
+
for (let k = stack.length - 1; k >= 0; k--) {
|
|
956
|
+
if (stack[k].name === t.name) {
|
|
957
|
+
stack.length = k;
|
|
958
|
+
break;
|
|
959
|
+
}
|
|
960
|
+
}
|
|
961
|
+
}
|
|
962
|
+
return stack;
|
|
931
963
|
}
|
|
932
964
|
function splitMarkdownBlocks(src, tags) {
|
|
933
965
|
const names = tags.map((t) => t.toLowerCase());
|
|
@@ -971,43 +1003,45 @@ function streamingCut(src, tags, markdown) {
|
|
|
971
1003
|
const { tokens, openFenceStart } = scan(src, names, markdown);
|
|
972
1004
|
let cut = src.length;
|
|
973
1005
|
if (openFenceStart >= 0) cut = Math.min(cut, openFenceStart);
|
|
974
|
-
const stack =
|
|
975
|
-
for (const t of tokens) {
|
|
976
|
-
if (t.start >= cut) break;
|
|
977
|
-
if (t.kind === "open") {
|
|
978
|
-
stack.push(t);
|
|
979
|
-
continue;
|
|
980
|
-
}
|
|
981
|
-
for (let k = stack.length - 1; k >= 0; k--) {
|
|
982
|
-
if (stack[k].name === t.name) {
|
|
983
|
-
stack.length = k;
|
|
984
|
-
break;
|
|
985
|
-
}
|
|
986
|
-
}
|
|
987
|
-
}
|
|
1006
|
+
const stack = openTagsBefore(tokens, cut);
|
|
988
1007
|
if (stack.length) cut = Math.min(cut, stack[0].start);
|
|
989
1008
|
const lastLineStart = src.lastIndexOf("\n", cut - 1) + 1;
|
|
990
1009
|
const lastLine = src.slice(lastLineStart, cut);
|
|
991
1010
|
if (markdown && /^ {0,3}(`{1,2}|~{1,2})$/.test(lastLine)) {
|
|
992
1011
|
return Math.min(cut, lastLineStart);
|
|
993
1012
|
}
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1013
|
+
const lt = partialTagAt(lastLine, names, markdown);
|
|
1014
|
+
if (lt >= 0) cut = Math.min(cut, lastLineStart + lt);
|
|
1015
|
+
return cut;
|
|
1016
|
+
}
|
|
1017
|
+
function streamingProgressive(src, tags, markdown) {
|
|
1018
|
+
const names = tags.map((t) => t.toLowerCase());
|
|
1019
|
+
const { tokens, openFenceStart, openFence } = scan(src, names, markdown);
|
|
1020
|
+
let cut = src.length;
|
|
1021
|
+
let fence = openFenceStart >= 0 ? openFence : null;
|
|
1022
|
+
const lastLineStart = src.lastIndexOf("\n", cut - 1) + 1;
|
|
1023
|
+
const lastLine = src.slice(lastLineStart);
|
|
1024
|
+
if (fence) {
|
|
1025
|
+
if (openFenceStart === lastLineStart) {
|
|
1026
|
+
cut = openFenceStart;
|
|
1027
|
+
fence = null;
|
|
1028
|
+
} else {
|
|
1029
|
+
const m = /^ {0,3}(`+|~+)$/.exec(lastLine);
|
|
1030
|
+
if (m && m[1][0] === fence.char && m[1].length < fence.len) cut = lastLineStart;
|
|
1008
1031
|
}
|
|
1032
|
+
} else if (markdown && /^ {0,3}(`{1,2}|~{1,2})$/.test(lastLine)) {
|
|
1033
|
+
cut = lastLineStart;
|
|
1034
|
+
} else {
|
|
1035
|
+
const lt = partialTagAt(lastLine, names, markdown);
|
|
1036
|
+
if (lt >= 0) cut = lastLineStart + lt;
|
|
1009
1037
|
}
|
|
1010
|
-
|
|
1038
|
+
let out = src.slice(0, cut);
|
|
1039
|
+
if (fence) {
|
|
1040
|
+
out += (out.endsWith("\n") ? "" : "\n") + fence.char.repeat(fence.len) + "\n";
|
|
1041
|
+
}
|
|
1042
|
+
const stack = openTagsBefore(tokens, fence ? openFenceStart : cut);
|
|
1043
|
+
for (let k = stack.length - 1; k >= 0; k--) out += `</${stack[k].name}>`;
|
|
1044
|
+
return { source: out, openFence: fence !== null };
|
|
1011
1045
|
}
|
|
1012
1046
|
var FEEDBACK_MS = 2e3;
|
|
1013
1047
|
var WRITE_TIMEOUT_MS = 5e3;
|
|
@@ -1065,12 +1099,17 @@ function cellText(cell) {
|
|
|
1065
1099
|
for (const br of Array.from(clone.querySelectorAll("br"))) br.replaceWith(" ");
|
|
1066
1100
|
return (clone.textContent ?? "").replace(/[\t\r\n]+/g, " ").replace(/ {2,}/g, " ").trim();
|
|
1067
1101
|
}
|
|
1068
|
-
function HtmlChunk({
|
|
1102
|
+
function HtmlChunk({
|
|
1103
|
+
html,
|
|
1104
|
+
fromMarkdown,
|
|
1105
|
+
tail = false
|
|
1106
|
+
}) {
|
|
1069
1107
|
const ref = useRef(null);
|
|
1070
1108
|
const ctx = useContext(CopyContext);
|
|
1071
1109
|
const [targets, setTargets] = useState([]);
|
|
1072
1110
|
const code = ctx?.code ?? false;
|
|
1073
1111
|
const table = ctx?.table ?? false;
|
|
1112
|
+
const holdLastCode = tail && (ctx?.holdTailCode ?? false);
|
|
1074
1113
|
const inner = useMemo(() => ({ __html: html }), [html]);
|
|
1075
1114
|
useLayoutEffect(() => {
|
|
1076
1115
|
const root = ref.current;
|
|
@@ -1080,7 +1119,9 @@ function HtmlChunk({ html, fromMarkdown }) {
|
|
|
1080
1119
|
}
|
|
1081
1120
|
const found = [];
|
|
1082
1121
|
const selector = [code && "pre", table && "table"].filter(Boolean).join(",");
|
|
1122
|
+
const inProgress = holdLastCode ? Array.from(root.querySelectorAll("pre")).pop() : void 0;
|
|
1083
1123
|
for (const el of Array.from(root.querySelectorAll(selector))) {
|
|
1124
|
+
if (el === inProgress) continue;
|
|
1084
1125
|
const kind = el.tagName === "PRE" ? "code" : "table";
|
|
1085
1126
|
const wrapper = document.createElement("div");
|
|
1086
1127
|
wrapper.setAttribute("data-react-fancy-content-copy-block", kind);
|
|
@@ -1100,7 +1141,7 @@ function HtmlChunk({ html, fromMarkdown }) {
|
|
|
1100
1141
|
if (wrapper?.parentNode) wrapper.replaceWith(t.el);
|
|
1101
1142
|
}
|
|
1102
1143
|
};
|
|
1103
|
-
}, [html, code, table]);
|
|
1144
|
+
}, [html, code, table, holdLastCode]);
|
|
1104
1145
|
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
1105
1146
|
/* @__PURE__ */ jsx("div", { ref, dangerouslySetInnerHTML: inner }),
|
|
1106
1147
|
targets.map(
|
|
@@ -1170,7 +1211,11 @@ function useCopyPipeline({
|
|
|
1170
1211
|
);
|
|
1171
1212
|
}
|
|
1172
1213
|
var MAX_DEPTH = 8;
|
|
1173
|
-
function
|
|
1214
|
+
function lastIndexWith(items, renders) {
|
|
1215
|
+
for (let i = items.length - 1; i >= 0; i--) if (renders(items[i])) return i;
|
|
1216
|
+
return -1;
|
|
1217
|
+
}
|
|
1218
|
+
function ContentBody({ source, format, extensions, unsafe, depth = 0, tail = false }) {
|
|
1174
1219
|
const active = depth >= MAX_DEPTH ? [] : extensions;
|
|
1175
1220
|
const blocks = useMemo(() => {
|
|
1176
1221
|
if (format !== "markdown") return null;
|
|
@@ -1179,8 +1224,19 @@ function ContentBody({ source, format, extensions, unsafe, depth = 0 }) {
|
|
|
1179
1224
|
}, [source, format, active]);
|
|
1180
1225
|
if (!blocks) {
|
|
1181
1226
|
const html = unsafe ? source : sanitizeHtml(source);
|
|
1182
|
-
return /* @__PURE__ */ jsx(
|
|
1227
|
+
return /* @__PURE__ */ jsx(
|
|
1228
|
+
RenderedContent,
|
|
1229
|
+
{
|
|
1230
|
+
html,
|
|
1231
|
+
extensions: active,
|
|
1232
|
+
unsafe,
|
|
1233
|
+
fromMarkdown: false,
|
|
1234
|
+
depth,
|
|
1235
|
+
tail
|
|
1236
|
+
}
|
|
1237
|
+
);
|
|
1183
1238
|
}
|
|
1239
|
+
const last = tail ? lastIndexWith(blocks, (b) => b.type === "tag" || b.source.trim() !== "") : -1;
|
|
1184
1240
|
return /* @__PURE__ */ jsx(Fragment, { children: blocks.map((block, i) => {
|
|
1185
1241
|
if (block.type === "markdown") {
|
|
1186
1242
|
const raw = markdownToRawHtml(block.source);
|
|
@@ -1192,7 +1248,8 @@ function ContentBody({ source, format, extensions, unsafe, depth = 0 }) {
|
|
|
1192
1248
|
extensions: active,
|
|
1193
1249
|
unsafe,
|
|
1194
1250
|
fromMarkdown: true,
|
|
1195
|
-
depth
|
|
1251
|
+
depth,
|
|
1252
|
+
tail: i === last
|
|
1196
1253
|
},
|
|
1197
1254
|
i
|
|
1198
1255
|
);
|
|
@@ -1208,21 +1265,38 @@ function ContentBody({ source, format, extensions, unsafe, depth = 0 }) {
|
|
|
1208
1265
|
format: "markdown",
|
|
1209
1266
|
extensions: active,
|
|
1210
1267
|
unsafe,
|
|
1211
|
-
depth: depth + 1
|
|
1268
|
+
depth: depth + 1,
|
|
1269
|
+
tail: i === last
|
|
1212
1270
|
}
|
|
1213
1271
|
) }) }, i);
|
|
1214
1272
|
}) });
|
|
1215
1273
|
}
|
|
1216
|
-
function RenderedContent({
|
|
1274
|
+
function RenderedContent({
|
|
1275
|
+
html,
|
|
1276
|
+
extensions,
|
|
1277
|
+
unsafe,
|
|
1278
|
+
fromMarkdown,
|
|
1279
|
+
depth,
|
|
1280
|
+
tail = false
|
|
1281
|
+
}) {
|
|
1217
1282
|
const segments = useMemo(() => parseSegments(html, extensions), [html, extensions]);
|
|
1218
1283
|
const renderHtml = (content) => unsafe ? content : sanitizeHtml(content);
|
|
1219
1284
|
if (segments.length === 0) return null;
|
|
1220
1285
|
if (segments.length === 1 && segments[0].type === "html") {
|
|
1221
|
-
return /* @__PURE__ */ jsx(HtmlChunk, { html: renderHtml(segments[0].content), fromMarkdown });
|
|
1286
|
+
return /* @__PURE__ */ jsx(HtmlChunk, { html: renderHtml(segments[0].content), fromMarkdown, tail });
|
|
1222
1287
|
}
|
|
1288
|
+
const last = tail ? lastIndexWith(segments, (s) => s.type === "extension" || s.content.trim() !== "") : -1;
|
|
1223
1289
|
return /* @__PURE__ */ jsx(Fragment, { children: segments.map((segment, i) => {
|
|
1224
1290
|
if (segment.type === "html") {
|
|
1225
|
-
return segment.content ? /* @__PURE__ */ jsx(
|
|
1291
|
+
return segment.content ? /* @__PURE__ */ jsx(
|
|
1292
|
+
HtmlChunk,
|
|
1293
|
+
{
|
|
1294
|
+
html: renderHtml(segment.content),
|
|
1295
|
+
fromMarkdown,
|
|
1296
|
+
tail: i === last
|
|
1297
|
+
},
|
|
1298
|
+
i
|
|
1299
|
+
) : null;
|
|
1226
1300
|
}
|
|
1227
1301
|
const ext = extensions.find((e) => e.tag.toLowerCase() === segment.tag);
|
|
1228
1302
|
if (!ext) return null;
|
|
@@ -1235,7 +1309,8 @@ function RenderedContent({ html, extensions, unsafe, fromMarkdown, depth }) {
|
|
|
1235
1309
|
format: "html",
|
|
1236
1310
|
extensions,
|
|
1237
1311
|
unsafe,
|
|
1238
|
-
depth: depth + 1
|
|
1312
|
+
depth: depth + 1,
|
|
1313
|
+
tail: i === last
|
|
1239
1314
|
}
|
|
1240
1315
|
) : void 0;
|
|
1241
1316
|
return /* @__PURE__ */ jsx(Wrapper, { "data-render-extension": segment.tag, children: /* @__PURE__ */ jsx(Component, { content: segment.content, attributes: segment.attributes, children }) }, i);
|
|
@@ -1261,15 +1336,14 @@ function ContentRenderer({
|
|
|
1261
1336
|
);
|
|
1262
1337
|
const safe = value ?? "";
|
|
1263
1338
|
const resolvedFormat = format === "auto" ? detectFormat(safe) : format;
|
|
1264
|
-
const
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
);
|
|
1271
|
-
|
|
1272
|
-
}, [safe, streaming, extensions, resolvedFormat]);
|
|
1339
|
+
const progressive = streaming === "progressive";
|
|
1340
|
+
const { source, openFence } = useMemo(() => {
|
|
1341
|
+
if (!streaming) return { source: safe, openFence: false };
|
|
1342
|
+
const tags = extensions.map((e) => e.tag);
|
|
1343
|
+
const markdown = resolvedFormat === "markdown";
|
|
1344
|
+
if (progressive) return streamingProgressive(safe, tags, markdown);
|
|
1345
|
+
return { source: safe.slice(0, streamingCut(safe, tags, markdown)), openFence: false };
|
|
1346
|
+
}, [safe, streaming, progressive, extensions, resolvedFormat]);
|
|
1273
1347
|
const codeOn = copyButtons === true || typeof copyButtons === "object" && copyButtons.code !== false;
|
|
1274
1348
|
const tableOn = copyButtons === true || typeof copyButtons === "object" && copyButtons.table !== false;
|
|
1275
1349
|
const copyOn = codeOn || tableOn;
|
|
@@ -1281,8 +1355,8 @@ function ContentRenderer({
|
|
|
1281
1355
|
}, []);
|
|
1282
1356
|
const copy = useCopyPipeline({ writer: clipboardWriter, onCopy, rootRef, announce });
|
|
1283
1357
|
const copyContext = useMemo(
|
|
1284
|
-
() => ({ code: codeOn, table: tableOn, copy }),
|
|
1285
|
-
[codeOn, tableOn, copy]
|
|
1358
|
+
() => ({ code: codeOn, table: tableOn, copy, holdTailCode: openFence }),
|
|
1359
|
+
[codeOn, tableOn, copy, openFence]
|
|
1286
1360
|
);
|
|
1287
1361
|
return /* @__PURE__ */ jsxs(
|
|
1288
1362
|
"div",
|
|
@@ -1290,6 +1364,7 @@ function ContentRenderer({
|
|
|
1290
1364
|
ref: rootRef,
|
|
1291
1365
|
"data-react-fancy-content-renderer": "",
|
|
1292
1366
|
"data-streaming": streaming ? "true" : void 0,
|
|
1367
|
+
"data-streaming-mode": progressive ? "progressive" : void 0,
|
|
1293
1368
|
style: { lineHeight: lineSpacing },
|
|
1294
1369
|
className: cn("text-sm", proseClasses, className),
|
|
1295
1370
|
children: [
|
|
@@ -1299,7 +1374,8 @@ function ContentRenderer({
|
|
|
1299
1374
|
source,
|
|
1300
1375
|
format: resolvedFormat,
|
|
1301
1376
|
extensions,
|
|
1302
|
-
unsafe
|
|
1377
|
+
unsafe,
|
|
1378
|
+
tail: true
|
|
1303
1379
|
}
|
|
1304
1380
|
) }),
|
|
1305
1381
|
copyOn && /* @__PURE__ */ jsx(
|
|
@@ -1320,5 +1396,5 @@ function ContentRenderer({
|
|
|
1320
1396
|
ContentRenderer.displayName = "ContentRenderer";
|
|
1321
1397
|
|
|
1322
1398
|
export { ContentRenderer, detectFormat, extensionEditorClasses, htmlToMarkdown, markdownToEditorHtml, mergeExtensions, proseClasses, registerExtension, registerExtensions, renderMarkdown };
|
|
1323
|
-
//# sourceMappingURL=chunk-
|
|
1324
|
-
//# sourceMappingURL=chunk-
|
|
1399
|
+
//# sourceMappingURL=chunk-2LQR4EAN.js.map
|
|
1400
|
+
//# sourceMappingURL=chunk-2LQR4EAN.js.map
|