kindy-editor 3.2.2 → 3.2.3
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/dist-lib/assets/{worker-CHtzL1YW.js → worker-BIFTSRF2.js} +7043 -7021
- package/dist-lib/{browser-BxYyzpLt.js → browser-xuRja-pP.js} +2 -2
- package/dist-lib/builder.js +7 -7
- package/dist-lib/changeAggregator-ymKDXGA8.js +376 -0
- package/dist-lib/{editorApp-N20nk5uQ.js → editorApp-zIpjgEaZ.js} +7281 -7230
- package/dist-lib/events.js +119 -43
- package/dist-lib/export.js +1739 -1732
- package/dist-lib/generate-toc.js +5 -5
- package/dist-lib/import.js +2 -2
- package/dist-lib/{kindy-editor-D4xSNtvA.js → kindy-editor-DuN692fi.js} +5 -5
- package/dist-lib/kindy-editor.js +9 -6
- package/dist-lib/{lists-DpmgbDyt.js → lists-BVCy5dMa.js} +2 -2
- package/dist-lib/measure.js +3 -3
- package/dist-lib/{measureHost-C2NOdNkI.js → measureHost-y37gLU6M.js} +2 -2
- package/dist-lib/{metrics-DHgtBP-R.js → metrics-CdfHD-8y.js} +3 -3
- package/dist-lib/{paintStyle-Cjk9egel.js → paintStyle-Cb7IGKPW.js} +51 -51
- package/dist-lib/{parse-JE_w0NNt.js → parse-DJZd-2A7.js} +20 -20
- package/dist-lib/{pipeline-CgbvBwNe.js → pipeline-DKgBMmw1.js} +301 -301
- package/dist-lib/recalc-docx.js +2 -2
- package/dist-lib/{recalcToc-DKJK9f5X.js → recalcToc-DCykIt4o.js} +80 -80
- package/dist-lib/{styleProps-OePkgWnD.js → styleProps-Dg3Wf-ZJ.js} +3 -3
- package/dist-lib/{stylesheet-COz2MSt6.js → stylesheet-EQFx9l5c.js} +3 -3
- package/dist-lib/{toc-C6XLbDEA.js → toc-BIj8uQ-W.js} +48 -48
- package/dist-lib/{webmcp-DrpPKUeh.js → webmcp-QA5rVg1F.js} +2 -2
- package/docs/http-event-sink.md +7 -0
- package/docs/public-events.md +102 -0
- package/docs/track-changes-events.md +551 -0
- package/package.json +2 -2
- package/types/events.d.ts +67 -1
- package/types/kindy-editor.d.ts +4 -0
- package/dist-lib/eventHub-CZFpTVqe.js +0 -71
- package/dist-lib/events-D_WEQDa6.js +0 -104
- package/docs/.vitepress/cache/deps/@theme_index.js +0 -275
- package/docs/.vitepress/cache/deps/@theme_index.js.map +0 -7
- package/docs/.vitepress/cache/deps/_metadata.json +0 -40
- package/docs/.vitepress/cache/deps/chunk-FNDFPMRQ.js +0 -13131
- package/docs/.vitepress/cache/deps/chunk-FNDFPMRQ.js.map +0 -7
- package/docs/.vitepress/cache/deps/chunk-IOFLAFGL.js +0 -9719
- package/docs/.vitepress/cache/deps/chunk-IOFLAFGL.js.map +0 -7
- package/docs/.vitepress/cache/deps/package.json +0 -3
- package/docs/.vitepress/cache/deps/vitepress___@vue_devtools-api.js +0 -4483
- package/docs/.vitepress/cache/deps/vitepress___@vue_devtools-api.js.map +0 -7
- package/docs/.vitepress/cache/deps/vitepress___@vueuse_core.js +0 -583
- package/docs/.vitepress/cache/deps/vitepress___@vueuse_core.js.map +0 -7
- package/docs/.vitepress/cache/deps/vue.js +0 -347
- package/docs/.vitepress/cache/deps/vue.js.map +0 -7
- package/docs/.vitepress/config.ts +0 -32
package/docs/public-events.md
CHANGED
|
@@ -110,6 +110,108 @@ includes `newlyMentionedUserIds`, and tombstoning emits
|
|
|
110
110
|
`review.comment.deleted`. See [Review comments and @mentions](./review-comments.md)
|
|
111
111
|
for the integration and notification flow.
|
|
112
112
|
|
|
113
|
+
## Document change events: Raw vs Semantic layer
|
|
114
|
+
|
|
115
|
+
Kindy Editor divides content changes into two distinct tiers:
|
|
116
|
+
|
|
117
|
+
```text
|
|
118
|
+
User Input / Keystrokes
|
|
119
|
+
│
|
|
120
|
+
▼
|
|
121
|
+
[ RAW LAYER ] document.change.applied (keystroke mutations, internal / collab / debug)
|
|
122
|
+
│
|
|
123
|
+
▼
|
|
124
|
+
ChangeAggregator (Session Buffer, IME, continuous typing merge, no-op filter)
|
|
125
|
+
│
|
|
126
|
+
├────── CommitPolicy (idle debounce ~1s, blur, save, block jump, enter)
|
|
127
|
+
│
|
|
128
|
+
▼
|
|
129
|
+
[ SEMANTIC LAYER ] document.change.committed (clean diffs for CRM, Audit, AI, Outbox)
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
### 1. Raw layer: `document.change.applied`
|
|
133
|
+
Fires immediately on every editor mutation (keystrokes, backspace, paste, undo/redo).
|
|
134
|
+
Best used for: internal extensions, presence sync, local undo history, or low-level operation debugging.
|
|
135
|
+
|
|
136
|
+
Payload (`DocumentChangeEventData`):
|
|
137
|
+
```ts
|
|
138
|
+
interface DocumentChangeEventData {
|
|
139
|
+
origin: "typing" | "command" | "paste" | "undo" | "redo";
|
|
140
|
+
operationCount: number;
|
|
141
|
+
affectedBlockIds: string[];
|
|
142
|
+
operations?: Op[]; // Present when detail !== "metadata"
|
|
143
|
+
textDiffs?: TextDiffSummary[]; // Discrete changes computed against pre-mutation shadow state
|
|
144
|
+
selectionBefore?: DocSelection | null;
|
|
145
|
+
selectionAfter?: DocSelection | null;
|
|
146
|
+
change?: Change;
|
|
147
|
+
}
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
### 2. Semantic layer: `document.change.committed`
|
|
151
|
+
Fires only when a logical editing session finishes across a commit boundary (idle debounce, blur, save, block switch).
|
|
152
|
+
Best used for: **external integrations (CRM, Audit Log, AI processors, Webhook sinks, Notifications)**.
|
|
153
|
+
|
|
154
|
+
Payload (`DocumentChangeCommittedData`):
|
|
155
|
+
```ts
|
|
156
|
+
interface DocumentChangeCommittedData {
|
|
157
|
+
transactionId: string;
|
|
158
|
+
changeGroupId: string; // Stable group id tying all transactions in this session
|
|
159
|
+
beforeVersion: number; // Document baseline version before session began
|
|
160
|
+
afterVersion: number; // Document version after session completed
|
|
161
|
+
origin: "typing" | "command" | "paste" | "undo" | "redo";
|
|
162
|
+
textDiffs: TextDiffSummary[]; // Clean, aggregated text diffs (no-ops stripped)
|
|
163
|
+
intent?: string;
|
|
164
|
+
affectedBlockIds?: string[];
|
|
165
|
+
}
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
### `TextDiffSummary` structure
|
|
169
|
+
```ts
|
|
170
|
+
interface TextDiffSummary {
|
|
171
|
+
action: "insert" | "delete" | "replace";
|
|
172
|
+
blockId: string;
|
|
173
|
+
offset: number;
|
|
174
|
+
fromText: string;
|
|
175
|
+
toText: string;
|
|
176
|
+
}
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
### How specific scenarios are handled:
|
|
180
|
+
|
|
181
|
+
1. **Continuous Typing Session (Gõ liên tục):**
|
|
182
|
+
- User types characters one by one.
|
|
183
|
+
- Raw `document.change.applied` events fire on each keystroke for immediate UI responsiveness.
|
|
184
|
+
- `ChangeAggregator` buffers the keystrokes without spamming downstream sinks.
|
|
185
|
+
- When the user stops typing for ~1000ms (idle) or clicks outside (blur), a single `document.change.committed` is emitted with the consolidated change (e.g., `action: "insert"`, `fromText: ""`, `toText: "Đà Nẵng"`).
|
|
186
|
+
|
|
187
|
+
2. **Selected Range Replacement (Bôi đen ghi đè):**
|
|
188
|
+
- User selects `"Hà Nội"` and types `"Đà Nẵng"`.
|
|
189
|
+
- The editor runs `deleteRange` followed by `insertText`.
|
|
190
|
+
- Shadow state evaluation and `mergeRelatedChanges` combine them into:
|
|
191
|
+
`{ action: "replace", blockId: "p-5", offset: 13, fromText: "Hà Nội", toText: "Đà Nẵng" }`.
|
|
192
|
+
|
|
193
|
+
3. **Multiple Independent Edits in One Transaction / Session (Nhiều sửa đổi độc lập):**
|
|
194
|
+
- E.g. Editing `"50"` -> `"75"` at offset 5 and `"30"` -> `"45"` at offset 25.
|
|
195
|
+
- `textDiffs` contains two separate items, preserving exact offsets without string concatenation corruption:
|
|
196
|
+
```json
|
|
197
|
+
[
|
|
198
|
+
{ "action": "replace", "blockId": "p-5", "offset": 5, "fromText": "50", "toText": "75" },
|
|
199
|
+
{ "action": "replace", "blockId": "p-5", "offset": 25, "fromText": "30", "toText": "45" }
|
|
200
|
+
]
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
4. **No-op Edits (Gõ rồi xóa về nguyên bản):**
|
|
204
|
+
- User types `" thêm"` and immediately backspaces it back to original text.
|
|
205
|
+
- When session boundary triggers, `ChangeAggregator` compares baseline vs current text.
|
|
206
|
+
- Since text is identical, **no event is emitted**, preventing dirty garbage data from reaching downstream databases.
|
|
207
|
+
|
|
208
|
+
5. **Batch Commands & Paste (Dán hoặc chạy command):**
|
|
209
|
+
- Paste, cut, or formatting commands commit immediately (`CommitBoundaryReason = "explicit"`).
|
|
210
|
+
- Any prior typing session is automatically flushed before the new action begins.
|
|
211
|
+
|
|
212
|
+
6. **Cursor Jump / Block Switch (Chuyển khối hoặc nhảy chuột):**
|
|
213
|
+
- If user edits block `p-1`, then clicks block `p-2` or moves caret far away, `CommitPolicy` detects a boundary (`"selection-jump"` / `"block-switch"`), flushes block `p-1`'s session, and opens a new session for `p-2`.
|
|
214
|
+
|
|
113
215
|
## What each layer owns
|
|
114
216
|
|
|
115
217
|
The editor library owns content changes, review operations, editor state,
|
|
@@ -0,0 +1,551 @@
|
|
|
1
|
+
# Document Change Tracking & Semantic Events
|
|
2
|
+
|
|
3
|
+
Kindy Editor cung cấp hệ thống theo dõi thay đổi (Change Tracking & Audit) được thiết kế theo **mô hình 2 tầng (Two-Tier Event Architecture)**:
|
|
4
|
+
1. **Tầng Kỹ thuật (Raw Layer)**: Phản ánh chi tiết từng thao tác mutation, phục vụ nội bộ editor và real-time collaboration.
|
|
5
|
+
2. **Tầng Ngữ nghĩa (Semantic Layer)**: Tập hợp phiên chỉnh sửa, làm sạch dữ liệu, loại bỏ thao tác vô hiệu (no-op) và cung cấp diffs chính xác (`fromText` ➔ `toText`) cho các hệ thống tích hợp bên ngoài (CRM, Audit Log, AI, Notification).
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## 1. Tổng quan Kiến trúc 2 tầng (Two-Tier Architecture)
|
|
10
|
+
|
|
11
|
+
```text
|
|
12
|
+
User Typing / Backspace / Paste / Command
|
|
13
|
+
│
|
|
14
|
+
▼
|
|
15
|
+
commitCore (Chụp snapshot blockTextBefore trước khi runOps)
|
|
16
|
+
│
|
|
17
|
+
▼
|
|
18
|
+
┌─────────────────────────────────────────────────────────────┐
|
|
19
|
+
│ RAW LAYER (Kỹ thuật) │
|
|
20
|
+
│ document.change.applied │
|
|
21
|
+
│ - Phát sinh: Ngay lập tức sau mỗi mutation / keystroke │
|
|
22
|
+
│ - Mục đích: Collab caret sync, Undo/Redo, Debug, Plugin │
|
|
23
|
+
│ - Payload: { ops, affectedBlockIds, textDiffs } │
|
|
24
|
+
└─────────────────────────────────────────────────────────────┘
|
|
25
|
+
│
|
|
26
|
+
▼
|
|
27
|
+
┌─────────────────────────────────────────────────────────────┐
|
|
28
|
+
│ Change Session Aggregator │
|
|
29
|
+
│ - Lưu baseline snapshot khi phiên bắt đầu │
|
|
30
|
+
│ - Gom các ký tự gõ liên tiếp thành chuỗi hoàn chỉnh │
|
|
31
|
+
│ - Gom delete + insert cùng vị trí thành replace │
|
|
32
|
+
│ - Lọc bỏ thao tác no-op (gõ rồi xoá về nguyên bản) │
|
|
33
|
+
└─────────────────────────────────────────────────────────────┘
|
|
34
|
+
│
|
|
35
|
+
├───── CommitPolicy (Idle ~1s, Blur, Save, Jump)
|
|
36
|
+
▼
|
|
37
|
+
┌─────────────────────────────────────────────────────────────┐
|
|
38
|
+
│ SEMANTIC LAYER (Ngữ nghĩa) │
|
|
39
|
+
│ document.change.committed │
|
|
40
|
+
│ - Phát sinh: KHI PHIÊN CHỈNH SỬA KẾT THÚC │
|
|
41
|
+
│ - Mục đích: Hệ thống ngoài, CRM, Audit DB, AI, Webhook │
|
|
42
|
+
│ - Payload: { changeGroupId, versions, origin, textDiffs } │
|
|
43
|
+
└─────────────────────────────────────────────────────────────┘
|
|
44
|
+
│
|
|
45
|
+
▼
|
|
46
|
+
createHttpEventSink (Filter eventTypes)
|
|
47
|
+
│
|
|
48
|
+
▼
|
|
49
|
+
Hệ thống nhận (Integration / Backend)
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
---
|
|
53
|
+
|
|
54
|
+
## 2. Cấu trúc Contract & Schema Dữ liệu
|
|
55
|
+
|
|
56
|
+
### 2.1. Cấu trúc `TextDiffSummary`
|
|
57
|
+
Mỗi thay đổi văn bản độc lập được chuẩn hoá thành một object trong mảng `textDiffs`:
|
|
58
|
+
|
|
59
|
+
```ts
|
|
60
|
+
export interface TextDiffSummary {
|
|
61
|
+
/** Hành động: chèn mới, xoá, hoặc thay thế */
|
|
62
|
+
action: "insert" | "delete" | "replace";
|
|
63
|
+
/** ID của khối đoạn văn hoặc cell chứa thay đổi */
|
|
64
|
+
blockId: string;
|
|
65
|
+
/** Vị trí bắt đầu thay đổi trong block */
|
|
66
|
+
offset: number;
|
|
67
|
+
/** Nội dung text trước khi sửa */
|
|
68
|
+
fromText: string;
|
|
69
|
+
/** Nội dung text sau khi sửa */
|
|
70
|
+
toText: string;
|
|
71
|
+
}
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
### 2.2. Payload của `document.change.committed` (`event.data`)
|
|
75
|
+
Đây là event chính dành cho downstream integration:
|
|
76
|
+
|
|
77
|
+
```ts
|
|
78
|
+
export interface DocumentChangeCommittedData {
|
|
79
|
+
/** Transaction id cuối cùng của phiên */
|
|
80
|
+
transactionId: string;
|
|
81
|
+
/** ID định danh phiên chỉnh sửa (gộp toàn bộ keystrokes trong session) */
|
|
82
|
+
changeGroupId: string;
|
|
83
|
+
/** Phiên bản tài liệu TRƯỚC khi phiên bắt đầu */
|
|
84
|
+
beforeVersion: number;
|
|
85
|
+
/** Phiên bản tài liệu SAU khi phiên kết thúc */
|
|
86
|
+
afterVersion: number;
|
|
87
|
+
/** Nguồn gốc thao tác */
|
|
88
|
+
origin: "typing" | "command" | "paste" | "undo" | "redo";
|
|
89
|
+
/** Danh sách các thay đổi text sạch sẽ */
|
|
90
|
+
textDiffs: TextDiffSummary[];
|
|
91
|
+
/** Ý định thao tác (nếu có từ command layer) */
|
|
92
|
+
intent?: string;
|
|
93
|
+
/** Danh sách các block IDs bị ảnh hưởng */
|
|
94
|
+
affectedBlockIds?: string[];
|
|
95
|
+
}
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
### 2.3. Envelope chuẩn của sự kiện
|
|
99
|
+
Mọi sự kiện đều nằm trong envelope đồng nhất:
|
|
100
|
+
|
|
101
|
+
```json
|
|
102
|
+
{
|
|
103
|
+
"schemaVersion": "1.0",
|
|
104
|
+
"id": "evt_0199c4b2-8411",
|
|
105
|
+
"type": "document.change.committed",
|
|
106
|
+
"occurredAt": "2026-10-06T15:50:30.000Z",
|
|
107
|
+
"source": "local",
|
|
108
|
+
"actor": {
|
|
109
|
+
"id": "u-42",
|
|
110
|
+
"firstName": "An",
|
|
111
|
+
"lastName": "Nguyễn"
|
|
112
|
+
},
|
|
113
|
+
"document": {
|
|
114
|
+
"id": "contract-2026",
|
|
115
|
+
"baseVersion": 105,
|
|
116
|
+
"version": 108
|
|
117
|
+
},
|
|
118
|
+
"transaction": {
|
|
119
|
+
"id": "tx-889",
|
|
120
|
+
"status": "committed"
|
|
121
|
+
},
|
|
122
|
+
"data": {
|
|
123
|
+
"transactionId": "tx-889",
|
|
124
|
+
"changeGroupId": "chg-176239",
|
|
125
|
+
"beforeVersion": 105,
|
|
126
|
+
"afterVersion": 108,
|
|
127
|
+
"origin": "typing",
|
|
128
|
+
"textDiffs": [
|
|
129
|
+
{
|
|
130
|
+
"action": "replace",
|
|
131
|
+
"blockId": "p-5",
|
|
132
|
+
"offset": 24,
|
|
133
|
+
"fromText": "Hà Nội",
|
|
134
|
+
"toText": "Đà Nẵng"
|
|
135
|
+
}
|
|
136
|
+
]
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
---
|
|
142
|
+
|
|
143
|
+
## 3. Cơ chế Xử lý & Thuật toán
|
|
144
|
+
|
|
145
|
+
### 3.1. Shadow State Simulation (Chống lệch offset)
|
|
146
|
+
Khi một transaction chứa nhiều ops liên tiếp:
|
|
147
|
+
1. `deleteRange(1, 2)` trên `"abcdef"` biến văn bản thành `"acdef"`.
|
|
148
|
+
2. `deleteRange(2, 3)` tiếp theo đang thao tác trên `"acdef"` (ký tự `"d"`), chứ không phải trên chuỗi gốc `"abcdef"` (ký tự `"c"`).
|
|
149
|
+
|
|
150
|
+
`computeTextDiffs()` áp dụng tuần tự từng op lên một **in-memory shadow state**:
|
|
151
|
+
- Snapshot ban đầu được chụp trước khi gọi `runOps()`.
|
|
152
|
+
- Mỗi op tính toán `fromText` trực tiếp từ trạng thái hiện tại của shadow state và cập nhật shadow state cho op kế tiếp.
|
|
153
|
+
- Đảm bảo trích xuất chính xác 100% `fromText` ngay cả khi có hàng chục ops trong 1 transaction.
|
|
154
|
+
|
|
155
|
+
### 3.2. Merge Related Changes (Gộp thao tác thông minh)
|
|
156
|
+
- **Delete + Insert tại cùng offset**: Tự động chuyển đổi thành `action: "replace"` (`"Hà Nội"` ➔ `"Đà Nẵng"`).
|
|
157
|
+
- **Gõ tiến liên tục (Continuous typing)**: Tự động gom các ký tự liền kề thành 1 text hoàn chỉnh.
|
|
158
|
+
- **Backspace lùi liên tục**: Tự động gom thành 1 lệnh delete với span mở rộng.
|
|
159
|
+
- **Nhiều vị trí độc lập**: Nếu thay đổi ở các offset khác nhau hoặc các block khác nhau, giữ nguyên thành các item tách biệt trong `textDiffs` (không bao giờ gộp chuỗi sai lệch).
|
|
160
|
+
|
|
161
|
+
### 3.3. Zero Dirty Data (Lọc bỏ No-op)
|
|
162
|
+
Nếu người dùng gõ thêm nội dung rồi nhấn Backspace xoá hết về trạng thái ban đầu:
|
|
163
|
+
- Khi phiên commit, `ChangeAggregator` so sánh baseline snapshot ban đầu và snapshot hiện tại.
|
|
164
|
+
- Kết quả diff là rỗng (`[]`).
|
|
165
|
+
- **Không phát bất kỳ event nào ra ngoài**, ngăn ngừa hoàn toàn rác dữ liệu ở downstream.
|
|
166
|
+
|
|
167
|
+
---
|
|
168
|
+
|
|
169
|
+
## 4. Ranh giới Commit (Commit Policy Boundaries)
|
|
170
|
+
|
|
171
|
+
Sự kiện `document.change.committed` được kích hoạt khi nào?
|
|
172
|
+
|
|
173
|
+
Không chỉ dựa vào debounce đơn thuần, hệ thống áp dụng nhiều ranh giới tự nhiên của người dùng:
|
|
174
|
+
|
|
175
|
+
| Ranh giới | Điều kiện kích hoạt | Ý nghĩa |
|
|
176
|
+
|---|---|---|
|
|
177
|
+
| **Idle Debounce** | Người dùng dừng gõ ~800–1200ms (mặc định 1000ms) | Hoàn thành một cụm ý hoặc câu |
|
|
178
|
+
| **Blur** | Editor mất focus ra ngoài container | Người dùng chuyển sang trường khác hoặc tab khác |
|
|
179
|
+
| **Save / Export** | Người dùng bấm Lưu (Ctrl+S) hoặc Export | Chốt trạng thái tài liệu để ghi nhận |
|
|
180
|
+
| **Selection Jump** | Caret nhảy cóc cách xa vị trí đang gõ (> 2 ký tự) | Người dùng chuyển sự chú ý sang vị trí khác |
|
|
181
|
+
| **Block Switch** | Chuyển sang paragraph hoặc block khác | Hoàn thành đoạn hiện tại |
|
|
182
|
+
| **Origin Switch** | Chuyển đổi giữa Typing, Paste, Command | Kết thúc luồng gõ trước khi nhận dữ liệu mới |
|
|
183
|
+
| **Destroy** | Đóng editor instance | Thu dọn và flush toàn bộ session chưa gửi |
|
|
184
|
+
|
|
185
|
+
---
|
|
186
|
+
|
|
187
|
+
## 5. Hướng dẫn Tích hợp Từng Trường hợp Cụ thể
|
|
188
|
+
|
|
189
|
+
### Trường hợp 1: Người dùng gõ phím liên tục
|
|
190
|
+
- **Thao tác**: Người dùng gõ `"H"`, `"à"`, `" "`, `"N"`, `"ộ"`, `"i"`.
|
|
191
|
+
- **Hành vi**:
|
|
192
|
+
- Tầng RAW: Phát 6 sự kiện `document.change.applied`.
|
|
193
|
+
- Tầng SEMANTIC: Đợi người dùng dừng gõ 1000ms, phát duy nhất 1 sự kiện `document.change.committed`:
|
|
194
|
+
```json
|
|
195
|
+
{
|
|
196
|
+
"action": "insert",
|
|
197
|
+
"blockId": "p-1",
|
|
198
|
+
"offset": 0,
|
|
199
|
+
"fromText": "",
|
|
200
|
+
"toText": "Hà Nội"
|
|
201
|
+
}
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
### Trường hợp 2: Gõ rồi xoá về nguyên bản (No-op)
|
|
205
|
+
- **Thao tác**: Văn bản gốc là `"Việt Nam"`. Người dùng gõ `" quê hương"`, rồi backspace xoá hết về `"Việt Nam"`.
|
|
206
|
+
- **Hành vi**:
|
|
207
|
+
- Khi idle timer kích hoạt, `ChangeAggregator` thấy `initialText === currentText`.
|
|
208
|
+
- **Không phát event `document.change.committed`**.
|
|
209
|
+
|
|
210
|
+
### Trường hợp 3: Bôi đen thay thế
|
|
211
|
+
- **Thao tác**: Người dùng bôi đen chữ `"50 triệu"` và gõ `"75 triệu"`.
|
|
212
|
+
- **Hành vi**:
|
|
213
|
+
- Tự động nhận diện `delete` + `insert` tại cùng offset:
|
|
214
|
+
```json
|
|
215
|
+
{
|
|
216
|
+
"action": "replace",
|
|
217
|
+
"blockId": "p-1",
|
|
218
|
+
"offset": 10,
|
|
219
|
+
"fromText": "50 triệu",
|
|
220
|
+
"toText": "75 triệu"
|
|
221
|
+
}
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
### Trường hợp 4: Nhiều chỉnh sửa độc lập trong cùng một thao tác
|
|
225
|
+
- **Thao tác**: Lệnh Replace-All đổi `"50"` thành `"75"` ở offset 5 và `"30"` thành `"45"` ở offset 25.
|
|
226
|
+
- **Hành vi**: Mảng `textDiffs` chứa 2 phần tử độc lập:
|
|
227
|
+
```json
|
|
228
|
+
[
|
|
229
|
+
{ "action": "replace", "blockId": "p-5", "offset": 5, "fromText": "50", "toText": "75" },
|
|
230
|
+
{ "action": "replace", "blockId": "p-5", "offset": 25, "fromText": "30", "toText": "45" }
|
|
231
|
+
]
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
### Trường hợp 5: Nhảy chuột hoặc click ra ngoài
|
|
235
|
+
- **Thao tác**: Người dùng đang gõ dở ở đoạn 1 thì click chuột sang đoạn 2 hoặc click ra ngoài editor.
|
|
236
|
+
- **Hành vi**: `CommitPolicy` lập tức flush phiên của đoạn 1 mà không cần chờ hết 1000ms idle timer.
|
|
237
|
+
|
|
238
|
+
---
|
|
239
|
+
|
|
240
|
+
## 6. Code Mẫu Tích hợp Toàn diện
|
|
241
|
+
|
|
242
|
+
### 6.1. Phía Client (Browser)
|
|
243
|
+
|
|
244
|
+
Sử dụng `createHttpEventSink` với tuỳ chọn lọc `eventTypes` để chỉ chuyển tiếp các sự kiện sạch:
|
|
245
|
+
|
|
246
|
+
```ts
|
|
247
|
+
import { KindyEditor, createHttpEventSink } from "kindy-editor";
|
|
248
|
+
|
|
249
|
+
// 1. Khởi tạo HTTP Sink chỉ nhận Semantic Events
|
|
250
|
+
const sink = createHttpEventSink({
|
|
251
|
+
endpoint: "https://api.yourcompany.com/v1/editor/events",
|
|
252
|
+
headers: async () => ({
|
|
253
|
+
Authorization: `Bearer ${await getAuthToken()}`,
|
|
254
|
+
}),
|
|
255
|
+
// QUAN TRỌNG: Chỉ gửi event sạch, loại bỏ spam keystroke
|
|
256
|
+
eventTypes: [
|
|
257
|
+
"document.change.committed",
|
|
258
|
+
"document.export.completed",
|
|
259
|
+
"review.comment.added",
|
|
260
|
+
"review.comment.deleted",
|
|
261
|
+
],
|
|
262
|
+
batchSize: 25,
|
|
263
|
+
flushIntervalMs: 1000,
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
// 2. Khởi tạo Editor
|
|
267
|
+
const editor = new KindyEditor({
|
|
268
|
+
container: document.getElementById("editor-container")!,
|
|
269
|
+
user: { id: "usr-01", firstName: "Văn", lastName: "Trần" },
|
|
270
|
+
events: { sink },
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
// 3. Đảm bảo flush dữ liệu khi người dùng đóng trang
|
|
274
|
+
window.addEventListener("pagehide", () => void sink.flush());
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
### 6.2. Phía Server Consumer (Node.js / Express / NestJS)
|
|
278
|
+
|
|
279
|
+
Consumer cần đảm bảo tính Idempotency và xử lý đúng thứ tự phiên bản:
|
|
280
|
+
|
|
281
|
+
```ts
|
|
282
|
+
app.post("/v1/editor/events", async (req, res) => {
|
|
283
|
+
const { events } = req.body;
|
|
284
|
+
|
|
285
|
+
for (const event of events) {
|
|
286
|
+
if (event.type === "document.change.committed") {
|
|
287
|
+
const { id, document, actor, occurredAt, data } = event;
|
|
288
|
+
const { changeGroupId, afterVersion, origin, textDiffs } = data;
|
|
289
|
+
|
|
290
|
+
// 1. Chống duplicate delivery dựa trên event.id
|
|
291
|
+
if (await db.processedEvents.exists(id)) {
|
|
292
|
+
continue;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// 2. Chống out-of-order (sự kiện cũ đến muộn không được ghi đè bản mới)
|
|
296
|
+
const currentDoc = await db.documents.find(document.id);
|
|
297
|
+
if (currentDoc && currentDoc.version >= afterVersion) {
|
|
298
|
+
// Ghi nhận audit log nhưng không update state
|
|
299
|
+
await db.auditLogs.insert({ eventId: id, ignoredState: true });
|
|
300
|
+
await db.processedEvents.insert(id);
|
|
301
|
+
continue;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// 3. Lưu Audit Log ngữ nghĩa (Ai sửa? Sửa từ gì sang gì? Lúc nào?)
|
|
305
|
+
await db.auditLogs.insert({
|
|
306
|
+
eventId: id,
|
|
307
|
+
documentId: document.id,
|
|
308
|
+
changeGroupId,
|
|
309
|
+
actor: actor ? `${actor.firstName} ${actor.lastName}` : "Unknown",
|
|
310
|
+
origin,
|
|
311
|
+
occurredAt,
|
|
312
|
+
textDiffs, // Mảng [{ action, blockId, offset, fromText, toText }]
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
// 4. Cập nhật phiên bản văn bản hiện tại (UPSERT)
|
|
316
|
+
await db.documents.upsert(
|
|
317
|
+
{ id: document.id },
|
|
318
|
+
{ version: afterVersion, lastModifiedAt: occurredAt }
|
|
319
|
+
);
|
|
320
|
+
|
|
321
|
+
// 5. Đánh dấu đã xử lý
|
|
322
|
+
await db.processedEvents.insert(id);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
res.status(202).json({ success: true });
|
|
327
|
+
});
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
---
|
|
331
|
+
|
|
332
|
+
## 7. Xác định Vị trí Dòng/Đoạn & Ngữ cảnh Thay đổi (Location & Context Mapping)
|
|
333
|
+
|
|
334
|
+
### 7.1. Tại sao hệ thống quản lý theo "Đoạn (Block)" thay vì "Dòng (Line)"?
|
|
335
|
+
Trong các trình soạn thảo tài liệu chuẩn Word/Canvas (Page Layout Engine):
|
|
336
|
+
* **Số dòng hiển thị (Visual Lines)** không cố định: Khi bạn đổi cỡ chữ, đổi lề trang (margins), đổi khổ giấy từ A4 sang Letter, hoặc thay đổi zoom thì số dòng trên màn hình sẽ tự động co giãn và ngắt dòng khác nhau (*soft line break*).
|
|
337
|
+
* Do đó, đơn vị cấu trúc ngữ nghĩa vững chắc và duy nhất của tài liệu là **Block (Đoạn văn - Paragraph)** có `blockId` cố định và vị trí ký tự **`offset`** (tính từ ký tự 0 đến cuối đoạn).
|
|
338
|
+
|
|
339
|
+
### 7.2. Cách tra cứu Vị trí Đoạn văn & Ngữ cảnh Câu văn từ `blockId`
|
|
340
|
+
Để biến `blockId: "p-5"` thành thông tin con người dễ đọc hiểu (*"Đoạn 2, mục 1. Thời hạn hợp đồng"*), phía tích hợp có thể tra cứu trực tiếp từ cây tài liệu của Editor:
|
|
341
|
+
|
|
342
|
+
```ts
|
|
343
|
+
import type { KindyEditor } from "kindy-editor";
|
|
344
|
+
|
|
345
|
+
/** Tra cứu vị trí đoạn văn, tiêu đề mục bao bọc và ngữ cảnh cả câu */
|
|
346
|
+
export async function resolveChangeLocation(
|
|
347
|
+
editor: KindyEditor,
|
|
348
|
+
diff: { blockId: string; offset: number; fromText: string; toText: string }
|
|
349
|
+
) {
|
|
350
|
+
const handle = await editor.whenReady();
|
|
351
|
+
const doc = handle.getDocument();
|
|
352
|
+
const blocks = doc.blocks;
|
|
353
|
+
|
|
354
|
+
const blockIndex = blocks.findIndex((b) => b.id === diff.blockId);
|
|
355
|
+
if (blockIndex === -1) return null;
|
|
356
|
+
|
|
357
|
+
const targetBlock = blocks[blockIndex];
|
|
358
|
+
const blockText = targetBlock.kind === "paragraph"
|
|
359
|
+
? targetBlock.runs.map((r) => r.text).join("")
|
|
360
|
+
: "";
|
|
361
|
+
|
|
362
|
+
// 1. Tìm tiêu đề (Heading) gần nhất phía trên đoạn này
|
|
363
|
+
let nearestHeading = "";
|
|
364
|
+
for (let i = blockIndex; i >= 0; i--) {
|
|
365
|
+
const b = blocks[i];
|
|
366
|
+
if (b.kind === "paragraph" && b.style?.outlineLevel !== undefined && b.style.outlineLevel <= 2) {
|
|
367
|
+
nearestHeading = b.runs.map((r) => r.text).join("");
|
|
368
|
+
break;
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// 2. Trích xuất ngữ cảnh cả câu văn xung quanh phần sửa
|
|
373
|
+
const start = Math.max(0, diff.offset - 30);
|
|
374
|
+
const end = Math.min(blockText.length, diff.offset + (diff.toText.length || diff.fromText.length) + 30);
|
|
375
|
+
const prefix = (start > 0 ? "…" : "") + blockText.slice(start, diff.offset);
|
|
376
|
+
const suffix = blockText.slice(diff.offset + (diff.toText.length || 0), end) + (end < blockText.length ? "…" : "");
|
|
377
|
+
|
|
378
|
+
const sentenceContext = `${prefix}[~${diff.fromText}~ ➔ ${diff.toText}]${suffix}`;
|
|
379
|
+
|
|
380
|
+
return {
|
|
381
|
+
paragraphNumber: blockIndex + 1, // Đoạn thứ mấy trong tài liệu
|
|
382
|
+
totalParagraphs: blocks.length,
|
|
383
|
+
heading: nearestHeading, // Nằm dưới mục nào
|
|
384
|
+
fullText: blockText,
|
|
385
|
+
sentenceContext, // Ngữ cảnh câu văn
|
|
386
|
+
};
|
|
387
|
+
}
|
|
388
|
+
```
|
|
389
|
+
|
|
390
|
+
### 7.3. Tương tác Thị giác 2 chiều: Cuộn đến dòng & Highlight phát sáng
|
|
391
|
+
Khi hiển thị danh sách lịch sử sửa đổi (Change History panel), bạn có thể cho phép người dùng bấm vào một thay đổi để **Editor tự cuộn trang đến đúng dòng đó** và **vẽ viền phát sáng (pulse highlight)** trực tiếp lên trang văn bản:
|
|
392
|
+
|
|
393
|
+
```ts
|
|
394
|
+
/** Bấm vào sự kiện -> Tự động nhảy đến vị trí sửa trên trang soạn thảo */
|
|
395
|
+
export async function jumpToDiff(
|
|
396
|
+
editor: KindyEditor,
|
|
397
|
+
diff: { blockId: string; offset: number; fromText: string; toText: string }
|
|
398
|
+
) {
|
|
399
|
+
const length = Math.max(1, diff.toText.length || diff.fromText.length);
|
|
400
|
+
const range = {
|
|
401
|
+
anchor: { blockId: diff.blockId, offset: diff.offset },
|
|
402
|
+
focus: { blockId: diff.blockId, offset: diff.offset + length },
|
|
403
|
+
};
|
|
404
|
+
|
|
405
|
+
// 1. Cuộn trang êm ái tới đúng vị trí đoạn văn
|
|
406
|
+
await editor.revealRange(range, { behavior: "smooth", align: "center" });
|
|
407
|
+
|
|
408
|
+
// 2. Vẽ viền highlight phát sáng trong 2.5 giây
|
|
409
|
+
await editor.setExternalAnnotations([
|
|
410
|
+
{
|
|
411
|
+
id: "change-pulse-highlight",
|
|
412
|
+
range,
|
|
413
|
+
style: {
|
|
414
|
+
background: "rgba(16, 185, 129, 0.4)", // Màu xanh ngọc nhẹ
|
|
415
|
+
borderColor: "#059669",
|
|
416
|
+
borderWidth: 2,
|
|
417
|
+
},
|
|
418
|
+
},
|
|
419
|
+
]);
|
|
420
|
+
|
|
421
|
+
setTimeout(() => {
|
|
422
|
+
void editor.clearExternalAnnotations();
|
|
423
|
+
}, 2500);
|
|
424
|
+
}
|
|
425
|
+
```
|
|
426
|
+
|
|
427
|
+
---
|
|
428
|
+
|
|
429
|
+
## 8. Chế độ Track Changes Thị giác Trực tiếp (Visual Suggest Mode)
|
|
430
|
+
|
|
431
|
+
Hệ thống của Kindy Editor phân biệt rõ 2 nhu cầu:
|
|
432
|
+
1. **Audit / Event Tracking (Chạy ngầm)**: Người dùng soạn thảo bình thường (chữ hiển thị sạch sẽ), nhưng toàn bộ hành vi sửa đổi được ghi lại dưới dạng event `document.change.committed` cho DB / AI.
|
|
433
|
+
2. **Visual Suggest Mode (Hiển thị vết mực trên trang giấy giống Word / Google Docs)**: Chữ cũ bị gạch ngang màu đỏ, chữ mới được gạch chân màu xanh, kèm bong bóng nhận xét ở lề phải.
|
|
434
|
+
|
|
435
|
+
### 8.1. Kích hoạt Chế độ Suggest Mode
|
|
436
|
+
Bạn có thể thiết lập chế độ này khi khởi tạo hoặc chuyển đổi linh hoạt qua API:
|
|
437
|
+
|
|
438
|
+
```ts
|
|
439
|
+
// Cách 1: Thiết lập ngay khi khởi tạo
|
|
440
|
+
const editor = new KindyEditor({
|
|
441
|
+
container: document.getElementById("editor")!,
|
|
442
|
+
mode: "suggest", // Chế độ gợi ý chỉnh sửa
|
|
443
|
+
allowedModes: ["edit", "suggest", "view"],
|
|
444
|
+
});
|
|
445
|
+
|
|
446
|
+
// Cách 2: Chuyển đổi qua API
|
|
447
|
+
await editor.setMode("suggest");
|
|
448
|
+
```
|
|
449
|
+
|
|
450
|
+
### 8.2. Phê duyệt hoặc Từ chối Thay đổi (Accept / Reject)
|
|
451
|
+
Khi ở chế độ `suggest`, người duyệt có thể duyệt từng thay đổi hoặc duyệt toàn bộ:
|
|
452
|
+
|
|
453
|
+
```ts
|
|
454
|
+
// Duyệt 1 đề xuất cụ thể
|
|
455
|
+
await editor.acceptSuggestion(suggestionId);
|
|
456
|
+
|
|
457
|
+
// Từ chối 1 đề xuất
|
|
458
|
+
await editor.rejectSuggestion(suggestionId);
|
|
459
|
+
|
|
460
|
+
// Duyệt tất cả đề xuất trong tài liệu
|
|
461
|
+
await editor.acceptAllSuggestions();
|
|
462
|
+
|
|
463
|
+
// Từ chối tất cả đề xuất
|
|
464
|
+
await editor.rejectAllSuggestions();
|
|
465
|
+
```
|
|
466
|
+
|
|
467
|
+
---
|
|
468
|
+
|
|
469
|
+
## 9. Thiết kế Database Schema Hoàn chỉnh cho Audit Log & Version History
|
|
470
|
+
|
|
471
|
+
Dưới đây là schema quan hệ chuẩn (PostgreSQL / MySQL) được khuyến nghị để lưu trữ toàn diện:
|
|
472
|
+
|
|
473
|
+
### 9.1. Bảng `document_change_sessions` (Lưu từng phiên commit)
|
|
474
|
+
```sql
|
|
475
|
+
CREATE TABLE document_change_sessions (
|
|
476
|
+
id VARCHAR(64) PRIMARY KEY, -- changeGroupId (vd: chg-176239)
|
|
477
|
+
document_id VARCHAR(64) NOT NULL,
|
|
478
|
+
event_id VARCHAR(64) UNIQUE NOT NULL, -- event.id (chống duplicate)
|
|
479
|
+
actor_id VARCHAR(64), -- actor.id (người thực hiện)
|
|
480
|
+
actor_name VARCHAR(255), -- Tên người sửa
|
|
481
|
+
origin VARCHAR(32) NOT NULL, -- typing, paste, command, undo...
|
|
482
|
+
before_version INT NOT NULL, -- Phiên bản trước
|
|
483
|
+
after_version INT NOT NULL, -- Phiên bản sau
|
|
484
|
+
occurred_at TIMESTAMP WITH TIME ZONE NOT NULL,
|
|
485
|
+
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
|
486
|
+
);
|
|
487
|
+
|
|
488
|
+
CREATE INDEX idx_change_sessions_doc ON document_change_sessions(document_id, occurred_at);
|
|
489
|
+
```
|
|
490
|
+
|
|
491
|
+
### 9.2. Bảng `document_text_diffs` (Lưu chi tiết từng đoạn văn bản đổi)
|
|
492
|
+
```sql
|
|
493
|
+
CREATE TABLE document_text_diffs (
|
|
494
|
+
id BIGSERIAL PRIMARY KEY,
|
|
495
|
+
session_id VARCHAR(64) REFERENCES document_change_sessions(id) ON DELETE CASCADE,
|
|
496
|
+
document_id VARCHAR(64) NOT NULL,
|
|
497
|
+
action VARCHAR(16) NOT NULL, -- insert, delete, replace
|
|
498
|
+
block_id VARCHAR(64) NOT NULL, -- ID đoạn văn (vd: p-5)
|
|
499
|
+
paragraph_number INT, -- Đoạn số mấy (vd: Đoạn 2)
|
|
500
|
+
heading_title VARCHAR(255), -- Tiêu đề mục cha (nếu có)
|
|
501
|
+
offset_start INT NOT NULL,
|
|
502
|
+
from_text TEXT, -- Text cũ
|
|
503
|
+
to_text TEXT, -- Text mới
|
|
504
|
+
sentence_context TEXT -- Trích xuất cả câu văn ngữ cảnh
|
|
505
|
+
);
|
|
506
|
+
|
|
507
|
+
CREATE INDEX idx_diffs_doc_block ON document_text_diffs(document_id, block_id);
|
|
508
|
+
```
|
|
509
|
+
|
|
510
|
+
### 9.3. Câu lệnh Truy vấn Mẫu (SQL Queries)
|
|
511
|
+
**Xem lịch sử thay đổi của một tài liệu theo thứ tự thời gian:**
|
|
512
|
+
```sql
|
|
513
|
+
SELECT
|
|
514
|
+
s.occurred_at,
|
|
515
|
+
s.actor_name,
|
|
516
|
+
s.origin,
|
|
517
|
+
s.before_version || ' ➔ ' || s.after_version AS version_transition,
|
|
518
|
+
d.action,
|
|
519
|
+
d.paragraph_number,
|
|
520
|
+
d.heading_title,
|
|
521
|
+
d.from_text,
|
|
522
|
+
d.to_text,
|
|
523
|
+
d.sentence_context
|
|
524
|
+
FROM document_change_sessions s
|
|
525
|
+
JOIN document_text_diffs d ON s.id = d.session_id
|
|
526
|
+
WHERE s.document_id = 'contract-2026'
|
|
527
|
+
ORDER BY s.occurred_at DESC;
|
|
528
|
+
```
|
|
529
|
+
|
|
530
|
+
---
|
|
531
|
+
|
|
532
|
+
## 10. Checklist Tích hợp & Best Practices khi Triển khai
|
|
533
|
+
|
|
534
|
+
1. **Chỉ gửi Semantic Events ra ngoài**: Luôn cấu hình `eventTypes: ["document.change.committed", ...]` trong `createHttpEventSink`. Tránh lưu `document.change.applied` vào cơ sở dữ liệu trừ khi bạn có nhu cầu đặc thù là Replay từng phím gõ.
|
|
535
|
+
2. **Đảm bảo tính Idempotency**: Mọi sự kiện đều có trường `event.id` duy nhất. Phía Backend Consumer bắt buộc phải kiểm tra `event.id` trước khi xử lý để chống duplicate do cơ chế retry mạng.
|
|
536
|
+
3. **Flush dữ liệu khi đóng trang**: Luôn gắn listener `window.addEventListener("pagehide", () => void sink.flush())` để không bỏ sót các thao tác gõ cuối cùng trước khi người dùng đóng trình duyệt.
|
|
537
|
+
4. **Bảo mật & Redaction dữ liệu nhạy cảm**: Nếu văn bản có chứa thông tin nhạy cảm (mật khẩu, số thẻ ngân hàng, CCCD), hãy sử dụng callback `redact` trong cấu hình `events`:
|
|
538
|
+
```ts
|
|
539
|
+
const editor = new KindyEditor({
|
|
540
|
+
container,
|
|
541
|
+
events: {
|
|
542
|
+
sink,
|
|
543
|
+
redact: (event) => {
|
|
544
|
+
// Lọc hoặc che mờ dữ liệu nhạy cảm trong event.data trước khi gửi đi
|
|
545
|
+
return event;
|
|
546
|
+
},
|
|
547
|
+
},
|
|
548
|
+
});
|
|
549
|
+
```
|
|
550
|
+
5. **Định vị trực quan nâng cao**: Sử dụng kết hợp `editor.revealRange()` và `editor.setExternalAnnotations()` trên Frontend để người dùng bấm vào danh sách lịch sử sửa đổi là màn hình tự nhảy tới đúng dòng và highlight phát sáng.
|
|
551
|
+
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "kindy-editor",
|
|
3
|
-
"version": "3.2.
|
|
3
|
+
"version": "3.2.3",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Trình soạn thảo tài liệu Word chính xác, vẽ bằng canvas",
|
|
6
6
|
"license": "MIT",
|
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
"files": [
|
|
41
41
|
"dist-lib",
|
|
42
42
|
"types",
|
|
43
|
-
"docs",
|
|
43
|
+
"docs/*.md",
|
|
44
44
|
"README.md"
|
|
45
45
|
],
|
|
46
46
|
"scripts": {
|