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.
Files changed (107) hide show
  1. package/LICENSE +3 -0
  2. package/NOTICE.md +7 -0
  3. package/README.md +101 -2
  4. package/dist/cjs/document.js +19 -0
  5. package/dist/cjs/index.js +25 -0
  6. package/dist/cjs/mindmap.js +22 -0
  7. package/dist/cjs/package.json +1 -0
  8. package/dist/cjs/schema.js +11 -0
  9. package/dist/cjs/shared/anchor.js +201 -0
  10. package/dist/cjs/shared/callout.js +64 -0
  11. package/dist/cjs/shared/code-block.js +33 -0
  12. package/dist/cjs/shared/code-languages.js +51 -0
  13. package/dist/cjs/shared/doc-compatibility.js +23 -0
  14. package/dist/cjs/shared/doc-text.js +52 -0
  15. package/dist/cjs/shared/document-colors.js +61 -0
  16. package/dist/cjs/shared/inline-comments.js +179 -0
  17. package/dist/cjs/shared/mind-map-folding.js +14 -0
  18. package/dist/cjs/shared/mind-map-phase2.js +133 -0
  19. package/dist/cjs/shared/mind-map.js +445 -0
  20. package/dist/cjs/shared/mind-topic-text.js +57 -0
  21. package/dist/cjs/shared/node-catalog.js +62 -0
  22. package/dist/cjs/shared/presence.js +141 -0
  23. package/dist/cjs/shared/safe-href.js +36 -0
  24. package/dist/cjs/shared/table-cell-format.js +18 -0
  25. package/dist/cjs/shared/table-layout.js +28 -0
  26. package/dist/cjs/shared/types.js +2 -0
  27. package/dist/cjs/shared/video-embed.js +102 -0
  28. package/dist/cjs/shared/workbook.js +152 -0
  29. package/dist/cjs/spreadsheet.js +17 -0
  30. package/dist/cjs/types.js +17 -0
  31. package/dist/cjs/validation/callout.js +17 -0
  32. package/dist/cjs/validation/doc-node-whitelist.js +71 -0
  33. package/dist/cjs/validation/error.js +17 -0
  34. package/dist/cjs/validation/mind-map.js +129 -0
  35. package/dist/cjs/validation/schema-version.js +5 -0
  36. package/dist/cjs/validation/schema.js +376 -0
  37. package/dist/cjs/validation/video-embed.js +17 -0
  38. package/dist/cjs/validation/workbook.js +17 -0
  39. package/dist/esm/document.js +3 -0
  40. package/dist/esm/index.js +3 -0
  41. package/dist/esm/mindmap.js +4 -0
  42. package/dist/esm/schema.js +3 -0
  43. package/dist/esm/shared/anchor.js +192 -0
  44. package/dist/esm/shared/callout.js +56 -0
  45. package/dist/esm/shared/code-block.js +25 -0
  46. package/dist/esm/shared/code-languages.js +46 -0
  47. package/dist/esm/shared/doc-compatibility.js +20 -0
  48. package/dist/esm/shared/doc-text.js +48 -0
  49. package/dist/esm/shared/document-colors.js +56 -0
  50. package/dist/esm/shared/inline-comments.js +173 -0
  51. package/dist/esm/shared/mind-map-folding.js +10 -0
  52. package/dist/esm/shared/mind-map-phase2.js +121 -0
  53. package/dist/esm/shared/mind-map.js +427 -0
  54. package/dist/esm/shared/mind-topic-text.js +49 -0
  55. package/dist/esm/shared/node-catalog.js +57 -0
  56. package/dist/esm/shared/presence.js +130 -0
  57. package/dist/esm/shared/safe-href.js +32 -0
  58. package/dist/esm/shared/table-cell-format.js +14 -0
  59. package/dist/esm/shared/table-layout.js +23 -0
  60. package/dist/esm/shared/types.js +1 -0
  61. package/dist/esm/shared/video-embed.js +96 -0
  62. package/dist/esm/shared/workbook.js +144 -0
  63. package/dist/esm/spreadsheet.js +1 -0
  64. package/dist/esm/types.js +1 -0
  65. package/dist/esm/validation/callout.js +1 -0
  66. package/dist/esm/validation/doc-node-whitelist.js +68 -0
  67. package/dist/esm/validation/error.js +13 -0
  68. package/dist/esm/validation/mind-map.js +125 -0
  69. package/dist/esm/validation/schema-version.js +2 -0
  70. package/dist/esm/validation/schema.js +372 -0
  71. package/dist/esm/validation/video-embed.js +1 -0
  72. package/dist/esm/validation/workbook.js +1 -0
  73. package/dist/types/document.d.ts +4 -0
  74. package/dist/types/index.d.ts +4 -0
  75. package/dist/types/mindmap.d.ts +4 -0
  76. package/dist/types/schema.d.ts +4 -0
  77. package/dist/types/shared/anchor.d.ts +87 -0
  78. package/dist/types/shared/callout.d.ts +27 -0
  79. package/dist/types/shared/code-block.d.ts +26 -0
  80. package/dist/types/shared/code-languages.d.ts +26 -0
  81. package/dist/types/shared/doc-compatibility.d.ts +1 -0
  82. package/dist/types/shared/doc-text.d.ts +21 -0
  83. package/dist/types/shared/document-colors.d.ts +106 -0
  84. package/dist/types/shared/inline-comments.d.ts +71 -0
  85. package/dist/types/shared/mind-map-folding.d.ts +5 -0
  86. package/dist/types/shared/mind-map-phase2.d.ts +19 -0
  87. package/dist/types/shared/mind-map.d.ts +134 -0
  88. package/dist/types/shared/mind-topic-text.d.ts +18 -0
  89. package/dist/types/shared/node-catalog.d.ts +30 -0
  90. package/dist/types/shared/presence.d.ts +69 -0
  91. package/dist/types/shared/safe-href.d.ts +3 -0
  92. package/dist/types/shared/table-cell-format.d.ts +24 -0
  93. package/dist/types/shared/table-layout.d.ts +7 -0
  94. package/dist/types/shared/types.d.ts +44 -0
  95. package/dist/types/shared/video-embed.d.ts +11 -0
  96. package/dist/types/shared/workbook.d.ts +56 -0
  97. package/dist/types/spreadsheet.d.ts +1 -0
  98. package/dist/types/types.d.ts +3 -0
  99. package/dist/types/validation/callout.d.ts +1 -0
  100. package/dist/types/validation/doc-node-whitelist.d.ts +21 -0
  101. package/dist/types/validation/error.d.ts +7 -0
  102. package/dist/types/validation/mind-map.d.ts +22 -0
  103. package/dist/types/validation/schema-version.d.ts +2 -0
  104. package/dist/types/validation/schema.d.ts +49 -0
  105. package/dist/types/validation/video-embed.d.ts +1 -0
  106. package/dist/types/validation/workbook.d.ts +1 -0
  107. package/package.json +87 -5
package/LICENSE ADDED
@@ -0,0 +1,3 @@
1
+ UNLICENSED
2
+
3
+ No license to use, copy, modify or distribute the Wendang source is granted by this file. Contact the maintainer for terms. Third-party components retain their own licenses; see NOTICE.md.
package/NOTICE.md ADDED
@@ -0,0 +1,7 @@
1
+ # Third-party notices
2
+
3
+ Generated from the installed runtime dependency graph; package licenses remain unchanged.
4
+ Wendang itself remains UNLICENSED. This inventory is not a grant of rights or a statement that optional Pro services are included.
5
+
6
+ | Package | Version | Declared license | Included upstream notice |
7
+ | --- | --- | --- | --- |
package/README.md CHANGED
@@ -1,5 +1,104 @@
1
1
  # wendang
2
2
 
3
- 项目筹备中,当前版本暂不提供可用功能。
3
+ 文档、电子表格、思维导图的内容协议与校验核心。适用于前端数据层和 Node 服务端,不包含可视编辑器。当前源码 0.1.0 为开发版,npm 0.0.1 仅为占位。
4
4
 
5
- This package is under development. No public API is available yet.
5
+ 源码位于 [ztes/wendang 的 packages/core](https://gitee.com/ztes/wendang/tree/main/packages/core)。本项目单仓双包维护,开发、问题和版本规范见 [仓库 README](https://gitee.com/ztes/wendang/blob/main/README.md)。
6
+
7
+ ## 安装与使用
8
+
9
+ 对应版本正式发布后:
10
+
11
+ ```sh
12
+ npm install wendang
13
+ ```
14
+
15
+ ```ts
16
+ import { ContentSchemaService, SCHEMA_VERSION } from 'wendang/schema'
17
+ import { emptyWorkbook, validateWorkbook } from 'wendang/spreadsheet'
18
+ import { createMindMap, readMindMap, mindMapToJson } from 'wendang/mindmap'
19
+ import type { ProseMirrorDoc, WorkbookDocument } from 'wendang/types'
20
+
21
+ const result = new ContentSchemaService().validate({
22
+ type: 'doc',
23
+ content: [{ type: 'paragraph', content: [{ type: 'text', text: '你好' }] }],
24
+ }, SCHEMA_VERSION)
25
+
26
+ const book = validateWorkbook(emptyWorkbook(crypto.randomUUID()))
27
+ const map = readMindMap(createMindMap('both'))
28
+ ```
29
+
30
+ 支持 Node >=20.19、ESM 和 CommonJS:
31
+
32
+ ```js
33
+ const { ContentSchemaService, SCHEMA_VERSION } = require('wendang/schema')
34
+ const result = new ContentSchemaService().validate(
35
+ { type: 'doc', content: [{ type: 'paragraph' }] },
36
+ SCHEMA_VERSION,
37
+ )
38
+ ```
39
+
40
+ 同步校验不发网络请求,不初始化框架、登录、数据库或 DOM。布局默认使用确定的保守度量,Vue 层按需注入浏览器字体度量;核心不执行 Excel 公式。
41
+
42
+ ## API 入口
43
+
44
+ | 入口 | 内容 |
45
+ | --- | --- |
46
+ | `wendang` | schema、版本、主要类型、创建工作簿 / 导图 |
47
+ | `wendang/schema` | `ContentSchemaService`、`ContentValidationError`、`SCHEMA_VERSION`、节点 / mark 白名单 |
48
+ | `wendang/document` | 文档类型、节点目录、文本工具和 schema |
49
+ | `wendang/spreadsheet` | 工作簿类型、`emptyWorkbook`、`validateWorkbook`、`workbookText` |
50
+ | `wendang/mindmap` | 创建 / 解析 / 序列化、布局、分支、搜索及校验 |
51
+ | `wendang/types` | 内容、工作簿、导图和讨论类型 |
52
+
53
+ `internal/*` / `validation/*` 仅为现有应用迁移兼容入口,三方新增代码优先使用上表入口,不依赖 dist 深层文件。
54
+
55
+ ## 文档校验与服务端规范
56
+
57
+ `new ContentSchemaService().validate(raw, schemaVersion)` 返回:
58
+
59
+ - `doc`、`schemaVersion`:按支持规则校验 / 处理后的内容;
60
+ - `attachmentIds`、`linkTargetIds`、`mentionUserIds`:供业务授权、引用、通知使用。
61
+
62
+ 失败抛 `ContentValidationError`,包含 `message`、`code`(`validation.failed`)、`status`(422)、可选 `details`;宿主自行转为框架异常。schemaVersion 必须为正整数。未知节点拒绝,普通脏属性按白名单处理,未知更高版本拒绝写入。
63
+
64
+ **校验不等于授权。** 后端仍需检查写入权限、附件归属、引用可见性和审批发布;前端检查不替代后端。当前普通文档 schemaVersion 为 **10**,不是 npm 包版本。
65
+
66
+ 当前协议的页面、用户、附件引用使用 UUID。宿主若使用数字或其他业务主键,应建立稳定映射;不要把存储 URL 或临时 token 当作 ID。
67
+
68
+ ## 工作簿规范
69
+
70
+ ```ts
71
+ const doc = emptyWorkbook(crypto.randomUUID())
72
+ doc.workbook.sheets.sheet1.cellData = {
73
+ 0: {
74
+ 0: { v: 12, t: 2 },
75
+ 1: { f: '=A1*2', v: 24, t: 2 },
76
+ },
77
+ }
78
+ const snapshot = validateWorkbook(doc)
79
+ ```
80
+
81
+ 根为 `{ type: 'workbook', formatVersion: 1, workbook: ... }`。`f` 为表达式,`v` 为数值或保存的公式缓存。最多 20 Sheet、每表 10000 行 / 200 列、实际单元格总数 50000、JSON 4MiB。验证拒绝危险属性、非法值、越界、重叠合并,并返回独立快照。
82
+
83
+ workbook 含 Univer 1.1.0 兼容数据与资源,是已版本化的引擎适配格式;不承诺任意引擎大版本无损兼容。XLSX 转换在 `wendang-vue/spreadsheet/xlsx`,本包不引入 ExcelJS。
84
+
85
+ ## 思维导图规范
86
+
87
+ `createMindMap(layout)` 创建持久化 `MindMapJson`,`readMindMap(json)` 返回 `MindMapModel | null`,`mindMapToJson(model)` 序列化。当前 formatVersion 为 **3**,支持读取已有受支持旧版。
88
+
89
+ 主题和关系线使用稳定 ID;主题包含 parentId、order、文字及可选富文本、描述、备注、附件与样式。单图最多 500 主题,标题 2000 字符。视口、选区、折叠属于视图状态。复制分支重建身份,不能直接沿用旧讨论锚点。
90
+
91
+ ## 升级与扩展
92
+
93
+ 1. SDK semver、文档 schemaVersion、工作簿 / 导图 formatVersion 分开管理。
94
+ 2. 关闭新增按钮不能剥离已有内容;不支持的更高版本提示升级或只读保留。
95
+ 3. 不把 HTML、截图或绘图坐标当作唯一源数据。
96
+ 4. 核心不自动迁移宿主数据库;迁移必须有明确操作、样本和回退,失败保留原文。
97
+ 5. 自定义节点必须同步编辑、阅读、校验和导出,不仅放宽前端 JSON。
98
+ 6. ESM / CommonJS 通过实际 tarball 消费验证,禁止靠源码路径掩盖缺失导出。
99
+
100
+ ## 开发与发布
101
+
102
+ 在仓库根运行 `pnpm build`、`pnpm test`、`pnpm typecheck`、`pnpm pack:all`、`pnpm check:package`。只发布 dist、文档及许可文件。完整本地 link 和发布流程见仓库 README。
103
+
104
+ 当前许可 `UNLICENSED`;维护者另行决定自身授权,不改变第三方依赖授权边界。
@@ -0,0 +1,19 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
+ };
16
+ Object.defineProperty(exports, "__esModule", { value: true });
17
+ __exportStar(require("./schema.js"), exports);
18
+ __exportStar(require("./shared/node-catalog.js"), exports);
19
+ __exportStar(require("./shared/doc-text.js"), exports);
@@ -0,0 +1,25 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
+ };
16
+ Object.defineProperty(exports, "__esModule", { value: true });
17
+ exports.mindMapToJson = exports.readMindMap = exports.createMindMap = exports.validateWorkbook = exports.emptyWorkbook = void 0;
18
+ __exportStar(require("./schema.js"), exports);
19
+ var spreadsheet_js_1 = require("./spreadsheet.js");
20
+ Object.defineProperty(exports, "emptyWorkbook", { enumerable: true, get: function () { return spreadsheet_js_1.emptyWorkbook; } });
21
+ Object.defineProperty(exports, "validateWorkbook", { enumerable: true, get: function () { return spreadsheet_js_1.validateWorkbook; } });
22
+ var mindmap_js_1 = require("./mindmap.js");
23
+ Object.defineProperty(exports, "createMindMap", { enumerable: true, get: function () { return mindmap_js_1.createMindMap; } });
24
+ Object.defineProperty(exports, "readMindMap", { enumerable: true, get: function () { return mindmap_js_1.readMindMap; } });
25
+ Object.defineProperty(exports, "mindMapToJson", { enumerable: true, get: function () { return mindmap_js_1.mindMapToJson; } });
@@ -0,0 +1,22 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
+ };
16
+ Object.defineProperty(exports, "__esModule", { value: true });
17
+ exports.validateMindMap = void 0;
18
+ __exportStar(require("./shared/mind-map.js"), exports);
19
+ __exportStar(require("./shared/mind-map-phase2.js"), exports);
20
+ __exportStar(require("./shared/mind-map-folding.js"), exports);
21
+ var mind_map_js_1 = require("./validation/mind-map.js");
22
+ Object.defineProperty(exports, "validateMindMap", { enumerable: true, get: function () { return mind_map_js_1.validateMindMap; } });
@@ -0,0 +1 @@
1
+ {"type":"commonjs"}
@@ -0,0 +1,11 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ALLOWED_MARKS = exports.ALLOWED_NODES = exports.ContentValidationError = exports.SCHEMA_VERSION = exports.ContentSchemaService = void 0;
4
+ var schema_js_1 = require("./validation/schema.js");
5
+ Object.defineProperty(exports, "ContentSchemaService", { enumerable: true, get: function () { return schema_js_1.ContentSchemaService; } });
6
+ Object.defineProperty(exports, "SCHEMA_VERSION", { enumerable: true, get: function () { return schema_js_1.SCHEMA_VERSION; } });
7
+ var error_js_1 = require("./validation/error.js");
8
+ Object.defineProperty(exports, "ContentValidationError", { enumerable: true, get: function () { return error_js_1.ContentValidationError; } });
9
+ var doc_node_whitelist_js_1 = require("./validation/doc-node-whitelist.js");
10
+ Object.defineProperty(exports, "ALLOWED_NODES", { enumerable: true, get: function () { return doc_node_whitelist_js_1.ALLOWED_NODES; } });
11
+ Object.defineProperty(exports, "ALLOWED_MARKS", { enumerable: true, get: function () { return doc_node_whitelist_js_1.ALLOWED_MARKS; } });
@@ -0,0 +1,201 @@
1
+ "use strict";
2
+ /**
3
+ * 行内评论的锚点定位(纯函数,可单测)。
4
+ *
5
+ * ## 为什么不能存位置
6
+ *
7
+ * 直觉做法是「记录选中文字的 start/end 偏移」。但文档会被编辑:
8
+ * 在选区前面插入一个字,所有后续位置全部偏移——锚点立刻指向错误文字,
9
+ * 而且**错得很安静**(评论跑到别的地方,没人会立刻发现)。
10
+ *
11
+ * ## 采用方案:文本引用(TextQuoteSelector 思路,W3C Web Annotation)
12
+ *
13
+ * 存三样东西:
14
+ * - `quote`:选中的原文
15
+ * - `prefix` / `suffix`:前后各一小段上下文(用于区分重复文本)
16
+ *
17
+ * 定位时在文档纯文本里搜索 quote;有多处命中时,用 prefix/suffix 的匹配长度打分,
18
+ * 取最匹配的一处。这样:
19
+ * - 在文档**别处**编辑(不碰该段文字)→ 锚点依然准确定位;
20
+ * - 该段文字本身被改动 → 匹配不到,此时用 prefix+suffix 的接缝尽力定位,
21
+ * 返回一个「降级」结果(exact=false),前端据此提示「原文已改动」;
22
+ * - 完全找不到(整段被删)→ 返回 null,前端把评论归入「已失联」,
23
+ * **而不是指向一处无关文字**。
24
+ *
25
+ * 宁可显示「已失联」,也不要指向错误位置——后者会让用户读到与上下文无关的评论,
26
+ * 比看得见的缺失更有害。
27
+ */
28
+ Object.defineProperty(exports, "__esModule", { value: true });
29
+ exports.ANCHOR_QUOTE_MAX = exports.ANCHOR_CONTEXT_LENGTH = void 0;
30
+ exports.flattenDocText = flattenDocText;
31
+ exports.locateBlock = locateBlock;
32
+ exports.resolveAnchor = resolveAnchor;
33
+ exports.commonSuffixLength = commonSuffixLength;
34
+ exports.commonPrefixLength = commonPrefixLength;
35
+ exports.clipContext = clipContext;
36
+ /** 上下文最大长度(与提交时截断长度一致)。 */
37
+ exports.ANCHOR_CONTEXT_LENGTH = 64;
38
+ exports.ANCHOR_QUOTE_MAX = 500;
39
+ /**
40
+ * 把若干块文本拼成扁平文本,记录片段边界。
41
+ * 块之间插入换行(可用块间匹配不应跨越块边界,换行同时避免把块首块尾误连成词)。
42
+ */
43
+ function flattenDocText(blocks) {
44
+ const segments = [];
45
+ let text = '';
46
+ blocks.forEach((blockText, blockIndex) => {
47
+ if (blockIndex > 0)
48
+ text += '\n';
49
+ segments.push({ start: text.length, blockIndex, length: blockText.length });
50
+ text += blockText;
51
+ });
52
+ return { text, segments };
53
+ }
54
+ /** 由扁平字符偏移定位到 (块索引, 块内偏移)。 */
55
+ function locateBlock(flat, charOffset) {
56
+ for (const seg of flat.segments) {
57
+ if (charOffset >= seg.start && charOffset <= seg.start + seg.length) {
58
+ return { blockIndex: seg.blockIndex, offsetInBlock: charOffset - seg.start };
59
+ }
60
+ }
61
+ // 落在块间换行或末尾时,归到最后一个片段
62
+ const last = flat.segments[flat.segments.length - 1];
63
+ if (!last)
64
+ return null;
65
+ if (charOffset > last.start + last.length) {
66
+ return { blockIndex: last.blockIndex, offsetInBlock: last.length };
67
+ }
68
+ return null;
69
+ }
70
+ /**
71
+ * 定位锚点。返回 null 表示无法可信定位(整段被删),调用方应标记为「已失联」。
72
+ */
73
+ function resolveAnchor(text, anchor) {
74
+ const quote = (anchor.quote ?? '').trim();
75
+ if (quote.length === 0)
76
+ return null;
77
+ const prefix = anchor.prefix ?? '';
78
+ const suffix = anchor.suffix ?? '';
79
+ const expectedStart = anchor.start ?? -1;
80
+ // 1) 收集 quote 的全部出现位置
81
+ const candidates = [];
82
+ let idx = text.indexOf(quote);
83
+ while (idx !== -1) {
84
+ candidates.push(idx);
85
+ // 从下一个字符继续,避免重叠匹配导致死循环
86
+ idx = text.indexOf(quote, idx + 1);
87
+ }
88
+ if (candidates.length > 0) {
89
+ let best = null;
90
+ for (const start of candidates) {
91
+ const score = scoreCandidate(text, start, quote.length, prefix, suffix, expectedStart);
92
+ if (!best || score > best.score)
93
+ best = { start, score };
94
+ }
95
+ if (best) {
96
+ return {
97
+ from: best.start,
98
+ to: best.start + quote.length,
99
+ score: best.score,
100
+ exact: true,
101
+ };
102
+ }
103
+ }
104
+ // 2) quote 已不在文档中:用上下文降级定位,返回零长度锚点。
105
+ // 两种情况:
106
+ // a) quote 整段被删除 → prefix 与 suffix 在文档中相邻,用接缝精确落点;
107
+ // b) quote 被改写但上下文还在 → 用较长的一侧贴住,落在其边缘。
108
+ // 两者都标记 exact=false,前端据此提示「原文已改动」。
109
+ // 2a) prefix 与 suffix 相邻(说明中间的原引用被删掉了)
110
+ if (prefix.length > 0 && suffix.length > 0) {
111
+ const needle = prefix + suffix;
112
+ const at = text.indexOf(needle);
113
+ if (at !== -1) {
114
+ const pos = at + prefix.length;
115
+ return { from: pos, to: pos, score: 50, exact: false };
116
+ }
117
+ }
118
+ // 2b) 用较长的一侧贴住边缘。
119
+ // 注意两点(都是实测教训):
120
+ // - 阈值取 4 而非 8:中文一个词常只有 2–4 字,阈值过高会让中文文档
121
+ // 几乎无法降级定位;
122
+ // - 必须遍历**全部**出现位置并取最接近原偏移的那个:
123
+ // 短上下文在文档中可能重复,取首个出现会落到错误位置。
124
+ const side = prefix.length >= suffix.length ? prefix : suffix;
125
+ if (side.length >= 4) {
126
+ const at = nearestOccurrence(text, side, expectedStart);
127
+ if (at !== -1) {
128
+ const pos = prefix.length >= suffix.length ? at + side.length : at;
129
+ return { from: pos, to: pos, score: 10, exact: false };
130
+ }
131
+ }
132
+ return null;
133
+ }
134
+ /**
135
+ * 在文本中找出 needle 的全部出现位置,返回最接近 expected 的那个。
136
+ * expected < 0(无提示)时退化为返回第一个出现位置。
137
+ * 这样短上下文重复时不会盲目落到首次出现处。
138
+ */
139
+ function nearestOccurrence(text, needle, expected) {
140
+ if (needle.length === 0)
141
+ return -1;
142
+ const positions = [];
143
+ let idx = text.indexOf(needle);
144
+ while (idx !== -1) {
145
+ positions.push(idx);
146
+ if (positions.length > 500)
147
+ break; // 防御:极端重复文本
148
+ idx = text.indexOf(needle, idx + 1);
149
+ }
150
+ if (positions.length === 0)
151
+ return -1;
152
+ if (expected < 0)
153
+ return positions[0];
154
+ let best = positions[0];
155
+ for (const p of positions) {
156
+ if (Math.abs(p - expected) < Math.abs(best - expected))
157
+ best = p;
158
+ }
159
+ return best;
160
+ }
161
+ /**
162
+ * 给候选位置打分:
163
+ * - 引用本身匹配是基础分;
164
+ * - 前后文匹配越长越可信(能区分「同一句话在文档里出现多次」);
165
+ * - 与提交时偏移越近,略微加分(仅在文本重复且上下文缺失时起作用)。
166
+ */
167
+ function scoreCandidate(text, start, quoteLength, prefix, suffix, expectedStart) {
168
+ let score = 1000;
169
+ if (prefix.length > 0) {
170
+ const actualPrefix = text.slice(Math.max(0, start - prefix.length), start);
171
+ score += commonSuffixLength(actualPrefix, prefix) * 4;
172
+ }
173
+ if (suffix.length > 0) {
174
+ const actualSuffix = text.slice(start + quoteLength, start + quoteLength + suffix.length);
175
+ score += commonPrefixLength(actualSuffix, suffix) * 4;
176
+ }
177
+ if (expectedStart >= 0) {
178
+ score -= Math.min(Math.abs(start - expectedStart), 200);
179
+ }
180
+ return score;
181
+ }
182
+ /** 两个字符串从末尾开始相同的字符数。 */
183
+ function commonSuffixLength(a, b) {
184
+ let n = 0;
185
+ while (n < a.length && n < b.length && a[a.length - 1 - n] === b[b.length - 1 - n]) {
186
+ n += 1;
187
+ }
188
+ return n;
189
+ }
190
+ /** 两个字符串从头开始相同的字符数。 */
191
+ function commonPrefixLength(a, b) {
192
+ let n = 0;
193
+ while (n < a.length && n < b.length && a[n] === b[n]) {
194
+ n += 1;
195
+ }
196
+ return n;
197
+ }
198
+ /** 截断上下文到限定长度(提交时使用)。 */
199
+ function clipContext(text) {
200
+ return text.slice(0, exports.ANCHOR_CONTEXT_LENGTH);
201
+ }
@@ -0,0 +1,64 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.CALLOUT_FILLS = exports.CALLOUT_COLORS = exports.CALLOUT_ICONS = exports.CALLOUT_TONES = void 0;
4
+ exports.validCalloutAttrs = validCalloutAttrs;
5
+ exports.effectiveCallout = effectiveCallout;
6
+ exports.calloutDOMAttributes = calloutDOMAttributes;
7
+ exports.calloutMetadata = calloutMetadata;
8
+ exports.readCalloutMetadata = readCalloutMetadata;
9
+ /** Document attributes only; shared with server validation by a source parity test. */
10
+ exports.CALLOUT_TONES = ['info', 'success', 'warning', 'danger'];
11
+ exports.CALLOUT_ICONS = ['💡', 'ℹ️', '✅', '⚠️', '❗', '📌', '🎯', '💬', '📖', '📝', '🚀', '🔔', '🔎', '⭐', '🌅', '🛠️', '📅', '🔒', '❤️', '🎉', '❓', '📎', '🏁', '🔥'];
12
+ exports.CALLOUT_COLORS = ['gray', 'red', 'orange', 'yellow', 'green', 'blue', 'purple'];
13
+ exports.CALLOUT_FILLS = [...exports.CALLOUT_COLORS, ...exports.CALLOUT_COLORS.map(c => `${c}-strong`)];
14
+ function validCalloutAttrs(raw) {
15
+ if (!raw || typeof raw !== 'object' || Array.isArray(raw))
16
+ return false;
17
+ const attrs = raw;
18
+ const valid = (key, values) => attrs[key] === undefined || attrs[key] === null ||
19
+ (typeof attrs[key] === 'string' && values.includes(attrs[key]));
20
+ return valid('tone', exports.CALLOUT_TONES) && valid('icon', ['', ...exports.CALLOUT_ICONS]) && valid('textColor', exports.CALLOUT_COLORS) &&
21
+ valid('borderColor', ['none', ...exports.CALLOUT_COLORS]) && valid('backgroundColor', ['none', ...exports.CALLOUT_FILLS]);
22
+ }
23
+ function effectiveCallout(attrs = {}) {
24
+ const tone = exports.CALLOUT_TONES.includes(attrs.tone) ? attrs.tone : 'info';
25
+ const hue = { info: 'blue', success: 'green', warning: 'orange', danger: 'red' }[tone];
26
+ return { tone, icon: attrs.icon === null || attrs.icon === undefined ? { info: '💡', success: '✅', warning: '⚠️', danger: '❗' }[tone] :
27
+ ['', ...exports.CALLOUT_ICONS].includes(attrs.icon) ? attrs.icon : '💡',
28
+ textColor: exports.CALLOUT_COLORS.includes(attrs.textColor) ? attrs.textColor : null,
29
+ borderColor: ['none', ...exports.CALLOUT_COLORS].includes(attrs.borderColor) ? attrs.borderColor : hue,
30
+ backgroundColor: ['none', ...exports.CALLOUT_FILLS].includes(attrs.backgroundColor) ? attrs.backgroundColor : hue };
31
+ }
32
+ function calloutDOMAttributes(attrs = {}) {
33
+ const value = effectiveCallout(attrs);
34
+ return { 'data-callout': 'true', 'data-tone': value.tone, 'data-callout-icon': attrs.icon ?? 'default',
35
+ ...Object.fromEntries(['textColor', 'borderColor', 'backgroundColor'].filter(key => attrs[key] != null)
36
+ .map(key => [`data-callout-${key.toLowerCase()}`, String(attrs[key])])),
37
+ ...(value.icon === '' ? { 'data-icon-hidden': 'true' } : {}),
38
+ style: [`--callout-border:${value.borderColor === 'none' ? 'transparent' : `var(--callout-border-${value.borderColor})`}`,
39
+ `--callout-fill:${value.backgroundColor === 'none' ? 'transparent' : `var(--doc-highlight-${value.backgroundColor === 'gray' ? 'gray-soft' : value.backgroundColor})`}`,
40
+ `--callout-text:${value.textColor ? `var(--doc-text-${value.textColor})` : 'var(--content-text-color, var(--color-text))'}`].join(';') };
41
+ }
42
+ /** Optional Markdown metadata never contains free CSS or HTML. */
43
+ function calloutMetadata(attrs) {
44
+ if (!validCalloutAttrs(attrs))
45
+ return '';
46
+ const extra = Object.fromEntries(['icon', 'textColor', 'borderColor', 'backgroundColor']
47
+ .filter(key => attrs[key] !== undefined && attrs[key] !== null)
48
+ .map(key => [key, attrs[key]]));
49
+ return Object.keys(extra).length ? ` <!--ywiki-callout:${encodeURIComponent(JSON.stringify(extra))}-->` : '';
50
+ }
51
+ function readCalloutMetadata(text) {
52
+ const match = /^\s*<!--ywiki-callout:([^\s<>]{1,4096})-->\s*/.exec(text);
53
+ if (!match)
54
+ return null;
55
+ try {
56
+ const attrs = JSON.parse(decodeURIComponent(match[1]));
57
+ if (!validCalloutAttrs(attrs) || Object.keys(attrs).some(k => !['icon', 'textColor', 'borderColor', 'backgroundColor'].includes(k)))
58
+ return null;
59
+ return { attrs, length: match[0].length };
60
+ }
61
+ catch {
62
+ return null;
63
+ }
64
+ }
@@ -0,0 +1,33 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.CODE_THEMES = void 0;
4
+ exports.codeBlockDisplay = codeBlockDisplay;
5
+ exports.rememberCodeTheme = rememberCodeTheme;
6
+ exports.codeLineCount = codeLineCount;
7
+ exports.codeLineNumbers = codeLineNumbers;
8
+ exports.codeBlockClass = codeBlockClass;
9
+ /** Presentation metadata only; code, whitespace and the document schema remain unchanged. */
10
+ exports.CODE_THEMES = [
11
+ { value: 'auto', label: '跟随页面' },
12
+ { value: 'paper', label: '纸白' },
13
+ { value: 'soft', label: '柔和' },
14
+ { value: 'night', label: '深色' },
15
+ ];
16
+ function codeBlockDisplay() {
17
+ return { collapsed: false, lineNumbers: true, limited: true, theme: 'auto' };
18
+ }
19
+ function rememberCodeTheme(_theme) { }
20
+ function codeLineCount(text) {
21
+ let count = 1;
22
+ for (let i = 0; i < text.length; i++)
23
+ if (text[i] === '\n')
24
+ count++;
25
+ return count;
26
+ }
27
+ function codeLineNumbers(text) {
28
+ return Array.from({ length: codeLineCount(text) }, (_, i) => String(i + 1)).join('\n');
29
+ }
30
+ function codeBlockClass(display) {
31
+ return ['ywiki-code-panel', `code-theme-${display.theme}`, ...(display.collapsed ? ['is-collapsed'] : []),
32
+ ...(display.lineNumbers ? [] : ['without-line-numbers']), ...(display.limited ? [] : ['is-unlimited'])];
33
+ }
@@ -0,0 +1,51 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.CODE_LANGUAGES = void 0;
4
+ exports.codeLanguageLabel = codeLanguageLabel;
5
+ exports.isKnownCodeLanguage = isKnownCodeLanguage;
6
+ /** 空字符串表示「纯文本 / 不指定」。 */
7
+ exports.CODE_LANGUAGES = [
8
+ { value: '', label: '纯文本' },
9
+ { value: 'bash', label: 'Bash / Shell' },
10
+ { value: 'json', label: 'JSON' },
11
+ { value: 'yaml', label: 'YAML' },
12
+ { value: 'sql', label: 'SQL' },
13
+ { value: 'markdown', label: 'Markdown' },
14
+ { value: 'diff', label: 'Diff' },
15
+ { value: 'javascript', label: 'JavaScript' },
16
+ { value: 'typescript', label: 'TypeScript' },
17
+ { value: 'jsx', label: 'JSX' },
18
+ { value: 'tsx', label: 'TSX' },
19
+ { value: 'python', label: 'Python' },
20
+ { value: 'java', label: 'Java' },
21
+ { value: 'go', label: 'Go' },
22
+ { value: 'rust', label: 'Rust' },
23
+ { value: 'c', label: 'C' },
24
+ { value: 'cpp', label: 'C++' },
25
+ { value: 'csharp', label: 'C#' },
26
+ { value: 'php', label: 'PHP' },
27
+ { value: 'ruby', label: 'Ruby' },
28
+ { value: 'kotlin', label: 'Kotlin' },
29
+ { value: 'swift', label: 'Swift' },
30
+ { value: 'html', label: 'HTML' },
31
+ { value: 'css', label: 'CSS' },
32
+ { value: 'xml', label: 'XML' },
33
+ { value: 'dockerfile', label: 'Dockerfile' },
34
+ { value: 'ini', label: 'INI / TOML' },
35
+ ];
36
+ const LABEL_BY_VALUE = new Map(exports.CODE_LANGUAGES.map((l) => [l.value, l.label]));
37
+ /**
38
+ * 取语言的展示标签。
39
+ *
40
+ * 未知语言(如用户从 Markdown 导入的少见围栏标记)直接显示原值——
41
+ * 不隐藏、不改写,避免"我明明标了语言却看不到"。
42
+ */
43
+ function codeLanguageLabel(value) {
44
+ if (!value)
45
+ return '';
46
+ return LABEL_BY_VALUE.get(value) ?? value;
47
+ }
48
+ /** 是否为目录内已知语言(用于选择器回显:未知值时追加一个临时选项)。 */
49
+ function isKnownCodeLanguage(value) {
50
+ return LABEL_BY_VALUE.has(value);
51
+ }
@@ -0,0 +1,23 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.supportsDocument = supportsDocument;
4
+ const node_catalog_js_1 = require("./node-catalog.js");
5
+ const schema_js_1 = require("../schema.js");
6
+ const mind_map_js_1 = require("./mind-map.js");
7
+ const callout_js_1 = require("./callout.js");
8
+ function supportsDocument(raw, version = 1) {
9
+ if (version > schema_js_1.SCHEMA_VERSION)
10
+ return false;
11
+ if (!raw)
12
+ return true;
13
+ if (typeof raw !== 'object')
14
+ return false;
15
+ const doc = raw;
16
+ if (!doc.type || !(0, node_catalog_js_1.nodeMeta)(doc.type))
17
+ return false;
18
+ if (doc.type === 'mindMap' && !(0, mind_map_js_1.readMindMap)(doc))
19
+ return false;
20
+ if (doc.type === 'callout' && !(0, callout_js_1.validCalloutAttrs)(doc.attrs ?? {}))
21
+ return false;
22
+ return !doc.content || Array.isArray(doc.content) && doc.content.every(node => supportsDocument(node));
23
+ }
@@ -0,0 +1,52 @@
1
+ "use strict";
2
+ /**
3
+ * 从 ProseMirror 文档提取用于锚点定位的块级纯文本。
4
+ *
5
+ * 与 anchor.ts 配合:锚点用「在扁平文本中的字符偏移」表达,
6
+ * 这里负责把文档结构摊平,并保留「块 → 字符区间」的映射,
7
+ * 以便把定位结果映射回具体的块(前端据此在对应块上加高亮)。
8
+ *
9
+ * 与后端 DocSchemaService.extractPlainText 保持一致的遍历顺序(先文本、后子节点),
10
+ * 否则同一段内容在提交锚点与解析锚点时会得到不同的字符偏移。
11
+ */
12
+ Object.defineProperty(exports, "__esModule", { value: true });
13
+ exports.extractBlockTexts = extractBlockTexts;
14
+ exports.plainTextOf = plainTextOf;
15
+ /** 提取顶层块(含嵌套块)的纯文本。列表会递归取出每一项。 */
16
+ function extractBlockTexts(doc) {
17
+ const blocks = [];
18
+ const walk = (node, topIndex) => {
19
+ // 块级容器(段落/标题/列表项/引用/代码块等)各自作为一个「块」
20
+ const isBlockContainer = [
21
+ 'paragraph',
22
+ 'heading',
23
+ 'codeBlock',
24
+ 'blockquote',
25
+ 'listItem',
26
+ 'taskItem',
27
+ 'tableCell',
28
+ 'tableHeader',
29
+ 'mindMapTopic',
30
+ ].includes(node.type ?? '');
31
+ if (isBlockContainer) {
32
+ blocks.push({ index: topIndex, text: plainTextOf(node) });
33
+ return;
34
+ }
35
+ for (const child of node.content ?? [])
36
+ walk(child, topIndex);
37
+ };
38
+ (doc?.content ?? []).forEach((node, i) => walk(node, i));
39
+ return blocks.filter((b) => b.text.length > 0);
40
+ }
41
+ /** 取节点的全部文本(含后代)。 */
42
+ function plainTextOf(node) {
43
+ const buf = [];
44
+ const walk = (n) => {
45
+ if (typeof n.text === 'string')
46
+ buf.push(n.text);
47
+ for (const c of n.content ?? [])
48
+ walk(c);
49
+ };
50
+ walk(node);
51
+ return buf.join('');
52
+ }