@phuong-tran-redoc/document-engine-core 0.0.36 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,51 +1,55 @@
1
1
  {
2
2
  "name": "@phuong-tran-redoc/document-engine-core",
3
- "version": "0.0.36",
3
+ "version": "0.1.0",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
- "author": "Duc Phuong (Jack) <tdp99.business@gmail.com>",
6
+ "author": "Realestatedoc (Redoc)",
7
+ "contributors": [
8
+ "Duc Phuong (Jack) <tdp99.business@gmail.com>"
9
+ ],
7
10
  "description": "Framework-agnostic core library for Document Engine",
8
11
  "main": "./src/index.js",
9
12
  "types": "./src/index.d.ts",
10
13
  "repository": {
11
14
  "type": "git",
12
- "url": "https://github.com/phuong-tran-redoc/document-engine"
15
+ "url": "git+https://github.com/phuong-tran-redoc/document-engine.git"
13
16
  },
14
17
  "publishConfig": {
15
18
  "directory": "../../dist/libs/document-engine-core",
16
19
  "access": "public"
17
20
  },
18
21
  "dependencies": {
19
- "tslib": "^2.3.0",
20
- "@tiptap/core": "^3.10.5",
21
- "@tiptap/extension-blockquote": "^3.10.5",
22
- "@tiptap/extension-bold": "^3.10.5",
23
- "@tiptap/extension-bubble-menu": "^3.10.5",
24
- "@tiptap/extension-code": "^3.10.5",
25
- "@tiptap/extension-code-block": "^3.10.5",
26
- "@tiptap/extension-document": "^3.10.5",
27
- "@tiptap/extension-floating-menu": "^3.10.5",
28
- "@tiptap/extension-hard-break": "^3.10.5",
29
- "@tiptap/extension-heading": "^3.10.5",
30
- "@tiptap/extension-horizontal-rule": "^3.10.5",
31
- "@tiptap/extension-image": "^3.10.5",
32
- "@tiptap/extension-italic": "^3.10.5",
33
- "@tiptap/extension-link": "^3.10.5",
34
- "@tiptap/extension-list": "^3.10.5",
35
- "@tiptap/extension-paragraph": "^3.10.5",
36
- "@tiptap/extension-strike": "^3.10.5",
37
- "@tiptap/extension-subscript": "^3.10.5",
38
- "@tiptap/extension-superscript": "^3.10.5",
39
- "@tiptap/extension-table": "^3.10.5",
40
- "@tiptap/extension-text": "^3.10.5",
41
- "@tiptap/extension-text-align": "^3.10.5",
42
- "@tiptap/extension-text-style": "^3.10.5",
43
- "@tiptap/extension-underline": "^3.10.5",
44
- "@tiptap/extensions": "^3.10.5",
45
- "@tiptap/pm": "^3.10.5"
22
+ "@tiptap/core": "^3.26.0",
23
+ "@tiptap/extension-blockquote": "^3.26.0",
24
+ "@tiptap/extension-bold": "^3.26.0",
25
+ "@tiptap/extension-bubble-menu": "^3.26.0",
26
+ "@tiptap/extension-code": "^3.26.0",
27
+ "@tiptap/extension-code-block": "^3.26.0",
28
+ "@tiptap/extension-document": "^3.26.0",
29
+ "@tiptap/extension-floating-menu": "^3.26.0",
30
+ "@tiptap/extension-hard-break": "^3.26.0",
31
+ "@tiptap/extension-heading": "^3.26.0",
32
+ "@tiptap/extension-horizontal-rule": "^3.26.0",
33
+ "@tiptap/extension-image": "^3.26.0",
34
+ "@tiptap/extension-italic": "^3.26.0",
35
+ "@tiptap/extension-link": "^3.26.0",
36
+ "@tiptap/extension-list": "^3.26.0",
37
+ "@tiptap/extension-paragraph": "^3.26.0",
38
+ "@tiptap/extension-strike": "^3.26.0",
39
+ "@tiptap/extension-subscript": "^3.26.0",
40
+ "@tiptap/extension-superscript": "^3.26.0",
41
+ "@tiptap/extension-table": "^3.26.0",
42
+ "@tiptap/extension-text": "^3.26.0",
43
+ "@tiptap/extension-text-align": "^3.26.0",
44
+ "@tiptap/extension-text-style": "^3.26.0",
45
+ "@tiptap/extension-underline": "^3.26.0",
46
+ "@tiptap/extensions": "^3.26.0",
47
+ "@tiptap/html": "^3.26.0",
48
+ "@tiptap/pm": "^3.26.0",
49
+ "tslib": "^2.3.0"
46
50
  },
47
51
  "peerDependencies": {
48
52
  "lodash-es": "^4.17.10"
49
53
  },
50
54
  "module": "./src/index.js"
51
- }
55
+ }
@@ -0,0 +1,13 @@
1
+ import { Editor } from '@tiptap/core';
2
+ import type { Extensions } from '@tiptap/core';
3
+ /**
4
+ * Create a test editor instance with minimal required extensions
5
+ * @param extensions Additional extensions to include
6
+ * @param content Initial content (HTML string or JSON)
7
+ * @returns Editor instance
8
+ */
9
+ export declare function createTestEditor(extensions?: Extensions, content?: string | Record<string, unknown>): Editor;
10
+ /**
11
+ * Destroy editor and cleanup
12
+ */
13
+ export declare function destroyEditor(editor: Editor): void;
@@ -0,0 +1,25 @@
1
+ import { Editor } from '@tiptap/core';
2
+ import { Document } from '@tiptap/extension-document';
3
+ import { Paragraph } from '@tiptap/extension-paragraph';
4
+ import { Text } from '@tiptap/extension-text';
5
+ /**
6
+ * Create a test editor instance with minimal required extensions
7
+ * @param extensions Additional extensions to include
8
+ * @param content Initial content (HTML string or JSON)
9
+ * @returns Editor instance
10
+ */ export function createTestEditor(extensions = [], content = '') {
11
+ return new Editor({
12
+ extensions: [
13
+ Document,
14
+ Paragraph,
15
+ Text,
16
+ ...extensions
17
+ ],
18
+ content
19
+ });
20
+ }
21
+ /**
22
+ * Destroy editor and cleanup
23
+ */ export function destroyEditor(editor) {
24
+ editor.destroy();
25
+ }
@@ -12,7 +12,16 @@ export const CustomOrderedList = OrderedList.extend({
12
12
  ...this.parent?.(),
13
13
  'data-list-style-type': {
14
14
  default: this.options.listStyleType,
15
- parseHTML: (element)=>element.getAttribute('data-list-style-type') || this.options.listStyleType,
15
+ parseHTML: (element)=>{
16
+ // Priority 1: Parse from data-list-style-type attribute (our format)
17
+ const dataAttr = element.getAttribute('data-list-style-type');
18
+ if (dataAttr) return dataAttr;
19
+ // Priority 2: Parse from CSS list-style-type property (other editors' format)
20
+ const styleAttr = element.style.listStyleType;
21
+ if (styleAttr) return styleAttr;
22
+ // Default fallback
23
+ return this.options.listStyleType;
24
+ },
16
25
  renderHTML: (attributes)=>{
17
26
  const styleType = attributes['data-list-style-type'];
18
27
  if (!styleType || styleType === 'decimal') return {
@@ -10,13 +10,13 @@ export const EditableRegion = Node.create({
10
10
  parseHTML () {
11
11
  return [
12
12
  // ----------------------------------------------------------
13
- // RULE 1: Standard Tiptap Format (Ưu tiên cao nhất)
13
+ // RULE 1: Standard Tiptap Format (highest priority)
14
14
  // ----------------------------------------------------------
15
15
  {
16
16
  tag: 'span[data-editable-region]'
17
17
  },
18
18
  // ----------------------------------------------------------
19
- // RULE 2: CKEditor Format (Ưu tiên thấp nhất)
19
+ // RULE 2: CKEditor Format (lowest priority)
20
20
  // ----------------------------------------------------------
21
21
  {
22
22
  tag: 'span.restricted-editing-exception'
@@ -1,6 +1,6 @@
1
1
  import { Extension } from '@tiptap/core';
2
2
  /**
3
3
  * Extension PercentageColumnResizing
4
- * * Thay thế logic resize cột mặc định bằng logic dựa trên %.
4
+ * * Replaces the default column-resize logic with percentage-based logic.
5
5
  */
6
6
  export declare const PercentageColumnResizing: Extension<any, any>;
@@ -4,55 +4,55 @@ import { tableNodeTypes } from '@tiptap/pm/tables';
4
4
  import { Decoration, DecorationSet } from '@tiptap/pm/view';
5
5
  import { getTableDOMFromView } from '../utils';
6
6
  /**
7
- * Hàm xử lý khi di chuột (kéo)
7
+ * Handler for mouse move (dragging)
8
8
  */ function handleMouseMoveEvent(event, dragInfo) {
9
9
  if (!dragInfo.isDragging) return;
10
10
  event.preventDefault();
11
11
  const { startX, tableWidthPx, initialLeftWidth, initialRightWidth, leftColIndex, tablePos, tableNode, view } = dragInfo;
12
- // 1. Tính toán delta (pixel)
12
+ // 1. Compute the delta (pixels)
13
13
  const deltaX = event.clientX - startX;
14
- // 2. Chuyển delta pixel sang delta percentage
15
- // Đây là logic quan trọng nhất
14
+ // 2. Convert the pixel delta into a percentage delta
15
+ // This is the most important piece of logic
16
16
  const deltaPercent = deltaX / tableWidthPx * 100;
17
- // 3. Tính toán width mới
17
+ // 3. Compute the new widths
18
18
  let newLeftWidth = initialLeftWidth + deltaPercent;
19
19
  let newRightWidth = initialRightWidth - deltaPercent;
20
- // 4. Áp dụng ràng buộc (ví dụ: min-width 5%)
21
- const minWidth = 5; // Cấu hình min-width
20
+ // 4. Apply constraints (e.g. min-width 5%)
21
+ const minWidth = 5; // min-width configuration
22
22
  if (newLeftWidth < minWidth) {
23
23
  const diff = minWidth - newLeftWidth;
24
24
  newLeftWidth = minWidth;
25
- newRightWidth = newRightWidth - diff; // Cột phải bù phần bị thiếu
25
+ newRightWidth = newRightWidth - diff; // Right column absorbs the shortfall
26
26
  } else if (newRightWidth < minWidth) {
27
27
  const diff = minWidth - newRightWidth;
28
28
  newRightWidth = minWidth;
29
- newLeftWidth = newLeftWidth - diff; // Cột trái bù phần bị thiếu
29
+ newLeftWidth = newLeftWidth - diff; // Left column absorbs the shortfall
30
30
  }
31
- // Đảm bảo không có cột nào bị âm (nếu kéo quá nhanh)
31
+ // Make sure no column goes negative (in case of a very fast drag)
32
32
  if (newLeftWidth < 0) newLeftWidth = 0;
33
33
  if (newRightWidth < 0) newRightWidth = 0;
34
- // 5. Tạo mảng colwidths mới
34
+ // 5. Build the new colwidths array
35
35
  const newColWidths = [
36
36
  ...tableNode?.attrs?.['colwidths']
37
37
  ];
38
38
  newColWidths[leftColIndex] = newLeftWidth;
39
39
  newColWidths[leftColIndex + 1] = newRightWidth;
40
- // 6. Dispatch transaction để cập nhật state
40
+ // 6. Dispatch a transaction to update the state
41
41
  if (tableNode && view) {
42
42
  const tr = view.state.tr.setNodeMarkup(tablePos, null, {
43
43
  ...tableNode.attrs,
44
44
  colwidths: newColWidths
45
45
  });
46
- // Dùng dispatchTransaction để Tiptap/ProseMirror tự cập nhật UI
46
+ // Use dispatchTransaction so Tiptap/ProseMirror updates the UI itself
47
47
  view.dispatch(tr);
48
48
  }
49
49
  }
50
50
  /**
51
- * Hàm xử lý khi nhả chuột (kết thúc kéo)
51
+ * Handler for mouse up (end of drag)
52
52
  */ function handleMouseUpEvent(event, dragInfo) {
53
53
  if (!dragInfo.isDragging) return;
54
54
  event.preventDefault();
55
- // Reset trạng thái
55
+ // Reset the state
56
56
  dragInfo = {
57
57
  isDragging: false,
58
58
  startX: 0,
@@ -67,12 +67,12 @@ import { getTableDOMFromView } from '../utils';
67
67
  }
68
68
  /**
69
69
  * Extension PercentageColumnResizing
70
- * * Thay thế logic resize cột mặc định bằng logic dựa trên %.
70
+ * * Replaces the default column-resize logic with percentage-based logic.
71
71
  */ export const PercentageColumnResizing = Extension.create({
72
72
  name: 'percentageColumnResizing',
73
73
  addProseMirrorPlugins () {
74
- // Một đối tượng tạm thời để lưu trữ trạng thái kéo-thả
75
- // Chúng ta không dùng Plugin State vì nó không cần thiết phải "undo"
74
+ // A temporary object to hold the drag-and-drop state
75
+ // We don't use Plugin State because this state never needs to be "undone"
76
76
  let dragInfo = {
77
77
  isDragging: false,
78
78
  startX: 0,
@@ -90,40 +90,40 @@ import { getTableDOMFromView } from '../utils';
90
90
  key: new PluginKey('percentageColumnResizing'),
91
91
  props: {
92
92
  /**
93
- * Vẽ các resize handles vào DOM
93
+ * Draw the resize handles into the DOM
94
94
  */ decorations (state) {
95
95
  if (!editorInstance?.isEditable) return;
96
96
  const decorations = [];
97
97
  const { doc } = state;
98
98
  const tableTypes = tableNodeTypes(state.schema);
99
- // Duyệt qua tất cả node trong document
99
+ // Walk every node in the document
100
100
  doc.descendants((node, pos)=>{
101
101
  if (node.type.name !== tableTypes.table.name) return;
102
102
  const colwidths = node.attrs['colwidths'];
103
103
  if (!colwidths || colwidths.length === 0) return;
104
104
  let accumulatedWidth = 0;
105
- // Tạo handle cho N-1 cột (không cần cho cột cuối)
105
+ // Create a handle for N-1 columns (the last column doesn't need one)
106
106
  for(let i = 0; i < colwidths.length - 1; i++){
107
107
  accumulatedWidth += colwidths[i] ?? 0;
108
- // Tạo một widget 'div' để làm handle
108
+ // Create a 'div' widget to act as the handle
109
109
  const handle = document.createElement('div');
110
110
  handle.className = 'pm-col-resizer';
111
111
  handle.style.left = `calc(${accumulatedWidth}% - 2px)`;
112
- // Lưu vị trí cột để biết đang resize cột nào
112
+ // Store the column position so we know which column is being resized
113
113
  handle.setAttribute('data-col-index', i.toString());
114
114
  handle.setAttribute('data-table-pos', pos.toString());
115
- // Đặt widget ngay sau thẻ <table> (pos + 1)
115
+ // Place the widget right after the <table> tag (pos + 1)
116
116
  decorations.push(Decoration.widget(pos + 1, handle));
117
117
  }
118
118
  });
119
119
  return DecorationSet.create(doc, decorations);
120
120
  },
121
121
  /**
122
- * Xử lý sự kiện mousedown trên handle
122
+ * Handle the mousedown event on a handle
123
123
  */ handleDOMEvents: {
124
124
  mousedown (view, event) {
125
125
  const target = event.target;
126
- // Chỉ kích hoạt khi click vào handle
126
+ // Only trigger when clicking on a handle
127
127
  if (!target.classList.contains('pm-col-resizer')) return false;
128
128
  event.preventDefault();
129
129
  const colIndex = parseInt(target.getAttribute('data-col-index') ?? '0', 10);
@@ -134,7 +134,7 @@ import { getTableDOMFromView } from '../utils';
134
134
  // Get table DOM element (handles NodeView wrappers and nested tables)
135
135
  const tableDOM = getTableDOMFromView(view, tablePos);
136
136
  if (!tableDOM) return false;
137
- // Lưu thông tin cần thiết cho việc kéo
137
+ // Store the info needed for dragging
138
138
  dragInfo = {
139
139
  isDragging: true,
140
140
  startX: event.clientX,
@@ -154,7 +154,7 @@ import { getTableDOMFromView } from '../utils';
154
154
  };
155
155
  window.addEventListener('mousemove', handleMouseMove, true);
156
156
  window.addEventListener('mouseup', handleMouseUp, true);
157
- return true; // Đã xử lý event
157
+ return true; // Event handled
158
158
  }
159
159
  }
160
160
  }
@@ -1,6 +1,5 @@
1
1
  import { Extension, Node } from '@tiptap/core';
2
2
  import { TableKitOptions, TableOptions } from '@tiptap/extension-table';
3
- export declare const MIN_NEW_COL_WIDTH = 5;
4
3
  export declare const TableDefaultAttributes: {
5
4
  border: {
6
5
  style: string;
@@ -37,5 +36,7 @@ declare module '@tiptap/core' {
37
36
  };
38
37
  }
39
38
  }
40
- export declare const StyledTable: Node<TableOptions, any>;
39
+ export declare const StyledTable: Node<TableOptions & {
40
+ enableNodeView?: boolean;
41
+ }, any>;
41
42
  export declare const StyledTableKit: Extension<TableKitOptions, any>;
@@ -2,10 +2,10 @@ import { Extension } from '@tiptap/core';
2
2
  import { Table, TableCell, TableHeader, TableRow } from '@tiptap/extension-table';
3
3
  import { TextSelection } from '@tiptap/pm/state';
4
4
  import { addColumnAfter, addColumnBefore, deleteColumn, selectionCell, TableMap } from '@tiptap/pm/tables';
5
+ import { MIN_NEW_COL_WIDTH } from '../constants/table.constant';
5
6
  import { getCursorCellInfo } from '../utils';
6
7
  import { createTableNodeView } from '../views';
7
8
  import { PercentageColumnResizing } from './table-resizing.extension';
8
- export const MIN_NEW_COL_WIDTH = 5.0;
9
9
  /**
10
10
  * Calculate new column widths when adding a column, ensuring no column goes below MIN_NEW_COL_WIDTH
11
11
  * @param oldColWidths - Array of current column widths
@@ -168,7 +168,7 @@ export const StyledTable = Table.extend({
168
168
  default: null,
169
169
  renderHTML: (attributes)=>{
170
170
  if (!attributes['colwidths']) {
171
- return {}; // Không có gì để render
171
+ return {}; // Nothing to render
172
172
  }
173
173
  return {
174
174
  'data-colwidths': JSON.stringify(attributes['colwidths'])
@@ -198,7 +198,7 @@ export const StyledTable = Table.extend({
198
198
  renderHTML ({ node, HTMLAttributes }) {
199
199
  const colwidths = node.attrs?.['colwidths'];
200
200
  const domNodes = [];
201
- // Render <colgroup> nếu có colwidths
201
+ // Render <colgroup> when colwidths are present
202
202
  if (colwidths) {
203
203
  const colgroup = [
204
204
  'colgroup',
@@ -214,13 +214,13 @@ export const StyledTable = Table.extend({
214
214
  });
215
215
  domNodes.push(colgroup);
216
216
  }
217
- // Thêm <tbody> (0 là 'content hole')
217
+ // Add <tbody> (0 is the 'content hole')
218
218
  domNodes.push([
219
219
  'tbody',
220
220
  0
221
221
  ]);
222
- // Merge các HTML attributes (bao gồm cả global attributes)
223
- // HTMLAttributes đã được Tiptap merge sẵn từ getRenderedAttributes
222
+ // Merge the HTML attributes (including global attributes)
223
+ // HTMLAttributes has already been merged by Tiptap via getRenderedAttributes
224
224
  return [
225
225
  'table',
226
226
  HTMLAttributes,
@@ -257,30 +257,30 @@ export const StyledTable = Table.extend({
257
257
  },
258
258
  insertTable: ({ rows, cols, withHeaderRow })=>({ tr, dispatch, editor })=>{
259
259
  const { schema } = editor.state;
260
- // --- BƯỚC 1: Tính toán colWidths ---
260
+ // --- STEP 1: Compute colWidths ---
261
261
  const calculatedColWidths = [];
262
262
  const defaultWidth = 100 / cols;
263
263
  for(let i = 0; i < cols; i += 1){
264
264
  calculatedColWidths.push(defaultWidth);
265
265
  }
266
- // Ví dụ: cols = 3 -> calculatedColWidths = [33.33, 33.33, 33.33]
267
- // --- BƯỚC 2: Tạo nội dung bảng (rows, cells) ---
268
- // Chúng ta phải tự xây dựng các node con
266
+ // Example: cols = 3 -> calculatedColWidths = [33.33, 33.33, 33.33]
267
+ // --- STEP 2: Build the table content (rows, cells) ---
268
+ // We have to build the child nodes ourselves
269
269
  const cellType = schema.nodes['tableCell'];
270
270
  const headerType = schema.nodes['tableHeader'];
271
271
  const rowType = schema.nodes['tableRow'];
272
272
  const tableRows = [];
273
- // Hàm trợ giúp tạo 1 cell rỗng
273
+ // Helper to create a single empty cell
274
274
  const createEmptyCell = (type)=>{
275
275
  const emptyParagraph = schema.nodes['paragraph'].create();
276
276
  return type.create(null, emptyParagraph);
277
277
  };
278
- // Tạo các hàng
278
+ // Create the rows
279
279
  for(let r = 0; r < rows; r += 1){
280
280
  const tableCells = [];
281
281
  for(let c = 0; c < cols; c += 1){
282
282
  let cellNode;
283
- // Nếu là hàng đầu tiên và có 'withHeaderRow'
283
+ // If this is the first row and 'withHeaderRow' is set
284
284
  if (r === 0 && withHeaderRow) {
285
285
  cellNode = createEmptyCell(headerType);
286
286
  } else {
@@ -288,23 +288,23 @@ export const StyledTable = Table.extend({
288
288
  }
289
289
  tableCells.push(cellNode);
290
290
  }
291
- // Thêm hàng mới vào mảng các hàng
291
+ // Add the new row to the rows array
292
292
  tableRows.push(rowType.create(null, tableCells));
293
293
  }
294
- // --- BƯỚC 3: Tạo node Table chính VỚI ATTRIBUTE 'colwidths' ---
294
+ // --- STEP 3: Create the main Table node WITH the 'colwidths' ATTRIBUTE ---
295
295
  const tableNode = schema.nodes['table'].create({
296
- // Đây là mấu chốt: gán mảng % của chúng ta vào đây
296
+ // This is the key part: assign our percentage array here
297
297
  colwidths: calculatedColWidths
298
- }, tableRows // Nội dung (các node 'tableRow')
298
+ }, tableRows // Content (the 'tableRow' nodes)
299
299
  );
300
- // --- BƯỚC 4: Dispatch transaction (Original code)---
300
+ // --- STEP 4: Dispatch transaction (Original code)---
301
301
  if (dispatch) {
302
302
  const offset = tr.selection.from + 1;
303
303
  tr.replaceSelectionWith(tableNode).scrollIntoView().setSelection(TextSelection.near(tr.doc.resolve(offset)));
304
304
  }
305
305
  return true;
306
306
  },
307
- // 2. LỆNH ADDCOLUMNAFTER
307
+ // 2. ADDCOLUMNAFTER COMMAND
308
308
  addColumnAfter: ()=>({ state, dispatch })=>{
309
309
  const $cell = selectionCell(state);
310
310
  if (!$cell) return false;
@@ -334,7 +334,7 @@ export const StyledTable = Table.extend({
334
334
  }
335
335
  return true;
336
336
  },
337
- // 3. LỆNH ADDCOLUMNBEFORE
337
+ // 3. ADDCOLUMNBEFORE COMMAND
338
338
  addColumnBefore: ()=>({ state, dispatch })=>{
339
339
  const $cell = selectionCell(state);
340
340
  if (!$cell) return false;
@@ -360,7 +360,7 @@ export const StyledTable = Table.extend({
360
360
  }
361
361
  return true;
362
362
  },
363
- // 4. LỆNH DELETECOLUMN
363
+ // 4. DELETECOLUMN COMMAND
364
364
  deleteColumn: ()=>({ state, dispatch })=>{
365
365
  const $cell = selectionCell(state);
366
366
  if (!$cell) return false;
@@ -439,7 +439,7 @@ export const StyledTable = Table.extend({
439
439
  addNodeView () {
440
440
  const options = this.options;
441
441
  if (options.enableNodeView === false) {
442
- return undefined;
442
+ return null;
443
443
  }
444
444
  // Only use custom NodeView (with handle) when editor is editable
445
445
  return (props)=>{
package/src/index.d.ts CHANGED
@@ -1,5 +1,7 @@
1
1
  export * from './constants';
2
2
  export * from './extensions';
3
+ export * from './kit';
4
+ export * from './migrations';
3
5
  export * from './models';
4
6
  export * from './nodes';
5
7
  export * from './types';
package/src/index.js CHANGED
@@ -1,5 +1,7 @@
1
1
  export * from './constants';
2
2
  export * from './extensions';
3
+ export * from './kit';
4
+ export * from './migrations';
3
5
  export * from './models';
4
6
  export * from './nodes';
5
7
  export * from './types';
@@ -0,0 +1,22 @@
1
+ import type { Extensions } from '@tiptap/core';
2
+ /**
3
+ * Canonical default extension list for headless (Node-safe) serialization.
4
+ *
5
+ * This is the schema half of the editor's runtime kit (`DocumentEngineKit` in
6
+ * the Angular wrapper): every node, mark, and HTML-affecting attribute the
7
+ * editor can persist, so {@link generateHTML} can turn stored document JSON back
8
+ * into HTML without a browser.
9
+ *
10
+ * It deliberately omits editing-only plugins (history/undo-redo, drop/gap
11
+ * cursors, placeholder, character count, list keymaps, trailing node). Those
12
+ * shape the live editing experience but contribute nothing to serialized
13
+ * output, and some attach view-level behavior that has no meaning headless.
14
+ *
15
+ * Only `renderHTML` is exercised during serialization — node views (which touch
16
+ * the DOM, e.g. `DynamicField.addNodeView`) are never invoked here, so this list
17
+ * is safe to import and run in Node.
18
+ *
19
+ * Callers needing a different schema (e.g. restricted-editing regions, or a
20
+ * trimmed set) should pass their own array to {@link generateHTML} instead.
21
+ */
22
+ export declare const defaultExtensions: Extensions;
@@ -0,0 +1,86 @@
1
+ import Blockquote from '@tiptap/extension-blockquote';
2
+ import Bold from '@tiptap/extension-bold';
3
+ import Code from '@tiptap/extension-code';
4
+ import CodeBlock from '@tiptap/extension-code-block';
5
+ import { Document } from '@tiptap/extension-document';
6
+ import { HardBreak } from '@tiptap/extension-hard-break';
7
+ import { HorizontalRule } from '@tiptap/extension-horizontal-rule';
8
+ import { Image } from '@tiptap/extension-image';
9
+ import Italic from '@tiptap/extension-italic';
10
+ import Link from '@tiptap/extension-link';
11
+ import { BulletList, ListItem } from '@tiptap/extension-list';
12
+ import { Paragraph } from '@tiptap/extension-paragraph';
13
+ import Strike from '@tiptap/extension-strike';
14
+ import { Subscript } from '@tiptap/extension-subscript';
15
+ import { Superscript } from '@tiptap/extension-superscript';
16
+ import { Text } from '@tiptap/extension-text';
17
+ import { TextAlign } from '@tiptap/extension-text-align';
18
+ import { TextStyleKit } from '@tiptap/extension-text-style';
19
+ import Underline from '@tiptap/extension-underline';
20
+ import { CustomOrderedList } from '../extensions/ordered-list.extension';
21
+ import { StyledTableKit } from '../extensions/table-style.extension';
22
+ import { DynamicField } from '../nodes/dynamic-field.node';
23
+ import { NotumHeading } from '../nodes/heading.node';
24
+ import { ImageRef } from '../nodes/image-ref.node';
25
+ import { PageBreak } from '../nodes/page-break.node';
26
+ /**
27
+ * Canonical default extension list for headless (Node-safe) serialization.
28
+ *
29
+ * This is the schema half of the editor's runtime kit (`DocumentEngineKit` in
30
+ * the Angular wrapper): every node, mark, and HTML-affecting attribute the
31
+ * editor can persist, so {@link generateHTML} can turn stored document JSON back
32
+ * into HTML without a browser.
33
+ *
34
+ * It deliberately omits editing-only plugins (history/undo-redo, drop/gap
35
+ * cursors, placeholder, character count, list keymaps, trailing node). Those
36
+ * shape the live editing experience but contribute nothing to serialized
37
+ * output, and some attach view-level behavior that has no meaning headless.
38
+ *
39
+ * Only `renderHTML` is exercised during serialization — node views (which touch
40
+ * the DOM, e.g. `DynamicField.addNodeView`) are never invoked here, so this list
41
+ * is safe to import and run in Node.
42
+ *
43
+ * Callers needing a different schema (e.g. restricted-editing regions, or a
44
+ * trimmed set) should pass their own array to {@link generateHTML} instead.
45
+ */ export const defaultExtensions = [
46
+ // Structure
47
+ Document,
48
+ Paragraph,
49
+ Text,
50
+ NotumHeading,
51
+ HardBreak,
52
+ HorizontalRule,
53
+ Blockquote,
54
+ CodeBlock,
55
+ // Lists
56
+ BulletList,
57
+ ListItem,
58
+ CustomOrderedList,
59
+ // Marks
60
+ Bold,
61
+ Italic,
62
+ Underline,
63
+ Strike,
64
+ Code,
65
+ Link,
66
+ Subscript,
67
+ Superscript,
68
+ TextStyleKit,
69
+ // Block-level alignment (emits `text-align` only when explicitly set).
70
+ // NOTE: `Indent` is intentionally excluded — its `renderHTML` emits an empty
71
+ // `style=""` on every block even when unset, which pollutes otherwise-semantic
72
+ // output. Consumers that persist indentation can pass a custom extension array.
73
+ TextAlign.configure({
74
+ types: [
75
+ 'paragraph',
76
+ 'heading'
77
+ ]
78
+ }),
79
+ // Content nodes
80
+ Image,
81
+ ImageRef,
82
+ PageBreak,
83
+ DynamicField,
84
+ // Tables (table / row / header / cell nodes)
85
+ StyledTableKit
86
+ ];
@@ -0,0 +1,23 @@
1
+ import type { Extensions, JSONContent } from '@tiptap/core';
2
+ /**
3
+ * Serialize a Tiptap/ProseMirror document (JSON) to an HTML string.
4
+ *
5
+ * Environment-aware and Node-safe. Tiptap ships two serializers and the wrong
6
+ * one throws:
7
+ * - `@tiptap/html` needs a real `document` (browser / jsdom).
8
+ * - `@tiptap/html/server` is backed by happy-dom and runs headlessly in Node.
9
+ *
10
+ * The serializer is chosen at call time from whether a DOM is present, and
11
+ * imported lazily — so the heavy happy-dom build is never pulled into a browser
12
+ * editor bundle that only ever edits (and never serializes server-side), and
13
+ * Node backends never touch the browser-only build. This is why the function is
14
+ * async.
15
+ *
16
+ * @param doc The document as ProseMirror JSON (a `doc` node).
17
+ * @param extensions Schema to serialize against. Defaults to
18
+ * {@link defaultExtensions} (the editor's canonical kit). Pass
19
+ * a custom array when your documents use a different schema.
20
+ * @returns Semantic HTML. Presentation styling and sanitization (e.g. DOMPurify)
21
+ * are the consumer's responsibility — this helper does neither.
22
+ */
23
+ export declare function generateHTML(doc: JSONContent, extensions?: Extensions): Promise<string>;
@@ -0,0 +1,40 @@
1
+ import { defaultExtensions } from './default-extensions';
2
+ /**
3
+ * Serialize a Tiptap/ProseMirror document (JSON) to an HTML string.
4
+ *
5
+ * Environment-aware and Node-safe. Tiptap ships two serializers and the wrong
6
+ * one throws:
7
+ * - `@tiptap/html` needs a real `document` (browser / jsdom).
8
+ * - `@tiptap/html/server` is backed by happy-dom and runs headlessly in Node.
9
+ *
10
+ * The serializer is chosen at call time from whether a DOM is present, and
11
+ * imported lazily — so the heavy happy-dom build is never pulled into a browser
12
+ * editor bundle that only ever edits (and never serializes server-side), and
13
+ * Node backends never touch the browser-only build. This is why the function is
14
+ * async.
15
+ *
16
+ * @param doc The document as ProseMirror JSON (a `doc` node).
17
+ * @param extensions Schema to serialize against. Defaults to
18
+ * {@link defaultExtensions} (the editor's canonical kit). Pass
19
+ * a custom array when your documents use a different schema.
20
+ * @returns Semantic HTML. Presentation styling and sanitization (e.g. DOMPurify)
21
+ * are the consumer's responsibility — this helper does neither.
22
+ */ export async function generateHTML(doc, extensions = defaultExtensions) {
23
+ if (typeof document !== 'undefined') {
24
+ // browser / jsdom: uses the ambient DOM
25
+ const serializer = await import('@tiptap/html');
26
+ return serializer.generateHTML(doc, extensions);
27
+ }
28
+ // Node: happy-dom backed, no real DOM needed. The specifier is built at runtime
29
+ // (not a static string literal) and flagged for webpack/vite so browser
30
+ // bundlers (esbuild/webpack/vite) can't statically resolve it and therefore
31
+ // never pull happy-dom — and its Node built-ins — into a client bundle. The
32
+ // `typeof document` guard means this branch never executes in a browser anyway.
33
+ const serverEntry = [
34
+ '@tiptap',
35
+ 'html',
36
+ 'server'
37
+ ].join('/');
38
+ const serializer = await import(/* webpackIgnore: true */ /* @vite-ignore */ serverEntry);
39
+ return serializer.generateHTML(doc, extensions);
40
+ }
@@ -0,0 +1,2 @@
1
+ export * from './default-extensions';
2
+ export * from './generate-html';
@@ -0,0 +1,2 @@
1
+ export * from './default-extensions';
2
+ export * from './generate-html';
@@ -0,0 +1,27 @@
1
+ import type { DocMigrationRegistry } from './types';
2
+ /**
3
+ * The current schema version. `EditorDocument`s authored by this build of the
4
+ * editor carry this value, and {@link migrateDoc} upgrades older documents up to
5
+ * it.
6
+ *
7
+ * Bump this by exactly one whenever a schema change requires transforming stored
8
+ * content, and add the matching entry (keyed by the previous version) to
9
+ * {@link docMigrations}.
10
+ */
11
+ export declare const LATEST_SCHEMA_VERSION = 1;
12
+ /**
13
+ * Ordered migration steps, keyed by the source version they upgrade from.
14
+ *
15
+ * v0.1.0 ships an identity (empty) set: there is nothing to migrate yet, but the
16
+ * registry, walker ({@link migrateDoc}), and version field exist so future
17
+ * schema bumps are non-breaking. To add one, e.g. for the 1 -> 2 bump:
18
+ *
19
+ * ```ts
20
+ * export const docMigrations: DocMigrationRegistry = {
21
+ * 1: (doc) => ({ schemaVersion: 2, content: transformV1ToV2(doc.content) }),
22
+ * };
23
+ * ```
24
+ *
25
+ * Each entry must be pure (no mutation, no browser APIs).
26
+ */
27
+ export declare const docMigrations: DocMigrationRegistry;
@@ -0,0 +1,24 @@
1
+ /**
2
+ * The current schema version. `EditorDocument`s authored by this build of the
3
+ * editor carry this value, and {@link migrateDoc} upgrades older documents up to
4
+ * it.
5
+ *
6
+ * Bump this by exactly one whenever a schema change requires transforming stored
7
+ * content, and add the matching entry (keyed by the previous version) to
8
+ * {@link docMigrations}.
9
+ */ export const LATEST_SCHEMA_VERSION = 1;
10
+ /**
11
+ * Ordered migration steps, keyed by the source version they upgrade from.
12
+ *
13
+ * v0.1.0 ships an identity (empty) set: there is nothing to migrate yet, but the
14
+ * registry, walker ({@link migrateDoc}), and version field exist so future
15
+ * schema bumps are non-breaking. To add one, e.g. for the 1 -> 2 bump:
16
+ *
17
+ * ```ts
18
+ * export const docMigrations: DocMigrationRegistry = {
19
+ * 1: (doc) => ({ schemaVersion: 2, content: transformV1ToV2(doc.content) }),
20
+ * };
21
+ * ```
22
+ *
23
+ * Each entry must be pure (no mutation, no browser APIs).
24
+ */ export const docMigrations = {};
@@ -0,0 +1,3 @@
1
+ export * from './types';
2
+ export * from './doc-migrations';
3
+ export * from './migrate-doc';
@@ -0,0 +1,3 @@
1
+ export * from './types';
2
+ export * from './doc-migrations';
3
+ export * from './migrate-doc';
@@ -0,0 +1,16 @@
1
+ import type { DocMigrationRegistry, EditorDocument } from './types';
2
+ /**
3
+ * Upgrade a stored document to {@link LATEST_SCHEMA_VERSION} by applying each
4
+ * registered migration in order.
5
+ *
6
+ * Pure and idempotent: a document already at the latest version is returned
7
+ * unchanged (a re-stamped copy), so batch runs are safe to re-run. A document
8
+ * with a missing/invalid `schemaVersion` is treated as version 1 (the earliest).
9
+ *
10
+ * @param doc The document to migrate.
11
+ * @param migrations Registry to apply. Defaults to {@link docMigrations}; the
12
+ * override exists for testing the walker in isolation.
13
+ * @param latest Target version. Defaults to {@link LATEST_SCHEMA_VERSION}.
14
+ * @returns A new `EditorDocument` at `latest`.
15
+ */
16
+ export declare function migrateDoc(doc: EditorDocument, migrations?: DocMigrationRegistry, latest?: number): EditorDocument;
@@ -0,0 +1,32 @@
1
+ import { docMigrations, LATEST_SCHEMA_VERSION } from './doc-migrations';
2
+ /**
3
+ * Upgrade a stored document to {@link LATEST_SCHEMA_VERSION} by applying each
4
+ * registered migration in order.
5
+ *
6
+ * Pure and idempotent: a document already at the latest version is returned
7
+ * unchanged (a re-stamped copy), so batch runs are safe to re-run. A document
8
+ * with a missing/invalid `schemaVersion` is treated as version 1 (the earliest).
9
+ *
10
+ * @param doc The document to migrate.
11
+ * @param migrations Registry to apply. Defaults to {@link docMigrations}; the
12
+ * override exists for testing the walker in isolation.
13
+ * @param latest Target version. Defaults to {@link LATEST_SCHEMA_VERSION}.
14
+ * @returns A new `EditorDocument` at `latest`.
15
+ */ export function migrateDoc(doc, migrations = docMigrations, latest = LATEST_SCHEMA_VERSION) {
16
+ let current = {
17
+ ...doc,
18
+ schemaVersion: Number.isInteger(doc.schemaVersion) && doc.schemaVersion >= 1 ? doc.schemaVersion : 1
19
+ };
20
+ while(current.schemaVersion < latest){
21
+ const migrate = migrations[current.schemaVersion];
22
+ if (!migrate) {
23
+ throw new Error(`[migrateDoc] No migration registered for schema version ${current.schemaVersion} ` + `(target ${latest}).`);
24
+ }
25
+ const next = migrate(current);
26
+ if (next.schemaVersion <= current.schemaVersion) {
27
+ throw new Error(`[migrateDoc] Migration from version ${current.schemaVersion} did not advance the ` + `version (got ${next.schemaVersion}); migrations must be strictly increasing.`);
28
+ }
29
+ current = next;
30
+ }
31
+ return current;
32
+ }
@@ -0,0 +1,25 @@
1
+ import type { JSONContent } from '@tiptap/core';
2
+ /**
3
+ * A stored document plus the schema version its `content` was authored against.
4
+ *
5
+ * Documents are persisted as this thin wrapper (rather than stamping a version
6
+ * attribute onto the ProseMirror `doc` node) so versioning stays a storage-layer
7
+ * concern: the editor schema is untouched, and a backend can read/migrate the
8
+ * version without instantiating an editor.
9
+ */
10
+ export interface EditorDocument {
11
+ /** Schema version `content` conforms to. See {@link LATEST_SCHEMA_VERSION}. */
12
+ schemaVersion: number;
13
+ /** The document body as ProseMirror/Tiptap JSON. */
14
+ content: JSONContent;
15
+ }
16
+ /**
17
+ * A single, pure migration step: upgrade a document from version `N` to `N + 1`.
18
+ * Must not mutate its input and must not depend on browser APIs.
19
+ */
20
+ export type DocMigration = (doc: EditorDocument) => EditorDocument;
21
+ /**
22
+ * Ordered registry of migration steps, keyed by the *source* version each step
23
+ * upgrades from. Entry `N` takes a v`N` document and returns a v`N + 1` one.
24
+ */
25
+ export type DocMigrationRegistry = Record<number, DocMigration>;
@@ -0,0 +1,4 @@
1
+ /**
2
+ * Ordered registry of migration steps, keyed by the *source* version each step
3
+ * upgrades from. Entry `N` takes a v`N` document and returns a v`N + 1` one.
4
+ */ export { };
@@ -0,0 +1,40 @@
1
+ import { Node } from '@tiptap/core';
2
+ /**
3
+ * Where the caption sits relative to the image.
4
+ */
5
+ export type ImageRefCaptionPosition = 'top' | 'bottom';
6
+ /**
7
+ * Attributes carried by an `image-ref` node.
8
+ *
9
+ * Only `imageId` is required. The node is intentionally **presentation-free**:
10
+ * it stores an opaque media id, never a URL or width/style. The real responsive
11
+ * image is resolved downstream (consumer side) from `imageId`.
12
+ */
13
+ export interface ImageRefAttributes {
14
+ imageId: string;
15
+ /** Plain text only — serialized as `<figcaption>` text content; any nested markup is flattened on round-trip. */
16
+ caption?: string | null;
17
+ captionPosition?: ImageRefCaptionPosition | null;
18
+ }
19
+ export interface ImageRefOptions {
20
+ HTMLAttributes: Record<string, unknown>;
21
+ }
22
+ declare module '@tiptap/core' {
23
+ interface Commands<ReturnType> {
24
+ imageRef: {
25
+ /** Insert an `image-ref` block. `imageId` is required. */
26
+ insertImageRef: (attributes: ImageRefAttributes) => ReturnType;
27
+ };
28
+ }
29
+ }
30
+ /**
31
+ * `image-ref` — a semantic, URL-free reference to a media asset.
32
+ *
33
+ * Renders a `<figure data-block="image-ref" data-image-id="…">` (with an optional
34
+ * `<figcaption>`), decoupling document content from the resolved image URL. The
35
+ * consumer hydrates the figure by attribute (`data-image-id`) at render time —
36
+ * the document itself never embeds a URL or any presentation. In the editor the
37
+ * node shows a placeholder (it has no URL to display); resolving and showing the
38
+ * real image is the consumer's / media-picker's job.
39
+ */
40
+ export declare const ImageRef: Node<ImageRefOptions, any>;
@@ -0,0 +1,138 @@
1
+ import { Node, mergeAttributes } from '@tiptap/core';
2
+ /**
3
+ * `image-ref` — a semantic, URL-free reference to a media asset.
4
+ *
5
+ * Renders a `<figure data-block="image-ref" data-image-id="…">` (with an optional
6
+ * `<figcaption>`), decoupling document content from the resolved image URL. The
7
+ * consumer hydrates the figure by attribute (`data-image-id`) at render time —
8
+ * the document itself never embeds a URL or any presentation. In the editor the
9
+ * node shows a placeholder (it has no URL to display); resolving and showing the
10
+ * real image is the consumer's / media-picker's job.
11
+ */ export const ImageRef = Node.create({
12
+ name: 'imageRef',
13
+ group: 'block',
14
+ atom: true,
15
+ selectable: true,
16
+ draggable: true,
17
+ addOptions () {
18
+ return {
19
+ HTMLAttributes: {}
20
+ };
21
+ },
22
+ addAttributes () {
23
+ return {
24
+ imageId: {
25
+ default: null,
26
+ parseHTML: (element)=>element.getAttribute('data-image-id'),
27
+ renderHTML: (attributes)=>{
28
+ if (!attributes['imageId']) return {};
29
+ return {
30
+ 'data-image-id': attributes['imageId']
31
+ };
32
+ }
33
+ },
34
+ caption: {
35
+ default: null,
36
+ // Caption is rendered as <figcaption> text content (below), not a data
37
+ // attribute — so parse it back from the figcaption child for round-trip.
38
+ parseHTML: (element)=>{
39
+ const figcaption = element.querySelector(':scope > figcaption');
40
+ return figcaption?.textContent?.trim() || null;
41
+ },
42
+ renderHTML: ()=>({})
43
+ },
44
+ captionPosition: {
45
+ default: null,
46
+ // Normalize to the typed union: absent attribute stays `null` (preserves
47
+ // the byte-identical round-trip), any present-but-unknown value coerces to
48
+ // `'bottom'` so a non-`'top' | 'bottom'` string can never leak through.
49
+ parseHTML: (element)=>{
50
+ const raw = element.getAttribute('data-caption-position');
51
+ if (raw === null) return null;
52
+ return raw === 'top' ? 'top' : 'bottom';
53
+ },
54
+ renderHTML: (attributes)=>{
55
+ if (!attributes['captionPosition']) return {};
56
+ return {
57
+ 'data-caption-position': attributes['captionPosition']
58
+ };
59
+ }
60
+ }
61
+ };
62
+ },
63
+ parseHTML () {
64
+ return [
65
+ {
66
+ tag: 'figure[data-block="image-ref"]'
67
+ }
68
+ ];
69
+ },
70
+ renderHTML ({ HTMLAttributes, node }) {
71
+ const { caption } = node.attrs;
72
+ const figure = mergeAttributes(this.options.HTMLAttributes, HTMLAttributes, {
73
+ 'data-block': 'image-ref'
74
+ });
75
+ // No <img> is emitted — the asset is URL-free. The caption order relative to
76
+ // the (not-yet-resolved) image is carried by `data-caption-position` for the
77
+ // consumer to honor at hydration time; the figure itself just holds the text.
78
+ if (!caption) {
79
+ return [
80
+ 'figure',
81
+ figure
82
+ ];
83
+ }
84
+ const figcaption = [
85
+ 'figcaption',
86
+ {},
87
+ caption
88
+ ];
89
+ return [
90
+ 'figure',
91
+ figure,
92
+ figcaption
93
+ ];
94
+ },
95
+ /**
96
+ * Editor-only placeholder. The DOM here is chrome (never serialized — that is
97
+ * `renderHTML`'s job); it just shows which media the block references.
98
+ */ addNodeView () {
99
+ return ({ node })=>{
100
+ const { imageId, caption, captionPosition } = node.attrs;
101
+ const dom = document.createElement('figure');
102
+ dom.classList.add('image-ref-placeholder');
103
+ dom.setAttribute('data-block', 'image-ref');
104
+ if (imageId) dom.setAttribute('data-image-id', imageId);
105
+ if (captionPosition) dom.setAttribute('data-caption-position', captionPosition);
106
+ dom.style.cssText = 'margin:1rem 0;display:flex;flex-direction:column;gap:.5rem;';
107
+ const box = document.createElement('div');
108
+ box.style.cssText = 'display:flex;align-items:center;gap:.5rem;padding:1rem;border:1px dashed currentColor;' + 'border-radius:.5rem;opacity:.65;font-size:.875rem;justify-content:center;';
109
+ box.textContent = `🖼 image-ref · id: ${imageId || '—'}`;
110
+ const figcaption = document.createElement('figcaption');
111
+ figcaption.style.cssText = 'font-size:.8125rem;opacity:.7;text-align:center;';
112
+ if (caption) figcaption.textContent = caption;
113
+ if (caption && captionPosition === 'top') {
114
+ dom.append(figcaption, box);
115
+ } else {
116
+ dom.append(box);
117
+ if (caption) dom.append(figcaption);
118
+ }
119
+ return {
120
+ dom
121
+ };
122
+ };
123
+ },
124
+ addCommands () {
125
+ return {
126
+ insertImageRef: (attributes)=>({ chain })=>{
127
+ if (!attributes.imageId) {
128
+ console.error('[ImageRef] imageId is required.');
129
+ return false;
130
+ }
131
+ return chain().insertContent({
132
+ type: this.name,
133
+ attrs: attributes
134
+ }).run();
135
+ }
136
+ };
137
+ }
138
+ });
@@ -1,3 +1,4 @@
1
1
  export * from './heading.node';
2
2
  export * from './dynamic-field.node';
3
3
  export * from './page-break.node';
4
+ export * from './image-ref.node';
@@ -1,3 +1,4 @@
1
1
  export * from './heading.node';
2
2
  export * from './dynamic-field.node';
3
3
  export * from './page-break.node';
4
+ export * from './image-ref.node';
@@ -1 +1 @@
1
- export type ListStyleType = 'decimal' | 'lower-alpha-dot' | 'lower-alpha-parens' | 'lower-roman-parens';
1
+ export type ListStyleType = 'decimal' | 'lower-alpha' | 'upper-alpha' | 'lower-roman' | 'upper-roman' | 'lower-alpha-dot' | 'lower-alpha-parens' | 'lower-roman-parens' | 'decimal-leading-zero' | 'lower-latin' | 'upper-latin';
@@ -1 +1,2 @@
1
+ // TODO CKEditor: Add lower-roman, decimal-leading-zero, upper-roman, lower-latin, upper-latin
1
2
  export { };
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Type shim for the `@tiptap/html/server` subpath.
3
+ *
4
+ * `@tiptap/html` only exposes its Node-safe (`/server`) entry through the
5
+ * package `exports` map. This library compiles under `moduleResolution: node10`,
6
+ * which ignores `exports` for type resolution, so TypeScript cannot find the
7
+ * subpath's declarations on its own. The runtime (Jest/Node) resolves `/server`
8
+ * via the exports map fine — this declaration only satisfies the type checker by
9
+ * pointing it at the same public types as the bare package.
10
+ */
@@ -2,31 +2,36 @@ import { CellSelection, findTable, selectionCell, TableMap } from '@tiptap/pm/ta
2
2
  import { isEqual } from 'lodash-es';
3
3
  export function getCursorCellInfo(state) {
4
4
  // 1. Dùng selectionCell để tìm vị trí bắt đầu của ô chứa con trỏ
5
- const cellInfo = selectionCell(state);
6
- // Nếu không tìm thấy (con trỏ không nằm trong ô nào), trả về null
7
- if (!cellInfo) return null;
8
- // cellInfo.$pos là một ResolvedPos trỏ tới vị trí bắt đầu của ô
9
- const table = findTable(cellInfo);
10
- if (!table) return null;
11
- const map = TableMap.get(table.node);
12
- // 2. Lấy vị trí tương đối của ô so với bảng
13
- const cellOffset = cellInfo.pos - table.start;
14
- // 3. Tìm chỉ số (index) của ô này trong "bản đồ" của bảng
15
- const cellIndex = map.map.indexOf(cellOffset);
16
- if (cellIndex === -1) {
5
+ try {
6
+ const cellInfo = selectionCell(state);
7
+ // Nếu không tìm thấy (con trỏ không nằm trong ô nào), trả về null
8
+ if (!cellInfo) return null;
9
+ // cellInfo.$pos là một ResolvedPos trỏ tới vị trí bắt đầu của ô
10
+ const table = findTable(cellInfo);
11
+ if (!table) return null;
12
+ const map = TableMap.get(table.node);
13
+ // 2. Lấy vị trí tương đối của ô so với bảng
14
+ const cellOffset = cellInfo.pos - table.start;
15
+ // 3. Tìm chỉ số (index) của ô này trong "bản đồ" của bảng
16
+ const cellIndex = map.map.indexOf(cellOffset);
17
+ if (cellIndex === -1) {
18
+ return null;
19
+ }
20
+ // 4. Từ chỉ số, tính ra rowIndex và colIndex
21
+ const rowIndex = Math.floor(cellIndex / map.width);
22
+ const colIndex = cellIndex % map.width;
23
+ return {
24
+ rowIndex,
25
+ colIndex,
26
+ cellIndex,
27
+ tableStart: table.start,
28
+ tableNode: table.node,
29
+ map
30
+ };
31
+ } catch {
32
+ // selectionCell throws RangeError when cursor is not in a table
17
33
  return null;
18
34
  }
19
- // 4. Từ chỉ số, tính ra rowIndex và colIndex
20
- const rowIndex = Math.floor(cellIndex / map.width);
21
- const colIndex = cellIndex % map.width;
22
- return {
23
- rowIndex,
24
- colIndex,
25
- cellIndex,
26
- tableStart: table.start,
27
- tableNode: table.node,
28
- map
29
- };
30
35
  }
31
36
  export function getSelectedCells(state) {
32
37
  const { selection } = state;