wendang 0.0.1 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +3 -0
- package/NOTICE.md +7 -0
- package/README.md +101 -2
- package/dist/cjs/document.js +19 -0
- package/dist/cjs/index.js +25 -0
- package/dist/cjs/mindmap.js +22 -0
- package/dist/cjs/package.json +1 -0
- package/dist/cjs/schema.js +11 -0
- package/dist/cjs/shared/anchor.js +201 -0
- package/dist/cjs/shared/callout.js +64 -0
- package/dist/cjs/shared/code-block.js +33 -0
- package/dist/cjs/shared/code-languages.js +51 -0
- package/dist/cjs/shared/doc-compatibility.js +23 -0
- package/dist/cjs/shared/doc-text.js +52 -0
- package/dist/cjs/shared/document-colors.js +61 -0
- package/dist/cjs/shared/inline-comments.js +179 -0
- package/dist/cjs/shared/mind-map-folding.js +14 -0
- package/dist/cjs/shared/mind-map-phase2.js +133 -0
- package/dist/cjs/shared/mind-map.js +445 -0
- package/dist/cjs/shared/mind-topic-text.js +57 -0
- package/dist/cjs/shared/node-catalog.js +62 -0
- package/dist/cjs/shared/presence.js +141 -0
- package/dist/cjs/shared/safe-href.js +36 -0
- package/dist/cjs/shared/table-cell-format.js +18 -0
- package/dist/cjs/shared/table-layout.js +28 -0
- package/dist/cjs/shared/types.js +2 -0
- package/dist/cjs/shared/video-embed.js +102 -0
- package/dist/cjs/shared/workbook.js +152 -0
- package/dist/cjs/spreadsheet.js +17 -0
- package/dist/cjs/types.js +17 -0
- package/dist/cjs/validation/callout.js +17 -0
- package/dist/cjs/validation/doc-node-whitelist.js +71 -0
- package/dist/cjs/validation/error.js +17 -0
- package/dist/cjs/validation/mind-map.js +129 -0
- package/dist/cjs/validation/schema-version.js +5 -0
- package/dist/cjs/validation/schema.js +376 -0
- package/dist/cjs/validation/video-embed.js +17 -0
- package/dist/cjs/validation/workbook.js +17 -0
- package/dist/esm/document.js +3 -0
- package/dist/esm/index.js +3 -0
- package/dist/esm/mindmap.js +4 -0
- package/dist/esm/schema.js +3 -0
- package/dist/esm/shared/anchor.js +192 -0
- package/dist/esm/shared/callout.js +56 -0
- package/dist/esm/shared/code-block.js +25 -0
- package/dist/esm/shared/code-languages.js +46 -0
- package/dist/esm/shared/doc-compatibility.js +20 -0
- package/dist/esm/shared/doc-text.js +48 -0
- package/dist/esm/shared/document-colors.js +56 -0
- package/dist/esm/shared/inline-comments.js +173 -0
- package/dist/esm/shared/mind-map-folding.js +10 -0
- package/dist/esm/shared/mind-map-phase2.js +121 -0
- package/dist/esm/shared/mind-map.js +427 -0
- package/dist/esm/shared/mind-topic-text.js +49 -0
- package/dist/esm/shared/node-catalog.js +57 -0
- package/dist/esm/shared/presence.js +130 -0
- package/dist/esm/shared/safe-href.js +32 -0
- package/dist/esm/shared/table-cell-format.js +14 -0
- package/dist/esm/shared/table-layout.js +23 -0
- package/dist/esm/shared/types.js +1 -0
- package/dist/esm/shared/video-embed.js +96 -0
- package/dist/esm/shared/workbook.js +144 -0
- package/dist/esm/spreadsheet.js +1 -0
- package/dist/esm/types.js +1 -0
- package/dist/esm/validation/callout.js +1 -0
- package/dist/esm/validation/doc-node-whitelist.js +68 -0
- package/dist/esm/validation/error.js +13 -0
- package/dist/esm/validation/mind-map.js +125 -0
- package/dist/esm/validation/schema-version.js +2 -0
- package/dist/esm/validation/schema.js +372 -0
- package/dist/esm/validation/video-embed.js +1 -0
- package/dist/esm/validation/workbook.js +1 -0
- package/dist/types/document.d.ts +4 -0
- package/dist/types/index.d.ts +4 -0
- package/dist/types/mindmap.d.ts +4 -0
- package/dist/types/schema.d.ts +4 -0
- package/dist/types/shared/anchor.d.ts +87 -0
- package/dist/types/shared/callout.d.ts +27 -0
- package/dist/types/shared/code-block.d.ts +26 -0
- package/dist/types/shared/code-languages.d.ts +26 -0
- package/dist/types/shared/doc-compatibility.d.ts +1 -0
- package/dist/types/shared/doc-text.d.ts +21 -0
- package/dist/types/shared/document-colors.d.ts +106 -0
- package/dist/types/shared/inline-comments.d.ts +71 -0
- package/dist/types/shared/mind-map-folding.d.ts +5 -0
- package/dist/types/shared/mind-map-phase2.d.ts +19 -0
- package/dist/types/shared/mind-map.d.ts +134 -0
- package/dist/types/shared/mind-topic-text.d.ts +18 -0
- package/dist/types/shared/node-catalog.d.ts +30 -0
- package/dist/types/shared/presence.d.ts +69 -0
- package/dist/types/shared/safe-href.d.ts +3 -0
- package/dist/types/shared/table-cell-format.d.ts +24 -0
- package/dist/types/shared/table-layout.d.ts +7 -0
- package/dist/types/shared/types.d.ts +44 -0
- package/dist/types/shared/video-embed.d.ts +11 -0
- package/dist/types/shared/workbook.d.ts +56 -0
- package/dist/types/spreadsheet.d.ts +1 -0
- package/dist/types/types.d.ts +3 -0
- package/dist/types/validation/callout.d.ts +1 -0
- package/dist/types/validation/doc-node-whitelist.d.ts +21 -0
- package/dist/types/validation/error.d.ts +7 -0
- package/dist/types/validation/mind-map.d.ts +22 -0
- package/dist/types/validation/schema-version.d.ts +2 -0
- package/dist/types/validation/schema.d.ts +49 -0
- package/dist/types/validation/video-embed.d.ts +1 -0
- package/dist/types/validation/workbook.d.ts +1 -0
- package/package.json +87 -5
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.HIGHLIGHT_COLORS = exports.TEXT_COLORS = void 0;
|
|
4
|
+
exports.paletteColor = paletteColor;
|
|
5
|
+
exports.documentColorAttributes = documentColorAttributes;
|
|
6
|
+
/**
|
|
7
|
+
* 文档文字与高亮的受限调色板。存储值保持原始 hex,显示色通过主题变量切换;
|
|
8
|
+
* 阅读路径复用此模块,不引入 Tiptap。历史色值均保留,复制时 data-color 可往返。
|
|
9
|
+
*/
|
|
10
|
+
exports.TEXT_COLORS = [
|
|
11
|
+
{ name: '默认', value: null, token: null },
|
|
12
|
+
{ name: '灰', value: '#868e96', token: 'gray' },
|
|
13
|
+
{ name: '红', value: '#d92c2c', token: 'red' },
|
|
14
|
+
{ name: '橙', value: '#e07b1a', token: 'orange' },
|
|
15
|
+
{ name: '黄褐', value: '#9a7b12', token: 'yellow' },
|
|
16
|
+
{ name: '绿', value: '#2f9e44', token: 'green' },
|
|
17
|
+
{ name: '蓝', value: '#1971c2', token: 'blue' },
|
|
18
|
+
{ name: '紫', value: '#7048e8', token: 'purple' },
|
|
19
|
+
];
|
|
20
|
+
exports.HIGHLIGHT_COLORS = [
|
|
21
|
+
{ name: '无', value: null, token: null },
|
|
22
|
+
{ name: '浅灰', value: '#f1f3f5', token: 'gray-soft' },
|
|
23
|
+
{ name: '浅红', value: '#ffe3e3', token: 'red' },
|
|
24
|
+
{ name: '浅橙', value: '#fff0db', token: 'orange' },
|
|
25
|
+
{ name: '浅黄', value: '#fff3bf', token: 'yellow' },
|
|
26
|
+
{ name: '浅绿', value: '#d3f9d8', token: 'green' },
|
|
27
|
+
{ name: '浅蓝', value: '#d0ebff', token: 'blue' },
|
|
28
|
+
{ name: '浅紫', value: '#e5dbff', token: 'purple' },
|
|
29
|
+
{ name: '灰', value: '#dee2e6', token: 'gray' },
|
|
30
|
+
{ name: '深灰', value: '#ced4da', token: 'gray-strong' },
|
|
31
|
+
{ name: '红', value: '#ffc9c9', token: 'red-strong' },
|
|
32
|
+
{ name: '橙', value: '#ffd8a8', token: 'orange-strong' },
|
|
33
|
+
{ name: '黄', value: '#ffec99', token: 'yellow-strong' },
|
|
34
|
+
{ name: '绿', value: '#b2f2bb', token: 'green-strong' },
|
|
35
|
+
{ name: '蓝', value: '#a5d8ff', token: 'blue-strong' },
|
|
36
|
+
{ name: '紫', value: '#d0bfff', token: 'purple-strong' },
|
|
37
|
+
];
|
|
38
|
+
function paletteColor(value, palette) {
|
|
39
|
+
const hex = value.trim().toLowerCase();
|
|
40
|
+
const rgb = /^rgb\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)$/.exec(hex);
|
|
41
|
+
if (rgb && rgb.slice(1).some(channel => Number(channel) > 255))
|
|
42
|
+
return null;
|
|
43
|
+
const normalized = rgb
|
|
44
|
+
? `#${rgb.slice(1).map(channel => Number(channel).toString(16).padStart(2, '0')).join('')}`
|
|
45
|
+
: hex;
|
|
46
|
+
return palette.some(color => color.value === normalized) ? normalized : null;
|
|
47
|
+
}
|
|
48
|
+
function documentColorAttributes(kind, value) {
|
|
49
|
+
if (typeof value !== 'string')
|
|
50
|
+
return {};
|
|
51
|
+
const palette = kind === 'text' ? exports.TEXT_COLORS : exports.HIGHLIGHT_COLORS;
|
|
52
|
+
const normalized = paletteColor(value, palette);
|
|
53
|
+
const color = palette.find(item => item.value === normalized);
|
|
54
|
+
if (!color?.value)
|
|
55
|
+
return {};
|
|
56
|
+
const property = kind === 'text' ? 'color' : 'background-color';
|
|
57
|
+
return {
|
|
58
|
+
'data-color': color.value,
|
|
59
|
+
style: `${property}: var(--doc-${kind}-${color.token}, ${color.value})`,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.buildTextRuns = buildTextRuns;
|
|
4
|
+
exports.buildFlatText = buildFlatText;
|
|
5
|
+
exports.resolveInlineComments = resolveInlineComments;
|
|
6
|
+
exports.splitTextByMarks = splitTextByMarks;
|
|
7
|
+
const anchor_js_1 = require("./anchor.js");
|
|
8
|
+
const doc_text_js_1 = require("./doc-text.js");
|
|
9
|
+
function buildTextRuns(doc) {
|
|
10
|
+
const runs = [];
|
|
11
|
+
let offset = 0;
|
|
12
|
+
/** 上一次是否为"块级片段结尾",用于决定是否插入分隔符 */
|
|
13
|
+
const blockTexts = [];
|
|
14
|
+
const collect = (node, out) => {
|
|
15
|
+
if (typeof node.text === 'string') {
|
|
16
|
+
out.push(node);
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
for (const c of node.content ?? [])
|
|
20
|
+
collect(c, out);
|
|
21
|
+
};
|
|
22
|
+
// 与 anchor 的 flattenDocText 一致:以「顶层块」为单位,块间插入分隔符,
|
|
23
|
+
// 防止块尾与块首被误连成一个匹配。
|
|
24
|
+
for (const top of doc?.content ?? []) {
|
|
25
|
+
const textNodes = [];
|
|
26
|
+
collect(top, textNodes);
|
|
27
|
+
const blockText = textNodes.map((n) => n.text ?? '').join('');
|
|
28
|
+
if (blockText.length === 0)
|
|
29
|
+
continue;
|
|
30
|
+
blockTexts.push(blockText);
|
|
31
|
+
const base = blockTexts.length > 1 ? offset + 1 : offset; // +1 为块间分隔符
|
|
32
|
+
let inner = 0;
|
|
33
|
+
for (const n of textNodes) {
|
|
34
|
+
const t = n.text ?? '';
|
|
35
|
+
if (t.length === 0)
|
|
36
|
+
continue;
|
|
37
|
+
runs.push({
|
|
38
|
+
index: runs.length,
|
|
39
|
+
text: t,
|
|
40
|
+
flatStart: base + inner,
|
|
41
|
+
flatEnd: base + inner + t.length,
|
|
42
|
+
});
|
|
43
|
+
inner += t.length;
|
|
44
|
+
}
|
|
45
|
+
offset = base + inner;
|
|
46
|
+
}
|
|
47
|
+
return runs;
|
|
48
|
+
}
|
|
49
|
+
/** 文档的扁平文本(与 buildTextRuns 的偏移体系一致)。 */
|
|
50
|
+
function buildFlatText(doc) {
|
|
51
|
+
const blocks = (doc?.content ?? [])
|
|
52
|
+
.map((top) => (0, doc_text_js_1.plainTextOf)(top))
|
|
53
|
+
.filter((t) => t.length > 0);
|
|
54
|
+
return (0, anchor_js_1.flattenDocText)(blocks).text;
|
|
55
|
+
}
|
|
56
|
+
/** 仅有锚点的评论才参与定位(页级评论不属于行内)。 */
|
|
57
|
+
function resolveInlineComments(doc, comments) {
|
|
58
|
+
const runs = buildTextRuns(doc);
|
|
59
|
+
const flatText = buildFlatText(doc);
|
|
60
|
+
const marks = [];
|
|
61
|
+
const orphanCommentIds = [];
|
|
62
|
+
// 只处理顶层评论:回复共用顶层锚点,不单独定位
|
|
63
|
+
for (const c of comments) {
|
|
64
|
+
if (c.deleted || !c.anchor)
|
|
65
|
+
continue;
|
|
66
|
+
if (c.anchor.prefix?.startsWith('mind:')) {
|
|
67
|
+
let exists = false;
|
|
68
|
+
const walk = (node) => {
|
|
69
|
+
if (node.type === 'mindMap' && node.attrs?.id === c.anchor.prefix.slice(5)) {
|
|
70
|
+
exists = !!node.content?.some(t => t.type === 'mindMapTopic' && t.attrs?.id === c.anchor.suffix);
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
for (const child of node.content ?? [])
|
|
74
|
+
walk(child);
|
|
75
|
+
};
|
|
76
|
+
if (doc)
|
|
77
|
+
walk(doc);
|
|
78
|
+
if (!exists)
|
|
79
|
+
orphanCommentIds.push(c.id);
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
if (c.parentId !== null)
|
|
83
|
+
continue;
|
|
84
|
+
const resolved = (0, anchor_js_1.resolveAnchor)(flatText, {
|
|
85
|
+
quote: c.anchor.quote,
|
|
86
|
+
prefix: c.anchor.prefix,
|
|
87
|
+
suffix: c.anchor.suffix,
|
|
88
|
+
start: c.anchor.start,
|
|
89
|
+
});
|
|
90
|
+
if (!resolved) {
|
|
91
|
+
orphanCommentIds.push(c.id);
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* 把扁平区间映射到「文本节点 + 节点内偏移」。
|
|
96
|
+
*
|
|
97
|
+
* 锚点可能跨越多个文本节点(例如引用跨越两个段落、或跨过一个行内原子
|
|
98
|
+
* 节点如 `[[页面引用]]`)。**每个被覆盖的文本节点都要产出一条 mark**,
|
|
99
|
+
* 否则高亮会在第一个节点末尾断掉——表现为「引用了两段,只有第一段有
|
|
100
|
+
* 底色」,用户会以为评论只针对半句话。
|
|
101
|
+
*
|
|
102
|
+
* 这里不再"跨节点时只取首个",而是逐节点裁剪区间:
|
|
103
|
+
* 节点内起点 = max(0, 锚点起点 - 该节点扁平起点)
|
|
104
|
+
* 节点内终点 = min(节点长度, 锚点终点 - 该节点扁平起点)
|
|
105
|
+
*/
|
|
106
|
+
const targets = runs.filter((r) => r.flatEnd > resolved.from && r.flatStart <= resolved.to);
|
|
107
|
+
if (targets.length === 0) {
|
|
108
|
+
orphanCommentIds.push(c.id);
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* 零宽落点(shifted):原文已被改动、仅靠上下文降级定位。
|
|
113
|
+
* `resolveAnchor` 用 from === to 表示这种状态,其语义是「在这一点的位置
|
|
114
|
+
* 插一个标记」而非覆盖一段文字,因此只在首个节点上产出一条零宽 mark。
|
|
115
|
+
* (不能用下面的逐节点裁剪——零宽区间会被 `to <= from` 全部过滤掉。)
|
|
116
|
+
*/
|
|
117
|
+
if (resolved.to <= resolved.from) {
|
|
118
|
+
const first = targets[0];
|
|
119
|
+
marks.push({
|
|
120
|
+
commentId: c.id,
|
|
121
|
+
textIndex: first.index,
|
|
122
|
+
from: Math.max(0, resolved.from - first.flatStart),
|
|
123
|
+
to: Math.max(0, resolved.from - first.flatStart),
|
|
124
|
+
state: 'shifted',
|
|
125
|
+
hasReplies: c.replies.length > 0,
|
|
126
|
+
});
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
const state = 'exact';
|
|
130
|
+
let produced = 0;
|
|
131
|
+
for (const run of targets) {
|
|
132
|
+
const from = Math.max(0, resolved.from - run.flatStart);
|
|
133
|
+
const to = Math.min(run.text.length, resolved.to - run.flatStart);
|
|
134
|
+
if (to <= from)
|
|
135
|
+
continue;
|
|
136
|
+
marks.push({
|
|
137
|
+
commentId: c.id,
|
|
138
|
+
textIndex: run.index,
|
|
139
|
+
from,
|
|
140
|
+
to,
|
|
141
|
+
state,
|
|
142
|
+
hasReplies: c.replies.length > 0,
|
|
143
|
+
});
|
|
144
|
+
produced += 1;
|
|
145
|
+
}
|
|
146
|
+
// 区间落在节点边界上、没有任何实际覆盖 → 视为失联(宁可提示,不误标)
|
|
147
|
+
if (produced === 0)
|
|
148
|
+
orphanCommentIds.push(c.id);
|
|
149
|
+
}
|
|
150
|
+
const byTextIndex = new Map();
|
|
151
|
+
for (const m of marks) {
|
|
152
|
+
const arr = byTextIndex.get(m.textIndex) ?? [];
|
|
153
|
+
arr.push(m);
|
|
154
|
+
byTextIndex.set(m.textIndex, arr);
|
|
155
|
+
}
|
|
156
|
+
return { marks, orphanCommentIds, byTextIndex };
|
|
157
|
+
}
|
|
158
|
+
function splitTextByMarks(text, marks) {
|
|
159
|
+
if (marks.length === 0 || text.length === 0) {
|
|
160
|
+
return [{ text, marks: [] }];
|
|
161
|
+
}
|
|
162
|
+
// 收集切分点
|
|
163
|
+
const points = new Set([0, text.length]);
|
|
164
|
+
for (const m of marks) {
|
|
165
|
+
points.add(Math.max(0, Math.min(m.from, text.length)));
|
|
166
|
+
points.add(Math.max(0, Math.min(m.to, text.length)));
|
|
167
|
+
}
|
|
168
|
+
const sorted = [...points].sort((a, b) => a - b);
|
|
169
|
+
const pieces = [];
|
|
170
|
+
for (let i = 0; i < sorted.length - 1; i += 1) {
|
|
171
|
+
const start = sorted[i];
|
|
172
|
+
const end = sorted[i + 1];
|
|
173
|
+
if (end <= start)
|
|
174
|
+
continue;
|
|
175
|
+
const covering = marks.filter((m) => m.from <= start && m.to >= end);
|
|
176
|
+
pieces.push({ text: text.slice(start, end), marks: covering });
|
|
177
|
+
}
|
|
178
|
+
return pieces.length > 0 ? pieces : [{ text, marks: [] }];
|
|
179
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.normalizeMindFolds = normalizeMindFolds;
|
|
4
|
+
exports.firstLevelMindFolds = firstLevelMindFolds;
|
|
5
|
+
const mind_map_js_1 = require("./mind-map.js");
|
|
6
|
+
/** Root/leaf/stale entries have no local fold affordance and must not hide a whole map. */
|
|
7
|
+
function normalizeMindFolds(model, ids) {
|
|
8
|
+
const tree = (0, mind_map_js_1.mindMapTree)(model), parents = new Set(tree.map(t => t.parentId)), known = new Set(tree.map(t => t.id));
|
|
9
|
+
return new Set([...ids].filter(id => id !== model.rootId && known.has(id) && parents.has(id)));
|
|
10
|
+
}
|
|
11
|
+
/** Keep every first-level branch visible, collapsing only those with descendants. */
|
|
12
|
+
function firstLevelMindFolds(model) {
|
|
13
|
+
return normalizeMindFolds(model, (0, mind_map_js_1.mindMapTree)(model).filter(t => t.parentId === model.rootId).map(t => t.id));
|
|
14
|
+
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.MIND_BACKGROUNDS = exports.MIND_COLORS = exports.MIND_ICONS = void 0;
|
|
4
|
+
exports.topicTitle = topicTitle;
|
|
5
|
+
exports.topicField = topicField;
|
|
6
|
+
exports.branchModel = branchModel;
|
|
7
|
+
exports.ancestorIds = ancestorIds;
|
|
8
|
+
exports.searchMindMap = searchMindMap;
|
|
9
|
+
exports.parseMindMapImport = parseMindMapImport;
|
|
10
|
+
exports.mindMapTemplate = mindMapTemplate;
|
|
11
|
+
exports.importBranch = importBranch;
|
|
12
|
+
exports.parseMindBranch = parseMindBranch;
|
|
13
|
+
const mind_map_js_1 = require("./mind-map.js");
|
|
14
|
+
exports.MIND_ICONS = ['', '💡', '🎯', '📌', '🚩', '⭐', '✅', '⚠️', '💬', '📅', '🔎'];
|
|
15
|
+
exports.MIND_COLORS = ['', '#18243a', '#ffffff', '#3864ef', '#087f8c', '#7950c7', '#a66b08', '#d94848'];
|
|
16
|
+
exports.MIND_BACKGROUNDS = ['', '#f3f5fb', '#dce7ff', '#d3f4e9', '#ece0ff', '#fff0c2', '#ffe1e1', '#233044'];
|
|
17
|
+
function topicTitle(node) {
|
|
18
|
+
return (node.content ?? []).filter(c => c.type === 'text').map(c => c.text ?? '').join('');
|
|
19
|
+
}
|
|
20
|
+
function topicField(node, field) {
|
|
21
|
+
return (node.content?.find(c => c.type === (field === 'description' ? 'mindMapDescription' : 'mindMapNote'))?.content ?? [])
|
|
22
|
+
.map(c => c.text ?? '').join('');
|
|
23
|
+
}
|
|
24
|
+
function branchModel(model, id) {
|
|
25
|
+
const ids = (0, mind_map_js_1.mindMapDescendants)(model, id);
|
|
26
|
+
return { ...model, rootId: id, topics: (0, mind_map_js_1.mindMapTree)(model).filter(t => ids.has(t.id))
|
|
27
|
+
.map(t => t.id === id ? { ...t, parentId: '' } : t),
|
|
28
|
+
relations: model.relations?.filter(r => ids.has(r.source) && ids.has(r.target)) };
|
|
29
|
+
}
|
|
30
|
+
function ancestorIds(model, id) {
|
|
31
|
+
const byId = new Map((0, mind_map_js_1.mindMapTree)(model).map(t => [t.id, t])), result = [];
|
|
32
|
+
let current = byId.get(id);
|
|
33
|
+
while (current?.parentId && result.length < 500) {
|
|
34
|
+
result.push(current.parentId);
|
|
35
|
+
current = byId.get(current.parentId);
|
|
36
|
+
}
|
|
37
|
+
return result;
|
|
38
|
+
}
|
|
39
|
+
function searchMindMap(model, query, details = true) {
|
|
40
|
+
if (!query)
|
|
41
|
+
return [];
|
|
42
|
+
const needle = query.toLocaleLowerCase(), hits = [];
|
|
43
|
+
for (const topic of (0, mind_map_js_1.mindMapOutline)(model)) {
|
|
44
|
+
for (const field of (details ? ['title', 'description', 'note'] : ['title'])) {
|
|
45
|
+
const value = (field === 'title' ? topic.text : topic[field] ?? '').toLocaleLowerCase();
|
|
46
|
+
for (let at = 0; (at = value.indexOf(needle, at)) >= 0; at += needle.length) {
|
|
47
|
+
hits.push({ id: topic.id, field, from: at, to: at + needle.length });
|
|
48
|
+
if (hits.length > 5000)
|
|
49
|
+
return hits;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return hits;
|
|
54
|
+
}
|
|
55
|
+
function parseMindMapImport(raw) {
|
|
56
|
+
if (new TextEncoder().encode(raw).length > 2_000_000)
|
|
57
|
+
throw new Error('导入内容过大(最多 2 MB)');
|
|
58
|
+
if (raw.trimStart().startsWith('{')) {
|
|
59
|
+
const data = JSON.parse(raw);
|
|
60
|
+
const model = data.type === 'mindMap' ? (0, mind_map_js_1.readMindMap)(data) :
|
|
61
|
+
data.format === 'ywiki-mind-map' && [1, 2, mind_map_js_1.MIND_MAP_VERSION].includes(data.version) && Array.isArray(data.topics)
|
|
62
|
+
? (0, mind_map_js_1.readMindMap)((0, mind_map_js_1.mindMapToJson)(data)) : null;
|
|
63
|
+
if (!model)
|
|
64
|
+
throw new Error('导图结构或版本无效');
|
|
65
|
+
if (model.topics.some(t => t.id !== model.rootId && !model.topics.some(p => p.id === t.parentId)))
|
|
66
|
+
throw new Error('导入结构包含不存在的父主题');
|
|
67
|
+
if (model.relations?.some(r => !model.topics.some(t => t.id === r.source) || !model.topics.some(t => t.id === r.target)))
|
|
68
|
+
throw new Error('导入关系的端点不存在');
|
|
69
|
+
for (const topic of model.topics) {
|
|
70
|
+
const seen = new Set();
|
|
71
|
+
let current = topic;
|
|
72
|
+
while (current?.parentId) {
|
|
73
|
+
if (seen.has(current.id))
|
|
74
|
+
throw new Error('导入结构存在循环关系');
|
|
75
|
+
seen.add(current.id);
|
|
76
|
+
current = model.topics.find(t => t.id === current.parentId);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return (0, mind_map_js_1.readMindMap)((0, mind_map_js_1.mindMapToJson)(model, true));
|
|
80
|
+
}
|
|
81
|
+
const lines = raw.replace(/\r/g, '').split('\n').filter(l => l.trim());
|
|
82
|
+
if (!lines.length)
|
|
83
|
+
throw new Error('请输入缩进列表或导图 JSON');
|
|
84
|
+
if (lines.length > 499)
|
|
85
|
+
throw new Error('导入列表最多 499 行,加中心主题后最多 500 个主题');
|
|
86
|
+
const model = (0, mind_map_js_1.readMindMap)((0, mind_map_js_1.createMindMap)());
|
|
87
|
+
model.topics = [model.topics[0]];
|
|
88
|
+
model.topics[0].text = '导入的主题';
|
|
89
|
+
const stack = [{ indent: -1, id: model.rootId }];
|
|
90
|
+
for (let i = 0; i < lines.length; i++) {
|
|
91
|
+
const line = lines[i], white = line.match(/^\s*/)?.[0] ?? '', indent = white.replace(/\t/g, ' ').length;
|
|
92
|
+
const text = line.trim().replace(/^(?:[-*+]\s+|\d+[.)]\s+)/, '');
|
|
93
|
+
if (text.length > 2000)
|
|
94
|
+
throw new Error(`第 ${i + 1} 行超过 2000 字符`);
|
|
95
|
+
while (stack.length > 1 && stack.at(-1).indent >= indent)
|
|
96
|
+
stack.pop();
|
|
97
|
+
if (stack.length > 50)
|
|
98
|
+
throw new Error(`第 ${i + 1} 行嵌套超过 50 层`);
|
|
99
|
+
const id = (0, mind_map_js_1.mindMapId)();
|
|
100
|
+
model.topics.push({ id, parentId: stack.at(-1).id, order: i, text });
|
|
101
|
+
stack.push({ indent, id });
|
|
102
|
+
}
|
|
103
|
+
return model;
|
|
104
|
+
}
|
|
105
|
+
function mindMapTemplate(kind) {
|
|
106
|
+
const data = {
|
|
107
|
+
project: ['项目拆解', '目标与范围', '需求梳理', '研发实现', '测试验收', '上线与复盘'],
|
|
108
|
+
meeting: ['会议讨论', '议题', '背景信息', '讨论结论', '待办事项', '负责人'],
|
|
109
|
+
knowledge: ['知识目录', '基础概念', '核心原理', '实践案例', '常见问题', '参考资料'],
|
|
110
|
+
}[kind];
|
|
111
|
+
const m = (0, mind_map_js_1.readMindMap)((0, mind_map_js_1.createMindMap)('both'));
|
|
112
|
+
m.topics = data.map((text, i) => ({ id: i ? (0, mind_map_js_1.mindMapId)() : m.rootId, parentId: i ? m.rootId : '', order: i, text }));
|
|
113
|
+
return m;
|
|
114
|
+
}
|
|
115
|
+
function importBranch(model) {
|
|
116
|
+
return (0, mind_map_js_1.mindMapToJson)(model, true).content ?? [];
|
|
117
|
+
}
|
|
118
|
+
function parseMindBranch(raw) {
|
|
119
|
+
if (!raw.startsWith('YWIKI_MINDMAP_BRANCH_V1\n') || raw.length > 2_000_000)
|
|
120
|
+
throw new Error('不是有效的导图分支');
|
|
121
|
+
const content = JSON.parse(raw.slice(raw.indexOf('\n') + 1));
|
|
122
|
+
if (!Array.isArray(content) || content.length > 1500)
|
|
123
|
+
throw new Error('分支内容过大');
|
|
124
|
+
const topics = content.filter(t => t?.type === 'mindMapTopic');
|
|
125
|
+
if (!topics.length || !topics[0].attrs)
|
|
126
|
+
throw new Error('分支缺少主题');
|
|
127
|
+
const normalized = content.map(t => t === topics[0] ? { ...t, attrs: { ...t.attrs, parentId: '' } } : t);
|
|
128
|
+
const model = (0, mind_map_js_1.readMindMap)({ type: 'mindMap', attrs: { id: 'clipboard', rootId: topics[0].attrs.id,
|
|
129
|
+
formatVersion: mind_map_js_1.MIND_MAP_VERSION, layout: 'right', theme: 'blue', height: 380 }, content: normalized });
|
|
130
|
+
if (!model)
|
|
131
|
+
throw new Error('分支结构不合法');
|
|
132
|
+
return importBranch(parseMindMapImport(JSON.stringify((0, mind_map_js_1.mindMapToJson)(model))));
|
|
133
|
+
}
|