wendang 0.1.0 → 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.
Files changed (98) hide show
  1. package/LICENSE +43 -2
  2. package/NOTICE.md +1 -1
  3. package/README.md +120 -52
  4. package/dist/cjs/document.js +1 -19
  5. package/dist/cjs/index.js +1 -25
  6. package/dist/cjs/mindmap.js +1 -22
  7. package/dist/cjs/schema.js +1 -11
  8. package/dist/cjs/shared/anchor.js +1 -201
  9. package/dist/cjs/shared/callout.js +1 -64
  10. package/dist/cjs/shared/code-block.js +1 -33
  11. package/dist/cjs/shared/code-languages.js +1 -51
  12. package/dist/cjs/shared/doc-compatibility.js +1 -23
  13. package/dist/cjs/shared/doc-text.js +1 -52
  14. package/dist/cjs/shared/document-colors.js +1 -61
  15. package/dist/cjs/shared/headings.js +1 -0
  16. package/dist/cjs/shared/inline-comments.js +1 -179
  17. package/dist/cjs/shared/mind-map-folding.js +1 -14
  18. package/dist/cjs/shared/mind-map-phase2.js +1 -133
  19. package/dist/cjs/shared/mind-map.js +1 -445
  20. package/dist/cjs/shared/mind-topic-text.js +1 -57
  21. package/dist/cjs/shared/node-catalog.js +1 -62
  22. package/dist/cjs/shared/presence.js +1 -141
  23. package/dist/cjs/shared/safe-href.js +1 -36
  24. package/dist/cjs/shared/table-cell-format.js +1 -18
  25. package/dist/cjs/shared/table-layout.js +1 -28
  26. package/dist/cjs/shared/types.js +1 -2
  27. package/dist/cjs/shared/video-embed.js +1 -102
  28. package/dist/cjs/shared/workbook.js +1 -152
  29. package/dist/cjs/spreadsheet.js +1 -17
  30. package/dist/cjs/types.js +1 -17
  31. package/dist/cjs/validation/callout.js +1 -17
  32. package/dist/cjs/validation/doc-node-whitelist.js +1 -71
  33. package/dist/cjs/validation/error.js +1 -17
  34. package/dist/cjs/validation/mind-map.js +1 -129
  35. package/dist/cjs/validation/schema-version.js +1 -5
  36. package/dist/cjs/validation/schema.js +1 -376
  37. package/dist/cjs/validation/video-embed.js +1 -17
  38. package/dist/cjs/validation/workbook.js +1 -17
  39. package/dist/esm/document.js +1 -3
  40. package/dist/esm/index.js +1 -3
  41. package/dist/esm/mindmap.js +1 -4
  42. package/dist/esm/schema.js +1 -3
  43. package/dist/esm/shared/anchor.js +1 -192
  44. package/dist/esm/shared/callout.js +1 -56
  45. package/dist/esm/shared/code-block.js +1 -25
  46. package/dist/esm/shared/code-languages.js +1 -46
  47. package/dist/esm/shared/doc-compatibility.js +1 -20
  48. package/dist/esm/shared/doc-text.js +1 -48
  49. package/dist/esm/shared/document-colors.js +1 -56
  50. package/dist/esm/shared/headings.js +1 -0
  51. package/dist/esm/shared/inline-comments.js +1 -173
  52. package/dist/esm/shared/mind-map-folding.js +1 -10
  53. package/dist/esm/shared/mind-map-phase2.js +1 -121
  54. package/dist/esm/shared/mind-map.js +1 -427
  55. package/dist/esm/shared/mind-topic-text.js +1 -49
  56. package/dist/esm/shared/node-catalog.js +1 -57
  57. package/dist/esm/shared/presence.js +1 -130
  58. package/dist/esm/shared/safe-href.js +1 -32
  59. package/dist/esm/shared/table-cell-format.js +1 -14
  60. package/dist/esm/shared/table-layout.js +1 -23
  61. package/dist/esm/shared/types.js +1 -1
  62. package/dist/esm/shared/video-embed.js +1 -96
  63. package/dist/esm/shared/workbook.js +1 -144
  64. package/dist/esm/spreadsheet.js +1 -1
  65. package/dist/esm/types.js +1 -1
  66. package/dist/esm/validation/callout.js +1 -1
  67. package/dist/esm/validation/doc-node-whitelist.js +1 -68
  68. package/dist/esm/validation/error.js +1 -13
  69. package/dist/esm/validation/mind-map.js +1 -125
  70. package/dist/esm/validation/schema-version.js +1 -2
  71. package/dist/esm/validation/schema.js +1 -372
  72. package/dist/esm/validation/video-embed.js +1 -1
  73. package/dist/esm/validation/workbook.js +1 -1
  74. package/dist/types/document.d.ts +1 -0
  75. package/dist/types/shared/anchor.d.ts +0 -55
  76. package/dist/types/shared/callout.d.ts +39 -2
  77. package/dist/types/shared/code-block.d.ts +0 -1
  78. package/dist/types/shared/code-languages.d.ts +0 -19
  79. package/dist/types/shared/doc-text.d.ts +0 -14
  80. package/dist/types/shared/document-colors.d.ts +0 -4
  81. package/dist/types/shared/headings.d.ts +7 -0
  82. package/dist/types/shared/inline-comments.d.ts +0 -42
  83. package/dist/types/shared/mind-map-folding.d.ts +0 -2
  84. package/dist/types/shared/mind-map.d.ts +0 -4
  85. package/dist/types/shared/mind-topic-text.d.ts +0 -2
  86. package/dist/types/shared/node-catalog.d.ts +0 -17
  87. package/dist/types/shared/presence.d.ts +0 -51
  88. package/dist/types/shared/safe-href.d.ts +0 -1
  89. package/dist/types/shared/table-cell-format.d.ts +0 -1
  90. package/dist/types/shared/table-layout.d.ts +0 -3
  91. package/dist/types/shared/types.d.ts +0 -4
  92. package/dist/types/shared/video-embed.d.ts +9 -2
  93. package/dist/types/shared/workbook.d.ts +0 -2
  94. package/dist/types/validation/doc-node-whitelist.d.ts +0 -19
  95. package/dist/types/validation/mind-map.d.ts +0 -2
  96. package/dist/types/validation/schema-version.d.ts +0 -1
  97. package/dist/types/validation/schema.d.ts +0 -34
  98. package/package.json +9 -9
@@ -1,427 +1 @@
1
- import { isSafeHref } from './safe-href.js';
2
- import { readableTextColor } from './presence.js';
3
- import { mindTopicLineHeight, mindTopicTextSize, MIND_TOPIC_FONT } from './mind-topic-text.js';
4
- /** Engine-independent document format. Topic text remains ordinary ProseMirror text. */
5
- export const MIND_MAP_VERSION = 3;
6
- export const MIND_MAP_MAX_TOPICS = 500;
7
- export const MIND_MAP_MAX_TEXT = 2000;
8
- export const MIND_MAP_LAYOUTS = ['right', 'left', 'both', 'down'];
9
- export const MIND_MAP_THEMES = ['blue', 'teal', 'violet', 'amber'];
10
- export const mindMapId = () => crypto.randomUUID();
11
- const validId = (id) => typeof id === 'string' && /^[\w-]{1,80}$/.test(id);
12
- export function createMindMap(layout = 'right') {
13
- const id = mindMapId(), rootId = mindMapId();
14
- return {
15
- type: 'mindMap', attrs: { id, rootId, formatVersion: MIND_MAP_VERSION, layout, theme: 'blue', height: 380 },
16
- content: [
17
- { type: 'mindMapTopic', attrs: { id: rootId, parentId: '', order: 0 }, content: [{ type: 'text', text: '中心主题' }] },
18
- ...['分支主题 1', '分支主题 2', '分支主题 3'].map((text, order) => ({
19
- type: 'mindMapTopic', attrs: { id: mindMapId(), parentId: rootId, order },
20
- content: [{ type: 'text', text }],
21
- })),
22
- ],
23
- };
24
- }
25
- export function readMindMap(node) {
26
- const a = node?.attrs;
27
- if (node?.type !== 'mindMap' || !a || ![1, 2, MIND_MAP_VERSION].includes(Number(a.formatVersion)) ||
28
- !validId(a.id) || !validId(a.rootId) || !Array.isArray(node.content) ||
29
- node.content.length < 1 || node.content.filter(t => t?.type === 'mindMapTopic').length > MIND_MAP_MAX_TOPICS || node.content.length > 1500)
30
- return null;
31
- const topics = [];
32
- const ids = new Set();
33
- for (const topic of node.content.filter(t => t?.type !== 'mindMapRelation')) {
34
- if (!topic || typeof topic !== 'object')
35
- return null;
36
- const t = topic.attrs;
37
- if (topic.type !== 'mindMapTopic' || !t || !validId(t.id) || ids.has(t.id) ||
38
- typeof t.parentId !== 'string' || (t.parentId !== '' && !validId(t.parentId)) ||
39
- typeof t.order !== 'number' || !Number.isFinite(t.order) ||
40
- Math.abs(t.order) > 1e12 || (topic.content !== undefined && !Array.isArray(topic.content)) ||
41
- topic.content?.some(c => !c || !['text', 'mindMapDescription', 'mindMapNote'].includes(c.type ?? '') || (c.type === 'text' && typeof c.text !== 'string')))
42
- return null;
43
- const text = (topic.content ?? []).filter(c => c.type === 'text').map(c => c.text ?? '').join('');
44
- if (text.length > MIND_MAP_MAX_TEXT)
45
- return null;
46
- ids.add(t.id);
47
- const extra = {};
48
- if (t.color && !/^#[0-9a-f]{6}$/i.test(String(t.color)))
49
- return null;
50
- if (t.background && !/^#[0-9a-f]{6}$/i.test(String(t.background)))
51
- return null;
52
- for (const key of ['color', 'background'])
53
- if (t[key] && !['#18243a', '#ffffff', '#3864ef', '#087f8c', '#7950c7', '#a66b08', '#d94848', '#f3f5fb', '#dce7ff', '#d3f4e9', '#ece0ff', '#fff0c2', '#ffe1e1', '#233044'].includes(String(t[key])))
54
- return null;
55
- if (t.shape && !['round', 'pill', 'text'].includes(String(t.shape)))
56
- return null;
57
- if (t.fontSize && ![12, 14, 16, 18, 20].includes(Number(t.fontSize)))
58
- return null;
59
- if (t.align && !['left', 'center', 'right'].includes(String(t.align)))
60
- return null;
61
- if (t.done !== undefined && t.done !== null && typeof t.done !== 'boolean')
62
- return null;
63
- if (t.side !== undefined && t.side !== null && t.side !== -1 && t.side !== 1)
64
- return null;
65
- if (t.icon && !['💡', '🎯', '📌', '🚩', '⭐', '✅', '⚠️', '💬', '📅', '🔎'].includes(String(t.icon)))
66
- return null;
67
- if (t.attachmentId && !(typeof t.attachmentId === 'string' && /^[0-9a-f-]{36}$/i.test(t.attachmentId)))
68
- return null;
69
- const fields = new Set();
70
- let fieldsStarted = false, noteStarted = false;
71
- for (const child of topic.content ?? []) {
72
- if (child.marks !== undefined && (!Array.isArray(child.marks) || child.marks.some(m => !m || !['bold', 'italic', 'strike', 'textStyle', 'highlight', 'link'].includes(m.type))))
73
- return null;
74
- if (child.type !== 'text' && ((child.content !== undefined && !Array.isArray(child.content)) || child.content?.some(n => n.type !== 'text' || typeof n.text !== 'string') || (child.content ?? []).reduce((sum, n) => sum + (n.text?.length ?? 0), 0) > 4000))
75
- return null;
76
- if (child.type === 'text' && fieldsStarted)
77
- return null;
78
- if (child.type !== 'text') {
79
- if (fields.has(child.type) || (child.type === 'mindMapDescription' && noteStarted))
80
- return null;
81
- fields.add(child.type);
82
- fieldsStarted = true;
83
- if (child.type === 'mindMapNote')
84
- noteStarted = true;
85
- }
86
- }
87
- for (const key of ['shape', 'color', 'background', 'fontSize', 'align', 'icon', 'done', 'attachmentId', 'side'])
88
- if (t[key] !== undefined && t[key] !== null && t[key] !== '')
89
- Object.assign(extra, { [key]: t[key] });
90
- const runs = (topic.content ?? []).filter(c => c.type === 'text').map(c => ({ text: c.text ?? '', ...(c.marks?.length ? { marks: c.marks } : {}) }));
91
- if (runs.some(r => r.marks?.length))
92
- extra.runs = runs;
93
- for (const [type, key] of [['mindMapDescription', 'description'], ['mindMapNote', 'note']]) {
94
- const child = topic.content?.find(c => c.type === type);
95
- if (child)
96
- extra[key] = (child.content ?? []).map(c => c.text ?? '').join('');
97
- }
98
- topics.push({ id: t.id, parentId: t.parentId, order: t.order, text, ...extra });
99
- }
100
- if (!ids.has(a.rootId))
101
- return null;
102
- const relations = node.content.filter(c => c.type === 'mindMapRelation');
103
- const relationIds = new Set();
104
- for (const r of relations) {
105
- const p = r.attrs;
106
- if (!p || !validId(p.id) || ids.has(p.id) || relationIds.has(p.id) || !validId(p.source) || !validId(p.target) ||
107
- p.source === p.target || !['related', 'depends', 'reference'].includes(String(p.kind)) ||
108
- typeof p.label !== 'string' || p.label.length > 200)
109
- return null;
110
- relationIds.add(p.id);
111
- }
112
- return {
113
- id: a.id, rootId: a.rootId, topics,
114
- ...(a.branchStyle ? { branchStyle: a.branchStyle } : {}),
115
- ...(node.content.some(c => c.type === 'mindMapRelation') ? { relations: node.content.filter(c => c.type === 'mindMapRelation').map(c => ({ ...c.attrs })) } : {}),
116
- layout: MIND_MAP_LAYOUTS.includes(a.layout) ? a.layout : 'right',
117
- theme: MIND_MAP_THEMES.includes(a.theme) ? a.theme : 'blue',
118
- height: typeof a.height === 'number' && Number.isFinite(a.height) ? Math.min(800, Math.max(240, a.height)) : 380,
119
- };
120
- }
121
- /** Missing parents and concurrent cycles keep every surviving topic, deterministically. */
122
- export function mindMapTree(model) {
123
- const topics = model.topics.map(t => ({ ...t }));
124
- const byId = new Map(topics.map(t => [t.id, t]));
125
- for (const t of topics) {
126
- if (t.id === model.rootId)
127
- t.parentId = '';
128
- else if (!byId.has(t.parentId) || t.parentId === t.id)
129
- t.parentId = model.rootId;
130
- }
131
- for (const start of [...topics].sort((a, b) => a.id.localeCompare(b.id))) {
132
- const path = [], seen = new Set();
133
- let current = start;
134
- while (current && current.id !== model.rootId) {
135
- if (seen.has(current.id)) {
136
- const cycle = path.slice(path.indexOf(current.id)).sort();
137
- byId.get(cycle[0]).parentId = model.rootId;
138
- break;
139
- }
140
- seen.add(current.id);
141
- path.push(current.id);
142
- current = byId.get(current.parentId);
143
- }
144
- }
145
- return topics.sort((a, b) => a.order - b.order || a.id.localeCompare(b.id));
146
- }
147
- export function mindMapDescendants(model, id) {
148
- const tree = mindMapTree(model), ids = new Set([id]);
149
- for (let changed = true; changed;) {
150
- changed = false;
151
- for (const t of tree)
152
- if (ids.has(t.parentId) && !ids.has(t.id)) {
153
- ids.add(t.id);
154
- changed = true;
155
- }
156
- }
157
- return ids;
158
- }
159
- export function canMoveMindTopic(model, id, parentId) {
160
- return id !== model.rootId && model.topics.some(t => t.id === id) &&
161
- model.topics.some(t => t.id === parentId) && !mindMapDescendants(model, id).has(parentId);
162
- }
163
- /** Only first-level topics own a side; all descendants inherit their branch direction. */
164
- export function mindMapBranchSides(model) {
165
- return new Map(mindMapTree(model).filter(t => t.parentId === model.rootId).map((t, i) => [t.id, model.layout === 'left' ? -1 : model.layout === 'both' ? t.side ?? (i % 2 ? -1 : 1) : 1]));
166
- }
167
- /** Shared by the live drop preview and the single document transaction on release. */
168
- export function projectMindSideMove(model, id, side, beforeId) {
169
- if (model.layout === 'down' || (side !== -1 && side !== 1) || !canMoveMindTopic(model, id, model.rootId))
170
- return null;
171
- const sides = mindMapBranchSides(model);
172
- const siblings = mindMapTree(model).filter(t => t.parentId === model.rootId && t.id !== id && sides.get(t.id) === side);
173
- const index = beforeId ? siblings.findIndex(t => t.id === beforeId) : siblings.length;
174
- if (index < 0)
175
- return null;
176
- const order = siblings.map(t => t.id);
177
- order.splice(index, 0, id);
178
- const orders = new Map(order.map((key, i) => [key, i * 1024]));
179
- return { ...model, layout: 'both', topics: model.topics.map(t => t.id === id
180
- ? { ...t, parentId: model.rootId, side, order: orders.get(id) }
181
- : t.parentId === model.rootId ? { ...t, side: sides.get(t.id), order: orders.get(t.id) ?? t.order } : t) };
182
- }
183
- export function textDifference(before, after) {
184
- let from = 0, tail = 0;
185
- while (from < before.length && from < after.length && before[from] === after[from])
186
- from++;
187
- // Do not split surrogate pairs when inserting/removing emoji.
188
- if (from > 0 && /[\uD800-\uDBFF]/.test(before[from - 1]))
189
- from--;
190
- while (tail < before.length - from && tail < after.length - from &&
191
- before[before.length - tail - 1] === after[after.length - tail - 1])
192
- tail++;
193
- if (tail > 0 && /[\uDC00-\uDFFF]/.test(before[before.length - tail]))
194
- tail--;
195
- return { from, to: before.length - tail, text: after.slice(from, after.length - tail) };
196
- }
197
- function wrapTopic(text, maxUnits = 14) {
198
- const lines = [];
199
- for (const line of (text || '未命名主题').split('\n')) {
200
- let part = '', units = 0;
201
- for (const ch of line) {
202
- const size = /[\u0000-\u00ff]/.test(ch) ? 0.6 : 1;
203
- if (units + size > maxUnits) {
204
- lines.push(part);
205
- part = '';
206
- units = 0;
207
- }
208
- part += ch;
209
- units += size;
210
- }
211
- lines.push(part || ' ');
212
- }
213
- return lines;
214
- }
215
- export function layoutMindMap(model, collapsed = new Set()) {
216
- const tree = mindMapTree(model), byParent = new Map();
217
- for (const t of tree) {
218
- const list = byParent.get(t.parentId) ?? [];
219
- list.push(t);
220
- byParent.set(t.parentId, list);
221
- }
222
- const root = tree.find(t => t.id === model.rootId);
223
- const dimensions = new Map(tree.map(t => {
224
- const measured = mindTopicTextSize(t.text, t.fontSize || 14, t.id === root.id, t.description || '', !!t.runs?.some(r => r.marks?.some(m => m.type === 'bold')), t.attachmentId ? 180 : 0);
225
- return [t.id, { lines: measured.lines, width: measured.width,
226
- height: Math.max(44, measured.titleHeight + 22) + (t.description ? measured.descriptionLines.length * 18 + 6 : 0) +
227
- (t.attachmentId ? 104 : 0) + (t.icon || t.done ? 20 : 0) + (t.note ? 20 : 0) }];
228
- }));
229
- const kids = (id) => collapsed.has(id) ? [] : byParent.get(id) ?? [];
230
- const nodes = [];
231
- const span = (t) => Math.max(model.layout === 'down' ? dimensions.get(t.id).width : dimensions.get(t.id).height, kids(t.id).reduce((n, c) => n + span(c) + 20, -20));
232
- const place = (t, x, y, side) => {
233
- const d = dimensions.get(t.id);
234
- nodes.push({ ...t, ...d, x, y, side });
235
- const children = kids(t.id), total = children.reduce((n, c) => n + span(c) + 20, -20);
236
- let cursor = (model.layout === 'down' ? x + d.width / 2 : y + d.height / 2) - total / 2;
237
- for (const c of children) {
238
- const cd = dimensions.get(c.id), s = span(c);
239
- if (model.layout === 'down')
240
- place(c, cursor + s / 2 - cd.width / 2, y + d.height + 60, side);
241
- else
242
- place(c, side === 1 ? x + d.width + 64 : x - cd.width - 64, cursor + s / 2 - cd.height / 2, side);
243
- cursor += s + 20;
244
- }
245
- };
246
- if (model.layout === 'both') {
247
- const d = dimensions.get(root.id);
248
- nodes.push({ ...root, ...d, x: 0, y: 0, side: 1 });
249
- const children = kids(root.id);
250
- const sides = mindMapBranchSides(model);
251
- for (const side of [-1, 1]) {
252
- const branches = children.filter(c => sides.get(c.id) === side);
253
- let cursor = d.height / 2 - branches.reduce((n, c) => n + span(c) + 20, -20) / 2;
254
- for (const c of branches) {
255
- const cd = dimensions.get(c.id), s = span(c);
256
- place(c, side === 1 ? d.width + 64 : -cd.width - 64, cursor + s / 2 - cd.height / 2, side);
257
- cursor += s + 20;
258
- }
259
- }
260
- }
261
- else
262
- place(root, 0, 0, model.layout === 'left' ? -1 : 1);
263
- const placed = new Map(nodes.map(n => [n.id, n]));
264
- const edges = nodes.filter(n => n.id !== root.id).map(n => {
265
- const p = placed.get(n.parentId);
266
- let x1, y1, x2, y2, path;
267
- if (model.layout === 'down') {
268
- x1 = p.x + p.width / 2;
269
- y1 = p.y + p.height;
270
- x2 = n.x + n.width / 2;
271
- y2 = n.y;
272
- path = `M${x1},${y1} C${x1},${(y1 + y2) / 2} ${x2},${(y1 + y2) / 2} ${x2},${y2}`;
273
- }
274
- else {
275
- x1 = p.x + (n.side === 1 ? p.width : 0);
276
- y1 = p.y + p.height / 2;
277
- x2 = n.x + (n.side === 1 ? 0 : n.width);
278
- y2 = n.y + n.height / 2;
279
- path = `M${x1},${y1} C${(x1 + x2) / 2},${y1} ${(x1 + x2) / 2},${y2} ${x2},${y2}`;
280
- }
281
- if (model.branchStyle === 'straight')
282
- path = `M${x1},${y1} L${x2},${y2}`;
283
- if (model.branchStyle === 'step')
284
- path = model.layout === 'down'
285
- ? `M${x1},${y1} V${(y1 + y2) / 2} H${x2} V${y2}` : `M${x1},${y1} H${(x1 + x2) / 2} V${y2} H${x2}`;
286
- return { id: `${p.id}-${n.id}`, source: p.id, target: n.id, path };
287
- });
288
- const relationBounds = (model.relations ?? []).flatMap(r => {
289
- const a = placed.get(r.source), b = placed.get(r.target);
290
- if (!a || !b)
291
- return [];
292
- const center = (a.x + a.width / 2 + b.x + b.width / 2) / 2, half = [...r.label].length * 7 / 2;
293
- return [{ left: center - half, right: center + half, top: Math.min(a.y, b.y) - 68 }];
294
- });
295
- const x = Math.min(...nodes.map(n => n.x), ...relationBounds.map(r => r.left)) - 32, y = Math.min(...nodes.map(n => n.y), ...relationBounds.map(r => r.top)) - 32;
296
- return { nodes, edges, bounds: { x, y,
297
- width: Math.max(...nodes.map(n => n.x + n.width), ...relationBounds.map(r => r.right)) - x + 32,
298
- height: Math.max(...nodes.map(n => n.y + n.height)) - y + 32 } };
299
- }
300
- export const MIND_MAP_PALETTE = {
301
- blue: '#3864ef', teal: '#087f8c', violet: '#7950c7', amber: '#a66b08',
302
- };
303
- const escapeXml = (s) => s.replace(/[&<>"']/g, ch => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&apos;' })[ch]);
304
- function svgRichLine(topic, line, offset) {
305
- if (!topic.runs?.length)
306
- return escapeXml(line);
307
- const chars = [];
308
- let position = offset;
309
- for (const ch of line) {
310
- let cursor = 0;
311
- const run = topic.runs.find(r => { const start = cursor; cursor += r.text.length; return position >= start && position < cursor; });
312
- const marks = run?.marks ?? [], style = [];
313
- for (const m of marks) {
314
- if (m.type === 'bold')
315
- style.push('font-weight:700');
316
- if (m.type === 'italic')
317
- style.push('font-style:italic');
318
- if (m.type === 'strike')
319
- style.push('text-decoration:line-through');
320
- if (m.type === 'link')
321
- style.push('text-decoration:underline');
322
- if (m.type === 'textStyle' && /^#[0-9a-f]{6}$/i.test(String(m.attrs?.color)))
323
- style.push(`fill:${m.attrs.color}`);
324
- }
325
- chars.push(`<tspan${style.length ? ` style="${style.join(';')}"` : ''}>${escapeXml(ch)}</tspan>`);
326
- position += ch.length;
327
- }
328
- return chars.join('');
329
- }
330
- function svgHighlights(topic, line, offset, x, y) {
331
- if (!topic.runs?.some(r => r.marks?.some(m => m.type === 'highlight')))
332
- return '';
333
- const font = topic.fontSize || 14;
334
- const width = (ch) => (/[\u0000-\u00ff]/.test(ch) ? 8 : 14) * font / 14;
335
- const total = [...line].reduce((sum, ch) => sum + width(ch), 0);
336
- let left = topic.align === 'left' ? x + 16 : topic.align === 'right' ? x + topic.width - total - 16 : x + (topic.width - total) / 2;
337
- let position = offset;
338
- const rects = [];
339
- for (const ch of line) {
340
- let cursor = 0;
341
- const run = topic.runs.find(r => { const start = cursor; cursor += r.text.length; return position >= start && position < cursor; });
342
- const color = run?.marks?.find(m => m.type === 'highlight')?.attrs?.color;
343
- const w = width(ch);
344
- if (/^#[0-9a-f]{6}$/i.test(String(color)))
345
- rects.push(`<rect x="${left}" y="${y - font}" width="${w}" height="${font + 4}" rx="2" fill="${color}"/>`);
346
- left += w;
347
- position += ch.length;
348
- }
349
- return rects.join('');
350
- }
351
- export function mindMapSvg(model, options = {}) {
352
- const drawing = layoutMindMap(model), b = drawing.bounds, color = MIND_MAP_PALETTE[model.theme];
353
- return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="${b.x} ${b.y} ${b.width} ${b.height}" width="${Math.ceil(b.width)}" height="${Math.ceil(b.height)}">` +
354
- (options.transparent ? '' : `<rect x="${b.x}" y="${b.y}" width="${b.width}" height="${b.height}" fill="#fff"/>`) +
355
- drawing.edges.map(e => `<path d="${e.path}" fill="none" stroke="${color}" stroke-width="2"/>`).join('') +
356
- drawing.nodes.map(n => {
357
- const lineHeight = mindTopicLineHeight(n.fontSize || 14), baseline = 11 + (lineHeight - (n.fontSize || 14)) / 2 + (n.fontSize || 14) * .8;
358
- const fill = n.background || (n.id === model.rootId ? color : '#f3f5fb');
359
- const ink = n.color || (n.shape === 'text' ? '#25314a' : n.background ? readableTextColor(n.background) : n.id === model.rootId ? '#fff' : '#25314a');
360
- const description = n.description ? wrapTopic(n.description) : [];
361
- const image = n.attachmentId && options.images?.[n.attachmentId];
362
- return `<g>` + (n.shape === 'text' ? '' : `<rect x="${n.x}" y="${n.y}" width="${n.width}" height="${n.height}" rx="${n.shape === 'pill' ? 24 : 9}" fill="${escapeXml(fill)}" stroke="${color}" stroke-opacity=".3"/>`) +
363
- (n.icon || n.done ? `<text x="${n.x + n.width / 2}" y="${n.y + 22}" text-anchor="middle" font-size="16">${n.done ? '☑ ' : ''}${escapeXml(n.icon || '')}</text>` : '') +
364
- n.lines.map((line, i) => svgHighlights(n, line, n.lines.slice(0, i).reduce((sum, l) => sum + l.length, 0), n.x, n.y + baseline + i * lineHeight + (n.icon || n.done ? 20 : 0))).join('') +
365
- n.lines.map((line, i) => `<text x="${n.x + (n.align === 'left' ? 16 : n.align === 'right' ? n.width - 16 : n.width / 2)}" y="${n.y + baseline + i * lineHeight + (n.icon || n.done ? 20 : 0)}" text-anchor="${n.align === 'left' ? 'start' : n.align === 'right' ? 'end' : 'middle'}" font-family="${escapeXml(MIND_TOPIC_FONT)}" font-size="${n.fontSize || 14}" fill="${escapeXml(ink)}">${svgRichLine(n, line, n.lines.slice(0, i).reduce((sum, l) => sum + l.length, 0))}</text>`).join('') +
366
- description.map((line, i) => `<text x="${n.x + n.width / 2}" y="${n.y + 28 + n.lines.length * lineHeight + i * 18 + (n.icon || n.done ? 20 : 0)}" text-anchor="middle" font-size="12" fill="#677184">${escapeXml(line)}</text>`).join('') +
367
- (n.note ? `<text x="${n.x + n.width / 2}" y="${n.y + n.height - (n.attachmentId ? 112 : 10)}" text-anchor="middle" font-size="10" fill="#677184">▤ 备注</text>` : '') +
368
- (image && /^data:image\/(png|jpeg|webp|gif);base64,/.test(image) ? `<image x="${n.x + 10}" y="${n.y + n.height - 105}" width="${n.width - 20}" height="95" href="${image}" preserveAspectRatio="xMidYMid meet"/>` : '') + '</g>';
369
- }).join('') + mindRelationPaths(model, drawing).map(r => `<path d="${r.path}" fill="none" stroke="#8b5fc7" stroke-width="2" stroke-dasharray="6 4"/><text x="${r.x}" y="${r.y}" font-size="12" fill="#7950c7">${escapeXml(r.label || ({ related: '相关', depends: '依赖', reference: '参考' }[r.kind]))}</text>`).join('') + '</svg>';
370
- }
371
- export function mindMapOutline(model) {
372
- const tree = mindMapTree(model), result = [];
373
- const visit = (id, depth) => {
374
- const topic = tree.find(t => t.id === id);
375
- if (!topic)
376
- return;
377
- result.push({ ...topic, depth });
378
- for (const child of tree.filter(t => t.parentId === id))
379
- visit(child.id, depth + 1);
380
- };
381
- visit(model.rootId, 0);
382
- return result;
383
- }
384
- export function mindMapMarkdown(model) {
385
- const escape = (value) => value.replace(/([\\`*_[\]<>])/g, '\\$1').replace(/\n/g, ' ');
386
- return mindMapOutline(model).map(t => {
387
- const title = (t.runs ?? [{ text: t.text }]).map(run => {
388
- let value = escape(run.text);
389
- for (const mark of run.marks ?? []) {
390
- if (mark.type === 'bold')
391
- value = `**${value}**`;
392
- if (mark.type === 'italic')
393
- value = `*${value}*`;
394
- if (mark.type === 'strike')
395
- value = `~~${value}~~`;
396
- if (mark.type === 'link' && isSafeHref(mark.attrs?.href))
397
- value = `[${value}](${String(mark.attrs.href).replace(/[()]/g, ch => encodeURIComponent(ch))})`;
398
- }
399
- return value;
400
- }).join('') || '未命名主题';
401
- return `${' '.repeat(t.depth)}- ${t.done ? '[x] ' : ''}${t.icon ? t.icon + ' ' : ''}${title}${t.description ? ' — ' + escape(t.description) : ''}${t.note ? '(备注:' + escape(t.note) + ')' : ''}${t.attachmentId ? ' [图片附件:' + t.attachmentId + ']' : ''}`;
402
- }).join('\n') + ((model.relations?.length ?? 0) > 0 ? '\n\n关系:\n' + model.relations.map(r => `- ${escape(model.topics.find(t => t.id === r.source)?.text || '已移除主题')} → ${escape(model.topics.find(t => t.id === r.target)?.text || '已移除主题')}:${escape(r.label || r.kind)}`).join('\n') : '');
403
- }
404
- export function mindRelationPaths(model, drawing) {
405
- const nodes = new Map(drawing.nodes.map(n => [n.id, n]));
406
- return (model.relations ?? []).flatMap(r => {
407
- const a = nodes.get(r.source), b = nodes.get(r.target);
408
- if (!a || !b || a === b)
409
- return [];
410
- const x1 = a.x + a.width / 2, y1 = a.y, x2 = b.x + b.width / 2, y2 = b.y;
411
- const bend = Math.min(y1, y2) - 48;
412
- return [{ ...r, path: `M${x1},${y1} C${x1},${bend} ${x2},${bend} ${x2},${y2}`, x: (x1 + x2) / 2, y: bend + 8 }];
413
- });
414
- }
415
- export function mindMapToJson(model, fresh = false) {
416
- const ids = new Map(model.topics.map(t => [t.id, fresh ? mindMapId() : t.id]));
417
- return { type: 'mindMap', attrs: { id: fresh ? mindMapId() : model.id, rootId: ids.get(model.rootId),
418
- formatVersion: MIND_MAP_VERSION, layout: model.layout, theme: model.theme, height: model.height, branchStyle: model.branchStyle || 'curve' },
419
- content: [...model.topics.map(t => {
420
- const { text, runs, description, note, ...attrs } = t;
421
- return { type: 'mindMapTopic', attrs: { ...attrs, id: ids.get(t.id), parentId: ids.get(t.parentId) ?? '' },
422
- content: [...(runs ?? (text ? [{ text }] : [])).map(r => ({ type: 'text', ...r })),
423
- ...(description ? [{ type: 'mindMapDescription', content: [{ type: 'text', text: description }] }] : []),
424
- ...(note ? [{ type: 'mindMapNote', content: [{ type: 'text', text: note }] }] : [])] };
425
- }), ...(model.relations ?? []).map(r => ({ type: 'mindMapRelation', attrs: { ...r,
426
- id: fresh ? mindMapId() : r.id, source: ids.get(r.source), target: ids.get(r.target) } }))] };
427
- }
1
+ import{isSafeHref as t}from"./safe-href.js";import{readableTextColor as e}from"./presence.js";import{mindTopicLineHeight as n,mindTopicTextSize as i,MIND_TOPIC_FONT as o}from"./mind-topic-text.js";export const MIND_MAP_VERSION=3;export const MIND_MAP_MAX_TOPICS=500;export const MIND_MAP_MAX_TEXT=2e3;export const MIND_MAP_LAYOUTS=["right","left","both","down"];export const MIND_MAP_THEMES=["blue","teal","violet","amber"];export const mindMapId=()=>crypto.randomUUID();const validId=t=>"string"==typeof t&&/^[\w-]{1,80}$/.test(t);export function createMindMap(t="right"){const e=mindMapId(),n=mindMapId();return{type:"mindMap",attrs:{id:e,rootId:n,formatVersion:MIND_MAP_VERSION,layout:t,theme:"blue",height:380},content:[{type:"mindMapTopic",attrs:{id:n,parentId:"",order:0},content:[{type:"text",text:"中心主题"}]},...["分支主题 1","分支主题 2","分支主题 3"].map((t,e)=>({type:"mindMapTopic",attrs:{id:mindMapId(),parentId:n,order:e},content:[{type:"text",text:t}]}))]}}export function readMindMap(t){const e=t?.attrs;if("mindMap"!==t?.type||!e||![1,2,MIND_MAP_VERSION].includes(Number(e.formatVersion))||!validId(e.id)||!validId(e.rootId)||!Array.isArray(t.content)||t.content.length<1||t.content.filter(t=>"mindMapTopic"===t?.type).length>500||t.content.length>1500)return null;const n=[],i=new Set;for(const e of t.content.filter(t=>"mindMapRelation"!==t?.type)){if(!e||"object"!=typeof e)return null;const t=e.attrs;if("mindMapTopic"!==e.type||!t||!validId(t.id)||i.has(t.id)||"string"!=typeof t.parentId||""!==t.parentId&&!validId(t.parentId)||"number"!=typeof t.order||!Number.isFinite(t.order)||Math.abs(t.order)>1e12||void 0!==e.content&&!Array.isArray(e.content)||e.content?.some(t=>!t||!["text","mindMapDescription","mindMapNote"].includes(t.type??"")||"text"===t.type&&"string"!=typeof t.text))return null;const o=(e.content??[]).filter(t=>"text"===t.type).map(t=>t.text??"").join("");if(o.length>2e3)return null;i.add(t.id);const r={};if(t.color&&!/^#[0-9a-f]{6}$/i.test(String(t.color)))return null;if(t.background&&!/^#[0-9a-f]{6}$/i.test(String(t.background)))return null;for(const e of["color","background"])if(t[e]&&!["#18243a","#ffffff","#3864ef","#087f8c","#7950c7","#a66b08","#d94848","#f3f5fb","#dce7ff","#d3f4e9","#ece0ff","#fff0c2","#ffe1e1","#233044"].includes(String(t[e])))return null;if(t.shape&&!["round","pill","text"].includes(String(t.shape)))return null;if(t.fontSize&&![12,14,16,18,20].includes(Number(t.fontSize)))return null;if(t.align&&!["left","center","right"].includes(String(t.align)))return null;if(void 0!==t.done&&null!==t.done&&"boolean"!=typeof t.done)return null;if(void 0!==t.side&&null!==t.side&&-1!==t.side&&1!==t.side)return null;if(t.icon&&!["💡","🎯","📌","🚩","⭐","✅","⚠️","💬","📅","🔎"].includes(String(t.icon)))return null;if(t.attachmentId&&("string"!=typeof t.attachmentId||!/^[0-9a-f-]{36}$/i.test(t.attachmentId)))return null;const d=new Set;let a=!1,s=!1;for(const t of e.content??[]){if(void 0!==t.marks&&(!Array.isArray(t.marks)||t.marks.some(t=>!t||!["bold","italic","strike","textStyle","highlight","link"].includes(t.type))))return null;if("text"!==t.type&&(void 0!==t.content&&!Array.isArray(t.content)||t.content?.some(t=>"text"!==t.type||"string"!=typeof t.text)||(t.content??[]).reduce((t,e)=>t+(e.text?.length??0),0)>4e3))return null;if("text"===t.type&&a)return null;if("text"!==t.type){if(d.has(t.type)||"mindMapDescription"===t.type&&s)return null;d.add(t.type),a=!0,"mindMapNote"===t.type&&(s=!0)}}for(const e of["shape","color","background","fontSize","align","icon","done","attachmentId","side"])void 0!==t[e]&&null!==t[e]&&""!==t[e]&&Object.assign(r,{[e]:t[e]});const p=(e.content??[]).filter(t=>"text"===t.type).map(t=>({text:t.text??"",...t.marks?.length?{marks:t.marks}:{}}));p.some(t=>t.marks?.length)&&(r.runs=p);for(const[t,n]of[["mindMapDescription","description"],["mindMapNote","note"]]){const i=e.content?.find(e=>e.type===t);i&&(r[n]=(i.content??[]).map(t=>t.text??"").join(""))}n.push({id:t.id,parentId:t.parentId,order:t.order,text:o,...r})}if(!i.has(e.rootId))return null;const o=t.content.filter(t=>"mindMapRelation"===t.type),r=new Set;for(const t of o){const e=t.attrs;if(!e||!validId(e.id)||i.has(e.id)||r.has(e.id)||!validId(e.source)||!validId(e.target)||e.source===e.target||!["related","depends","reference"].includes(String(e.kind))||"string"!=typeof e.label||e.label.length>200)return null;r.add(e.id)}return{id:e.id,rootId:e.rootId,topics:n,...e.branchStyle?{branchStyle:e.branchStyle}:{},...t.content.some(t=>"mindMapRelation"===t.type)?{relations:t.content.filter(t=>"mindMapRelation"===t.type).map(t=>({...t.attrs}))}:{},layout:MIND_MAP_LAYOUTS.includes(e.layout)?e.layout:"right",theme:MIND_MAP_THEMES.includes(e.theme)?e.theme:"blue",height:"number"==typeof e.height&&Number.isFinite(e.height)?Math.min(800,Math.max(240,e.height)):380}}export function mindMapTree(t){const e=t.topics.map(t=>({...t})),n=new Map(e.map(t=>[t.id,t]));for(const i of e)i.id===t.rootId?i.parentId="":n.has(i.parentId)&&i.parentId!==i.id||(i.parentId=t.rootId);for(const i of[...e].sort((t,e)=>t.id.localeCompare(e.id))){const e=[],o=new Set;let r=i;for(;r&&r.id!==t.rootId;){if(o.has(r.id)){const i=e.slice(e.indexOf(r.id)).sort();n.get(i[0]).parentId=t.rootId;break}o.add(r.id),e.push(r.id),r=n.get(r.parentId)}}return e.sort((t,e)=>t.order-e.order||t.id.localeCompare(e.id))}export function mindMapDescendants(t,e){const n=mindMapTree(t),i=new Set([e]);for(let t=!0;t;){t=!1;for(const e of n)i.has(e.parentId)&&!i.has(e.id)&&(i.add(e.id),t=!0)}return i}export function canMoveMindTopic(t,e,n){return e!==t.rootId&&t.topics.some(t=>t.id===e)&&t.topics.some(t=>t.id===n)&&!mindMapDescendants(t,e).has(n)}export function mindMapBranchSides(t){return new Map(mindMapTree(t).filter(e=>e.parentId===t.rootId).map((e,n)=>[e.id,"left"===t.layout?-1:"both"===t.layout?e.side??(n%2?-1:1):1]))}export function projectMindSideMove(t,e,n,i){if("down"===t.layout||-1!==n&&1!==n||!canMoveMindTopic(t,e,t.rootId))return null;const o=mindMapBranchSides(t),r=mindMapTree(t).filter(i=>i.parentId===t.rootId&&i.id!==e&&o.get(i.id)===n),d=i?r.findIndex(t=>t.id===i):r.length;if(d<0)return null;const a=r.map(t=>t.id);a.splice(d,0,e);const s=new Map(a.map((t,e)=>[t,1024*e]));return{...t,layout:"both",topics:t.topics.map(i=>i.id===e?{...i,parentId:t.rootId,side:n,order:s.get(e)}:i.parentId===t.rootId?{...i,side:o.get(i.id),order:s.get(i.id)??i.order}:i)}}export function textDifference(t,e){let n=0,i=0;for(;n<t.length&&n<e.length&&t[n]===e[n];)n++;for(n>0&&/[\uD800-\uDBFF]/.test(t[n-1])&&n--;i<t.length-n&&i<e.length-n&&t[t.length-i-1]===e[e.length-i-1];)i++;return i>0&&/[\uDC00-\uDFFF]/.test(t[t.length-i])&&i--,{from:n,to:t.length-i,text:e.slice(n,e.length-i)}}export function layoutMindMap(t,e=new Set){const n=mindMapTree(t),o=new Map;for(const t of n){const e=o.get(t.parentId)??[];e.push(t),o.set(t.parentId,e)}const r=n.find(e=>e.id===t.rootId),d=new Map(n.map(t=>{const e=i(t.text,t.fontSize||14,t.id===r.id,t.description||"",!!t.runs?.some(t=>t.marks?.some(t=>"bold"===t.type)),t.attachmentId?180:0);return[t.id,{lines:e.lines,width:e.width,height:Math.max(44,e.titleHeight+22)+(t.description?18*e.descriptionLines.length+6:0)+(t.attachmentId?104:0)+(t.icon||t.done?20:0)+(t.note?20:0)}]})),kids=t=>e.has(t)?[]:o.get(t)??[],a=[],span=e=>Math.max("down"===t.layout?d.get(e.id).width:d.get(e.id).height,kids(e.id).reduce((t,e)=>t+span(e)+20,-20)),place=(e,n,i,o)=>{const r=d.get(e.id);a.push({...e,...r,x:n,y:i,side:o});const s=kids(e.id),p=s.reduce((t,e)=>t+span(e)+20,-20);let c=("down"===t.layout?n+r.width/2:i+r.height/2)-p/2;for(const e of s){const a=d.get(e.id),s=span(e);"down"===t.layout?place(e,c+s/2-a.width/2,i+r.height+60,o):place(e,1===o?n+r.width+64:n-a.width-64,c+s/2-a.height/2,o),c+=s+20}};if("both"===t.layout){const e=d.get(r.id);a.push({...r,...e,x:0,y:0,side:1});const n=kids(r.id),i=mindMapBranchSides(t);for(const t of[-1,1]){const o=n.filter(e=>i.get(e.id)===t);let r=e.height/2-o.reduce((t,e)=>t+span(e)+20,-20)/2;for(const n of o){const i=d.get(n.id),o=span(n);place(n,1===t?e.width+64:-i.width-64,r+o/2-i.height/2,t),r+=o+20}}}else place(r,0,0,"left"===t.layout?-1:1);const s=new Map(a.map(t=>[t.id,t])),p=a.filter(t=>t.id!==r.id).map(e=>{const n=s.get(e.parentId);let i,o,r,d,a;return"down"===t.layout?(i=n.x+n.width/2,o=n.y+n.height,r=e.x+e.width/2,d=e.y,a=`M${i},${o} C${i},${(o+d)/2} ${r},${(o+d)/2} ${r},${d}`):(i=n.x+(1===e.side?n.width:0),o=n.y+n.height/2,r=e.x+(1===e.side?0:e.width),d=e.y+e.height/2,a=`M${i},${o} C${(i+r)/2},${o} ${(i+r)/2},${d} ${r},${d}`),"straight"===t.branchStyle&&(a=`M${i},${o} L${r},${d}`),"step"===t.branchStyle&&(a="down"===t.layout?`M${i},${o} V${(o+d)/2} H${r} V${d}`:`M${i},${o} H${(i+r)/2} V${d} H${r}`),{id:`${n.id}-${e.id}`,source:n.id,target:e.id,path:a}}),c=(t.relations??[]).flatMap(t=>{const e=s.get(t.source),n=s.get(t.target);if(!e||!n)return[];const i=(e.x+e.width/2+n.x+n.width/2)/2,o=7*[...t.label].length/2;return[{left:i-o,right:i+o,top:Math.min(e.y,n.y)-68}]}),l=Math.min(...a.map(t=>t.x),...c.map(t=>t.left))-32,h=Math.min(...a.map(t=>t.y),...c.map(t=>t.top))-32;return{nodes:a,edges:p,bounds:{x:l,y:h,width:Math.max(...a.map(t=>t.x+t.width),...c.map(t=>t.right))-l+32,height:Math.max(...a.map(t=>t.y+t.height))-h+32}}}export const MIND_MAP_PALETTE={blue:"#3864ef",teal:"#087f8c",violet:"#7950c7",amber:"#a66b08"};const escapeXml=t=>t.replace(/[&<>"']/g,t=>({"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;","'":"&apos;"}[t]));export function mindMapSvg(t,i={}){const r=layoutMindMap(t),d=r.bounds,a=MIND_MAP_PALETTE[t.theme];return`<svg xmlns="http://www.w3.org/2000/svg" viewBox="${d.x} ${d.y} ${d.width} ${d.height}" width="${Math.ceil(d.width)}" height="${Math.ceil(d.height)}">`+(i.transparent?"":`<rect x="${d.x}" y="${d.y}" width="${d.width}" height="${d.height}" fill="#fff"/>`)+r.edges.map(t=>`<path d="${t.path}" fill="none" stroke="${a}" stroke-width="2"/>`).join("")+r.nodes.map(r=>{const d=n(r.fontSize||14),s=11+(d-(r.fontSize||14))/2+.8*(r.fontSize||14),p=r.background||(r.id===t.rootId?a:"#f3f5fb"),c=r.color||("text"===r.shape?"#25314a":r.background?e(r.background):r.id===t.rootId?"#fff":"#25314a"),l=r.description?function wrapTopic(t,e=14){const n=[];for(const i of(t||"未命名主题").split("\n")){let t="",o=0;for(const r of i){const i=/[\u0000-\u00ff]/.test(r)?.6:1;o+i>e&&(n.push(t),t="",o=0),t+=r,o+=i}n.push(t||" ")}return n}(r.description):[],h=r.attachmentId&&i.images?.[r.attachmentId];return"<g>"+("text"===r.shape?"":`<rect x="${r.x}" y="${r.y}" width="${r.width}" height="${r.height}" rx="${"pill"===r.shape?24:9}" fill="${escapeXml(p)}" stroke="${a}" stroke-opacity=".3"/>`)+(r.icon||r.done?`<text x="${r.x+r.width/2}" y="${r.y+22}" text-anchor="middle" font-size="16">${r.done?"☑ ":""}${escapeXml(r.icon||"")}</text>`:"")+r.lines.map((t,e)=>function svgHighlights(t,e,n,i,o){if(!t.runs?.some(t=>t.marks?.some(t=>"highlight"===t.type)))return"";const r=t.fontSize||14,width=t=>(/[\u0000-\u00ff]/.test(t)?8:14)*r/14,d=[...e].reduce((t,e)=>t+width(e),0);let a="left"===t.align?i+16:"right"===t.align?i+t.width-d-16:i+(t.width-d)/2,s=n;const p=[];for(const n of e){let e=0;const i=t.runs.find(t=>{const n=e;return e+=t.text.length,s>=n&&s<e}),d=i?.marks?.find(t=>"highlight"===t.type)?.attrs?.color,c=width(n);/^#[0-9a-f]{6}$/i.test(String(d))&&p.push(`<rect x="${a}" y="${o-r}" width="${c}" height="${r+4}" rx="2" fill="${d}"/>`),a+=c,s+=n.length}return p.join("")}(r,t,r.lines.slice(0,e).reduce((t,e)=>t+e.length,0),r.x,r.y+s+e*d+(r.icon||r.done?20:0))).join("")+r.lines.map((t,e)=>`<text x="${r.x+("left"===r.align?16:"right"===r.align?r.width-16:r.width/2)}" y="${r.y+s+e*d+(r.icon||r.done?20:0)}" text-anchor="${"left"===r.align?"start":"right"===r.align?"end":"middle"}" font-family="${escapeXml(o)}" font-size="${r.fontSize||14}" fill="${escapeXml(c)}">${function svgRichLine(t,e,n){if(!t.runs?.length)return escapeXml(e);const i=[];let o=n;for(const n of e){let e=0;const r=t.runs.find(t=>{const n=e;return e+=t.text.length,o>=n&&o<e}),d=r?.marks??[],a=[];for(const t of d)"bold"===t.type&&a.push("font-weight:700"),"italic"===t.type&&a.push("font-style:italic"),"strike"===t.type&&a.push("text-decoration:line-through"),"link"===t.type&&a.push("text-decoration:underline"),"textStyle"===t.type&&/^#[0-9a-f]{6}$/i.test(String(t.attrs?.color))&&a.push(`fill:${t.attrs.color}`);i.push(`<tspan${a.length?` style="${a.join(";")}"`:""}>${escapeXml(n)}</tspan>`),o+=n.length}return i.join("")}(r,t,r.lines.slice(0,e).reduce((t,e)=>t+e.length,0))}</text>`).join("")+l.map((t,e)=>`<text x="${r.x+r.width/2}" y="${r.y+28+r.lines.length*d+18*e+(r.icon||r.done?20:0)}" text-anchor="middle" font-size="12" fill="#677184">${escapeXml(t)}</text>`).join("")+(r.note?`<text x="${r.x+r.width/2}" y="${r.y+r.height-(r.attachmentId?112:10)}" text-anchor="middle" font-size="10" fill="#677184">▤ 备注</text>`:"")+(h&&/^data:image\/(png|jpeg|webp|gif);base64,/.test(h)?`<image x="${r.x+10}" y="${r.y+r.height-105}" width="${r.width-20}" height="95" href="${h}" preserveAspectRatio="xMidYMid meet"/>`:"")+"</g>"}).join("")+mindRelationPaths(t,r).map(t=>`<path d="${t.path}" fill="none" stroke="#8b5fc7" stroke-width="2" stroke-dasharray="6 4"/><text x="${t.x}" y="${t.y}" font-size="12" fill="#7950c7">${escapeXml(t.label||{related:"相关",depends:"依赖",reference:"参考"}[t.kind])}</text>`).join("")+"</svg>"}export function mindMapOutline(t){const e=mindMapTree(t),n=[],visit=(t,i)=>{const o=e.find(e=>e.id===t);if(o){n.push({...o,depth:i});for(const n of e.filter(e=>e.parentId===t))visit(n.id,i+1)}};return visit(t.rootId,0),n}export function mindMapMarkdown(e){const escape=t=>t.replace(/([\\`*_[\]<>])/g,"\\$1").replace(/\n/g," ");return mindMapOutline(e).map(e=>{const n=(e.runs??[{text:e.text}]).map(e=>{let n=escape(e.text);for(const i of e.marks??[])"bold"===i.type&&(n=`**${n}**`),"italic"===i.type&&(n=`*${n}*`),"strike"===i.type&&(n=`~~${n}~~`),"link"===i.type&&t(i.attrs?.href)&&(n=`[${n}](${String(i.attrs.href).replace(/[()]/g,t=>encodeURIComponent(t))})`);return n}).join("")||"未命名主题";return`${" ".repeat(e.depth)}- ${e.done?"[x] ":""}${e.icon?e.icon+" ":""}${n}${e.description?" — "+escape(e.description):""}${e.note?"(备注:"+escape(e.note)+")":""}${e.attachmentId?" [图片附件:"+e.attachmentId+"]":""}`}).join("\n")+((e.relations?.length??0)>0?"\n\n关系:\n"+e.relations.map(t=>`- ${escape(e.topics.find(e=>e.id===t.source)?.text||"已移除主题")} → ${escape(e.topics.find(e=>e.id===t.target)?.text||"已移除主题")}:${escape(t.label||t.kind)}`).join("\n"):"")}export function mindRelationPaths(t,e){const n=new Map(e.nodes.map(t=>[t.id,t]));return(t.relations??[]).flatMap(t=>{const e=n.get(t.source),i=n.get(t.target);if(!e||!i||e===i)return[];const o=e.x+e.width/2,r=e.y,d=i.x+i.width/2,a=i.y,s=Math.min(r,a)-48;return[{...t,path:`M${o},${r} C${o},${s} ${d},${s} ${d},${a}`,x:(o+d)/2,y:s+8}]})}export function mindMapToJson(t,e=!1){const n=new Map(t.topics.map(t=>[t.id,e?mindMapId():t.id]));return{type:"mindMap",attrs:{id:e?mindMapId():t.id,rootId:n.get(t.rootId),formatVersion:MIND_MAP_VERSION,layout:t.layout,theme:t.theme,height:t.height,branchStyle:t.branchStyle||"curve"},content:[...t.topics.map(t=>{const{text:e,runs:i,description:o,note:r,...d}=t;return{type:"mindMapTopic",attrs:{...d,id:n.get(t.id),parentId:n.get(t.parentId)??""},content:[...(i??(e?[{text:e}]:[])).map(t=>({type:"text",...t})),...o?[{type:"mindMapDescription",content:[{type:"text",text:o}]}]:[],...r?[{type:"mindMapNote",content:[{type:"text",text:r}]}]:[]]}}),...(t.relations??[]).map(t=>({type:"mindMapRelation",attrs:{...t,id:e?mindMapId():t.id,source:n.get(t.source),target:n.get(t.target)}}))]}}
@@ -1,49 +1 @@
1
- export const MIND_TOPIC_LINE_HEIGHT = 22;
2
- export const mindTopicLineHeight = (fontSize = 14) => Math.max(MIND_TOPIC_LINE_HEIGHT, Math.ceil(fontSize * 1.4));
3
- export const MIND_TOPIC_PADDING = 34; // 16px * 2 padding + 1px * 2 borders.
4
- export const MIND_TOPIC_FONT = '-apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", sans-serif';
5
- const sizeCache = new Map();
6
- let factory;
7
- export function setMindTextMeasureFactory(value) { factory = value; sizeCache.clear(); }
8
- /** Real browser font metrics; non-DOM exports/tests keep a conservative text-only fallback. */
9
- export function mindTextMeasure(fontSize = 14, weight = 400) {
10
- const measured = factory?.(fontSize, weight);
11
- if (measured)
12
- return measured;
13
- return text => [...text].reduce((sum, ch) => sum + (/[\u0000-\u00ff]/.test(ch) ? 9 : 14) * fontSize / 14, 0);
14
- }
15
- /** Keep explicit empty lines and emoji clusters; presentation wrapping never edits the source. */
16
- export function wrapMindText(text, width, measure) {
17
- const lines = [];
18
- const segmenter = typeof Intl.Segmenter === 'function' ? new Intl.Segmenter(undefined, { granularity: 'grapheme' }) : null;
19
- for (const line of text.split('\n')) {
20
- let part = '';
21
- const segments = segmenter ? [...segmenter.segment(line)].map(s => s.segment) : [...line];
22
- for (const ch of segments) {
23
- if (part && measure(part + ch) > width) {
24
- lines.push(part);
25
- part = '';
26
- }
27
- part += ch;
28
- }
29
- lines.push(part);
30
- }
31
- return lines;
32
- }
33
- export function mindTopicTextSize(text, fontSize = 14, root = false, description = '', bold = false, minimumWidth = 0) {
34
- const key = JSON.stringify([text, fontSize, root, description, bold, minimumWidth]);
35
- const cached = sizeCache.get(key);
36
- if (cached)
37
- return cached;
38
- const measure = mindTextMeasure(fontSize, bold ? 700 : root ? 600 : 400), descriptionMeasure = mindTextMeasure(12);
39
- const source = text || '未命名主题';
40
- const longest = Math.max(...source.split('\n').map(measure), ...description.split('\n').map(descriptionMeasure));
41
- const width = Math.min(240, Math.max(root ? 128 : 100, minimumWidth, Math.ceil(longest + MIND_TOPIC_PADDING + 2)));
42
- const lines = wrapMindText(source, width - MIND_TOPIC_PADDING, measure);
43
- const descriptionLines = description ? wrapMindText(description, width - MIND_TOPIC_PADDING, descriptionMeasure) : [];
44
- const result = { width, lines, descriptionLines, titleHeight: lines.length * mindTopicLineHeight(fontSize) };
45
- if (sizeCache.size >= 512)
46
- sizeCache.delete(sizeCache.keys().next().value);
47
- sizeCache.set(key, result);
48
- return result;
49
- }
1
+ export const MIND_TOPIC_LINE_HEIGHT=22;export const mindTopicLineHeight=(e=14)=>Math.max(22,Math.ceil(1.4*e));export const MIND_TOPIC_PADDING=34;export const MIND_TOPIC_FONT='-apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", sans-serif';const e=new Map;let t;export function setMindTextMeasureFactory(n){t=n,e.clear()}export function mindTextMeasure(e=14,n=400){const i=t?.(e,n);return i||(t=>[...t].reduce((t,n)=>t+(/[\u0000-\u00ff]/.test(n)?9:14)*e/14,0))}export function wrapMindText(e,t,n){const i=[],o="function"==typeof Intl.Segmenter?new Intl.Segmenter(void 0,{granularity:"grapheme"}):null;for(const s of e.split("\n")){let e="";const r=o?[...o.segment(s)].map(e=>e.segment):[...s];for(const o of r)e&&n(e+o)>t&&(i.push(e),e=""),e+=o;i.push(e)}return i}export function mindTopicTextSize(t,n=14,i=!1,o="",s=!1,r=0){const a=JSON.stringify([t,n,i,o,s,r]),c=e.get(a);if(c)return c;const p=mindTextMeasure(n,s?700:i?600:400),u=mindTextMeasure(12),l=t||"未命名主题",m=Math.max(...l.split("\n").map(p),...o.split("\n").map(u)),x=Math.min(240,Math.max(i?128:100,r,Math.ceil(m+34+2))),M=wrapMindText(l,x-34,p),f={width:x,lines:M,descriptionLines:o?wrapMindText(o,x-34,u):[],titleHeight:M.length*mindTopicLineHeight(n)};return e.size>=512&&e.delete(e.keys().next().value),e.set(a,f),f}
@@ -1,57 +1 @@
1
- /**
2
- * 节点目录:编辑器的 nodeView 与只读渲染规则**共用的单一真源**
3
- * (对齐《Wiki好用技术方案》1.1)。
4
- *
5
- * 这里只放纯数据(无 Vue、无 Tiptap 依赖),因此阅读端可以只引它 + Renderer,
6
- * 而完全不加载编辑器代码;编辑器端再基于同一份目录生成扩展。
7
- *
8
- * 新增节点时必须同时更新:
9
- * 1) 本目录 2) 服务端 doc-schema.service.ts 白名单 3) 提升 schema 版本
10
- * 否则旧客户端会因「遇到不支持节点」而被服务端拒写(这是预期行为,避免静默丢内容)。
11
- */
12
- export const NODE_CATALOG = [
13
- { type: 'doc', kind: 'block', label: '文档', topLevel: true },
14
- { type: 'paragraph', kind: 'block', label: '段落', topLevel: true },
15
- { type: 'heading', kind: 'block', label: '标题', topLevel: true },
16
- { type: 'bulletList', kind: 'block', label: '无序列表', topLevel: true },
17
- { type: 'orderedList', kind: 'block', label: '有序列表', topLevel: true },
18
- { type: 'taskList', kind: 'block', label: '待办列表', topLevel: true },
19
- { type: 'listItem', kind: 'block', label: '列表项' },
20
- { type: 'taskItem', kind: 'block', label: '待办项' },
21
- { type: 'blockquote', kind: 'block', label: '引用块', topLevel: true },
22
- { type: 'codeBlock', kind: 'block', label: '代码块', topLevel: true },
23
- { type: 'horizontalRule', kind: 'atom', atom: true, label: '分割线', topLevel: true },
24
- { type: 'table', kind: 'block', label: '表格', topLevel: true },
25
- { type: 'tableRow', kind: 'block', label: '表格行' },
26
- { type: 'tableHeader', kind: 'block', label: '表头单元格' },
27
- { type: 'tableCell', kind: 'block', label: '单元格' },
28
- { type: 'image', kind: 'atom', atom: true, label: '图片', topLevel: true },
29
- { type: 'fileAttachment', kind: 'atom', atom: true, label: '视频或文件', topLevel: true },
30
- { type: 'videoEmbed', kind: 'atom', atom: true, label: '嵌入视频', topLevel: true },
31
- { type: 'mindMap', kind: 'block', label: '思维导图', topLevel: true },
32
- { type: 'mindMapDescription', kind: 'inline', label: '主题描述' },
33
- { type: 'mindMapNote', kind: 'inline', label: '主题备注' },
34
- { type: 'mindMapRelation', kind: 'block', label: '主题关系' },
35
- { type: 'mindMapTopic', kind: 'block', label: '导图主题' },
36
- { type: 'callout', kind: 'block', label: '高亮块', topLevel: true },
37
- // 分栏:columns 是顶层行,column 只在其内部(故 column 无 topLevel)
38
- { type: 'columns', kind: 'block', label: '分栏', topLevel: true },
39
- { type: 'column', kind: 'block', label: '栏' },
40
- { type: 'pageLink', kind: 'atom', atom: true, label: '页面引用' },
41
- { type: 'mention', kind: 'atom', atom: true, label: '提及用户' },
42
- { type: 'mathInline', kind: 'atom', atom: true, label: '行内公式' },
43
- { type: 'mathBlock', kind: 'atom', atom: true, label: '公式块', topLevel: true },
44
- { type: 'text', kind: 'text', label: '文本' },
45
- { type: 'hardBreak', kind: 'inline', atom: true, label: '换行' },
46
- ];
47
- const META_BY_TYPE = new Map(NODE_CATALOG.map((n) => [n.type, n]));
48
- export function nodeMeta(type) {
49
- return type ? META_BY_TYPE.get(type) : undefined;
50
- }
51
- /** 只读渲染遇到未知节点时使用的降级提示(不静默丢弃,向用户明示)。 */
52
- export const UNKNOWN_NODE_LABEL = '此内容由更高版本的编辑器创建,当前版本无法显示';
53
- /** 标题级别 → HTML 标签 */
54
- export function headingTag(level) {
55
- const n = typeof level === 'number' ? level : 1;
56
- return `h${Math.min(Math.max(n, 1), 6)}`;
57
- }
1
+ export const NODE_CATALOG=[{type:"doc",kind:"block",label:"文档",topLevel:!0},{type:"paragraph",kind:"block",label:"段落",topLevel:!0},{type:"heading",kind:"block",label:"标题",topLevel:!0},{type:"bulletList",kind:"block",label:"无序列表",topLevel:!0},{type:"orderedList",kind:"block",label:"有序列表",topLevel:!0},{type:"taskList",kind:"block",label:"待办列表",topLevel:!0},{type:"listItem",kind:"block",label:"列表项"},{type:"taskItem",kind:"block",label:"待办项"},{type:"blockquote",kind:"block",label:"引用块",topLevel:!0},{type:"codeBlock",kind:"block",label:"代码块",topLevel:!0},{type:"horizontalRule",kind:"atom",atom:!0,label:"分割线",topLevel:!0},{type:"table",kind:"block",label:"表格",topLevel:!0},{type:"tableRow",kind:"block",label:"表格行"},{type:"tableHeader",kind:"block",label:"表头单元格"},{type:"tableCell",kind:"block",label:"单元格"},{type:"image",kind:"atom",atom:!0,label:"图片",topLevel:!0},{type:"fileAttachment",kind:"atom",atom:!0,label:"视频或文件",topLevel:!0},{type:"videoEmbed",kind:"atom",atom:!0,label:"嵌入视频",topLevel:!0},{type:"mindMap",kind:"block",label:"思维导图",topLevel:!0},{type:"mindMapDescription",kind:"inline",label:"主题描述"},{type:"mindMapNote",kind:"inline",label:"主题备注"},{type:"mindMapRelation",kind:"block",label:"主题关系"},{type:"mindMapTopic",kind:"block",label:"导图主题"},{type:"callout",kind:"block",label:"高亮块",topLevel:!0},{type:"columns",kind:"block",label:"分栏",topLevel:!0},{type:"column",kind:"block",label:"栏"},{type:"pageLink",kind:"atom",atom:!0,label:"页面引用"},{type:"mention",kind:"atom",atom:!0,label:"提及用户"},{type:"mathInline",kind:"atom",atom:!0,label:"行内公式"},{type:"mathBlock",kind:"atom",atom:!0,label:"公式块",topLevel:!0},{type:"text",kind:"text",label:"文本"},{type:"hardBreak",kind:"inline",atom:!0,label:"换行"}];const e=new Map(NODE_CATALOG.map(e=>[e.type,e]));export function nodeMeta(l){return l?e.get(l):void 0}export const UNKNOWN_NODE_LABEL="此内容由更高版本的编辑器创建,当前版本无法显示";export function headingTag(e){const l="number"==typeof e?e:1;return`h${Math.min(Math.max(l,1),6)}`}