@fabrika/canvas-kernel 0.1.44 → 0.2.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/README.md +33 -67
- package/index.ts +19 -83
- package/package.json +68 -72
- package/src/client/AnnotationProvider.tsx +227 -268
- package/src/client/AnnotationSidebar.tsx +392 -338
- package/src/client/FileIcon.tsx +106 -65
- package/src/client/PlanRenderer.tsx +607 -551
- package/src/client/RenderErrorContext.ts +7 -7
- package/src/client/ResponsePreview.tsx +192 -173
- package/src/client/annotationDom.ts +43 -97
- package/src/client/annotationDraft.ts +98 -92
- package/src/client/appContext.tsx +40 -40
- package/src/client/components/Callout.tsx +20 -20
- package/src/client/components/Checklist.tsx +28 -28
- package/src/client/components/Choice.tsx +178 -159
- package/src/client/components/CodeBlock.tsx +26 -26
- package/src/client/components/Diff.tsx +17 -17
- package/src/client/components/FilePreview.tsx +99 -74
- package/src/client/components/Image.tsx +41 -39
- package/src/client/components/Markdown.tsx +33 -32
- package/src/client/components/Mermaid.tsx +592 -542
- package/src/client/components/Note.tsx +8 -8
- package/src/client/components/Priority.tsx +11 -11
- package/src/client/components/ResponseNote.tsx +17 -15
- package/src/client/components/Section.tsx +28 -26
- package/src/client/components/Table.tsx +34 -29
- package/src/client/components/Task.tsx +58 -60
- package/src/client/components/UserInput.tsx +169 -152
- package/src/client/components/index.ts +16 -16
- package/src/client/components/renderMarkdownChildren.tsx +86 -68
- package/src/client/diffLines.ts +51 -39
- package/src/client/generateMarkdown.ts +197 -191
- package/src/client/hostContext.tsx +34 -34
- package/src/client/index.ts +43 -125
- package/src/client/preact-all.ts +4 -4
- package/src/client/runtime.ts +58 -111
- package/src/client/styles.css +1 -74
- package/src/engine/compiler.ts +316 -313
- package/src/engine/paths.ts +44 -44
- package/src/engine/session.ts +623 -618
- package/src/engine/watcher.ts +63 -60
- package/src/engine/websocket.ts +123 -121
- package/annotate.ts +0 -94
- package/daemon.ts +0 -46
- package/src/client/AnnotationDraftFooter.tsx +0 -76
- package/src/client/AnnotationEditor.tsx +0 -320
- package/src/client/AnnotationList.tsx +0 -190
- package/src/client/Popover.tsx +0 -361
- package/src/client/SessionContext.tsx +0 -3
- package/src/client/annotationContext.ts +0 -130
- package/src/client/annotationMarkdown.ts +0 -102
- package/src/client/annotationTarget.ts +0 -73
- package/src/client/blockTarget.ts +0 -127
- package/src/client/highlightRange.ts +0 -262
- package/src/client/popoverPosition.ts +0 -46
- package/src/client/regionTarget.ts +0 -180
- package/src/client/testing/dom.ts +0 -58
- package/src/client/textTarget.ts +0 -31
- package/src/client/useRegionAnnotation.tsx +0 -188
- package/src/client/useTextAnnotation.tsx +0 -194
- package/src/client/utils.ts +0 -33
- package/src/engine/channel.ts +0 -278
- package/src/engine/http-utils.ts +0 -14
- package/src/engine/lifecycle.ts +0 -302
- package/src/engine/router.ts +0 -16
- package/src/engine/waiter.ts +0 -188
|
@@ -1,176 +1,193 @@
|
|
|
1
|
-
import
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
1
|
+
import { useAnnotations, useResponseRegistration } from '#canvas/runtime'
|
|
2
|
+
import { autoResizeTextarea } from '@fabrika/annotations'
|
|
3
|
+
import React, { useRef, useState } from 'react'
|
|
4
|
+
import { ResponseNote } from './ResponseNote'
|
|
5
5
|
|
|
6
6
|
/** Free text input from user */
|
|
7
7
|
interface UserInputProps {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
8
|
+
id: string
|
|
9
|
+
label: string
|
|
10
|
+
placeholder?: string
|
|
11
|
+
multiline?: boolean
|
|
12
|
+
required?: boolean
|
|
13
13
|
}
|
|
14
14
|
|
|
15
15
|
export function UserInput({ id, label, placeholder, multiline, required }: UserInputProps) {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
16
|
+
const { responses, setResponse } = useAnnotations()
|
|
17
|
+
useResponseRegistration(id)
|
|
18
|
+
const current = responses.get(id)
|
|
19
|
+
const value = (current?.value as string) ?? ''
|
|
20
|
+
const note = current?.note || ''
|
|
21
|
+
const [showNote, setShowNote] = useState(false)
|
|
22
|
+
const ref = useRef<HTMLTextAreaElement>(null)
|
|
23
23
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
24
|
+
if (!responses.has(id)) {
|
|
25
|
+
setResponse(id, { id, type: 'text', label, value: '', required })
|
|
26
|
+
}
|
|
27
27
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
28
|
+
const handleChange = (v: string) => {
|
|
29
|
+
setResponse(id, { ...current!, value: v })
|
|
30
|
+
}
|
|
31
31
|
|
|
32
|
-
|
|
32
|
+
const autoResize = (el: HTMLTextAreaElement) => autoResizeTextarea(el, multiline ? 60 : 32)
|
|
33
33
|
|
|
34
|
-
|
|
34
|
+
const showError = current?.required && !value.trim()
|
|
35
35
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
36
|
+
return (
|
|
37
|
+
<div data-md="userinput" data-md-label={label} className="-mx-4 px-4 py-3 my-1 rounded-lg transition-colors duration-150 hover:bg-bg-input">
|
|
38
|
+
<div className="flex items-baseline gap-2 mb-2">
|
|
39
|
+
<span className="text-[13px] font-body font-medium text-text-primary">{label}</span>
|
|
40
|
+
{required && <span className="text-[10px] text-accent-red font-body">*</span>}
|
|
41
|
+
</div>
|
|
42
|
+
<textarea
|
|
43
|
+
ref={ref}
|
|
44
|
+
value={value}
|
|
45
|
+
onChange={(e) => {
|
|
46
|
+
handleChange(e.currentTarget.value)
|
|
47
|
+
autoResize(e.currentTarget)
|
|
48
|
+
}}
|
|
49
|
+
placeholder={placeholder || 'Type your response...'}
|
|
50
|
+
rows={multiline ? 3 : 1}
|
|
51
|
+
className="w-full bg-bg-input text-[13px] font-body text-text-primary rounded-lg px-3 py-2 resize-none focus:outline-none border border-border-subtle focus:border-border-hover placeholder:text-text-tertiary transition-colors"
|
|
52
|
+
onInput={(e) => autoResize(e.currentTarget)}
|
|
53
|
+
onFocus={(e) => autoResize(e.currentTarget)}
|
|
54
|
+
/>
|
|
55
|
+
<ResponseNote show={showNote} note={note} onToggle={() => setShowNote(!showNote)} onChange={(n) => setResponse(id, { ...current!, note: n })} />
|
|
56
|
+
{showError && <p className="text-[11px] text-accent-red font-body mt-1">This field is required.</p>}
|
|
57
|
+
</div>
|
|
58
|
+
)
|
|
56
59
|
}
|
|
57
60
|
|
|
58
61
|
/** Numeric range / slider */
|
|
59
62
|
interface RangeInputProps {
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
63
|
+
id: string
|
|
64
|
+
label: string
|
|
65
|
+
min?: number
|
|
66
|
+
max?: number
|
|
67
|
+
step?: number
|
|
68
|
+
required?: boolean
|
|
69
|
+
minLabel?: string
|
|
70
|
+
maxLabel?: string
|
|
68
71
|
}
|
|
69
72
|
|
|
70
73
|
export function RangeInput({ id, label, min = 1, max = 10, step = 1, required, minLabel, maxLabel }: RangeInputProps) {
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
74
|
+
const { responses, setResponse } = useAnnotations()
|
|
75
|
+
useResponseRegistration(id)
|
|
76
|
+
const current = responses.get(id)
|
|
77
|
+
const value = current?.value as number | null
|
|
78
|
+
const note = current?.note || ''
|
|
79
|
+
const [showNote, setShowNote] = useState(false)
|
|
77
80
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
+
if (!responses.has(id)) {
|
|
82
|
+
setResponse(id, { id, type: 'range', label, value: null, required })
|
|
83
|
+
}
|
|
81
84
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
+
const handleChange = (v: number) => {
|
|
86
|
+
setResponse(id, { ...current!, value: v })
|
|
87
|
+
}
|
|
85
88
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
89
|
+
const hasValue = value !== null && value !== undefined
|
|
90
|
+
const displayValue = hasValue ? value : Math.round((min + max) / 2)
|
|
91
|
+
const pct = ((displayValue - min) / (max - min)) * 100
|
|
92
|
+
const stepCount = Math.round((max - min) / step)
|
|
93
|
+
const showDots = stepCount <= 20
|
|
91
94
|
|
|
92
|
-
|
|
95
|
+
const showError = current?.required && !hasValue
|
|
93
96
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
97
|
+
return (
|
|
98
|
+
<div data-md="rangeinput" data-md-label={label} className="-mx-4 px-4 py-3 my-1 rounded-lg transition-colors duration-150 hover:bg-bg-input">
|
|
99
|
+
<div className="flex items-baseline justify-between mb-1">
|
|
100
|
+
<div className="flex items-baseline gap-2">
|
|
101
|
+
<span className="text-[13px] font-body font-medium text-text-primary">{label}</span>
|
|
102
|
+
{required && <span className="text-[10px] text-accent-red font-body">*</span>}
|
|
103
|
+
</div>
|
|
104
|
+
<span className="flex items-center gap-1.5">
|
|
105
|
+
<span className={`text-[13px] font-mono ${hasValue ? 'text-accent-amber' : 'text-text-disabled'}`}>
|
|
106
|
+
{hasValue ? value : '—'}
|
|
107
|
+
</span>
|
|
108
|
+
{hasValue && (
|
|
109
|
+
<button
|
|
110
|
+
onClick={() => setResponse(id, { ...current!, value: null })}
|
|
111
|
+
className="text-text-disabled hover:text-text-tertiary transition-colors text-[10px] leading-none"
|
|
112
|
+
title="Clear"
|
|
113
|
+
>
|
|
114
|
+
✕
|
|
115
|
+
</button>
|
|
116
|
+
)}
|
|
117
|
+
</span>
|
|
118
|
+
</div>
|
|
119
|
+
<div className="relative h-6 flex items-center">
|
|
120
|
+
{/* Track background */}
|
|
121
|
+
<div className="absolute left-0 right-0 h-1.5 rounded-full bg-border-medium">
|
|
122
|
+
{hasValue && <div className="h-full rounded-full bg-accent-amber transition-all duration-150" style={{ width: `${pct}%` }} />}
|
|
123
|
+
</div>
|
|
124
|
+
{/* Discrete step dots */}
|
|
125
|
+
{showDots && Array.from({ length: stepCount + 1 }, (_, i) => {
|
|
126
|
+
const dotVal = min + i * step
|
|
127
|
+
const dotPct = ((dotVal - min) / (max - min)) * 100
|
|
128
|
+
const isSelected = hasValue && dotVal <= value!
|
|
129
|
+
const isActive = hasValue && dotVal === value
|
|
130
|
+
return (
|
|
131
|
+
<div
|
|
132
|
+
key={i}
|
|
133
|
+
className={`absolute rounded-full pointer-events-none transition-all duration-150 ${
|
|
134
|
+
isActive ? 'w-2.5 h-2.5 bg-accent-amber' : isSelected ? 'w-1.5 h-1.5 bg-accent-amber' : 'w-1.5 h-1.5 bg-border-hover'
|
|
135
|
+
}`}
|
|
136
|
+
style={{ left: `${dotPct}%`, transform: 'translate(-50%, -50%)', top: '50%' }}
|
|
137
|
+
/>
|
|
138
|
+
)
|
|
139
|
+
})}
|
|
140
|
+
{/* Thumb indicator */}
|
|
141
|
+
{hasValue
|
|
142
|
+
? (
|
|
143
|
+
<div
|
|
144
|
+
className="absolute h-5 w-5 rounded-full bg-accent-amber shadow-md pointer-events-none transition-all duration-150"
|
|
145
|
+
style={{ left: `calc(${pct}% - 10px)`, borderWidth: '2px', borderStyle: 'solid', borderColor: 'var(--color-bg-base)' }}
|
|
146
|
+
/>
|
|
147
|
+
)
|
|
148
|
+
: (
|
|
149
|
+
<div
|
|
150
|
+
className="absolute h-4 w-4 rounded-full bg-border-hover pointer-events-none opacity-50"
|
|
151
|
+
style={{ left: 'calc(50% - 8px)', borderWidth: '2px', borderStyle: 'solid', borderColor: 'var(--color-bg-base)' }}
|
|
152
|
+
/>
|
|
153
|
+
)}
|
|
154
|
+
<input
|
|
155
|
+
type="range"
|
|
156
|
+
min={min}
|
|
157
|
+
max={max}
|
|
158
|
+
step={step}
|
|
159
|
+
value={displayValue}
|
|
160
|
+
onChange={(e) => handleChange(Number(e.currentTarget.value))}
|
|
161
|
+
className="absolute inset-0 w-full opacity-0 cursor-pointer"
|
|
162
|
+
/>
|
|
163
|
+
</div>
|
|
164
|
+
{/* Labels: show step values if few enough, otherwise just min/max */}
|
|
165
|
+
{showDots && stepCount <= 10
|
|
166
|
+
? (
|
|
167
|
+
<div className="flex justify-between -mt-0.5 px-0">
|
|
168
|
+
{Array.from({ length: stepCount + 1 }, (_, i) => {
|
|
169
|
+
const dotVal = min + i * step
|
|
170
|
+
const isActive = hasValue && dotVal === value
|
|
171
|
+
return (
|
|
172
|
+
<span
|
|
173
|
+
key={i}
|
|
174
|
+
className={`text-[10px] font-mono transition-colors ${isActive ? 'text-accent-amber font-medium' : 'text-text-disabled'}`}
|
|
175
|
+
style={{ minWidth: '1ch', textAlign: 'center' }}
|
|
176
|
+
>
|
|
177
|
+
{dotVal}
|
|
178
|
+
</span>
|
|
179
|
+
)
|
|
180
|
+
})}
|
|
181
|
+
</div>
|
|
182
|
+
)
|
|
183
|
+
: (
|
|
184
|
+
<div className="flex justify-between -mt-0.5">
|
|
185
|
+
<span className="text-[11px] text-text-tertiary font-body">{minLabel || min}</span>
|
|
186
|
+
<span className="text-[11px] text-text-tertiary font-body">{maxLabel || max}</span>
|
|
187
|
+
</div>
|
|
188
|
+
)}
|
|
189
|
+
<ResponseNote show={showNote} note={note} onToggle={() => setShowNote(!showNote)} onChange={(n) => setResponse(id, { ...current!, note: n })} />
|
|
190
|
+
{showError && <p className="text-[11px] text-accent-red font-body mt-1">Please select a value.</p>}
|
|
191
|
+
</div>
|
|
192
|
+
)
|
|
176
193
|
}
|
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
// Components every canvas host provides. Hosts that ship extra components
|
|
2
2
|
// compose their own `#canvas/components` barrel over this one.
|
|
3
|
-
export {
|
|
4
|
-
export {
|
|
5
|
-
export {
|
|
6
|
-
export {
|
|
7
|
-
export {
|
|
8
|
-
export {
|
|
9
|
-
export {
|
|
10
|
-
export {
|
|
11
|
-
export {
|
|
12
|
-
export { Mermaid } from
|
|
13
|
-
export {
|
|
14
|
-
export {
|
|
15
|
-
export {
|
|
16
|
-
export {
|
|
17
|
-
export {
|
|
18
|
-
export {
|
|
3
|
+
export { useFeedback } from '#canvas/runtime'
|
|
4
|
+
export { Callout } from './Callout'
|
|
5
|
+
export { Checklist } from './Checklist'
|
|
6
|
+
export { Choice, MultiChoice } from './Choice'
|
|
7
|
+
export { CodeBlock } from './CodeBlock'
|
|
8
|
+
export { Diff } from './Diff'
|
|
9
|
+
export { FilePreview } from './FilePreview'
|
|
10
|
+
export { ImageView } from './Image'
|
|
11
|
+
export { Markdown } from './Markdown'
|
|
12
|
+
export { Mermaid } from './Mermaid'
|
|
13
|
+
export { Note } from './Note'
|
|
14
|
+
export { Priority } from './Priority'
|
|
15
|
+
export { Section } from './Section'
|
|
16
|
+
export { Table } from './Table'
|
|
17
|
+
export { Item, Task } from './Task'
|
|
18
|
+
export { RangeInput, UserInput } from './UserInput'
|
|
@@ -1,45 +1,63 @@
|
|
|
1
|
-
|
|
2
|
-
import { marked } from
|
|
1
|
+
// biome-ignore-all lint/security/noDangerouslySetInnerHtml: Canvas modules already execute as code in this document.
|
|
2
|
+
import { marked } from 'marked'
|
|
3
|
+
import React from 'react'
|
|
3
4
|
|
|
4
|
-
marked.setOptions({ gfm: true, breaks: false })
|
|
5
|
+
marked.setOptions({ gfm: true, breaks: false })
|
|
5
6
|
|
|
6
7
|
/** Host tags that flow inline and must not break a prose run into separate blocks. */
|
|
7
8
|
const INLINE_TAGS = new Set([
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
9
|
+
'a',
|
|
10
|
+
'abbr',
|
|
11
|
+
'b',
|
|
12
|
+
'br',
|
|
13
|
+
'code',
|
|
14
|
+
'del',
|
|
15
|
+
'em',
|
|
16
|
+
'i',
|
|
17
|
+
'kbd',
|
|
18
|
+
'mark',
|
|
19
|
+
'q',
|
|
20
|
+
's',
|
|
21
|
+
'small',
|
|
22
|
+
'span',
|
|
23
|
+
'strong',
|
|
24
|
+
'sub',
|
|
25
|
+
'sup',
|
|
26
|
+
'time',
|
|
27
|
+
'u',
|
|
28
|
+
])
|
|
11
29
|
|
|
12
30
|
/** Custom components that render inline (e.g. <Priority>). */
|
|
13
|
-
const INLINE_COMPONENTS = new Set([
|
|
31
|
+
const INLINE_COMPONENTS = new Set(['Priority'])
|
|
14
32
|
|
|
15
33
|
function isInlineNode(node: React.ReactNode): boolean {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
34
|
+
if (typeof node === 'string' || typeof node === 'number') return true
|
|
35
|
+
if (!React.isValidElement(node)) return false
|
|
36
|
+
// Read `type` structurally: preact/compat's isValidElement is not a type
|
|
37
|
+
// guard, so the narrowing React's typings give here does not survive there.
|
|
38
|
+
if (typeof node !== 'object' || node === null || !('type' in node)) return false
|
|
39
|
+
const type: unknown = node.type
|
|
40
|
+
if (typeof type === 'string') return INLINE_TAGS.has(type)
|
|
41
|
+
if (typeof type === 'function') {
|
|
42
|
+
const name = 'displayName' in type && typeof type.displayName === 'string' ? type.displayName : type.name
|
|
43
|
+
return name ? INLINE_COMPONENTS.has(name) : false
|
|
44
|
+
}
|
|
45
|
+
return false
|
|
28
46
|
}
|
|
29
47
|
|
|
30
48
|
function renderTextAsMarkdown(text: string, key: string): React.ReactNode {
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
49
|
+
const trimmed = text.trim()
|
|
50
|
+
if (!trimmed) return null
|
|
51
|
+
const html = marked.parse(trimmed, { async: false }) as string
|
|
52
|
+
return (
|
|
53
|
+
<div
|
|
54
|
+
key={key}
|
|
55
|
+
className="prose-canvas"
|
|
56
|
+
data-md="markdown"
|
|
57
|
+
data-md-source={trimmed}
|
|
58
|
+
dangerouslySetInnerHTML={{ __html: html }}
|
|
59
|
+
/>
|
|
60
|
+
)
|
|
43
61
|
}
|
|
44
62
|
|
|
45
63
|
/**
|
|
@@ -48,17 +66,17 @@ function renderTextAsMarkdown(text: string, key: string): React.ReactNode {
|
|
|
48
66
|
* No `data-md` attribute so the markdown exporter treats it as an inline container.
|
|
49
67
|
*/
|
|
50
68
|
function renderInlineRun(run: React.ReactNode[], key: string): React.ReactNode {
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
69
|
+
return (
|
|
70
|
+
<div key={key} className="prose-canvas">
|
|
71
|
+
{run.map((node, i) => {
|
|
72
|
+
if (typeof node === 'string' || typeof node === 'number') {
|
|
73
|
+
const html = marked.parseInline(String(node), { async: false }) as string
|
|
74
|
+
return <span key={i} dangerouslySetInnerHTML={{ __html: html }} />
|
|
75
|
+
}
|
|
76
|
+
return node
|
|
77
|
+
})}
|
|
78
|
+
</div>
|
|
79
|
+
)
|
|
62
80
|
}
|
|
63
81
|
|
|
64
82
|
/**
|
|
@@ -68,33 +86,33 @@ function renderInlineRun(run: React.ReactNode[], key: string): React.ReactNode {
|
|
|
68
86
|
* keeps flowing instead of stacking one fragment per line.
|
|
69
87
|
*/
|
|
70
88
|
export function renderMarkdownChildren(children: React.ReactNode): React.ReactNode {
|
|
71
|
-
|
|
72
|
-
|
|
89
|
+
const kids = React.Children.toArray(children)
|
|
90
|
+
if (kids.length === 0) return null
|
|
73
91
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
92
|
+
const result: React.ReactNode[] = []
|
|
93
|
+
let segment: React.ReactNode[] = []
|
|
94
|
+
let segIdx = 0
|
|
77
95
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
96
|
+
const flush = () => {
|
|
97
|
+
if (segment.length === 0) return
|
|
98
|
+
const hasInlineJsx = segment.some((n) => typeof n !== 'string' && typeof n !== 'number')
|
|
99
|
+
if (hasInlineJsx) {
|
|
100
|
+
result.push(renderInlineRun(segment, `md-${segIdx++}`))
|
|
101
|
+
} else {
|
|
102
|
+
const node = renderTextAsMarkdown(segment.join(''), `md-${segIdx++}`)
|
|
103
|
+
if (node) result.push(node)
|
|
104
|
+
}
|
|
105
|
+
segment = []
|
|
106
|
+
}
|
|
89
107
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
108
|
+
for (const child of kids) {
|
|
109
|
+
if (isInlineNode(child)) {
|
|
110
|
+
segment.push(child)
|
|
111
|
+
} else {
|
|
112
|
+
flush()
|
|
113
|
+
result.push(child)
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
flush()
|
|
117
|
+
return result
|
|
100
118
|
}
|