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

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.
@@ -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,9 @@ 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';
18
+ export { default as AiTableEditor } from './table/AiTableEditor.svelte';
19
+ export type { AiTableColumn, AiTableColumnType, AiTableCopyPayload, AiTableData, AiTableDownloadPayload, AiTableEditorChangeAction, AiTableEditorChangePayload, AiTableEditorProps, AiTableRow, } from './table/table-types.js';
@@ -11,3 +11,4 @@ export { downloadAiDocument, resolveDocumentDownloadActions, resolveDocumentMark
11
11
  export { renderMarkdownDocument } from './document-parse.js';
12
12
  export { parseMarkdown } from './markdown-parse.js';
13
13
  export { default as MarkdownRenderer } from './MarkdownRenderer.svelte';
14
+ export { default as AiTableEditor } from './table/AiTableEditor.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
  });