@phuong-tran-redoc/document-engine-core 0.0.41 → 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 +35 -31
- package/src/extensions/restricted-editing.extension.js +2 -2
- package/src/extensions/table-resizing.extension.d.ts +1 -1
- package/src/extensions/table-resizing.extension.js +28 -28
- package/src/extensions/table-style.extension.d.ts +3 -1
- package/src/extensions/table-style.extension.js +21 -21
- package/src/index.d.ts +2 -0
- package/src/index.js +2 -0
- package/src/kit/default-extensions.d.ts +22 -0
- package/src/kit/default-extensions.js +86 -0
- package/src/kit/generate-html.d.ts +23 -0
- package/src/kit/generate-html.js +40 -0
- package/src/kit/index.d.ts +2 -0
- package/src/kit/index.js +2 -0
- package/src/migrations/doc-migrations.d.ts +27 -0
- package/src/migrations/doc-migrations.js +24 -0
- package/src/migrations/index.d.ts +3 -0
- package/src/migrations/index.js +3 -0
- package/src/migrations/migrate-doc.d.ts +16 -0
- package/src/migrations/migrate-doc.js +32 -0
- package/src/migrations/types.d.ts +25 -0
- package/src/migrations/types.js +4 -0
- package/src/nodes/image-ref.node.d.ts +40 -0
- package/src/nodes/image-ref.node.js +138 -0
- package/src/nodes/index.d.ts +1 -0
- package/src/nodes/index.js +1 -0
- package/src/types/tiptap-html-server.d.js +10 -0
package/package.json
CHANGED
|
@@ -1,51 +1,55 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@phuong-tran-redoc/document-engine-core",
|
|
3
|
-
"version": "0.0
|
|
3
|
+
"version": "0.1.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"license": "MIT",
|
|
6
|
-
"author": "
|
|
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
|
-
"
|
|
20
|
-
"@tiptap/
|
|
21
|
-
"@tiptap/extension-
|
|
22
|
-
"@tiptap/extension-
|
|
23
|
-
"@tiptap/extension-
|
|
24
|
-
"@tiptap/extension-code": "^3.
|
|
25
|
-
"@tiptap/extension-
|
|
26
|
-
"@tiptap/extension-
|
|
27
|
-
"@tiptap/extension-
|
|
28
|
-
"@tiptap/extension-
|
|
29
|
-
"@tiptap/extension-
|
|
30
|
-
"@tiptap/extension-
|
|
31
|
-
"@tiptap/extension-
|
|
32
|
-
"@tiptap/extension-
|
|
33
|
-
"@tiptap/extension-
|
|
34
|
-
"@tiptap/extension-
|
|
35
|
-
"@tiptap/extension-
|
|
36
|
-
"@tiptap/extension-
|
|
37
|
-
"@tiptap/extension-
|
|
38
|
-
"@tiptap/extension-
|
|
39
|
-
"@tiptap/extension-
|
|
40
|
-
"@tiptap/extension-text": "^3.
|
|
41
|
-
"@tiptap/extension-text-
|
|
42
|
-
"@tiptap/extension-
|
|
43
|
-
"@tiptap/
|
|
44
|
-
"@tiptap/
|
|
45
|
-
"@tiptap/pm": "^3.
|
|
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
|
+
}
|
|
@@ -10,13 +10,13 @@ export const EditableRegion = Node.create({
|
|
|
10
10
|
parseHTML () {
|
|
11
11
|
return [
|
|
12
12
|
// ----------------------------------------------------------
|
|
13
|
-
// RULE 1: Standard Tiptap Format (
|
|
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 (
|
|
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
|
-
* *
|
|
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
|
-
*
|
|
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.
|
|
12
|
+
// 1. Compute the delta (pixels)
|
|
13
13
|
const deltaX = event.clientX - startX;
|
|
14
|
-
// 2.
|
|
15
|
-
//
|
|
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.
|
|
17
|
+
// 3. Compute the new widths
|
|
18
18
|
let newLeftWidth = initialLeftWidth + deltaPercent;
|
|
19
19
|
let newRightWidth = initialRightWidth - deltaPercent;
|
|
20
|
-
// 4.
|
|
21
|
-
const minWidth = 5; //
|
|
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; //
|
|
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; //
|
|
29
|
+
newLeftWidth = newLeftWidth - diff; // Left column absorbs the shortfall
|
|
30
30
|
}
|
|
31
|
-
//
|
|
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.
|
|
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
|
|
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
|
-
//
|
|
46
|
+
// Use dispatchTransaction so Tiptap/ProseMirror updates the UI itself
|
|
47
47
|
view.dispatch(tr);
|
|
48
48
|
}
|
|
49
49
|
}
|
|
50
50
|
/**
|
|
51
|
-
*
|
|
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
|
|
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
|
-
* *
|
|
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
|
-
//
|
|
75
|
-
//
|
|
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
|
-
*
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
*
|
|
122
|
+
* Handle the mousedown event on a handle
|
|
123
123
|
*/ handleDOMEvents: {
|
|
124
124
|
mousedown (view, event) {
|
|
125
125
|
const target = event.target;
|
|
126
|
-
//
|
|
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
|
-
//
|
|
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; //
|
|
157
|
+
return true; // Event handled
|
|
158
158
|
}
|
|
159
159
|
}
|
|
160
160
|
}
|
|
@@ -36,5 +36,7 @@ declare module '@tiptap/core' {
|
|
|
36
36
|
};
|
|
37
37
|
}
|
|
38
38
|
}
|
|
39
|
-
export declare const StyledTable: Node<TableOptions
|
|
39
|
+
export declare const StyledTable: Node<TableOptions & {
|
|
40
|
+
enableNodeView?: boolean;
|
|
41
|
+
}, any>;
|
|
40
42
|
export declare const StyledTableKit: Extension<TableKitOptions, any>;
|
|
@@ -168,7 +168,7 @@ export const StyledTable = Table.extend({
|
|
|
168
168
|
default: null,
|
|
169
169
|
renderHTML: (attributes)=>{
|
|
170
170
|
if (!attributes['colwidths']) {
|
|
171
|
-
return {}; //
|
|
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>
|
|
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
|
-
//
|
|
217
|
+
// Add <tbody> (0 is the 'content hole')
|
|
218
218
|
domNodes.push([
|
|
219
219
|
'tbody',
|
|
220
220
|
0
|
|
221
221
|
]);
|
|
222
|
-
// Merge
|
|
223
|
-
// HTMLAttributes
|
|
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
|
-
// ---
|
|
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
|
-
//
|
|
267
|
-
// ---
|
|
268
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
291
|
+
// Add the new row to the rows array
|
|
292
292
|
tableRows.push(rowType.create(null, tableCells));
|
|
293
293
|
}
|
|
294
|
-
// ---
|
|
294
|
+
// --- STEP 3: Create the main Table node WITH the 'colwidths' ATTRIBUTE ---
|
|
295
295
|
const tableNode = schema.nodes['table'].create({
|
|
296
|
-
//
|
|
296
|
+
// This is the key part: assign our percentage array here
|
|
297
297
|
colwidths: calculatedColWidths
|
|
298
|
-
}, tableRows //
|
|
298
|
+
}, tableRows // Content (the 'tableRow' nodes)
|
|
299
299
|
);
|
|
300
|
-
// ---
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
|
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
package/src/index.js
CHANGED
|
@@ -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
|
+
}
|
package/src/kit/index.js
ADDED
|
@@ -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,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,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
|
+
});
|
package/src/nodes/index.d.ts
CHANGED
package/src/nodes/index.js
CHANGED
|
@@ -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
|
+
*/
|