@jaychang1989/dsh-webchat 0.3.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/LICENSE +201 -0
- package/NOTICE +18 -0
- package/README.md +78 -0
- package/README.zh.md +83 -0
- package/cordis.patch.yml +22 -0
- package/lib/client.js +1725 -0
- package/lib/client.js.map +1 -0
- package/lib/index.js +3887 -0
- package/lib/types/client/api.d.ts +74 -0
- package/lib/types/client/controller.d.ts +19 -0
- package/lib/types/client/index.d.ts +30 -0
- package/lib/types/client/locales.d.ts +169 -0
- package/lib/types/client/mount.d.ts +23 -0
- package/lib/types/client/panel/Markdown.d.ts +15 -0
- package/lib/types/client/panel/WebChatPanel.d.ts +28 -0
- package/lib/types/client/sidebar-entry-core.d.ts +60 -0
- package/lib/types/client/sidebar-entry.d.ts +22 -0
- package/lib/types/engine/engine.d.ts +247 -0
- package/lib/types/engine/html-md.d.ts +23 -0
- package/lib/types/index.d.ts +63 -0
- package/lib/types/protocol.d.ts +107 -0
- package/lib/types/routes.d.ts +22 -0
- package/lib/types/store.d.ts +69 -0
- package/lib/types/tools.d.ts +27 -0
- package/lib/types/transfer.d.ts +138 -0
- package/package.json +96 -0
- package/src/client/api.ts +90 -0
- package/src/client/controller.ts +43 -0
- package/src/client/css-modules.d.ts +5 -0
- package/src/client/index.ts +75 -0
- package/src/client/locales.ts +172 -0
- package/src/client/mount.tsx +125 -0
- package/src/client/panel/Markdown.tsx +251 -0
- package/src/client/panel/WebChatPanel.tsx +760 -0
- package/src/client/panel/panel.module.css +936 -0
- package/src/client/sidebar-entry-core.ts +207 -0
- package/src/client/sidebar-entry.ts +49 -0
- package/src/engine/engine.ts +1385 -0
- package/src/engine/html-md.ts +126 -0
- package/src/index.ts +212 -0
- package/src/protocol.ts +116 -0
- package/src/routes.ts +307 -0
- package/src/store.ts +217 -0
- package/src/tools.ts +257 -0
- package/src/transfer.ts +602 -0
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Panel view mounting. The conversation slot is single-occupant and external
|
|
3
|
+
* plugins cannot declare slots, so the panel takes over the center column at
|
|
4
|
+
* the DOM level (same strategy as dsh-ssh / dsh-task-board): a container is
|
|
5
|
+
* appended inside the center column as an extra trailing child React never
|
|
6
|
+
* manages, and a stylesheet rule hides the conversation content while the
|
|
7
|
+
* panel is active. Toggling is a data attribute on <html> — no React
|
|
8
|
+
* involvement, so the conversation subtree underneath stays mounted and
|
|
9
|
+
* stateful.
|
|
10
|
+
*/
|
|
11
|
+
import { createRoot, type Root } from 'react-dom/client'
|
|
12
|
+
import type { ISessions, IWorkspaces } from '@deepseek-ai/dsh-client-runtime/client'
|
|
13
|
+
import type { WebChatApi } from './api.ts'
|
|
14
|
+
import type { PanelController } from './controller.ts'
|
|
15
|
+
import type { WebChatKey } from './locales.ts'
|
|
16
|
+
import { WebChatPanel } from './panel/WebChatPanel.tsx'
|
|
17
|
+
import css from './panel/panel.module.css'
|
|
18
|
+
|
|
19
|
+
/** The injected panel container (kept in the DOM, hidden when inactive). */
|
|
20
|
+
export const PANEL_VIEW_SELECTOR = '[data-dsh-webchat-view]'
|
|
21
|
+
|
|
22
|
+
const CONVERSATION_COLUMN_SELECTOR = '[data-pane="conversation"], [class*="centerCol"]'
|
|
23
|
+
const ACTIVE_ATTR = 'data-dsh-webchat-active'
|
|
24
|
+
/** The sibling panels' activation attributes, removed when this panel opens. */
|
|
25
|
+
const OTHER_ACTIVE_ATTRS = ['data-dsh-taskboard-active', 'data-dsh-ssh-active']
|
|
26
|
+
/** Cross-plugin activation event; detail is the activating panel name. */
|
|
27
|
+
const ACTIVATE_EVENT = 'dsh-panel-activate'
|
|
28
|
+
const PANEL_NAME = 'webchat'
|
|
29
|
+
|
|
30
|
+
/** Find the center column, or undefined while the frame is not mounted. */
|
|
31
|
+
function conversationColumn(): HTMLElement | undefined {
|
|
32
|
+
return document.querySelector<HTMLElement>(CONVERSATION_COLUMN_SELECTOR) ?? undefined
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface MountPanelDeps {
|
|
36
|
+
sessions: ISessions
|
|
37
|
+
workspaces: IWorkspaces
|
|
38
|
+
api: WebChatApi
|
|
39
|
+
controller: PanelController
|
|
40
|
+
/** Locale accessor bound to the plugin's registered dictionaries. */
|
|
41
|
+
tt: (key: WebChatKey) => string
|
|
42
|
+
/** Resolve the current session's cwd (for transfers / exports). */
|
|
43
|
+
currentCwd: () => string | undefined
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Mount the panel React tree into the center column and bind its visibility
|
|
48
|
+
* to the controller's panelOpen state.
|
|
49
|
+
* @param deps - sessions, api, controller, locale accessor.
|
|
50
|
+
* @returns disposer unmounting the tree and restoring the column.
|
|
51
|
+
*/
|
|
52
|
+
export function mountPanel(deps: MountPanelDeps): () => void {
|
|
53
|
+
const { sessions, workspaces, api, controller, tt, currentCwd } = deps
|
|
54
|
+
let root: Root | undefined
|
|
55
|
+
let container: HTMLDivElement | undefined
|
|
56
|
+
|
|
57
|
+
const ensure = (): void => {
|
|
58
|
+
if (container !== undefined) {
|
|
59
|
+
if (container.isConnected) return
|
|
60
|
+
// The conversation pane was replaced; drop the stale tree and remount.
|
|
61
|
+
root?.unmount()
|
|
62
|
+
root = undefined
|
|
63
|
+
container.remove()
|
|
64
|
+
container = undefined
|
|
65
|
+
}
|
|
66
|
+
const column = conversationColumn()
|
|
67
|
+
if (column === undefined) return
|
|
68
|
+
container = document.createElement('div')
|
|
69
|
+
container.dataset.dshWebchatView = ''
|
|
70
|
+
container.dataset.dshPlugin = 'webchat'
|
|
71
|
+
container.className = css.view
|
|
72
|
+
column.appendChild(container)
|
|
73
|
+
root = createRoot(container)
|
|
74
|
+
root.render(<WebChatPanel api={api} tt={tt} sessions={sessions} workspaces={workspaces} currentCwd={currentCwd} />)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// The frame mounts after boot settlement; watch for the column's arrival.
|
|
78
|
+
const waitObserver = new MutationObserver(() => { ensure() })
|
|
79
|
+
waitObserver.observe(document.body, { childList: true, subtree: true })
|
|
80
|
+
|
|
81
|
+
const applyActive = (): void => {
|
|
82
|
+
if (controller.getSnapshot().panelOpen) {
|
|
83
|
+
// Single-occupant center column: opening this panel must evict sibling
|
|
84
|
+
// panels (task board / ssh), both their html attributes and their
|
|
85
|
+
// controller state, otherwise visibility rules fight.
|
|
86
|
+
for (const attr of OTHER_ACTIVE_ATTRS) document.documentElement.removeAttribute(attr)
|
|
87
|
+
document.documentElement.setAttribute(ACTIVE_ATTR, '')
|
|
88
|
+
document.dispatchEvent(new CustomEvent(ACTIVATE_EVENT, { detail: PANEL_NAME }))
|
|
89
|
+
} else {
|
|
90
|
+
document.documentElement.removeAttribute(ACTIVE_ATTR)
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
const onOtherActivate = (event: Event): void => {
|
|
94
|
+
const detail = (event as CustomEvent).detail
|
|
95
|
+
if ((detail === 'taskboard' || detail === 'ssh') && controller.getSnapshot().panelOpen) {
|
|
96
|
+
controller.close()
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
// Jump out on sidebar context clicks: clicking a session/workspace row
|
|
100
|
+
// hands the center column back to the conversation. Capture phase.
|
|
101
|
+
const SIDEBAR_ROW_SELECTOR = '[class*="sessionRow"], [class*="projectRow"], [class*="searchResultRow"], [class*="searchResultWorkspace"], [class*="newSession"]'
|
|
102
|
+
const onClickSidebarRow = (event: MouseEvent): void => {
|
|
103
|
+
if (!controller.getSnapshot().panelOpen) return
|
|
104
|
+
const target = event.target as HTMLElement | null
|
|
105
|
+
if (target === null) return
|
|
106
|
+
if (target.closest(SIDEBAR_ROW_SELECTOR) !== null) controller.close()
|
|
107
|
+
}
|
|
108
|
+
document.addEventListener('click', onClickSidebarRow, true)
|
|
109
|
+
document.addEventListener(ACTIVATE_EVENT, onOtherActivate)
|
|
110
|
+
const unsubscribe = controller.subscribe(applyActive)
|
|
111
|
+
applyActive()
|
|
112
|
+
ensure()
|
|
113
|
+
|
|
114
|
+
return () => {
|
|
115
|
+
document.removeEventListener('click', onClickSidebarRow, true)
|
|
116
|
+
document.removeEventListener(ACTIVATE_EVENT, onOtherActivate)
|
|
117
|
+
waitObserver.disconnect()
|
|
118
|
+
unsubscribe()
|
|
119
|
+
document.documentElement.removeAttribute(ACTIVE_ATTR)
|
|
120
|
+
root?.unmount()
|
|
121
|
+
root = undefined
|
|
122
|
+
container?.remove()
|
|
123
|
+
container = undefined
|
|
124
|
+
}
|
|
125
|
+
}
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal markdown renderer for the web-chat panel. Deliberately small and
|
|
3
|
+
* dependency-free: headings, paragraphs, bold/italic, inline + fenced code,
|
|
4
|
+
* links, DeepSeek `[citation:N]` markers (rendered as superscript badges),
|
|
5
|
+
* unordered/ordered lists, blockquotes, GFM tables, horizontal rules, and
|
|
6
|
+
* <details> passthrough (used for DeepSeek R1 thinking blocks). Anything
|
|
7
|
+
* unrecognized renders as plain text — never raw HTML.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { createElement, type ReactNode } from 'react'
|
|
11
|
+
import css from './panel.module.css'
|
|
12
|
+
|
|
13
|
+
/** Escape text for safe rendering (no dangerouslySetInnerHTML anywhere). */
|
|
14
|
+
function esc(text: string): string {
|
|
15
|
+
return text.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Inline markdown → React nodes. Single tokenizer pass over the string:
|
|
20
|
+
* code spans win, then links, then `[citation:…]` markers, then bold, then
|
|
21
|
+
* italic — each non-token run is escaped text. Bold/italic recurse so a
|
|
22
|
+
* strong span can contain a citation or link.
|
|
23
|
+
*/
|
|
24
|
+
function inline(text: string, keyBase = 'i'): ReactNode[] {
|
|
25
|
+
const nodes: ReactNode[] = []
|
|
26
|
+
// eslint-disable-next-line no-useless-escape
|
|
27
|
+
const re = /(`[^`]+`)|(\[[^\]]+\]\([^)\s]+\))|(\[citation:[^\]]+\])|(\*\*[^*]+\*\*|__[^_]+__)|(\*[^*\n]+\*|_[^_\n]+_)/g
|
|
28
|
+
let last = 0
|
|
29
|
+
let index = 0
|
|
30
|
+
let match: RegExpExecArray | null
|
|
31
|
+
while ((match = re.exec(text)) !== null) {
|
|
32
|
+
if (match.index > last) nodes.push(esc(text.slice(last, match.index)))
|
|
33
|
+
const key = `${keyBase}-${index}`
|
|
34
|
+
if (match[1] !== undefined) {
|
|
35
|
+
// `code`
|
|
36
|
+
nodes.push(createElement('code', { key: `${key}-code` }, match[1].slice(1, -1)))
|
|
37
|
+
} else if (match[2] !== undefined) {
|
|
38
|
+
// [label](url)
|
|
39
|
+
const link = /^\[([^\]]+)\]\(([^)\s]+)\)$/.exec(match[2])
|
|
40
|
+
nodes.push(createElement('a', { key: `${key}-a`, href: link![2], target: '_blank', rel: 'noreferrer' }, link![1]))
|
|
41
|
+
} else if (match[3] !== undefined) {
|
|
42
|
+
// [citation:N] / [citation:N1,N2] — DeepSeek web search markers.
|
|
43
|
+
const nums = match[3].slice('[citation:'.length, -1).trim()
|
|
44
|
+
nodes.push(createElement('sup', { key: `${key}-cite`, className: css.citation }, nums))
|
|
45
|
+
} else if (match[4] !== undefined) {
|
|
46
|
+
// **bold** / __bold__
|
|
47
|
+
const inner = match[4].slice(2, -2)
|
|
48
|
+
nodes.push(createElement('strong', { key: `${key}-b` }, ...inline(inner, `${key}-b`)))
|
|
49
|
+
} else if (match[5] !== undefined) {
|
|
50
|
+
// *italic* / _italic_
|
|
51
|
+
const inner = match[5].slice(1, -1)
|
|
52
|
+
nodes.push(createElement('em', { key: `${key}-i` }, ...inline(inner, `${key}-i`)))
|
|
53
|
+
}
|
|
54
|
+
last = re.lastIndex
|
|
55
|
+
index++
|
|
56
|
+
}
|
|
57
|
+
if (last < text.length) nodes.push(esc(text.slice(last)))
|
|
58
|
+
return nodes
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** One fenced code block → <pre><code>. */
|
|
62
|
+
function codeBlock(language: string, body: string, key: string): ReactNode {
|
|
63
|
+
return createElement(
|
|
64
|
+
'pre',
|
|
65
|
+
{ key },
|
|
66
|
+
createElement('code', { className: language === '' ? undefined : `language-${language}` }, body),
|
|
67
|
+
)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Split a table row into cells, honoring escaped pipes (\|). */
|
|
71
|
+
function splitCells(line: string): string[] {
|
|
72
|
+
const cells: string[] = []
|
|
73
|
+
let current = ''
|
|
74
|
+
let escaped = false
|
|
75
|
+
for (const ch of line) {
|
|
76
|
+
if (escaped) { current += ch; escaped = false }
|
|
77
|
+
else if (ch === '\\') { escaped = true }
|
|
78
|
+
else if (ch === '|') { cells.push(current); current = '' }
|
|
79
|
+
else { current += ch }
|
|
80
|
+
}
|
|
81
|
+
cells.push(current)
|
|
82
|
+
// Drop the leading/trailing empty artifacts produced by a leading/trailing
|
|
83
|
+
// pipe, so `| a | b |` and `a | b` both yield ['a','b'].
|
|
84
|
+
let start = 0
|
|
85
|
+
let end = cells.length
|
|
86
|
+
while (start < end && cells[start].trim() === '') start++
|
|
87
|
+
while (end > start && cells[end - 1].trim() === '') end--
|
|
88
|
+
return cells.slice(start, end).map(cell => cell.trim())
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** True when a line is a GFM table delimiter row (`:---`, `---:`, `:---:`, …). */
|
|
92
|
+
function isDelimiterRow(line: string): boolean {
|
|
93
|
+
const cells = splitCells(line)
|
|
94
|
+
return cells.length > 0 && cells.every(cell => /^:?-{3,}:?$/.test(cell))
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** Render one block of markdown text into React nodes. */
|
|
98
|
+
export function renderMarkdown(source: string): ReactNode[] {
|
|
99
|
+
const raw = source.replace(/\r\n/g, '\n').trim()
|
|
100
|
+
if (raw === '') return []
|
|
101
|
+
const blocks: ReactNode[] = []
|
|
102
|
+
const lines = raw.split('\n')
|
|
103
|
+
let index = 0
|
|
104
|
+
let blockIndex = 0
|
|
105
|
+
|
|
106
|
+
const push = (node: ReactNode): void => {
|
|
107
|
+
blocks.push(createElement('div', { key: `b${blockIndex++}` }, node))
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
while (index < lines.length) {
|
|
111
|
+
const line = lines[index]
|
|
112
|
+
const trimmed = line.trim()
|
|
113
|
+
|
|
114
|
+
// fenced code
|
|
115
|
+
const fence = /^```([\w+-]*)\s*$/.exec(trimmed)
|
|
116
|
+
if (fence !== null) {
|
|
117
|
+
const language = fence[1] ?? ''
|
|
118
|
+
const body: string[] = []
|
|
119
|
+
index++
|
|
120
|
+
while (index < lines.length && !/^```\s*$/.test(lines[index].trim())) {
|
|
121
|
+
body.push(lines[index])
|
|
122
|
+
index++
|
|
123
|
+
}
|
|
124
|
+
index++ // closing fence
|
|
125
|
+
push(codeBlock(language, body.join('\n'), `code${blockIndex}`))
|
|
126
|
+
continue
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// headings
|
|
130
|
+
const heading = /^(#{1,6})\s+(.*)$/.exec(trimmed)
|
|
131
|
+
if (heading !== null) {
|
|
132
|
+
const level = heading[1].length
|
|
133
|
+
push(createElement(`h${level}` as 'h1', null, ...inline(heading[2], `h${blockIndex}`)))
|
|
134
|
+
index++
|
|
135
|
+
continue
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// GFM table (header row containing '|' immediately followed by a delimiter
|
|
139
|
+
// row). Checked before the horizontal-rule branch so a delimiter like
|
|
140
|
+
// `--- | ---` is never mistaken for an hr.
|
|
141
|
+
if (line.includes('|') && lines[index + 1] !== undefined && isDelimiterRow(lines[index + 1])) {
|
|
142
|
+
const header = splitCells(line)
|
|
143
|
+
index += 2
|
|
144
|
+
const rows: string[][] = []
|
|
145
|
+
while (index < lines.length) {
|
|
146
|
+
const row = lines[index]
|
|
147
|
+
if (row.trim() === '' || !row.includes('|')) break
|
|
148
|
+
rows.push(splitCells(row))
|
|
149
|
+
index++
|
|
150
|
+
}
|
|
151
|
+
const width = Math.max(header.length, ...rows.map(row => row.length))
|
|
152
|
+
const rowNode = (cells: string[], rowIndex: number): ReactNode =>
|
|
153
|
+
createElement('tr', { key: rowIndex }, Array.from({ length: width }, (_, col) =>
|
|
154
|
+
createElement('td', { key: col }, ...inline(cells[col] ?? '', `t${blockIndex}-${rowIndex}-${col}`))))
|
|
155
|
+
blocks.push(createElement(
|
|
156
|
+
'div',
|
|
157
|
+
{ key: `tw${blockIndex}`, className: css.tableWrap },
|
|
158
|
+
createElement('table', { key: `t${blockIndex}` },
|
|
159
|
+
createElement('thead', null, createElement('tr', null, header.map((cell, col) =>
|
|
160
|
+
createElement('th', { key: col }, ...inline(cell, `th${blockIndex}-${col}`))))),
|
|
161
|
+
createElement('tbody', null, rows.map(rowNode))),
|
|
162
|
+
))
|
|
163
|
+
blockIndex++
|
|
164
|
+
continue
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// horizontal rule
|
|
168
|
+
if (/^(-{3,}|\*{3,}|_{3,})$/.test(trimmed)) {
|
|
169
|
+
push(createElement('hr', { key: `hr${blockIndex}` }))
|
|
170
|
+
index++
|
|
171
|
+
continue
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// blockquote (collect consecutive quote lines)
|
|
175
|
+
if (trimmed.startsWith('>')) {
|
|
176
|
+
const quote: string[] = []
|
|
177
|
+
while (index < lines.length && lines[index].trim().startsWith('>')) {
|
|
178
|
+
quote.push(lines[index].trim().replace(/^>\s?/, ''))
|
|
179
|
+
index++
|
|
180
|
+
}
|
|
181
|
+
push(createElement('blockquote', { key: `q${blockIndex}` }, ...renderMarkdown(quote.join('\n'))))
|
|
182
|
+
continue
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// details (DeepSeek R1 thinking)
|
|
186
|
+
if (/^<details>/.test(trimmed)) {
|
|
187
|
+
const body: string[] = []
|
|
188
|
+
const summaryMatch = /^<summary>\s*(.*?)\s*<\/summary>/.exec(trimmed)
|
|
189
|
+
index++
|
|
190
|
+
while (index < lines.length && !/^<\/details>/.test(lines[index].trim())) {
|
|
191
|
+
body.push(lines[index])
|
|
192
|
+
index++
|
|
193
|
+
}
|
|
194
|
+
index++ // </details>
|
|
195
|
+
push(createElement(
|
|
196
|
+
'details',
|
|
197
|
+
{ key: `d${blockIndex}` },
|
|
198
|
+
createElement('summary', null, summaryMatch?.[1] ?? '详情'),
|
|
199
|
+
...renderMarkdown(body.join('\n')),
|
|
200
|
+
))
|
|
201
|
+
continue
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// unordered list
|
|
205
|
+
if (/^[-*+]\s+/.test(trimmed)) {
|
|
206
|
+
const items: string[] = []
|
|
207
|
+
while (index < lines.length && /^[-*+]\s+/.test(lines[index].trim())) {
|
|
208
|
+
items.push(lines[index].trim().replace(/^[-*+]\s+/, ''))
|
|
209
|
+
index++
|
|
210
|
+
}
|
|
211
|
+
push(createElement('ul', { key: `ul${blockIndex}` }, items.map((item, itemIndex) =>
|
|
212
|
+
createElement('li', { key: itemIndex }, ...inline(item, `uli${blockIndex}-${itemIndex}`)))))
|
|
213
|
+
continue
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// ordered list
|
|
217
|
+
if (/^\d+[.)]\s+/.test(trimmed)) {
|
|
218
|
+
const items: string[] = []
|
|
219
|
+
while (index < lines.length && /^\d+[.)]\s+/.test(lines[index].trim())) {
|
|
220
|
+
items.push(lines[index].trim().replace(/^\d+[.)]\s+/, ''))
|
|
221
|
+
index++
|
|
222
|
+
}
|
|
223
|
+
push(createElement('ol', { key: `ol${blockIndex}` }, items.map((item, itemIndex) =>
|
|
224
|
+
createElement('li', { key: itemIndex }, ...inline(item, `oli${blockIndex}-${itemIndex}`)))))
|
|
225
|
+
continue
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// plain paragraph: collect consecutive non-empty, non-special lines
|
|
229
|
+
const paragraph: string[] = []
|
|
230
|
+
while (index < lines.length) {
|
|
231
|
+
const current = lines[index].trim()
|
|
232
|
+
if (current === '') break
|
|
233
|
+
if (/^(```|#{1,6}\s|[-*+]\s|\d+[.)]\s|>\s|<\/(details|table)>)/.test(current)) break
|
|
234
|
+
if (current.includes('|') && lines[index + 1] !== undefined && isDelimiterRow(lines[index + 1])) break
|
|
235
|
+
paragraph.push(current)
|
|
236
|
+
index++
|
|
237
|
+
}
|
|
238
|
+
if (paragraph.length === 0) {
|
|
239
|
+
index++
|
|
240
|
+
continue
|
|
241
|
+
}
|
|
242
|
+
push(createElement('p', { key: `p${blockIndex}` }, ...inline(paragraph.join(' '), `p${blockIndex}`)))
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
return blocks
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/** The markdown renderer component. */
|
|
249
|
+
export function Markdown({ source }: { source: string }): ReactNode {
|
|
250
|
+
return createElement('div', { className: css.markdown }, ...renderMarkdown(source))
|
|
251
|
+
}
|