@h-ai/ui 0.1.0-alpha.20 → 0.1.0-alpha.22
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 +2 -0
- package/dist/components/scenes/ai/AiDocumentEditor.svelte +102 -0
- package/dist/components/scenes/ai/code-preview.js +9 -0
- package/dist/components/scenes/ai/document-parse.js +11 -0
- package/dist/components/scenes/ai/document-types.d.ts +1 -1
- package/dist/components/scenes/ai/mermaid-render.d.ts +14 -0
- package/dist/components/scenes/ai/mermaid-render.js +42 -0
- package/dist/messages/en-US.json +1 -0
- package/dist/messages/zh-CN.json +1 -0
- package/package.json +4 -3
package/README.md
CHANGED
|
@@ -267,6 +267,8 @@ components/
|
|
|
267
267
|
| `AiTableEditor` | AI 表格编辑器 | `columns`, `rows`, `editable`, `ondownload` |
|
|
268
268
|
|
|
269
269
|
> `AiDocumentEditor` 默认只允许 Markdown 内置预览。HTML / JS / CSS 等高风险预览需要显式设置 `allowUnsafeCodePreview`,或通过 `oncoderun` 返回受控的预览结果。
|
|
270
|
+
>
|
|
271
|
+
> Mermaid 图表开箱即用:文档(`sourceKind='document'`)里的 ```` ```mermaid ```` 代码块在阅读态自动渲染为图表;code 模式(`sourceKind='code'` + `showCodePreviewToggle`)下可在「代码 / 预览」间切换。Mermaid 以 `securityLevel: 'strict'` 渲染为消毒后的 SVG,无需开启 `allowUnsafeCodePreview`。
|
|
270
272
|
|
|
271
273
|
#### CRUD 场景(5 个)
|
|
272
274
|
|
|
@@ -36,6 +36,7 @@
|
|
|
36
36
|
import { resolveDocumentMarkdownContent } from './document-download.js'
|
|
37
37
|
import { renderMarkdownDocument } from './document-parse.js'
|
|
38
38
|
import { parseMarkdown } from './markdown-parse.js'
|
|
39
|
+
import { renderMermaidDiagram } from './mermaid-render.js'
|
|
39
40
|
|
|
40
41
|
interface SelectionToolbarPosition {
|
|
41
42
|
/** 选区工具条相对滚动容器的 top 坐标。 */
|
|
@@ -471,6 +472,7 @@
|
|
|
471
472
|
|
|
472
473
|
requestAnimationFrame(() => {
|
|
473
474
|
syncCodePreviewHosts()
|
|
475
|
+
syncMermaidHosts()
|
|
474
476
|
syncActiveHeadingFromScroll()
|
|
475
477
|
syncPinnedTitleVisibility()
|
|
476
478
|
})
|
|
@@ -909,6 +911,15 @@
|
|
|
909
911
|
? `<p class="hai-md-preview-desc">${escapePreviewText(result.description)}</p>`
|
|
910
912
|
: ''
|
|
911
913
|
|
|
914
|
+
if (result.kind === 'mermaid') {
|
|
915
|
+
host.innerHTML = `<div class="hai-md-preview-card hai-md-preview-mermaid"><div class="hai-md-preview-head">${previewTitle}</div>${previewDesc}<div class="hai-md-mermaid hai-md-mermaid-preview"></div></div>`
|
|
916
|
+
const mermaidEl = host.querySelector<HTMLElement>('.hai-md-mermaid-preview')
|
|
917
|
+
if (mermaidEl) {
|
|
918
|
+
void renderMermaidHost(mermaidEl, result.content)
|
|
919
|
+
}
|
|
920
|
+
return
|
|
921
|
+
}
|
|
922
|
+
|
|
912
923
|
if (result.kind === 'html') {
|
|
913
924
|
const previewSandbox = escapePreviewAttribute(
|
|
914
925
|
resolvePreviewSandbox(result.allowScripts),
|
|
@@ -931,6 +942,55 @@
|
|
|
931
942
|
host.innerHTML = `<div class="hai-md-preview-card"><div class="hai-md-preview-head">${previewTitle}</div>${previewDesc}<pre>${escapePreviewText(result.content)}</pre></div>`
|
|
932
943
|
}
|
|
933
944
|
|
|
945
|
+
/**
|
|
946
|
+
* mermaid 图表位于 `{@html}` 注入的 DOM 内部,渲染是异步的。
|
|
947
|
+
* 阅读态的图表块和 code 模式预览槽都复用这里把源码渲染为 SVG。
|
|
948
|
+
*/
|
|
949
|
+
function syncMermaidHosts(): void {
|
|
950
|
+
if (!previewHost) {
|
|
951
|
+
return
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
const hosts = previewHost.querySelectorAll<HTMLElement>('[data-mermaid-host]')
|
|
955
|
+
for (const host of hosts) {
|
|
956
|
+
if (
|
|
957
|
+
host.dataset.mermaidStatus === 'ready'
|
|
958
|
+
|| host.dataset.mermaidStatus === 'rendering'
|
|
959
|
+
) {
|
|
960
|
+
continue
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
const blockId = host.dataset.mermaidHost
|
|
964
|
+
if (!blockId) {
|
|
965
|
+
continue
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
const block = lookupCodeBlock(blockId)
|
|
969
|
+
if (!block) {
|
|
970
|
+
continue
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
void renderMermaidHost(host, block.code)
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
/**
|
|
978
|
+
* 把单个 mermaid 源码渲染进目标节点;语法错误时展示本地化错误提示。
|
|
979
|
+
* `mermaidStatus` 既避免同一帧重复渲染,也兜住多次 effect 触发。
|
|
980
|
+
*/
|
|
981
|
+
async function renderMermaidHost(host: HTMLElement, code: string): Promise<void> {
|
|
982
|
+
host.dataset.mermaidStatus = 'rendering'
|
|
983
|
+
try {
|
|
984
|
+
const svg = await renderMermaidDiagram(code)
|
|
985
|
+
host.innerHTML = svg
|
|
986
|
+
host.dataset.mermaidStatus = 'ready'
|
|
987
|
+
}
|
|
988
|
+
catch (error) {
|
|
989
|
+
host.dataset.mermaidStatus = 'error'
|
|
990
|
+
host.innerHTML = `<div class="hai-md-mermaid-error">${escapePreviewText(error instanceof Error ? error.message : uiM('markdown_mermaid_failed'))}</div>`
|
|
991
|
+
}
|
|
992
|
+
}
|
|
993
|
+
|
|
934
994
|
function escapePreviewText(value: string): string {
|
|
935
995
|
return value
|
|
936
996
|
.replace(/&/g, '&')
|
|
@@ -4418,6 +4478,48 @@
|
|
|
4418
4478
|
color: oklch(var(--bc));
|
|
4419
4479
|
}
|
|
4420
4480
|
|
|
4481
|
+
/* mermaid 图表:阅读态自动渲染,code 预览态复用同一容器。 */
|
|
4482
|
+
.hai-markdown :global(.hai-md-mermaid) {
|
|
4483
|
+
display: flex;
|
|
4484
|
+
align-items: center;
|
|
4485
|
+
justify-content: center;
|
|
4486
|
+
margin: 1.25em 0;
|
|
4487
|
+
padding: 1rem;
|
|
4488
|
+
border-radius: 0.95rem;
|
|
4489
|
+
border: 1px solid oklch(var(--bc) / 0.08);
|
|
4490
|
+
background: oklch(var(--b1));
|
|
4491
|
+
overflow-x: auto;
|
|
4492
|
+
}
|
|
4493
|
+
|
|
4494
|
+
.hai-markdown :global(.hai-md-mermaid svg) {
|
|
4495
|
+
max-width: 100%;
|
|
4496
|
+
height: auto;
|
|
4497
|
+
}
|
|
4498
|
+
|
|
4499
|
+
.hai-markdown :global(.hai-md-mermaid-error) {
|
|
4500
|
+
width: 100%;
|
|
4501
|
+
padding: 0.75rem 1rem;
|
|
4502
|
+
border-radius: 0.6rem;
|
|
4503
|
+
background: oklch(var(--er, 0.6 0.2 25) / 0.08);
|
|
4504
|
+
color: oklch(var(--er, 0.6 0.2 25));
|
|
4505
|
+
font-size: 0.85rem;
|
|
4506
|
+
line-height: 1.5;
|
|
4507
|
+
white-space: pre-wrap;
|
|
4508
|
+
text-align: left;
|
|
4509
|
+
}
|
|
4510
|
+
|
|
4511
|
+
:global(.hai-md-preview-mermaid) {
|
|
4512
|
+
display: flex;
|
|
4513
|
+
flex-direction: column;
|
|
4514
|
+
gap: 0.5rem;
|
|
4515
|
+
}
|
|
4516
|
+
|
|
4517
|
+
:global(.hai-md-mermaid-preview) {
|
|
4518
|
+
margin: 0;
|
|
4519
|
+
border: none;
|
|
4520
|
+
background: transparent;
|
|
4521
|
+
}
|
|
4522
|
+
|
|
4421
4523
|
.hai-markdown-code-only :global(.hai-md-code-block[data-code-view='preview'] .hai-md-preview-card) {
|
|
4422
4524
|
display: flex;
|
|
4423
4525
|
flex-direction: column;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { isMermaidLanguage } from './mermaid-render.js';
|
|
1
2
|
const HTML_DOCUMENT_PATTERN = /<\s*(?:!doctype|html|body|head|div|section|article|main|span|style|script|svg|canvas)\b/i;
|
|
2
3
|
const HTML_LANGUAGE_SET = new Set(['html', 'htm', 'xhtml', 'xml', 'svg']);
|
|
3
4
|
const JAVASCRIPT_LANGUAGE_SET = new Set(['javascript', 'js', 'typescript', 'ts']);
|
|
@@ -12,6 +13,14 @@ export function resolvePreviewSandbox(allowScripts) {
|
|
|
12
13
|
}
|
|
13
14
|
export function createBuiltInCodePreview(request, options) {
|
|
14
15
|
const language = (request.language ?? '').trim().toLowerCase();
|
|
16
|
+
// mermaid 由 securityLevel:'strict' 渲染为消毒后的 SVG,无需 opt-in 高风险开关。
|
|
17
|
+
if (isMermaidLanguage(language)) {
|
|
18
|
+
return {
|
|
19
|
+
kind: 'mermaid',
|
|
20
|
+
title: options.previewTitle,
|
|
21
|
+
content: request.code,
|
|
22
|
+
};
|
|
23
|
+
}
|
|
15
24
|
if (language === 'markdown' || language === 'md') {
|
|
16
25
|
return {
|
|
17
26
|
kind: 'markdown',
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
import { Marked } from 'marked';
|
|
7
7
|
import { createEditorMarkdownExtensions, extractPlainTextFromTokens, } from './editor-markdown-extensions.js';
|
|
8
8
|
import { highlightCode, isLanguageSupported } from './highlight.js';
|
|
9
|
+
import { isMermaidLanguage } from './mermaid-render.js';
|
|
9
10
|
/** Default parsing options for document mode. */
|
|
10
11
|
const DEFAULT_OPTIONS = {
|
|
11
12
|
enableHighlight: true,
|
|
@@ -103,6 +104,16 @@ function createRendererObject(options, state) {
|
|
|
103
104
|
code({ text, lang }) {
|
|
104
105
|
// rawLanguage is the original info string from the fence.
|
|
105
106
|
const rawLanguage = lang?.trim() || '';
|
|
107
|
+
// mermaid 块在阅读态自动渲染为图表;只有 code 模式的代码/预览切换才保留源码视图。
|
|
108
|
+
if (isMermaidLanguage(rawLanguage) && !options.showCodePreviewToggle) {
|
|
109
|
+
const mermaidBlockId = `hai-md-code-${state.codeBlocks.length + 1}`;
|
|
110
|
+
state.codeBlocks.push({
|
|
111
|
+
id: mermaidBlockId,
|
|
112
|
+
code: text,
|
|
113
|
+
language: rawLanguage,
|
|
114
|
+
});
|
|
115
|
+
return `<div class="hai-md-mermaid" data-mermaid-host="${escapeHtml(mermaidBlockId)}"></div>`;
|
|
116
|
+
}
|
|
106
117
|
// highlightLanguage is validated against supported languages.
|
|
107
118
|
const highlightLanguage = rawLanguage && isLanguageSupported(rawLanguage)
|
|
108
119
|
? rawLanguage
|
|
@@ -34,7 +34,7 @@ export interface MarkdownCodeRunRequest {
|
|
|
34
34
|
}
|
|
35
35
|
export interface MarkdownCodeRunResult {
|
|
36
36
|
/** Output kind that drives preview rendering. */
|
|
37
|
-
kind?: 'text' | 'markdown' | 'html';
|
|
37
|
+
kind?: 'text' | 'markdown' | 'html' | 'mermaid';
|
|
38
38
|
/** Preview output content. */
|
|
39
39
|
content: string;
|
|
40
40
|
/** Preview title; default text is used when omitted. */
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @h-ai/ui — Mermaid 图表渲染助手
|
|
3
|
+
*
|
|
4
|
+
* 以懒加载方式引入 mermaid,把图表源码渲染为安全的 SVG。
|
|
5
|
+
* 使用 `securityLevel: 'strict'`,由 mermaid 自身完成消毒,避免 {@html} 注入风险。
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* 判断 fence 语言是否为 mermaid。
|
|
9
|
+
*/
|
|
10
|
+
export declare function isMermaidLanguage(language: string | undefined | null): boolean;
|
|
11
|
+
/**
|
|
12
|
+
* 把 mermaid 源码渲染为 SVG 字符串;语法错误时抛出由调用方兜底展示。
|
|
13
|
+
*/
|
|
14
|
+
export declare function renderMermaidDiagram(code: string): Promise<string>;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @h-ai/ui — Mermaid 图表渲染助手
|
|
3
|
+
*
|
|
4
|
+
* 以懒加载方式引入 mermaid,把图表源码渲染为安全的 SVG。
|
|
5
|
+
* 使用 `securityLevel: 'strict'`,由 mermaid 自身完成消毒,避免 {@html} 注入风险。
|
|
6
|
+
*/
|
|
7
|
+
/** mermaid 模块只在首次渲染时动态加载,并缓存初始化后的实例。 */
|
|
8
|
+
let mermaidLoader = null;
|
|
9
|
+
/** render id 需要在页面内唯一,递增计数器即可保证不冲突。 */
|
|
10
|
+
let renderSeq = 0;
|
|
11
|
+
/**
|
|
12
|
+
* 判断 fence 语言是否为 mermaid。
|
|
13
|
+
*/
|
|
14
|
+
export function isMermaidLanguage(language) {
|
|
15
|
+
return (language ?? '').trim().toLowerCase() === 'mermaid';
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* 懒加载并初始化 mermaid,多次调用复用同一实例。
|
|
19
|
+
*/
|
|
20
|
+
async function loadMermaid() {
|
|
21
|
+
if (!mermaidLoader) {
|
|
22
|
+
mermaidLoader = import('mermaid').then((module) => {
|
|
23
|
+
const mermaid = module.default;
|
|
24
|
+
mermaid.initialize({
|
|
25
|
+
startOnLoad: false,
|
|
26
|
+
securityLevel: 'strict',
|
|
27
|
+
theme: 'default',
|
|
28
|
+
});
|
|
29
|
+
return mermaid;
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
return mermaidLoader;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* 把 mermaid 源码渲染为 SVG 字符串;语法错误时抛出由调用方兜底展示。
|
|
36
|
+
*/
|
|
37
|
+
export async function renderMermaidDiagram(code) {
|
|
38
|
+
const mermaid = await loadMermaid();
|
|
39
|
+
const renderId = `hai-mermaid-${Date.now()}-${renderSeq++}`;
|
|
40
|
+
const { svg } = await mermaid.render(renderId, code);
|
|
41
|
+
return svg;
|
|
42
|
+
}
|
package/dist/messages/en-US.json
CHANGED
|
@@ -248,6 +248,7 @@
|
|
|
248
248
|
"markdown_run_failed": "Run failed",
|
|
249
249
|
"markdown_run_unavailable": "No preview runner is configured for this code block.",
|
|
250
250
|
"markdown_run_unavailable_language": "Local preview for {language} is not supported. Reason: no runtime is configured for this language.",
|
|
251
|
+
"markdown_mermaid_failed": "Failed to render diagram",
|
|
251
252
|
"table_editor_eyebrow": "Editable table",
|
|
252
253
|
"table_copy": "Copy table",
|
|
253
254
|
"table_download": "Download table",
|
package/dist/messages/zh-CN.json
CHANGED
|
@@ -248,6 +248,7 @@
|
|
|
248
248
|
"markdown_run_failed": "运行失败",
|
|
249
249
|
"markdown_run_unavailable": "当前代码块未配置运行预览器。",
|
|
250
250
|
"markdown_run_unavailable_language": "暂不支持 {language} 的本地预览。原因:当前环境未配置该语言的运行器。",
|
|
251
|
+
"markdown_mermaid_failed": "图表渲染失败",
|
|
251
252
|
"table_editor_eyebrow": "可编辑表格",
|
|
252
253
|
"table_copy": "复制表格",
|
|
253
254
|
"table_download": "下载表格",
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@h-ai/ui",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.1.0-alpha.
|
|
4
|
+
"version": "0.1.0-alpha.22",
|
|
5
5
|
"description": "Hai Framework UI component library for Svelte 5 Runes.",
|
|
6
6
|
"license": "Apache-2.0",
|
|
7
7
|
"svelte": "./dist/index.js",
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
"access": "public"
|
|
34
34
|
},
|
|
35
35
|
"peerDependencies": {
|
|
36
|
-
"svelte": "^5.55.
|
|
36
|
+
"svelte": "^5.55.9"
|
|
37
37
|
},
|
|
38
38
|
"dependencies": {
|
|
39
39
|
"@internationalized/date": "^3.8.0",
|
|
@@ -42,9 +42,10 @@
|
|
|
42
42
|
"html2canvas": "^1.4.1",
|
|
43
43
|
"jspdf": "^4.2.1",
|
|
44
44
|
"marked": "^18.0.0",
|
|
45
|
+
"mermaid": "^11.12.0",
|
|
45
46
|
"shiki": "^4.0.2",
|
|
46
47
|
"tailwind-merge": "^3.5.0",
|
|
47
|
-
"@h-ai/core": "0.1.0-alpha.
|
|
48
|
+
"@h-ai/core": "0.1.0-alpha.22"
|
|
48
49
|
},
|
|
49
50
|
"devDependencies": {
|
|
50
51
|
"@sveltejs/package": "^2.5.7",
|