@h-ai/ui 0.1.0-alpha.10 → 0.1.0-alpha.11

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.
@@ -4,6 +4,7 @@
4
4
  * Extracts HTML, outline, and code block metadata for document-mode rendering.
5
5
  */
6
6
  import { Marked } from 'marked';
7
+ import { createEditorMarkdownExtensions, extractPlainTextFromTokens, } from './editor-markdown-extensions.js';
7
8
  import { highlightCode, isLanguageSupported } from './highlight.js';
8
9
  /** Default parsing options for document mode. */
9
10
  const DEFAULT_OPTIONS = {
@@ -82,8 +83,8 @@ function createRendererObject(options, state) {
82
83
  heading(token) {
83
84
  // depth is clamped to the valid heading range.
84
85
  const depth = Math.max(1, Math.min(6, token.depth));
85
- // text is the plain heading text used for outline labels.
86
- const text = token.text.trim();
86
+ // text 需要剥离自定义 span / 强调等 inline token,保证目录显示的是用户可见标题。
87
+ const text = extractPlainTextFromTokens(token.tokens).trim();
87
88
  // id is the stable anchor for outline navigation.
88
89
  const id = createHeadingId(state, text);
89
90
  state.outline.push({
@@ -205,6 +206,7 @@ function createRendererObject(options, state) {
205
206
  function createMarkedInstance(options, state) {
206
207
  return new Marked({
207
208
  renderer: createRendererObject(options, state),
209
+ extensions: createEditorMarkdownExtensions(),
208
210
  gfm: true,
209
211
  breaks: options.breaks,
210
212
  });
@@ -59,13 +59,31 @@ export interface MarkdownRewriteRequest {
59
59
  title?: string;
60
60
  }
61
61
  /**
62
- * Block-level format actions for the editor toolbar.
62
+ * Legacy block-level format actions preserved for backward compatibility.
63
63
  */
64
64
  export type MarkdownBlockFormatKind = 'heading' | 'bullet';
65
+ /**
66
+ * Rich block-style actions used by the current editor toolbar.
67
+ */
68
+ export type MarkdownBlockStyleKind = 'paragraph' | 'heading1' | 'heading2' | 'heading3' | 'heading4';
65
69
  /**
66
70
  * Inline format actions for the editor toolbar.
71
+ * `highlight` and `link` stay in the public union so older integrations keep compiling.
67
72
  */
68
73
  export type MarkdownInlineFormatKind = 'bold' | 'italic' | 'strike' | 'underline' | 'code' | 'highlight' | 'link';
74
+ /**
75
+ * Paragraph alignment actions for the editor toolbar.
76
+ */
77
+ export type MarkdownTextAlignKind = 'left' | 'center' | 'right' | 'justify';
78
+ /**
79
+ * 颜色面板一次只会修改前景色或背景色中的一个维度。
80
+ */
81
+ export interface MarkdownColorFormatRequest {
82
+ /** `text` 表示文字颜色,`background` 表示文本底色。 */
83
+ target: 'text' | 'background';
84
+ /** 颜色值为空时表示恢复默认。 */
85
+ value: string | null;
86
+ }
69
87
  export interface MarkdownToolbarDownloadAction {
70
88
  /** Action id used to identify the download target. */
71
89
  id: string;
@@ -74,6 +92,13 @@ export interface MarkdownToolbarDownloadAction {
74
92
  /** Optional short badge displayed beside the menu label. */
75
93
  badgeLabel?: string;
76
94
  }
95
+ /**
96
+ * Callback props are exposed to Svelte consumers, so we keep parameter checks bivariant.
97
+ * This lets older handlers with narrower unions remain assignable while the component gains new APIs.
98
+ */
99
+ type BivariantCallback<Args extends unknown[], Return = void> = {
100
+ bivarianceHack: (...args: Args) => Return;
101
+ }['bivarianceHack'];
77
102
  export interface AiDocumentEditorProps {
78
103
  /** Markdown source content. */
79
104
  content?: string;
@@ -153,12 +178,21 @@ export interface AiDocumentEditorProps {
153
178
  onhistory?: () => void;
154
179
  /** Custom full-document copy handler. */
155
180
  oncopydocument?: () => void | Promise<void>;
156
- /** Block-format action handler from the selection toolbar. */
157
- onapplyblockformat?: (kind: MarkdownBlockFormatKind) => void;
181
+ /** Legacy block-format action handler for older heading / bullet integrations. */
182
+ onapplyblockformat?: BivariantCallback<[kind: MarkdownBlockFormatKind], void>;
183
+ /** Rich block-style handler for paragraph and concrete heading-level changes. */
184
+ onapplyblockstyle?: BivariantCallback<[kind: MarkdownBlockStyleKind], void>;
158
185
  /** Inline-format action handler from the selection toolbar. */
159
- onapplyinlineformat?: (kind: MarkdownInlineFormatKind) => void;
186
+ onapplyinlineformat?: BivariantCallback<[kind: MarkdownInlineFormatKind], void>;
187
+ /** Paragraph alignment handler from the selection toolbar. */
188
+ onapplyalignment?: (kind: MarkdownTextAlignKind) => void;
189
+ /** Link add / edit / remove handler from the selection toolbar. */
190
+ onapplylink?: (href: string | null) => void;
191
+ /** Text or background color handler from the selection toolbar. */
192
+ onapplycolor?: (request: MarkdownColorFormatRequest) => void;
160
193
  /** Selection copy handler for custom behavior. */
161
194
  oncopyselection?: () => void | Promise<void>;
162
195
  /** Selection annotation handler. */
163
196
  onannotation?: () => void | Promise<void>;
164
197
  }
198
+ export {};
@@ -0,0 +1,11 @@
1
+ import type { Token, TokenizerAndRendererExtension } from 'marked';
2
+ /**
3
+ * Markdown heading 提取目录时需要一个“纯文本标题”。
4
+ * 这里显式递归常见 inline token,保证 `<hai-span>`、链接、强调等都能回落成纯文本。
5
+ */
6
+ export declare function extractPlainTextFromTokens(tokens: Token[] | undefined): string;
7
+ /**
8
+ * ChatWorkspace 编辑器会把“非标准 Markdown”能力写成自定义标签,
9
+ * 这里把它们注册为 marked 扩展,保证普通 Markdown 和编辑器增强能力共享同一条渲染链路。
10
+ */
11
+ export declare function createEditorMarkdownExtensions(): TokenizerAndRendererExtension[];
@@ -0,0 +1,184 @@
1
+ /**
2
+ * 编辑器扩展里只接受固定格式的十六进制颜色,避免把任意 style 串透传到渲染结果。
3
+ */
4
+ const HEX_COLOR_REGEX = /^#(?:[0-9a-f]{3}|[0-9a-f]{6}|[0-9a-f]{8})$/i;
5
+ /**
6
+ * 对齐扩展只开放编辑器工具条里的四种模式。
7
+ */
8
+ const ALIGN_VALUE_REGEX = /^(?:left|center|right|justify)$/i;
9
+ const HAI_ALIGN_BLOCK_REGEX = /^<hai-align\s+value="(left|center|right|justify)">\n?([\s\S]+?)\n?<\/hai-align>\n{0,2}/i;
10
+ const HAI_SPAN_INLINE_REGEX = /^<hai-span(?:\s+color="([^"]+)")?(?:\s+bg="([^"]+)")?\s*>([\s\S]+?)<\/hai-span>/i;
11
+ const HAI_UNDERLINE_REGEX = /^\+\+((?:\\.|[\s\S])+?)\+\+/;
12
+ const HAI_HIGHLIGHT_REGEX = /^==((?:\\.|[\s\S])+?)==/;
13
+ /**
14
+ * Markdown heading 提取目录时需要一个“纯文本标题”。
15
+ * 这里显式递归常见 inline token,保证 `<hai-span>`、链接、强调等都能回落成纯文本。
16
+ */
17
+ export function extractPlainTextFromTokens(tokens) {
18
+ if (!tokens || tokens.length === 0) {
19
+ return '';
20
+ }
21
+ return tokens.map((token) => {
22
+ if ('tokens' in token && Array.isArray(token.tokens)) {
23
+ return extractPlainTextFromTokens(token.tokens);
24
+ }
25
+ if ('text' in token && typeof token.text === 'string') {
26
+ return token.text;
27
+ }
28
+ if ('raw' in token && typeof token.raw === 'string') {
29
+ return token.raw;
30
+ }
31
+ return '';
32
+ }).join('');
33
+ }
34
+ function escapeHtml(text) {
35
+ return text
36
+ .replace(/&/g, '&amp;')
37
+ .replace(/</g, '&lt;')
38
+ .replace(/>/g, '&gt;')
39
+ .replace(/"/g, '&quot;')
40
+ .replace(/'/g, '&#39;');
41
+ }
42
+ function sanitizeColor(value) {
43
+ if (!value) {
44
+ return undefined;
45
+ }
46
+ const normalized = value.trim().toLowerCase();
47
+ return HEX_COLOR_REGEX.test(normalized) ? normalized : undefined;
48
+ }
49
+ function sanitizeAlign(value) {
50
+ if (!value) {
51
+ return undefined;
52
+ }
53
+ const normalized = value.trim().toLowerCase();
54
+ return ALIGN_VALUE_REGEX.test(normalized)
55
+ ? normalized
56
+ : undefined;
57
+ }
58
+ /**
59
+ * ChatWorkspace 编辑器会把“非标准 Markdown”能力写成自定义标签,
60
+ * 这里把它们注册为 marked 扩展,保证普通 Markdown 和编辑器增强能力共享同一条渲染链路。
61
+ */
62
+ export function createEditorMarkdownExtensions() {
63
+ return [
64
+ {
65
+ name: 'haiAlign',
66
+ level: 'block',
67
+ start(src) {
68
+ return src.indexOf('<hai-align');
69
+ },
70
+ tokenizer(src) {
71
+ const match = src.match(HAI_ALIGN_BLOCK_REGEX);
72
+ if (!match) {
73
+ return undefined;
74
+ }
75
+ const align = sanitizeAlign(match[1]);
76
+ if (!align) {
77
+ return undefined;
78
+ }
79
+ const body = match[2].trim();
80
+ return {
81
+ type: 'haiAlign',
82
+ raw: match[0],
83
+ align,
84
+ tokens: this.lexer.blockTokens(body),
85
+ };
86
+ },
87
+ renderer(token) {
88
+ const alignToken = token;
89
+ return `<div class="hai-md-align-block" data-hai-align="${escapeHtml(alignToken.align)}" style="text-align:${escapeHtml(alignToken.align)};">${this.parser.parse(alignToken.tokens)}</div>`;
90
+ },
91
+ childTokens: ['tokens'],
92
+ },
93
+ {
94
+ name: 'haiSpan',
95
+ level: 'inline',
96
+ start(src) {
97
+ return src.indexOf('<hai-span');
98
+ },
99
+ tokenizer(src) {
100
+ const match = src.match(HAI_SPAN_INLINE_REGEX);
101
+ if (!match) {
102
+ return undefined;
103
+ }
104
+ const color = sanitizeColor(match[1]);
105
+ const background = sanitizeColor(match[2]);
106
+ return {
107
+ type: 'haiSpan',
108
+ raw: match[0],
109
+ color,
110
+ background,
111
+ tokens: this.lexer.inlineTokens(match[3]),
112
+ };
113
+ },
114
+ renderer(token) {
115
+ const spanToken = token;
116
+ const style = [
117
+ spanToken.color ? `color:${spanToken.color}` : '',
118
+ spanToken.background ? `background-color:${spanToken.background}` : '',
119
+ ].filter(Boolean).join(';');
120
+ if (!style) {
121
+ return this.parser.parseInline(spanToken.tokens);
122
+ }
123
+ const attributes = [
124
+ 'class="hai-md-inline-style"',
125
+ `style="${escapeHtml(style)};"`,
126
+ spanToken.color
127
+ ? `data-hai-color="${escapeHtml(spanToken.color)}"`
128
+ : '',
129
+ spanToken.background
130
+ ? `data-hai-bg="${escapeHtml(spanToken.background)}"`
131
+ : '',
132
+ ].filter(Boolean).join(' ');
133
+ return `<span ${attributes}>${this.parser.parseInline(spanToken.tokens)}</span>`;
134
+ },
135
+ childTokens: ['tokens'],
136
+ },
137
+ {
138
+ name: 'haiUnderline',
139
+ level: 'inline',
140
+ start(src) {
141
+ return src.indexOf('++');
142
+ },
143
+ tokenizer(src) {
144
+ const match = src.match(HAI_UNDERLINE_REGEX);
145
+ if (!match) {
146
+ return undefined;
147
+ }
148
+ return {
149
+ type: 'haiUnderline',
150
+ raw: match[0],
151
+ tokens: this.lexer.inlineTokens(match[1]),
152
+ };
153
+ },
154
+ renderer(token) {
155
+ const underlineToken = token;
156
+ return `<u>${this.parser.parseInline(underlineToken.tokens)}</u>`;
157
+ },
158
+ childTokens: ['tokens'],
159
+ },
160
+ {
161
+ name: 'haiHighlight',
162
+ level: 'inline',
163
+ start(src) {
164
+ return src.indexOf('==');
165
+ },
166
+ tokenizer(src) {
167
+ const match = src.match(HAI_HIGHLIGHT_REGEX);
168
+ if (!match) {
169
+ return undefined;
170
+ }
171
+ return {
172
+ type: 'haiHighlight',
173
+ raw: match[0],
174
+ tokens: this.lexer.inlineTokens(match[1]),
175
+ };
176
+ },
177
+ renderer(token) {
178
+ const highlightToken = token;
179
+ return `<mark>${this.parser.parseInline(highlightToken.tokens)}</mark>`;
180
+ },
181
+ childTokens: ['tokens'],
182
+ },
183
+ ];
184
+ }
@@ -11,7 +11,7 @@ export { downloadAiDocument, resolveDocumentDownloadActions, resolveDocumentMark
11
11
  export type { AiDocumentDownloadFormat, AiDocumentDownloadRequest, AiDocumentResolvedDownloadAction, } from './document-download.js';
12
12
  export { renderMarkdownDocument } from './document-parse.js';
13
13
  export type { MarkdownDocumentParseOptions, MarkdownRenderResult, } from './document-parse.js';
14
- export type { AiDocumentEditorProps, MarkdownBlockFormatKind, MarkdownCodeBlockItem, MarkdownCodeRunRequest, MarkdownCodeRunResult, MarkdownInlineFormatKind, MarkdownOutlineItem, MarkdownRewriteAction, MarkdownRewriteRequest, MarkdownToolbarDownloadAction, } from './document-types.js';
14
+ export type { AiDocumentEditorProps, MarkdownBlockFormatKind, MarkdownBlockStyleKind, MarkdownCodeBlockItem, MarkdownCodeRunRequest, MarkdownCodeRunResult, MarkdownColorFormatRequest, MarkdownInlineFormatKind, MarkdownOutlineItem, MarkdownRewriteAction, MarkdownRewriteRequest, MarkdownTextAlignKind, MarkdownToolbarDownloadAction, } from './document-types.js';
15
15
  export { parseMarkdown } from './markdown-parse.js';
16
16
  export type { MarkdownParseOptions } from './markdown-parse.js';
17
17
  export { default as MarkdownRenderer } from './MarkdownRenderer.svelte';
@@ -6,6 +6,7 @@
6
6
  * @module markdown-parse
7
7
  */
8
8
  import { Marked } from 'marked';
9
+ import { createEditorMarkdownExtensions } from './editor-markdown-extensions.js';
9
10
  import { highlightCode, isLanguageSupported } from './highlight.js';
10
11
  const SAFE_LINK_HREF_REGEX = /^(?:https?:\/\/|\/|#|mailto:)/i;
11
12
  const SAFE_IMAGE_SRC_REGEX = /^(?:https?:\/\/|\/|data:image\/)/i;
@@ -117,6 +118,7 @@ function createRendererObject(options) {
117
118
  function createMarkedInstance(options) {
118
119
  return new Marked({
119
120
  renderer: createRendererObject(options),
121
+ extensions: createEditorMarkdownExtensions(),
120
122
  gfm: true,
121
123
  breaks: options.breaks,
122
124
  });
@@ -204,6 +204,11 @@
204
204
  "markdown_history": "History",
205
205
  "markdown_view_version": "View version",
206
206
  "markdown_format_heading": "Heading",
207
+ "markdown_format_paragraph": "Paragraph",
208
+ "markdown_format_heading_1": "Heading 1",
209
+ "markdown_format_heading_2": "Heading 2",
210
+ "markdown_format_heading_3": "Heading 3",
211
+ "markdown_format_heading_4": "Heading 4",
207
212
  "markdown_format_bullet": "Bullets",
208
213
  "markdown_format_bold": "Bold",
209
214
  "markdown_format_strike": "Strike",
@@ -212,6 +217,20 @@
212
217
  "markdown_format_link": "Link",
213
218
  "markdown_format_code": "Code",
214
219
  "markdown_format_highlight": "Highlight",
220
+ "markdown_align": "Align",
221
+ "markdown_align_left": "Align left",
222
+ "markdown_align_center": "Align center",
223
+ "markdown_align_right": "Align right",
224
+ "markdown_align_justify": "Justify",
225
+ "markdown_text_color": "Text color",
226
+ "markdown_background_color": "Background color",
227
+ "markdown_reset_default": "Reset default",
228
+ "markdown_color_dialog_label": "Text and background color",
229
+ "markdown_link_dialog_label": "Edit link",
230
+ "markdown_link_input_label": "Link URL",
231
+ "markdown_link_placeholder": "Paste or type a link",
232
+ "markdown_link_apply": "Apply link",
233
+ "markdown_link_remove": "Remove link",
215
234
  "markdown_copy_selection": "Copy",
216
235
  "markdown_annotation": "Annotate",
217
236
  "markdown_run_preview": "Run Preview",
@@ -204,6 +204,11 @@
204
204
  "markdown_history": "历史版本",
205
205
  "markdown_view_version": "查看版本",
206
206
  "markdown_format_heading": "标题",
207
+ "markdown_format_paragraph": "正文",
208
+ "markdown_format_heading_1": "一级标题",
209
+ "markdown_format_heading_2": "二级标题",
210
+ "markdown_format_heading_3": "三级标题",
211
+ "markdown_format_heading_4": "四级标题",
207
212
  "markdown_format_bullet": "列表",
208
213
  "markdown_format_bold": "加粗",
209
214
  "markdown_format_strike": "删除线",
@@ -212,6 +217,20 @@
212
217
  "markdown_format_link": "链接",
213
218
  "markdown_format_code": "代码",
214
219
  "markdown_format_highlight": "高亮",
220
+ "markdown_align": "对齐",
221
+ "markdown_align_left": "左对齐",
222
+ "markdown_align_center": "居中对齐",
223
+ "markdown_align_right": "右对齐",
224
+ "markdown_align_justify": "两端对齐",
225
+ "markdown_text_color": "字体颜色",
226
+ "markdown_background_color": "背景颜色",
227
+ "markdown_reset_default": "恢复默认",
228
+ "markdown_color_dialog_label": "文本和背景颜色",
229
+ "markdown_link_dialog_label": "编辑链接",
230
+ "markdown_link_input_label": "链接地址",
231
+ "markdown_link_placeholder": "输入或粘贴链接地址",
232
+ "markdown_link_apply": "应用链接",
233
+ "markdown_link_remove": "移除链接",
215
234
  "markdown_copy_selection": "复制",
216
235
  "markdown_annotation": "注释",
217
236
  "markdown_run_preview": "运行预览",
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.10",
4
+ "version": "0.1.0-alpha.11",
5
5
  "description": "Hai Framework UI component library for Svelte 5 Runes.",
6
6
  "license": "Apache-2.0",
7
7
  "svelte": "./dist/index.js",
@@ -42,7 +42,7 @@
42
42
  "marked": "^18.0.0",
43
43
  "shiki": "^4.0.2",
44
44
  "tailwind-merge": "^3.5.0",
45
- "@h-ai/core": "0.1.0-alpha.10"
45
+ "@h-ai/core": "0.1.0-alpha.11"
46
46
  },
47
47
  "devDependencies": {
48
48
  "@sveltejs/package": "^2.5.7",