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,56 +1 @@
1
- /** Document attributes only; shared with server validation by a source parity test. */
2
- export const CALLOUT_TONES = ['info', 'success', 'warning', 'danger'];
3
- export const CALLOUT_ICONS = ['💡', 'ℹ️', '✅', '⚠️', '❗', '📌', '🎯', '💬', '📖', '📝', '🚀', '🔔', '🔎', '⭐', '🌅', '🛠️', '📅', '🔒', '❤️', '🎉', '❓', '📎', '🏁', '🔥'];
4
- export const CALLOUT_COLORS = ['gray', 'red', 'orange', 'yellow', 'green', 'blue', 'purple'];
5
- export const CALLOUT_FILLS = [...CALLOUT_COLORS, ...CALLOUT_COLORS.map(c => `${c}-strong`)];
6
- export function validCalloutAttrs(raw) {
7
- if (!raw || typeof raw !== 'object' || Array.isArray(raw))
8
- return false;
9
- const attrs = raw;
10
- const valid = (key, values) => attrs[key] === undefined || attrs[key] === null ||
11
- (typeof attrs[key] === 'string' && values.includes(attrs[key]));
12
- return valid('tone', CALLOUT_TONES) && valid('icon', ['', ...CALLOUT_ICONS]) && valid('textColor', CALLOUT_COLORS) &&
13
- valid('borderColor', ['none', ...CALLOUT_COLORS]) && valid('backgroundColor', ['none', ...CALLOUT_FILLS]);
14
- }
15
- export function effectiveCallout(attrs = {}) {
16
- const tone = CALLOUT_TONES.includes(attrs.tone) ? attrs.tone : 'info';
17
- const hue = { info: 'blue', success: 'green', warning: 'orange', danger: 'red' }[tone];
18
- return { tone, icon: attrs.icon === null || attrs.icon === undefined ? { info: '💡', success: '✅', warning: '⚠️', danger: '❗' }[tone] :
19
- ['', ...CALLOUT_ICONS].includes(attrs.icon) ? attrs.icon : '💡',
20
- textColor: CALLOUT_COLORS.includes(attrs.textColor) ? attrs.textColor : null,
21
- borderColor: ['none', ...CALLOUT_COLORS].includes(attrs.borderColor) ? attrs.borderColor : hue,
22
- backgroundColor: ['none', ...CALLOUT_FILLS].includes(attrs.backgroundColor) ? attrs.backgroundColor : hue };
23
- }
24
- export function calloutDOMAttributes(attrs = {}) {
25
- const value = effectiveCallout(attrs);
26
- return { 'data-callout': 'true', 'data-tone': value.tone, 'data-callout-icon': attrs.icon ?? 'default',
27
- ...Object.fromEntries(['textColor', 'borderColor', 'backgroundColor'].filter(key => attrs[key] != null)
28
- .map(key => [`data-callout-${key.toLowerCase()}`, String(attrs[key])])),
29
- ...(value.icon === '' ? { 'data-icon-hidden': 'true' } : {}),
30
- style: [`--callout-border:${value.borderColor === 'none' ? 'transparent' : `var(--callout-border-${value.borderColor})`}`,
31
- `--callout-fill:${value.backgroundColor === 'none' ? 'transparent' : `var(--doc-highlight-${value.backgroundColor === 'gray' ? 'gray-soft' : value.backgroundColor})`}`,
32
- `--callout-text:${value.textColor ? `var(--doc-text-${value.textColor})` : 'var(--content-text-color, var(--color-text))'}`].join(';') };
33
- }
34
- /** Optional Markdown metadata never contains free CSS or HTML. */
35
- export function calloutMetadata(attrs) {
36
- if (!validCalloutAttrs(attrs))
37
- return '';
38
- const extra = Object.fromEntries(['icon', 'textColor', 'borderColor', 'backgroundColor']
39
- .filter(key => attrs[key] !== undefined && attrs[key] !== null)
40
- .map(key => [key, attrs[key]]));
41
- return Object.keys(extra).length ? ` <!--ywiki-callout:${encodeURIComponent(JSON.stringify(extra))}-->` : '';
42
- }
43
- export function readCalloutMetadata(text) {
44
- const match = /^\s*<!--ywiki-callout:([^\s<>]{1,4096})-->\s*/.exec(text);
45
- if (!match)
46
- return null;
47
- try {
48
- const attrs = JSON.parse(decodeURIComponent(match[1]));
49
- if (!validCalloutAttrs(attrs) || Object.keys(attrs).some(k => !['icon', 'textColor', 'borderColor', 'backgroundColor'].includes(k)))
50
- return null;
51
- return { attrs, length: match[0].length };
52
- }
53
- catch {
54
- return null;
55
- }
56
- }
1
+ export const CALLOUT_TONES=["info","success","warning","danger"];export const CALLOUT_ICONS=["💡","ℹ️","✅","⚠️","❗","📌","🎯","💬","📖","📝","🚀","🔔","🔎","⭐","🌅","🛠️","📅","🔒","❤️","🎉","❓","📎","🏁","🔥"];export const CALLOUT_COLORS=["gray","red","orange","yellow","green","blue","purple"];export const CALLOUT_FILLS=[...CALLOUT_COLORS,...CALLOUT_COLORS.map(o=>`${o}-strong`)];export const CALLOUT_SWATCH_FALLBACKS={textColor:{gray:"#646a73",red:"#d92c2c",orange:"#b95d0b",yellow:"#8a6c0a",green:"#237b35",blue:"#1971c2",purple:"#7048e8"},borderColor:{gray:"#c7cbd1",red:"#f0a2a2",orange:"#ffc078",yellow:"#e3ca71",green:"#91d4a0",blue:"#9ac5ef",purple:"#c0a4ed"},backgroundColor:{gray:"#f1f3f5",red:"#ffe3e3",orange:"#fff0db",yellow:"#fff3bf",green:"#d3f9d8",blue:"#d0ebff",purple:"#e5dbff","gray-strong":"#ced4da","red-strong":"#ffc9c9","orange-strong":"#ffd8a8","yellow-strong":"#ffec99","green-strong":"#b2f2bb","blue-strong":"#a5d8ff","purple-strong":"#d0bfff"}};export function calloutColorToken(o,r){return"textColor"===o?`--doc-text-${r}`:"borderColor"===o?`--callout-border-${r}`:`--doc-highlight-${"gray"===r?"gray-soft":r}`}export function calloutSwatchStyle(o,r){if(null===r||"none"===r)return{};const e=CALLOUT_SWATCH_FALLBACKS[o][r],t=e?`var(${calloutColorToken(o,r)}, ${e})`:`var(${calloutColorToken(o,r)})`;return"textColor"===o?{color:t}:{backgroundColor:t}}export function validCalloutAttrs(o){if(!o||"object"!=typeof o||Array.isArray(o))return!1;const r=o,valid=(o,e)=>void 0===r[o]||null===r[o]||"string"==typeof r[o]&&e.includes(r[o]);return valid("tone",CALLOUT_TONES)&&valid("icon",["",...CALLOUT_ICONS])&&valid("textColor",CALLOUT_COLORS)&&valid("borderColor",["none",...CALLOUT_COLORS])&&valid("backgroundColor",["none",...CALLOUT_FILLS])}export function effectiveCallout(o={}){const r=CALLOUT_TONES.includes(o.tone)?o.tone:"info",e={info:"blue",success:"green",warning:"orange",danger:"red"}[r];return{tone:r,icon:null===o.icon||void 0===o.icon?{info:"💡",success:"✅",warning:"⚠️",danger:"❗"}[r]:["",...CALLOUT_ICONS].includes(o.icon)?o.icon:"💡",textColor:CALLOUT_COLORS.includes(o.textColor)?o.textColor:null,borderColor:["none",...CALLOUT_COLORS].includes(o.borderColor)?o.borderColor:e,backgroundColor:["none",...CALLOUT_FILLS].includes(o.backgroundColor)?o.backgroundColor:e}}export function calloutDOMAttributes(o={}){const r=effectiveCallout(o),token=(o,r)=>`var(${o}, ${r})`,e="none"===r.borderColor?"transparent":token(calloutColorToken("borderColor",r.borderColor),CALLOUT_SWATCH_FALLBACKS.borderColor[r.borderColor]??"#9ac5ef"),t="none"===r.backgroundColor?"transparent":token(calloutColorToken("backgroundColor",r.backgroundColor),CALLOUT_SWATCH_FALLBACKS.backgroundColor[r.backgroundColor]??"#d0ebff"),l=r.textColor?token(calloutColorToken("textColor",r.textColor),CALLOUT_SWATCH_FALLBACKS.textColor[r.textColor]??"#1f2937"):"var(--content-text-color, var(--color-text))";return{"data-callout":"true","data-tone":r.tone,"data-callout-icon":o.icon??"default",...Object.fromEntries(["textColor","borderColor","backgroundColor"].filter(r=>null!=o[r]).map(r=>[`data-callout-${r.toLowerCase()}`,String(o[r])])),...""===r.icon?{"data-icon-hidden":"true"}:{},style:[`--callout-border:${e}`,`--callout-fill:${t}`,`--callout-text:${l}`].join(";")}}export function calloutMetadata(o){if(!validCalloutAttrs(o))return"";const r=Object.fromEntries(["icon","textColor","borderColor","backgroundColor"].filter(r=>void 0!==o[r]&&null!==o[r]).map(r=>[r,o[r]]));return Object.keys(r).length?` \x3c!--ywiki-callout:${encodeURIComponent(JSON.stringify(r))}--\x3e`:""}export function readCalloutMetadata(o){const r=/^\s*<!--ywiki-callout:([^\s<>]{1,4096})-->\s*/.exec(o);if(!r)return null;try{const o=JSON.parse(decodeURIComponent(r[1]));return!validCalloutAttrs(o)||Object.keys(o).some(o=>!["icon","textColor","borderColor","backgroundColor"].includes(o))?null:{attrs:o,length:r[0].length}}catch{return null}}
@@ -1,25 +1 @@
1
- /** Presentation metadata only; code, whitespace and the document schema remain unchanged. */
2
- export const CODE_THEMES = [
3
- { value: 'auto', label: '跟随页面' },
4
- { value: 'paper', label: '纸白' },
5
- { value: 'soft', label: '柔和' },
6
- { value: 'night', label: '深色' },
7
- ];
8
- export function codeBlockDisplay() {
9
- return { collapsed: false, lineNumbers: true, limited: true, theme: 'auto' };
10
- }
11
- export function rememberCodeTheme(_theme) { }
12
- export function codeLineCount(text) {
13
- let count = 1;
14
- for (let i = 0; i < text.length; i++)
15
- if (text[i] === '\n')
16
- count++;
17
- return count;
18
- }
19
- export function codeLineNumbers(text) {
20
- return Array.from({ length: codeLineCount(text) }, (_, i) => String(i + 1)).join('\n');
21
- }
22
- export function codeBlockClass(display) {
23
- return ['ywiki-code-panel', `code-theme-${display.theme}`, ...(display.collapsed ? ['is-collapsed'] : []),
24
- ...(display.lineNumbers ? [] : ['without-line-numbers']), ...(display.limited ? [] : ['is-unlimited'])];
25
- }
1
+ export const CODE_THEMES=[{value:"auto",label:"跟随页面"},{value:"paper",label:"纸白"},{value:"soft",label:"柔和"},{value:"night",label:"深色"}];export function codeBlockDisplay(){return{collapsed:!1,lineNumbers:!0,limited:!0,theme:"auto"}}export function rememberCodeTheme(e){}export function codeLineCount(e){let t=1;for(let o=0;o<e.length;o++)"\n"===e[o]&&t++;return t}export function codeLineNumbers(e){return Array.from({length:codeLineCount(e)},(e,t)=>String(t+1)).join("\n")}export function codeBlockClass(e){return["ywiki-code-panel",`code-theme-${e.theme}`,...e.collapsed?["is-collapsed"]:[],...e.lineNumbers?[]:["without-line-numbers"],...e.limited?[]:["is-unlimited"]]}
@@ -1,46 +1 @@
1
- /** 空字符串表示「纯文本 / 不指定」。 */
2
- export const CODE_LANGUAGES = [
3
- { value: '', label: '纯文本' },
4
- { value: 'bash', label: 'Bash / Shell' },
5
- { value: 'json', label: 'JSON' },
6
- { value: 'yaml', label: 'YAML' },
7
- { value: 'sql', label: 'SQL' },
8
- { value: 'markdown', label: 'Markdown' },
9
- { value: 'diff', label: 'Diff' },
10
- { value: 'javascript', label: 'JavaScript' },
11
- { value: 'typescript', label: 'TypeScript' },
12
- { value: 'jsx', label: 'JSX' },
13
- { value: 'tsx', label: 'TSX' },
14
- { value: 'python', label: 'Python' },
15
- { value: 'java', label: 'Java' },
16
- { value: 'go', label: 'Go' },
17
- { value: 'rust', label: 'Rust' },
18
- { value: 'c', label: 'C' },
19
- { value: 'cpp', label: 'C++' },
20
- { value: 'csharp', label: 'C#' },
21
- { value: 'php', label: 'PHP' },
22
- { value: 'ruby', label: 'Ruby' },
23
- { value: 'kotlin', label: 'Kotlin' },
24
- { value: 'swift', label: 'Swift' },
25
- { value: 'html', label: 'HTML' },
26
- { value: 'css', label: 'CSS' },
27
- { value: 'xml', label: 'XML' },
28
- { value: 'dockerfile', label: 'Dockerfile' },
29
- { value: 'ini', label: 'INI / TOML' },
30
- ];
31
- const LABEL_BY_VALUE = new Map(CODE_LANGUAGES.map((l) => [l.value, l.label]));
32
- /**
33
- * 取语言的展示标签。
34
- *
35
- * 未知语言(如用户从 Markdown 导入的少见围栏标记)直接显示原值——
36
- * 不隐藏、不改写,避免"我明明标了语言却看不到"。
37
- */
38
- export function codeLanguageLabel(value) {
39
- if (!value)
40
- return '';
41
- return LABEL_BY_VALUE.get(value) ?? value;
42
- }
43
- /** 是否为目录内已知语言(用于选择器回显:未知值时追加一个临时选项)。 */
44
- export function isKnownCodeLanguage(value) {
45
- return LABEL_BY_VALUE.has(value);
46
- }
1
+ export const CODE_LANGUAGES=[{value:"",label:"纯文本"},{value:"bash",label:"Bash / Shell"},{value:"json",label:"JSON"},{value:"yaml",label:"YAML"},{value:"sql",label:"SQL"},{value:"markdown",label:"Markdown"},{value:"diff",label:"Diff"},{value:"javascript",label:"JavaScript"},{value:"typescript",label:"TypeScript"},{value:"jsx",label:"JSX"},{value:"tsx",label:"TSX"},{value:"python",label:"Python"},{value:"java",label:"Java"},{value:"go",label:"Go"},{value:"rust",label:"Rust"},{value:"c",label:"C"},{value:"cpp",label:"C++"},{value:"csharp",label:"C#"},{value:"php",label:"PHP"},{value:"ruby",label:"Ruby"},{value:"kotlin",label:"Kotlin"},{value:"swift",label:"Swift"},{value:"html",label:"HTML"},{value:"css",label:"CSS"},{value:"xml",label:"XML"},{value:"dockerfile",label:"Dockerfile"},{value:"ini",label:"INI / TOML"}];const l=new Map(CODE_LANGUAGES.map(l=>[l.value,l.label]));export function codeLanguageLabel(a){return a?l.get(a)??a:""}export function isKnownCodeLanguage(a){return l.has(a)}
@@ -1,20 +1 @@
1
- import { nodeMeta } from './node-catalog.js';
2
- import { SCHEMA_VERSION as CLIENT_SCHEMA_VERSION } from '../schema.js';
3
- import { readMindMap } from './mind-map.js';
4
- import { validCalloutAttrs } from './callout.js';
5
- export function supportsDocument(raw, version = 1) {
6
- if (version > CLIENT_SCHEMA_VERSION)
7
- return false;
8
- if (!raw)
9
- return true;
10
- if (typeof raw !== 'object')
11
- return false;
12
- const doc = raw;
13
- if (!doc.type || !nodeMeta(doc.type))
14
- return false;
15
- if (doc.type === 'mindMap' && !readMindMap(doc))
16
- return false;
17
- if (doc.type === 'callout' && !validCalloutAttrs(doc.attrs ?? {}))
18
- return false;
19
- return !doc.content || Array.isArray(doc.content) && doc.content.every(node => supportsDocument(node));
20
- }
1
+ import{nodeMeta as t}from"./node-catalog.js";import{SCHEMA_VERSION as r}from"../schema.js";import{readMindMap as o}from"./mind-map.js";import{validCalloutAttrs as e}from"./callout.js";export function supportsDocument(n,p=1){if(p>r)return!1;if(!n)return!0;if("object"!=typeof n)return!1;const m=n;return!(!m.type||!t(m.type))&&!("mindMap"===m.type&&!o(m))&&!("callout"===m.type&&!e(m.attrs??{}))&&(!m.content||Array.isArray(m.content)&&m.content.every(t=>supportsDocument(t)))}
@@ -1,48 +1 @@
1
- /**
2
- * 从 ProseMirror 文档提取用于锚点定位的块级纯文本。
3
- *
4
- * 与 anchor.ts 配合:锚点用「在扁平文本中的字符偏移」表达,
5
- * 这里负责把文档结构摊平,并保留「块 → 字符区间」的映射,
6
- * 以便把定位结果映射回具体的块(前端据此在对应块上加高亮)。
7
- *
8
- * 与后端 DocSchemaService.extractPlainText 保持一致的遍历顺序(先文本、后子节点),
9
- * 否则同一段内容在提交锚点与解析锚点时会得到不同的字符偏移。
10
- */
11
- /** 提取顶层块(含嵌套块)的纯文本。列表会递归取出每一项。 */
12
- export function extractBlockTexts(doc) {
13
- const blocks = [];
14
- const walk = (node, topIndex) => {
15
- // 块级容器(段落/标题/列表项/引用/代码块等)各自作为一个「块」
16
- const isBlockContainer = [
17
- 'paragraph',
18
- 'heading',
19
- 'codeBlock',
20
- 'blockquote',
21
- 'listItem',
22
- 'taskItem',
23
- 'tableCell',
24
- 'tableHeader',
25
- 'mindMapTopic',
26
- ].includes(node.type ?? '');
27
- if (isBlockContainer) {
28
- blocks.push({ index: topIndex, text: plainTextOf(node) });
29
- return;
30
- }
31
- for (const child of node.content ?? [])
32
- walk(child, topIndex);
33
- };
34
- (doc?.content ?? []).forEach((node, i) => walk(node, i));
35
- return blocks.filter((b) => b.text.length > 0);
36
- }
37
- /** 取节点的全部文本(含后代)。 */
38
- export function plainTextOf(node) {
39
- const buf = [];
40
- const walk = (n) => {
41
- if (typeof n.text === 'string')
42
- buf.push(n.text);
43
- for (const c of n.content ?? [])
44
- walk(c);
45
- };
46
- walk(node);
47
- return buf.join('');
48
- }
1
+ export function extractBlockTexts(t){const e=[],walk=(t,n)=>{if(["paragraph","heading","codeBlock","blockquote","listItem","taskItem","tableCell","tableHeader","mindMapTopic"].includes(t.type??""))e.push({index:n,text:plainTextOf(t)});else for(const e of t.content??[])walk(e,n)};return(t?.content??[]).forEach((t,e)=>walk(t,e)),e.filter(t=>t.text.length>0)}export function plainTextOf(t){const e=[],walk=t=>{"string"==typeof t.text&&e.push(t.text);for(const e of t.content??[])walk(e)};return walk(t),e.join("")}
@@ -1,56 +1 @@
1
- /**
2
- * 文档文字与高亮的受限调色板。存储值保持原始 hex,显示色通过主题变量切换;
3
- * 阅读路径复用此模块,不引入 Tiptap。历史色值均保留,复制时 data-color 可往返。
4
- */
5
- export const TEXT_COLORS = [
6
- { name: '默认', value: null, token: null },
7
- { name: '灰', value: '#868e96', token: 'gray' },
8
- { name: '红', value: '#d92c2c', token: 'red' },
9
- { name: '橙', value: '#e07b1a', token: 'orange' },
10
- { name: '黄褐', value: '#9a7b12', token: 'yellow' },
11
- { name: '绿', value: '#2f9e44', token: 'green' },
12
- { name: '蓝', value: '#1971c2', token: 'blue' },
13
- { name: '紫', value: '#7048e8', token: 'purple' },
14
- ];
15
- export const HIGHLIGHT_COLORS = [
16
- { name: '无', value: null, token: null },
17
- { name: '浅灰', value: '#f1f3f5', token: 'gray-soft' },
18
- { name: '浅红', value: '#ffe3e3', token: 'red' },
19
- { name: '浅橙', value: '#fff0db', token: 'orange' },
20
- { name: '浅黄', value: '#fff3bf', token: 'yellow' },
21
- { name: '浅绿', value: '#d3f9d8', token: 'green' },
22
- { name: '浅蓝', value: '#d0ebff', token: 'blue' },
23
- { name: '浅紫', value: '#e5dbff', token: 'purple' },
24
- { name: '灰', value: '#dee2e6', token: 'gray' },
25
- { name: '深灰', value: '#ced4da', token: 'gray-strong' },
26
- { name: '红', value: '#ffc9c9', token: 'red-strong' },
27
- { name: '橙', value: '#ffd8a8', token: 'orange-strong' },
28
- { name: '黄', value: '#ffec99', token: 'yellow-strong' },
29
- { name: '绿', value: '#b2f2bb', token: 'green-strong' },
30
- { name: '蓝', value: '#a5d8ff', token: 'blue-strong' },
31
- { name: '紫', value: '#d0bfff', token: 'purple-strong' },
32
- ];
33
- export function paletteColor(value, palette) {
34
- const hex = value.trim().toLowerCase();
35
- const rgb = /^rgb\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)$/.exec(hex);
36
- if (rgb && rgb.slice(1).some(channel => Number(channel) > 255))
37
- return null;
38
- const normalized = rgb
39
- ? `#${rgb.slice(1).map(channel => Number(channel).toString(16).padStart(2, '0')).join('')}`
40
- : hex;
41
- return palette.some(color => color.value === normalized) ? normalized : null;
42
- }
43
- export function documentColorAttributes(kind, value) {
44
- if (typeof value !== 'string')
45
- return {};
46
- const palette = kind === 'text' ? TEXT_COLORS : HIGHLIGHT_COLORS;
47
- const normalized = paletteColor(value, palette);
48
- const color = palette.find(item => item.value === normalized);
49
- if (!color?.value)
50
- return {};
51
- const property = kind === 'text' ? 'color' : 'background-color';
52
- return {
53
- 'data-color': color.value,
54
- style: `${property}: var(--doc-${kind}-${color.token}, ${color.value})`,
55
- };
56
- }
1
+ export const TEXT_COLORS=[{name:"默认",value:null,token:null},{name:"灰",value:"#868e96",token:"gray"},{name:"红",value:"#d92c2c",token:"red"},{name:"橙",value:"#e07b1a",token:"orange"},{name:"黄褐",value:"#9a7b12",token:"yellow"},{name:"绿",value:"#2f9e44",token:"green"},{name:"蓝",value:"#1971c2",token:"blue"},{name:"紫",value:"#7048e8",token:"purple"}];export const HIGHLIGHT_COLORS=[{name:"无",value:null,token:null},{name:"浅灰",value:"#f1f3f5",token:"gray-soft"},{name:"浅红",value:"#ffe3e3",token:"red"},{name:"浅橙",value:"#fff0db",token:"orange"},{name:"浅黄",value:"#fff3bf",token:"yellow"},{name:"浅绿",value:"#d3f9d8",token:"green"},{name:"浅蓝",value:"#d0ebff",token:"blue"},{name:"浅紫",value:"#e5dbff",token:"purple"},{name:"灰",value:"#dee2e6",token:"gray"},{name:"深灰",value:"#ced4da",token:"gray-strong"},{name:"红",value:"#ffc9c9",token:"red-strong"},{name:"橙",value:"#ffd8a8",token:"orange-strong"},{name:"黄",value:"#ffec99",token:"yellow-strong"},{name:"绿",value:"#b2f2bb",token:"green-strong"},{name:"蓝",value:"#a5d8ff",token:"blue-strong"},{name:"紫",value:"#d0bfff",token:"purple-strong"}];export function paletteColor(e,n){const a=e.trim().toLowerCase(),o=/^rgb\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)$/.exec(a);if(o&&o.slice(1).some(e=>Number(e)>255))return null;const t=o?`#${o.slice(1).map(e=>Number(e).toString(16).padStart(2,"0")).join("")}`:a;return n.some(e=>e.value===t)?t:null}export function documentColorAttributes(e,n){if("string"!=typeof n)return{};const a="text"===e?TEXT_COLORS:HIGHLIGHT_COLORS,o=paletteColor(n,a),t=a.find(e=>e.value===o);if(!t?.value)return{};const l="text"===e?"color":"background-color";return{"data-color":t.value,style:`${l}: var(--doc-${e}-${t.token}, ${t.value})`}}
@@ -0,0 +1 @@
1
+ const t=new Set([1,2,3,4,5,6]);export function extractHeadings(e){const n=[];if(!e||!Array.isArray(e.content))return n;const walk=e=>{if("codeBlock"!==e.type){if("heading"===e.type){const o=Number(e.attrs?.level??1),r=plainTextOf(e).trim();return void(r&&n.push({level:t.has(o)?o:1,text:r,order:n.length}))}for(const t of e.content??[])walk(t)}};for(const t of e.content)walk(t);return n}function plainTextOf(t){if("string"==typeof t.text&&t.text)return t.text;let e="";for(const n of t.content??[])e+=plainTextOf(n);return e}
@@ -1,173 +1 @@
1
- import { flattenDocText, resolveAnchor } from './anchor.js';
2
- import { plainTextOf } from './doc-text.js';
3
- export function buildTextRuns(doc) {
4
- const runs = [];
5
- let offset = 0;
6
- /** 上一次是否为"块级片段结尾",用于决定是否插入分隔符 */
7
- const blockTexts = [];
8
- const collect = (node, out) => {
9
- if (typeof node.text === 'string') {
10
- out.push(node);
11
- return;
12
- }
13
- for (const c of node.content ?? [])
14
- collect(c, out);
15
- };
16
- // 与 anchor 的 flattenDocText 一致:以「顶层块」为单位,块间插入分隔符,
17
- // 防止块尾与块首被误连成一个匹配。
18
- for (const top of doc?.content ?? []) {
19
- const textNodes = [];
20
- collect(top, textNodes);
21
- const blockText = textNodes.map((n) => n.text ?? '').join('');
22
- if (blockText.length === 0)
23
- continue;
24
- blockTexts.push(blockText);
25
- const base = blockTexts.length > 1 ? offset + 1 : offset; // +1 为块间分隔符
26
- let inner = 0;
27
- for (const n of textNodes) {
28
- const t = n.text ?? '';
29
- if (t.length === 0)
30
- continue;
31
- runs.push({
32
- index: runs.length,
33
- text: t,
34
- flatStart: base + inner,
35
- flatEnd: base + inner + t.length,
36
- });
37
- inner += t.length;
38
- }
39
- offset = base + inner;
40
- }
41
- return runs;
42
- }
43
- /** 文档的扁平文本(与 buildTextRuns 的偏移体系一致)。 */
44
- export function buildFlatText(doc) {
45
- const blocks = (doc?.content ?? [])
46
- .map((top) => plainTextOf(top))
47
- .filter((t) => t.length > 0);
48
- return flattenDocText(blocks).text;
49
- }
50
- /** 仅有锚点的评论才参与定位(页级评论不属于行内)。 */
51
- export function resolveInlineComments(doc, comments) {
52
- const runs = buildTextRuns(doc);
53
- const flatText = buildFlatText(doc);
54
- const marks = [];
55
- const orphanCommentIds = [];
56
- // 只处理顶层评论:回复共用顶层锚点,不单独定位
57
- for (const c of comments) {
58
- if (c.deleted || !c.anchor)
59
- continue;
60
- if (c.anchor.prefix?.startsWith('mind:')) {
61
- let exists = false;
62
- const walk = (node) => {
63
- if (node.type === 'mindMap' && node.attrs?.id === c.anchor.prefix.slice(5)) {
64
- exists = !!node.content?.some(t => t.type === 'mindMapTopic' && t.attrs?.id === c.anchor.suffix);
65
- return;
66
- }
67
- for (const child of node.content ?? [])
68
- walk(child);
69
- };
70
- if (doc)
71
- walk(doc);
72
- if (!exists)
73
- orphanCommentIds.push(c.id);
74
- continue;
75
- }
76
- if (c.parentId !== null)
77
- continue;
78
- const resolved = resolveAnchor(flatText, {
79
- quote: c.anchor.quote,
80
- prefix: c.anchor.prefix,
81
- suffix: c.anchor.suffix,
82
- start: c.anchor.start,
83
- });
84
- if (!resolved) {
85
- orphanCommentIds.push(c.id);
86
- continue;
87
- }
88
- /**
89
- * 把扁平区间映射到「文本节点 + 节点内偏移」。
90
- *
91
- * 锚点可能跨越多个文本节点(例如引用跨越两个段落、或跨过一个行内原子
92
- * 节点如 `[[页面引用]]`)。**每个被覆盖的文本节点都要产出一条 mark**,
93
- * 否则高亮会在第一个节点末尾断掉——表现为「引用了两段,只有第一段有
94
- * 底色」,用户会以为评论只针对半句话。
95
- *
96
- * 这里不再"跨节点时只取首个",而是逐节点裁剪区间:
97
- * 节点内起点 = max(0, 锚点起点 - 该节点扁平起点)
98
- * 节点内终点 = min(节点长度, 锚点终点 - 该节点扁平起点)
99
- */
100
- const targets = runs.filter((r) => r.flatEnd > resolved.from && r.flatStart <= resolved.to);
101
- if (targets.length === 0) {
102
- orphanCommentIds.push(c.id);
103
- continue;
104
- }
105
- /**
106
- * 零宽落点(shifted):原文已被改动、仅靠上下文降级定位。
107
- * `resolveAnchor` 用 from === to 表示这种状态,其语义是「在这一点的位置
108
- * 插一个标记」而非覆盖一段文字,因此只在首个节点上产出一条零宽 mark。
109
- * (不能用下面的逐节点裁剪——零宽区间会被 `to <= from` 全部过滤掉。)
110
- */
111
- if (resolved.to <= resolved.from) {
112
- const first = targets[0];
113
- marks.push({
114
- commentId: c.id,
115
- textIndex: first.index,
116
- from: Math.max(0, resolved.from - first.flatStart),
117
- to: Math.max(0, resolved.from - first.flatStart),
118
- state: 'shifted',
119
- hasReplies: c.replies.length > 0,
120
- });
121
- continue;
122
- }
123
- const state = 'exact';
124
- let produced = 0;
125
- for (const run of targets) {
126
- const from = Math.max(0, resolved.from - run.flatStart);
127
- const to = Math.min(run.text.length, resolved.to - run.flatStart);
128
- if (to <= from)
129
- continue;
130
- marks.push({
131
- commentId: c.id,
132
- textIndex: run.index,
133
- from,
134
- to,
135
- state,
136
- hasReplies: c.replies.length > 0,
137
- });
138
- produced += 1;
139
- }
140
- // 区间落在节点边界上、没有任何实际覆盖 → 视为失联(宁可提示,不误标)
141
- if (produced === 0)
142
- orphanCommentIds.push(c.id);
143
- }
144
- const byTextIndex = new Map();
145
- for (const m of marks) {
146
- const arr = byTextIndex.get(m.textIndex) ?? [];
147
- arr.push(m);
148
- byTextIndex.set(m.textIndex, arr);
149
- }
150
- return { marks, orphanCommentIds, byTextIndex };
151
- }
152
- export function splitTextByMarks(text, marks) {
153
- if (marks.length === 0 || text.length === 0) {
154
- return [{ text, marks: [] }];
155
- }
156
- // 收集切分点
157
- const points = new Set([0, text.length]);
158
- for (const m of marks) {
159
- points.add(Math.max(0, Math.min(m.from, text.length)));
160
- points.add(Math.max(0, Math.min(m.to, text.length)));
161
- }
162
- const sorted = [...points].sort((a, b) => a - b);
163
- const pieces = [];
164
- for (let i = 0; i < sorted.length - 1; i += 1) {
165
- const start = sorted[i];
166
- const end = sorted[i + 1];
167
- if (end <= start)
168
- continue;
169
- const covering = marks.filter((m) => m.from <= start && m.to >= end);
170
- pieces.push({ text: text.slice(start, end), marks: covering });
171
- }
172
- return pieces.length > 0 ? pieces : [{ text, marks: [] }];
173
- }
1
+ import{flattenDocText as t,resolveAnchor as n}from"./anchor.js";import{plainTextOf as e}from"./doc-text.js";export function buildTextRuns(t){const n=[];let e=0;const o=[],collect=(t,n)=>{if("string"!=typeof t.text)for(const e of t.content??[])collect(e,n);else n.push(t)};for(const s of t?.content??[]){const t=[];collect(s,t);const r=t.map(t=>t.text??"").join("");if(0===r.length)continue;o.push(r);const i=o.length>1?e+1:e;let f=0;for(const e of t){const t=e.text??"";0!==t.length&&(n.push({index:n.length,text:t,flatStart:i+f,flatEnd:i+f+t.length}),f+=t.length)}e=i+f}return n}export function buildFlatText(n){const o=(n?.content??[]).map(t=>e(t)).filter(t=>t.length>0);return t(o).text}export function resolveInlineComments(t,e){const o=buildTextRuns(t),s=buildFlatText(t),r=[],i=[];for(const f of e){if(f.deleted||!f.anchor)continue;if(f.anchor.prefix?.startsWith("mind:")){let n=!1;const walk=t=>{if("mindMap"!==t.type||t.attrs?.id!==f.anchor.prefix.slice(5))for(const n of t.content??[])walk(n);else n=!!t.content?.some(t=>"mindMapTopic"===t.type&&t.attrs?.id===f.anchor.suffix)};t&&walk(t),n||i.push(f.id);continue}if(null!==f.parentId)continue;const e=n(s,{quote:f.anchor.quote,prefix:f.anchor.prefix,suffix:f.anchor.suffix,start:f.anchor.start});if(!e){i.push(f.id);continue}const a=o.filter(t=>t.flatEnd>e.from&&t.flatStart<=e.to);if(0===a.length){i.push(f.id);continue}if(e.to<=e.from){const t=a[0];r.push({commentId:f.id,textIndex:t.index,from:Math.max(0,e.from-t.flatStart),to:Math.max(0,e.from-t.flatStart),state:"shifted",hasReplies:f.replies.length>0});continue}const c="exact";let l=0;for(const t of a){const n=Math.max(0,e.from-t.flatStart),o=Math.min(t.text.length,e.to-t.flatStart);o<=n||(r.push({commentId:f.id,textIndex:t.index,from:n,to:o,state:c,hasReplies:f.replies.length>0}),l+=1)}0===l&&i.push(f.id)}const f=new Map;for(const t of r){const n=f.get(t.textIndex)??[];n.push(t),f.set(t.textIndex,n)}return{marks:r,orphanCommentIds:i,byTextIndex:f}}export function splitTextByMarks(t,n){if(0===n.length||0===t.length)return[{text:t,marks:[]}];const e=new Set([0,t.length]);for(const o of n)e.add(Math.max(0,Math.min(o.from,t.length))),e.add(Math.max(0,Math.min(o.to,t.length)));const o=[...e].sort((t,n)=>t-n),s=[];for(let e=0;e<o.length-1;e+=1){const r=o[e],i=o[e+1];if(i<=r)continue;const f=n.filter(t=>t.from<=r&&t.to>=i);s.push({text:t.slice(r,i),marks:f})}return s.length>0?s:[{text:t,marks:[]}]}
@@ -1,10 +1 @@
1
- import { mindMapTree } from './mind-map.js';
2
- /** Root/leaf/stale entries have no local fold affordance and must not hide a whole map. */
3
- export function normalizeMindFolds(model, ids) {
4
- const tree = mindMapTree(model), parents = new Set(tree.map(t => t.parentId)), known = new Set(tree.map(t => t.id));
5
- return new Set([...ids].filter(id => id !== model.rootId && known.has(id) && parents.has(id)));
6
- }
7
- /** Keep every first-level branch visible, collapsing only those with descendants. */
8
- export function firstLevelMindFolds(model) {
9
- return normalizeMindFolds(model, mindMapTree(model).filter(t => t.parentId === model.rootId).map(t => t.id));
10
- }
1
+ import{mindMapTree as e}from"./mind-map.js";export function normalizeMindFolds(n,t){const r=e(n),o=new Set(r.map(e=>e.parentId)),i=new Set(r.map(e=>e.id));return new Set([...t].filter(e=>e!==n.rootId&&i.has(e)&&o.has(e)))}export function firstLevelMindFolds(n){return normalizeMindFolds(n,e(n).filter(e=>e.parentId===n.rootId).map(e=>e.id))}
@@ -1,121 +1 @@
1
- import { createMindMap, mindMapDescendants, mindMapId, mindMapOutline, mindMapToJson, mindMapTree, readMindMap, MIND_MAP_VERSION, } from './mind-map.js';
2
- export const MIND_ICONS = ['', '💡', '🎯', '📌', '🚩', '⭐', '✅', '⚠️', '💬', '📅', '🔎'];
3
- export const MIND_COLORS = ['', '#18243a', '#ffffff', '#3864ef', '#087f8c', '#7950c7', '#a66b08', '#d94848'];
4
- export const MIND_BACKGROUNDS = ['', '#f3f5fb', '#dce7ff', '#d3f4e9', '#ece0ff', '#fff0c2', '#ffe1e1', '#233044'];
5
- export function topicTitle(node) {
6
- return (node.content ?? []).filter(c => c.type === 'text').map(c => c.text ?? '').join('');
7
- }
8
- export function topicField(node, field) {
9
- return (node.content?.find(c => c.type === (field === 'description' ? 'mindMapDescription' : 'mindMapNote'))?.content ?? [])
10
- .map(c => c.text ?? '').join('');
11
- }
12
- export function branchModel(model, id) {
13
- const ids = mindMapDescendants(model, id);
14
- return { ...model, rootId: id, topics: mindMapTree(model).filter(t => ids.has(t.id))
15
- .map(t => t.id === id ? { ...t, parentId: '' } : t),
16
- relations: model.relations?.filter(r => ids.has(r.source) && ids.has(r.target)) };
17
- }
18
- export function ancestorIds(model, id) {
19
- const byId = new Map(mindMapTree(model).map(t => [t.id, t])), result = [];
20
- let current = byId.get(id);
21
- while (current?.parentId && result.length < 500) {
22
- result.push(current.parentId);
23
- current = byId.get(current.parentId);
24
- }
25
- return result;
26
- }
27
- export function searchMindMap(model, query, details = true) {
28
- if (!query)
29
- return [];
30
- const needle = query.toLocaleLowerCase(), hits = [];
31
- for (const topic of mindMapOutline(model)) {
32
- for (const field of (details ? ['title', 'description', 'note'] : ['title'])) {
33
- const value = (field === 'title' ? topic.text : topic[field] ?? '').toLocaleLowerCase();
34
- for (let at = 0; (at = value.indexOf(needle, at)) >= 0; at += needle.length) {
35
- hits.push({ id: topic.id, field, from: at, to: at + needle.length });
36
- if (hits.length > 5000)
37
- return hits;
38
- }
39
- }
40
- }
41
- return hits;
42
- }
43
- export function parseMindMapImport(raw) {
44
- if (new TextEncoder().encode(raw).length > 2_000_000)
45
- throw new Error('导入内容过大(最多 2 MB)');
46
- if (raw.trimStart().startsWith('{')) {
47
- const data = JSON.parse(raw);
48
- const model = data.type === 'mindMap' ? readMindMap(data) :
49
- data.format === 'ywiki-mind-map' && [1, 2, MIND_MAP_VERSION].includes(data.version) && Array.isArray(data.topics)
50
- ? readMindMap(mindMapToJson(data)) : null;
51
- if (!model)
52
- throw new Error('导图结构或版本无效');
53
- if (model.topics.some(t => t.id !== model.rootId && !model.topics.some(p => p.id === t.parentId)))
54
- throw new Error('导入结构包含不存在的父主题');
55
- if (model.relations?.some(r => !model.topics.some(t => t.id === r.source) || !model.topics.some(t => t.id === r.target)))
56
- throw new Error('导入关系的端点不存在');
57
- for (const topic of model.topics) {
58
- const seen = new Set();
59
- let current = topic;
60
- while (current?.parentId) {
61
- if (seen.has(current.id))
62
- throw new Error('导入结构存在循环关系');
63
- seen.add(current.id);
64
- current = model.topics.find(t => t.id === current.parentId);
65
- }
66
- }
67
- return readMindMap(mindMapToJson(model, true));
68
- }
69
- const lines = raw.replace(/\r/g, '').split('\n').filter(l => l.trim());
70
- if (!lines.length)
71
- throw new Error('请输入缩进列表或导图 JSON');
72
- if (lines.length > 499)
73
- throw new Error('导入列表最多 499 行,加中心主题后最多 500 个主题');
74
- const model = readMindMap(createMindMap());
75
- model.topics = [model.topics[0]];
76
- model.topics[0].text = '导入的主题';
77
- const stack = [{ indent: -1, id: model.rootId }];
78
- for (let i = 0; i < lines.length; i++) {
79
- const line = lines[i], white = line.match(/^\s*/)?.[0] ?? '', indent = white.replace(/\t/g, ' ').length;
80
- const text = line.trim().replace(/^(?:[-*+]\s+|\d+[.)]\s+)/, '');
81
- if (text.length > 2000)
82
- throw new Error(`第 ${i + 1} 行超过 2000 字符`);
83
- while (stack.length > 1 && stack.at(-1).indent >= indent)
84
- stack.pop();
85
- if (stack.length > 50)
86
- throw new Error(`第 ${i + 1} 行嵌套超过 50 层`);
87
- const id = mindMapId();
88
- model.topics.push({ id, parentId: stack.at(-1).id, order: i, text });
89
- stack.push({ indent, id });
90
- }
91
- return model;
92
- }
93
- export function mindMapTemplate(kind) {
94
- const data = {
95
- project: ['项目拆解', '目标与范围', '需求梳理', '研发实现', '测试验收', '上线与复盘'],
96
- meeting: ['会议讨论', '议题', '背景信息', '讨论结论', '待办事项', '负责人'],
97
- knowledge: ['知识目录', '基础概念', '核心原理', '实践案例', '常见问题', '参考资料'],
98
- }[kind];
99
- const m = readMindMap(createMindMap('both'));
100
- m.topics = data.map((text, i) => ({ id: i ? mindMapId() : m.rootId, parentId: i ? m.rootId : '', order: i, text }));
101
- return m;
102
- }
103
- export function importBranch(model) {
104
- return mindMapToJson(model, true).content ?? [];
105
- }
106
- export function parseMindBranch(raw) {
107
- if (!raw.startsWith('YWIKI_MINDMAP_BRANCH_V1\n') || raw.length > 2_000_000)
108
- throw new Error('不是有效的导图分支');
109
- const content = JSON.parse(raw.slice(raw.indexOf('\n') + 1));
110
- if (!Array.isArray(content) || content.length > 1500)
111
- throw new Error('分支内容过大');
112
- const topics = content.filter(t => t?.type === 'mindMapTopic');
113
- if (!topics.length || !topics[0].attrs)
114
- throw new Error('分支缺少主题');
115
- const normalized = content.map(t => t === topics[0] ? { ...t, attrs: { ...t.attrs, parentId: '' } } : t);
116
- const model = readMindMap({ type: 'mindMap', attrs: { id: 'clipboard', rootId: topics[0].attrs.id,
117
- formatVersion: MIND_MAP_VERSION, layout: 'right', theme: 'blue', height: 380 }, content: normalized });
118
- if (!model)
119
- throw new Error('分支结构不合法');
120
- return importBranch(parseMindMapImport(JSON.stringify(mindMapToJson(model))));
121
- }
1
+ import{createMindMap as t,mindMapDescendants as r,mindMapId as e,mindMapOutline as o,mindMapToJson as n,mindMapTree as i,readMindMap as p,MIND_MAP_VERSION as s}from"./mind-map.js";export const MIND_ICONS=["","💡","🎯","📌","🚩","⭐","✅","⚠️","💬","📅","🔎"];export const MIND_COLORS=["","#18243a","#ffffff","#3864ef","#087f8c","#7950c7","#a66b08","#d94848"];export const MIND_BACKGROUNDS=["","#f3f5fb","#dce7ff","#d3f4e9","#ece0ff","#fff0c2","#ffe1e1","#233044"];export function topicTitle(t){return(t.content??[]).filter(t=>"text"===t.type).map(t=>t.text??"").join("")}export function topicField(t,r){return(t.content?.find(t=>t.type===("description"===r?"mindMapDescription":"mindMapNote"))?.content??[]).map(t=>t.text??"").join("")}export function branchModel(t,e){const o=r(t,e);return{...t,rootId:e,topics:i(t).filter(t=>o.has(t.id)).map(t=>t.id===e?{...t,parentId:""}:t),relations:t.relations?.filter(t=>o.has(t.source)&&o.has(t.target))}}export function ancestorIds(t,r){const e=new Map(i(t).map(t=>[t.id,t])),o=[];let n=e.get(r);for(;n?.parentId&&o.length<500;)o.push(n.parentId),n=e.get(n.parentId);return o}export function searchMindMap(t,r,e=!0){if(!r)return[];const n=r.toLocaleLowerCase(),i=[];for(const r of o(t))for(const t of e?["title","description","note"]:["title"]){const e=("title"===t?r.text:r[t]??"").toLocaleLowerCase();for(let o=0;(o=e.indexOf(n,o))>=0;o+=n.length)if(i.push({id:r.id,field:t,from:o,to:o+n.length}),i.length>5e3)return i}return i}export function parseMindMapImport(r){if((new TextEncoder).encode(r).length>2e6)throw new Error("导入内容过大(最多 2 MB)");if(r.trimStart().startsWith("{")){const t=JSON.parse(r),e="mindMap"===t.type?p(t):"ywiki-mind-map"===t.format&&[1,2,s].includes(t.version)&&Array.isArray(t.topics)?p(n(t)):null;if(!e)throw new Error("导图结构或版本无效");if(e.topics.some(t=>t.id!==e.rootId&&!e.topics.some(r=>r.id===t.parentId)))throw new Error("导入结构包含不存在的父主题");if(e.relations?.some(t=>!e.topics.some(r=>r.id===t.source)||!e.topics.some(r=>r.id===t.target)))throw new Error("导入关系的端点不存在");for(const t of e.topics){const r=new Set;let o=t;for(;o?.parentId;){if(r.has(o.id))throw new Error("导入结构存在循环关系");r.add(o.id),o=e.topics.find(t=>t.id===o.parentId)}}return p(n(e,!0))}const o=r.replace(/\r/g,"").split("\n").filter(t=>t.trim());if(!o.length)throw new Error("请输入缩进列表或导图 JSON");if(o.length>499)throw new Error("导入列表最多 499 行,加中心主题后最多 500 个主题");const i=p(t());i.topics=[i.topics[0]],i.topics[0].text="导入的主题";const c=[{indent:-1,id:i.rootId}];for(let t=0;t<o.length;t++){const r=o[t],n=(r.match(/^\s*/)?.[0]??"").replace(/\t/g," ").length,p=r.trim().replace(/^(?:[-*+]\s+|\d+[.)]\s+)/,"");if(p.length>2e3)throw new Error(`第 ${t+1} 行超过 2000 字符`);for(;c.length>1&&c.at(-1).indent>=n;)c.pop();if(c.length>50)throw new Error(`第 ${t+1} 行嵌套超过 50 层`);const s=e();i.topics.push({id:s,parentId:c.at(-1).id,order:t,text:p}),c.push({indent:n,id:s})}return i}export function mindMapTemplate(r){const o={project:["项目拆解","目标与范围","需求梳理","研发实现","测试验收","上线与复盘"],meeting:["会议讨论","议题","背景信息","讨论结论","待办事项","负责人"],knowledge:["知识目录","基础概念","核心原理","实践案例","常见问题","参考资料"]}[r],n=p(t("both"));return n.topics=o.map((t,r)=>({id:r?e():n.rootId,parentId:r?n.rootId:"",order:r,text:t})),n}export function importBranch(t){return n(t,!0).content??[]}export function parseMindBranch(t){if(!t.startsWith("YWIKI_MINDMAP_BRANCH_V1\n")||t.length>2e6)throw new Error("不是有效的导图分支");const r=JSON.parse(t.slice(t.indexOf("\n")+1));if(!Array.isArray(r)||r.length>1500)throw new Error("分支内容过大");const e=r.filter(t=>"mindMapTopic"===t?.type);if(!e.length||!e[0].attrs)throw new Error("分支缺少主题");const o=r.map(t=>t===e[0]?{...t,attrs:{...t.attrs,parentId:""}}:t),i=p({type:"mindMap",attrs:{id:"clipboard",rootId:e[0].attrs.id,formatVersion:s,layout:"right",theme:"blue",height:380},content:o});if(!i)throw new Error("分支结构不合法");return importBranch(parseMindMapImport(JSON.stringify(n(i))))}