@kedataindo/docflow-plugins 0.0.3 → 0.0.5
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/chunk-CLCSH4NX.js +1078 -0
- package/dist/chunk-MZZTJIFJ.js +1417 -0
- package/dist/citations.cjs +1458 -0
- package/dist/citations.d.cts +132 -0
- package/dist/citations.d.ts +132 -0
- package/dist/citations.js +18 -0
- package/dist/index.cjs +2565 -33
- package/dist/index.d.cts +217 -4
- package/dist/index.d.ts +217 -4
- package/dist/index.js +1139 -30
- package/package.json +26 -9
package/dist/index.js
CHANGED
|
@@ -1,5 +1,20 @@
|
|
|
1
|
+
import {
|
|
2
|
+
BibliographyNode,
|
|
3
|
+
CSL_LOCALE_EN_US,
|
|
4
|
+
CSL_STYLES,
|
|
5
|
+
CitationEngineExtension,
|
|
6
|
+
CitationNode,
|
|
7
|
+
CiteEngine,
|
|
8
|
+
DEFAULT_CSL_STYLE,
|
|
9
|
+
buildCitationNodes,
|
|
10
|
+
citationPlugin,
|
|
11
|
+
getCitationEngine,
|
|
12
|
+
nextCitationId,
|
|
13
|
+
sanitizeCiteprocHtml
|
|
14
|
+
} from "./chunk-MZZTJIFJ.js";
|
|
15
|
+
|
|
1
16
|
// src/formatting.ts
|
|
2
|
-
import { definePlugin } from "@
|
|
17
|
+
import { definePlugin } from "@kedataindo/docflow-core";
|
|
3
18
|
import Underline from "@tiptap/extension-underline";
|
|
4
19
|
var formattingPlugin = definePlugin({
|
|
5
20
|
id: "formatting",
|
|
@@ -13,7 +28,7 @@ var formattingPlugin = definePlugin({
|
|
|
13
28
|
});
|
|
14
29
|
|
|
15
30
|
// src/headings.ts
|
|
16
|
-
import { definePlugin as definePlugin2 } from "@
|
|
31
|
+
import { definePlugin as definePlugin2 } from "@kedataindo/docflow-core";
|
|
17
32
|
function toggleHeading(level) {
|
|
18
33
|
return (editor) => {
|
|
19
34
|
const commands = editor.commands;
|
|
@@ -46,7 +61,7 @@ var headingsPlugin = definePlugin2({
|
|
|
46
61
|
});
|
|
47
62
|
|
|
48
63
|
// src/lists.ts
|
|
49
|
-
import { definePlugin as definePlugin3 } from "@
|
|
64
|
+
import { definePlugin as definePlugin3 } from "@kedataindo/docflow-core";
|
|
50
65
|
import TaskList from "@tiptap/extension-task-list";
|
|
51
66
|
import TaskItem from "@tiptap/extension-task-item";
|
|
52
67
|
var listsPlugin = definePlugin3({
|
|
@@ -65,7 +80,7 @@ var listsPlugin = definePlugin3({
|
|
|
65
80
|
});
|
|
66
81
|
|
|
67
82
|
// src/alignment.ts
|
|
68
|
-
import { definePlugin as definePlugin4 } from "@
|
|
83
|
+
import { definePlugin as definePlugin4 } from "@kedataindo/docflow-core";
|
|
69
84
|
import TextAlign from "@tiptap/extension-text-align";
|
|
70
85
|
function setAlign(alignment) {
|
|
71
86
|
return (editor) => editor.commands.setTextAlign(alignment);
|
|
@@ -90,11 +105,11 @@ var alignmentPlugin = definePlugin4({
|
|
|
90
105
|
});
|
|
91
106
|
|
|
92
107
|
// src/link.ts
|
|
93
|
-
import { definePlugin as definePlugin5 } from "@
|
|
108
|
+
import { definePlugin as definePlugin5 } from "@kedataindo/docflow-core";
|
|
94
109
|
import Link from "@tiptap/extension-link";
|
|
95
110
|
var linkPlugin = definePlugin5({
|
|
96
111
|
id: "link",
|
|
97
|
-
tiptapExtensions: [Link.configure({ openOnClick:
|
|
112
|
+
tiptapExtensions: [Link.configure({ openOnClick: true })],
|
|
98
113
|
toolbar: [
|
|
99
114
|
{ id: "set-link", label: "Link", action: "setLink", iconComponent: "Link" }
|
|
100
115
|
],
|
|
@@ -115,9 +130,28 @@ var linkPlugin = definePlugin5({
|
|
|
115
130
|
});
|
|
116
131
|
|
|
117
132
|
// src/image.ts
|
|
118
|
-
import { definePlugin as definePlugin6 } from "@
|
|
133
|
+
import { definePlugin as definePlugin6 } from "@kedataindo/docflow-core";
|
|
119
134
|
import Image from "@tiptap/extension-image";
|
|
120
|
-
|
|
135
|
+
function pickImageFile() {
|
|
136
|
+
if (typeof document === "undefined") return Promise.resolve(null);
|
|
137
|
+
return new Promise((resolve) => {
|
|
138
|
+
const input = document.createElement("input");
|
|
139
|
+
input.type = "file";
|
|
140
|
+
input.accept = "image/*";
|
|
141
|
+
const cleanup = () => input.remove();
|
|
142
|
+
input.onchange = () => {
|
|
143
|
+
const file = input.files?.[0] ?? null;
|
|
144
|
+
cleanup();
|
|
145
|
+
resolve(file);
|
|
146
|
+
};
|
|
147
|
+
input.oncancel = () => {
|
|
148
|
+
cleanup();
|
|
149
|
+
resolve(null);
|
|
150
|
+
};
|
|
151
|
+
document.body.appendChild(input);
|
|
152
|
+
input.click();
|
|
153
|
+
});
|
|
154
|
+
}
|
|
121
155
|
var imagePlugin = definePlugin6({
|
|
122
156
|
id: "image",
|
|
123
157
|
tiptapExtensions: [Image],
|
|
@@ -126,26 +160,172 @@ var imagePlugin = definePlugin6({
|
|
|
126
160
|
commands: {
|
|
127
161
|
insertImage: (editor, ...args) => {
|
|
128
162
|
const options = args[0];
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
163
|
+
if (options?.src) {
|
|
164
|
+
return editor.commands.setImage({ src: options.src, alt: options.alt, title: options.title });
|
|
165
|
+
}
|
|
166
|
+
const context = editor.storage.editorContext;
|
|
167
|
+
const upload = context?.onImageUpload;
|
|
168
|
+
if (upload) {
|
|
169
|
+
void pickImageFile().then(async (file) => {
|
|
170
|
+
if (!file) return;
|
|
171
|
+
try {
|
|
172
|
+
const result = await upload(file);
|
|
173
|
+
if (result?.src) {
|
|
174
|
+
editor.commands.setImage({ src: result.src, alt: result.alt, title: result.title });
|
|
175
|
+
}
|
|
176
|
+
} catch (err) {
|
|
177
|
+
console.error("[image] upload failed:", err);
|
|
178
|
+
}
|
|
179
|
+
});
|
|
180
|
+
return true;
|
|
181
|
+
}
|
|
182
|
+
if (typeof window !== "undefined" && typeof window.prompt === "function") {
|
|
183
|
+
const src = window.prompt("Enter image URL:", "https://");
|
|
184
|
+
if (!src) return false;
|
|
185
|
+
return editor.commands.setImage({ src });
|
|
186
|
+
}
|
|
187
|
+
return false;
|
|
132
188
|
}
|
|
133
189
|
}
|
|
134
190
|
});
|
|
135
191
|
|
|
136
192
|
// src/table.ts
|
|
137
|
-
import { definePlugin as definePlugin7 } from "@
|
|
193
|
+
import { definePlugin as definePlugin7 } from "@kedataindo/docflow-core";
|
|
194
|
+
import { mergeAttributes } from "@tiptap/core";
|
|
138
195
|
import Table from "@tiptap/extension-table";
|
|
139
196
|
import TableRow from "@tiptap/extension-table-row";
|
|
140
197
|
import TableCell from "@tiptap/extension-table-cell";
|
|
141
198
|
import TableHeader from "@tiptap/extension-table-header";
|
|
199
|
+
var CustomTable = Table.extend({
|
|
200
|
+
renderHTML({ node, HTMLAttributes }) {
|
|
201
|
+
let colCount = 0;
|
|
202
|
+
const firstRow = node.firstChild;
|
|
203
|
+
if (firstRow) {
|
|
204
|
+
firstRow.forEach((cell) => {
|
|
205
|
+
colCount += cell.attrs.colspan || 1;
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
const cols = [];
|
|
209
|
+
let totalWidth = 0;
|
|
210
|
+
let hasExplicitWidths = false;
|
|
211
|
+
if (colCount > 0) {
|
|
212
|
+
for (let i = 0; i < colCount; i++) {
|
|
213
|
+
let width = null;
|
|
214
|
+
if (firstRow) {
|
|
215
|
+
let currentColIdx = 0;
|
|
216
|
+
firstRow.forEach((cell) => {
|
|
217
|
+
const colspan = cell.attrs.colspan || 1;
|
|
218
|
+
if (i >= currentColIdx && i < currentColIdx + colspan) {
|
|
219
|
+
const colwidth = cell.attrs.colwidth;
|
|
220
|
+
if (colwidth && Array.isArray(colwidth)) {
|
|
221
|
+
const indexInCell = i - currentColIdx;
|
|
222
|
+
const w = colwidth[indexInCell];
|
|
223
|
+
if (typeof w === "number" && w > 0) {
|
|
224
|
+
width = w;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
currentColIdx += colspan;
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
if (width) {
|
|
232
|
+
totalWidth += width;
|
|
233
|
+
hasExplicitWidths = true;
|
|
234
|
+
}
|
|
235
|
+
cols.push([
|
|
236
|
+
"col",
|
|
237
|
+
width ? { style: `width: ${width}px !important`, width: String(width) } : {}
|
|
238
|
+
]);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
const tableAttrs = mergeAttributes(HTMLAttributes);
|
|
242
|
+
if (hasExplicitWidths && totalWidth > 0) {
|
|
243
|
+
const existingStyle = tableAttrs.style || "";
|
|
244
|
+
tableAttrs.style = existingStyle ? `${existingStyle}; width: ${totalWidth}px !important` : `width: ${totalWidth}px !important`;
|
|
245
|
+
}
|
|
246
|
+
return [
|
|
247
|
+
"table",
|
|
248
|
+
tableAttrs,
|
|
249
|
+
["colgroup", ...cols],
|
|
250
|
+
["tbody", 0]
|
|
251
|
+
];
|
|
252
|
+
}
|
|
253
|
+
});
|
|
254
|
+
var CustomTableCell = TableCell.extend({
|
|
255
|
+
addAttributes() {
|
|
256
|
+
return {
|
|
257
|
+
...this.parent?.(),
|
|
258
|
+
colwidth: {
|
|
259
|
+
default: null,
|
|
260
|
+
parseHTML: (element) => {
|
|
261
|
+
const width = element.getAttribute("width") || element.style.width;
|
|
262
|
+
if (width) {
|
|
263
|
+
const parsed = parseInt(width, 10);
|
|
264
|
+
return isNaN(parsed) || parsed <= 0 ? null : [parsed];
|
|
265
|
+
}
|
|
266
|
+
const colwidth = element.getAttribute("colwidth");
|
|
267
|
+
return colwidth ? colwidth.split(",").map((w) => parseInt(w, 10)).filter((w) => !isNaN(w) && w > 0) : null;
|
|
268
|
+
},
|
|
269
|
+
renderHTML: (attributes) => {
|
|
270
|
+
if (!attributes.colwidth || !Array.isArray(attributes.colwidth) || attributes.colwidth.length === 0) {
|
|
271
|
+
return {};
|
|
272
|
+
}
|
|
273
|
+
const validWidths = attributes.colwidth.filter((w2) => typeof w2 === "number" && w2 > 0);
|
|
274
|
+
if (validWidths.length === 0) {
|
|
275
|
+
return {};
|
|
276
|
+
}
|
|
277
|
+
const w = validWidths[0];
|
|
278
|
+
return {
|
|
279
|
+
colwidth: validWidths.join(","),
|
|
280
|
+
style: `width: ${w}px !important`,
|
|
281
|
+
width: String(w)
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
});
|
|
288
|
+
var CustomTableHeader = TableHeader.extend({
|
|
289
|
+
addAttributes() {
|
|
290
|
+
return {
|
|
291
|
+
...this.parent?.(),
|
|
292
|
+
colwidth: {
|
|
293
|
+
default: null,
|
|
294
|
+
parseHTML: (element) => {
|
|
295
|
+
const width = element.getAttribute("width") || element.style.width;
|
|
296
|
+
if (width) {
|
|
297
|
+
const parsed = parseInt(width, 10);
|
|
298
|
+
return isNaN(parsed) || parsed <= 0 ? null : [parsed];
|
|
299
|
+
}
|
|
300
|
+
const colwidth = element.getAttribute("colwidth");
|
|
301
|
+
return colwidth ? colwidth.split(",").map((w) => parseInt(w, 10)).filter((w) => !isNaN(w) && w > 0) : null;
|
|
302
|
+
},
|
|
303
|
+
renderHTML: (attributes) => {
|
|
304
|
+
if (!attributes.colwidth || !Array.isArray(attributes.colwidth) || attributes.colwidth.length === 0) {
|
|
305
|
+
return {};
|
|
306
|
+
}
|
|
307
|
+
const validWidths = attributes.colwidth.filter((w2) => typeof w2 === "number" && w2 > 0);
|
|
308
|
+
if (validWidths.length === 0) {
|
|
309
|
+
return {};
|
|
310
|
+
}
|
|
311
|
+
const w = validWidths[0];
|
|
312
|
+
return {
|
|
313
|
+
colwidth: validWidths.join(","),
|
|
314
|
+
style: `width: ${w}px !important`,
|
|
315
|
+
width: String(w)
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
});
|
|
142
322
|
var tablePlugin = definePlugin7({
|
|
143
323
|
id: "table",
|
|
144
324
|
tiptapExtensions: [
|
|
145
|
-
|
|
325
|
+
CustomTable.configure({ resizable: false }),
|
|
146
326
|
TableRow,
|
|
147
|
-
|
|
148
|
-
|
|
327
|
+
CustomTableCell,
|
|
328
|
+
CustomTableHeader
|
|
149
329
|
],
|
|
150
330
|
toolbar: [
|
|
151
331
|
{ id: "insert-table", label: "Insert Table", action: "insertTable", args: [{ rows: 3, cols: 3, withHeaderRow: true }], iconComponent: "Table" }
|
|
@@ -154,7 +334,7 @@ var tablePlugin = definePlugin7({
|
|
|
154
334
|
});
|
|
155
335
|
|
|
156
336
|
// src/blockquote.ts
|
|
157
|
-
import { definePlugin as definePlugin8 } from "@
|
|
337
|
+
import { definePlugin as definePlugin8 } from "@kedataindo/docflow-core";
|
|
158
338
|
var blockquotePlugin = definePlugin8({
|
|
159
339
|
id: "blockquote",
|
|
160
340
|
toolbar: [{ id: "blockquote", label: "Blockquote", action: "toggleBlockquote", iconComponent: "Quote" }],
|
|
@@ -162,7 +342,7 @@ var blockquotePlugin = definePlugin8({
|
|
|
162
342
|
});
|
|
163
343
|
|
|
164
344
|
// src/codeBlock.ts
|
|
165
|
-
import { definePlugin as definePlugin9 } from "@
|
|
345
|
+
import { definePlugin as definePlugin9 } from "@kedataindo/docflow-core";
|
|
166
346
|
var codeBlockPlugin = definePlugin9({
|
|
167
347
|
id: "code-block",
|
|
168
348
|
toolbar: [{ id: "code-block", label: "Code Block", action: "toggleCodeBlock", iconComponent: "Code" }],
|
|
@@ -170,7 +350,7 @@ var codeBlockPlugin = definePlugin9({
|
|
|
170
350
|
});
|
|
171
351
|
|
|
172
352
|
// src/placeholder.ts
|
|
173
|
-
import { definePlugin as definePlugin10 } from "@
|
|
353
|
+
import { definePlugin as definePlugin10 } from "@kedataindo/docflow-core";
|
|
174
354
|
import Placeholder from "@tiptap/extension-placeholder";
|
|
175
355
|
function createPlaceholderPlugin(options = {}) {
|
|
176
356
|
return definePlugin10({
|
|
@@ -185,9 +365,9 @@ function createPlaceholderPlugin(options = {}) {
|
|
|
185
365
|
var placeholderPlugin = createPlaceholderPlugin();
|
|
186
366
|
|
|
187
367
|
// src/pageBreak.ts
|
|
188
|
-
import { Node, mergeAttributes } from "@tiptap/core";
|
|
189
|
-
import { definePlugin as definePlugin11 } from "@
|
|
190
|
-
var PageBreak =
|
|
368
|
+
import { Node as Node2, mergeAttributes as mergeAttributes2 } from "@tiptap/core";
|
|
369
|
+
import { definePlugin as definePlugin11 } from "@kedataindo/docflow-core";
|
|
370
|
+
var PageBreak = Node2.create({
|
|
191
371
|
name: "pageBreak",
|
|
192
372
|
group: "block",
|
|
193
373
|
selectable: false,
|
|
@@ -198,7 +378,7 @@ var PageBreak = Node.create({
|
|
|
198
378
|
renderHTML({ HTMLAttributes }) {
|
|
199
379
|
return [
|
|
200
380
|
"div",
|
|
201
|
-
|
|
381
|
+
mergeAttributes2(HTMLAttributes, {
|
|
202
382
|
"data-page-break": "true",
|
|
203
383
|
"data-node-type": "pageBreak",
|
|
204
384
|
class: "docs-editor-page-break"
|
|
@@ -243,9 +423,9 @@ var pageBreakPlugin = definePlugin11({
|
|
|
243
423
|
});
|
|
244
424
|
|
|
245
425
|
// src/footnote.ts
|
|
246
|
-
import { Node as
|
|
247
|
-
import { definePlugin as definePlugin12 } from "@
|
|
248
|
-
var FootnoteNode =
|
|
426
|
+
import { Node as Node3, mergeAttributes as mergeAttributes3 } from "@tiptap/core";
|
|
427
|
+
import { definePlugin as definePlugin12 } from "@kedataindo/docflow-core";
|
|
428
|
+
var FootnoteNode = Node3.create({
|
|
249
429
|
name: "footnote",
|
|
250
430
|
group: "inline",
|
|
251
431
|
inline: true,
|
|
@@ -258,6 +438,25 @@ var FootnoteNode = Node2.create({
|
|
|
258
438
|
default: "",
|
|
259
439
|
parseHTML: (el) => el.getAttribute("data-footnote-content") ?? "",
|
|
260
440
|
renderHTML: (attrs) => ({ "data-footnote-content": attrs.content })
|
|
441
|
+
},
|
|
442
|
+
// Phase 6: when sourceId is set, the footnote body is citeproc-rendered
|
|
443
|
+
// from the source (never typed, never stored as text). The `content`
|
|
444
|
+
// attr stays authoritative for free-text footnotes — existing documents
|
|
445
|
+
// render byte-identically (backward compatible).
|
|
446
|
+
citationId: {
|
|
447
|
+
default: null,
|
|
448
|
+
parseHTML: (el) => el.getAttribute("data-citation-id"),
|
|
449
|
+
renderHTML: (attrs) => attrs.citationId ? { "data-citation-id": attrs.citationId } : {}
|
|
450
|
+
},
|
|
451
|
+
sourceId: {
|
|
452
|
+
default: null,
|
|
453
|
+
parseHTML: (el) => el.getAttribute("data-footnote-source-id"),
|
|
454
|
+
renderHTML: (attrs) => attrs.sourceId ? { "data-footnote-source-id": attrs.sourceId } : {}
|
|
455
|
+
},
|
|
456
|
+
locator: {
|
|
457
|
+
default: "",
|
|
458
|
+
parseHTML: (el) => el.getAttribute("data-footnote-locator") ?? "",
|
|
459
|
+
renderHTML: (attrs) => attrs.locator ? { "data-footnote-locator": attrs.locator } : {}
|
|
261
460
|
}
|
|
262
461
|
};
|
|
263
462
|
},
|
|
@@ -265,7 +464,7 @@ var FootnoteNode = Node2.create({
|
|
|
265
464
|
return [{ tag: 'span[data-node-type="footnote"]' }];
|
|
266
465
|
},
|
|
267
466
|
renderHTML({ HTMLAttributes }) {
|
|
268
|
-
return ["span",
|
|
467
|
+
return ["span", mergeAttributes3(HTMLAttributes, {
|
|
269
468
|
"data-node-type": "footnote",
|
|
270
469
|
class: "docs-footnote-ref"
|
|
271
470
|
}), "1"];
|
|
@@ -281,6 +480,11 @@ var FootnoteNode = Node2.create({
|
|
|
281
480
|
dom.className = "docs-footnote-ref";
|
|
282
481
|
dom.setAttribute("data-node-type", "footnote");
|
|
283
482
|
dom.setAttribute("data-footnote-content", String(attrs.content ?? ""));
|
|
483
|
+
if (attrs.sourceId) {
|
|
484
|
+
dom.setAttribute("data-footnote-source-id", String(attrs.sourceId));
|
|
485
|
+
dom.setAttribute("data-citation-id", String(attrs.citationId ?? ""));
|
|
486
|
+
if (attrs.locator) dom.setAttribute("data-footnote-locator", String(attrs.locator));
|
|
487
|
+
}
|
|
284
488
|
dom.textContent = "1";
|
|
285
489
|
return {
|
|
286
490
|
dom,
|
|
@@ -289,6 +493,15 @@ var FootnoteNode = Node2.create({
|
|
|
289
493
|
try {
|
|
290
494
|
const updatedAttrs = updatedNode?.attrs ?? {};
|
|
291
495
|
dom.setAttribute("data-footnote-content", String(updatedAttrs.content ?? ""));
|
|
496
|
+
if (updatedAttrs.sourceId) {
|
|
497
|
+
dom.setAttribute("data-footnote-source-id", String(updatedAttrs.sourceId));
|
|
498
|
+
dom.setAttribute("data-citation-id", String(updatedAttrs.citationId ?? ""));
|
|
499
|
+
if (updatedAttrs.locator) dom.setAttribute("data-footnote-locator", String(updatedAttrs.locator));
|
|
500
|
+
} else {
|
|
501
|
+
dom.removeAttribute("data-footnote-source-id");
|
|
502
|
+
dom.removeAttribute("data-citation-id");
|
|
503
|
+
dom.removeAttribute("data-footnote-locator");
|
|
504
|
+
}
|
|
292
505
|
} catch {
|
|
293
506
|
}
|
|
294
507
|
return true;
|
|
@@ -312,9 +525,249 @@ var footnotePlugin = definePlugin12({
|
|
|
312
525
|
}
|
|
313
526
|
});
|
|
314
527
|
|
|
528
|
+
// src/toc.ts
|
|
529
|
+
import { Extension, Node as Node4, mergeAttributes as mergeAttributes4 } from "@tiptap/core";
|
|
530
|
+
import { definePlugin as definePlugin13 } from "@kedataindo/docflow-core";
|
|
531
|
+
function collectHeadingsFromDoc(doc) {
|
|
532
|
+
const headings = [];
|
|
533
|
+
doc.descendants((node, pos) => {
|
|
534
|
+
if (node.type.name === "heading") {
|
|
535
|
+
const level = node.attrs.level;
|
|
536
|
+
if (level === 1 || level === 2 || level === 3) {
|
|
537
|
+
headings.push({ level, text: node.textContent.trim(), pos });
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
return true;
|
|
541
|
+
});
|
|
542
|
+
return headings;
|
|
543
|
+
}
|
|
544
|
+
function collectHeadings(editor) {
|
|
545
|
+
return collectHeadingsFromDoc(editor.state.doc);
|
|
546
|
+
}
|
|
547
|
+
function createPageResolver(editor) {
|
|
548
|
+
let breakers = [];
|
|
549
|
+
try {
|
|
550
|
+
const pagination = editor.view.dom.querySelector("[data-rm-pagination]");
|
|
551
|
+
if (pagination) {
|
|
552
|
+
breakers = Array.from(pagination.querySelectorAll(".rm-page-break .breaker")).filter((el) => el instanceof HTMLElement);
|
|
553
|
+
}
|
|
554
|
+
} catch {
|
|
555
|
+
}
|
|
556
|
+
if (breakers.length === 0) return () => null;
|
|
557
|
+
const editorDom = editor.view.dom;
|
|
558
|
+
return (pos) => {
|
|
559
|
+
try {
|
|
560
|
+
const coords = editor.view.coordsAtPos(pos);
|
|
561
|
+
const top = coords.top - editorDom.getBoundingClientRect().top + editorDom.scrollTop;
|
|
562
|
+
let page = 1;
|
|
563
|
+
for (const breaker of breakers) {
|
|
564
|
+
if (top < breaker.offsetTop) return page;
|
|
565
|
+
page++;
|
|
566
|
+
}
|
|
567
|
+
return page;
|
|
568
|
+
} catch {
|
|
569
|
+
return null;
|
|
570
|
+
}
|
|
571
|
+
};
|
|
572
|
+
}
|
|
573
|
+
function buildTocEntryNodes(schema, headings, pageFor) {
|
|
574
|
+
const entryType = schema.nodes.tocEntry;
|
|
575
|
+
if (headings.length === 0) {
|
|
576
|
+
return [entryType.create({ level: 1, pos: 0 })];
|
|
577
|
+
}
|
|
578
|
+
return headings.map((h) => {
|
|
579
|
+
const content = [];
|
|
580
|
+
if (h.text) content.push(schema.text(h.text));
|
|
581
|
+
const page = pageFor(h.pos);
|
|
582
|
+
if (page !== null) content.push(schema.nodes.tocPageNum.create({ page }));
|
|
583
|
+
return entryType.create({ level: h.level, pos: h.pos }, content);
|
|
584
|
+
});
|
|
585
|
+
}
|
|
586
|
+
function buildTocContentJSON(editor) {
|
|
587
|
+
const headings = collectHeadings(editor);
|
|
588
|
+
if (headings.length === 0) {
|
|
589
|
+
return [{ type: "tocEntry", attrs: { level: 1, pos: 0 } }];
|
|
590
|
+
}
|
|
591
|
+
const pageFor = createPageResolver(editor);
|
|
592
|
+
return headings.map((h) => {
|
|
593
|
+
const content = [];
|
|
594
|
+
if (h.text) content.push({ type: "text", text: h.text });
|
|
595
|
+
const page = pageFor(h.pos);
|
|
596
|
+
if (page !== null) content.push({ type: "tocPageNum", attrs: { page } });
|
|
597
|
+
return {
|
|
598
|
+
type: "tocEntry",
|
|
599
|
+
attrs: { level: h.level, pos: h.pos },
|
|
600
|
+
...content.length > 0 ? { content } : {}
|
|
601
|
+
};
|
|
602
|
+
});
|
|
603
|
+
}
|
|
604
|
+
function applyTocRegeneration(editor, tr) {
|
|
605
|
+
const tocType = editor.schema.nodes.toc;
|
|
606
|
+
if (!tocType) return false;
|
|
607
|
+
const targets = [];
|
|
608
|
+
tr.doc.descendants((node, pos) => {
|
|
609
|
+
if (node.type.name === "toc") {
|
|
610
|
+
targets.push({ pos, nodeSize: node.nodeSize });
|
|
611
|
+
return false;
|
|
612
|
+
}
|
|
613
|
+
return true;
|
|
614
|
+
});
|
|
615
|
+
if (targets.length === 0) return false;
|
|
616
|
+
const entries = buildTocEntryNodes(editor.schema, collectHeadingsFromDoc(tr.doc), createPageResolver(editor));
|
|
617
|
+
for (const target of targets.reverse()) {
|
|
618
|
+
tr.replaceWith(target.pos, target.pos + target.nodeSize, tocType.create(null, entries));
|
|
619
|
+
}
|
|
620
|
+
return true;
|
|
621
|
+
}
|
|
622
|
+
function regenerateToc(editor) {
|
|
623
|
+
const tr = editor.state.tr;
|
|
624
|
+
if (!applyTocRegeneration(editor, tr)) return false;
|
|
625
|
+
editor.view.dispatch(tr);
|
|
626
|
+
return true;
|
|
627
|
+
}
|
|
628
|
+
var TocCommandsExtension = Extension.create({
|
|
629
|
+
name: "tocCommands",
|
|
630
|
+
addCommands() {
|
|
631
|
+
return {
|
|
632
|
+
insertToc: () => ({ editor, chain }) => chain().focus().insertContent({ type: "toc", content: buildTocContentJSON(editor) }).run(),
|
|
633
|
+
refreshToc: () => ({ editor, tr }) => applyTocRegeneration(editor, tr)
|
|
634
|
+
};
|
|
635
|
+
}
|
|
636
|
+
});
|
|
637
|
+
var TocPageNumNode = Node4.create({
|
|
638
|
+
name: "tocPageNum",
|
|
639
|
+
inline: true,
|
|
640
|
+
group: "inline",
|
|
641
|
+
atom: true,
|
|
642
|
+
selectable: false,
|
|
643
|
+
addAttributes() {
|
|
644
|
+
return {
|
|
645
|
+
page: {
|
|
646
|
+
default: null,
|
|
647
|
+
parseHTML: (el) => Number(el.textContent) || null,
|
|
648
|
+
renderHTML: (attrs) => attrs.page ? { "data-page": attrs.page } : {}
|
|
649
|
+
}
|
|
650
|
+
};
|
|
651
|
+
},
|
|
652
|
+
parseHTML() {
|
|
653
|
+
return [{ tag: "span[data-toc-page]" }];
|
|
654
|
+
},
|
|
655
|
+
renderHTML({ node, HTMLAttributes }) {
|
|
656
|
+
return ["span", mergeAttributes4(HTMLAttributes, {
|
|
657
|
+
"data-toc-page": "",
|
|
658
|
+
class: "docs-toc__page"
|
|
659
|
+
}), String(node.attrs.page ?? "")];
|
|
660
|
+
}
|
|
661
|
+
});
|
|
662
|
+
var TocEntryNode = Node4.create({
|
|
663
|
+
name: "tocEntry",
|
|
664
|
+
content: "inline*",
|
|
665
|
+
defining: true,
|
|
666
|
+
addAttributes() {
|
|
667
|
+
return {
|
|
668
|
+
level: {
|
|
669
|
+
default: 1,
|
|
670
|
+
parseHTML: (el) => Number(el.getAttribute("data-level") ?? 1),
|
|
671
|
+
renderHTML: (attrs) => ({ "data-level": attrs.level })
|
|
672
|
+
},
|
|
673
|
+
pos: {
|
|
674
|
+
default: 0,
|
|
675
|
+
parseHTML: (el) => Number(el.getAttribute("data-pos") ?? 0),
|
|
676
|
+
renderHTML: (attrs) => ({ "data-pos": attrs.pos })
|
|
677
|
+
}
|
|
678
|
+
};
|
|
679
|
+
},
|
|
680
|
+
parseHTML() {
|
|
681
|
+
return [{ tag: "p[data-toc-entry]" }];
|
|
682
|
+
},
|
|
683
|
+
renderHTML({ HTMLAttributes }) {
|
|
684
|
+
return ["p", mergeAttributes4(HTMLAttributes, {
|
|
685
|
+
"data-toc-entry": "",
|
|
686
|
+
class: "docs-toc__entry"
|
|
687
|
+
}), 0];
|
|
688
|
+
}
|
|
689
|
+
});
|
|
690
|
+
var TocNode = Node4.create({
|
|
691
|
+
name: "toc",
|
|
692
|
+
group: "block",
|
|
693
|
+
content: "tocEntry+",
|
|
694
|
+
defining: true,
|
|
695
|
+
parseHTML() {
|
|
696
|
+
return [{ tag: "div[data-toc]" }];
|
|
697
|
+
},
|
|
698
|
+
renderHTML({ HTMLAttributes }) {
|
|
699
|
+
return ["div", mergeAttributes4(HTMLAttributes, {
|
|
700
|
+
"data-toc": "",
|
|
701
|
+
class: "docs-toc"
|
|
702
|
+
}), 0];
|
|
703
|
+
},
|
|
704
|
+
// Return type cast to `any` to bypass TipTap's strict NodeViewRenderer typing
|
|
705
|
+
// (same pattern as FootnoteNode / BibliographyNode).
|
|
706
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
707
|
+
addNodeView() {
|
|
708
|
+
return (props) => {
|
|
709
|
+
const editor = props.editor;
|
|
710
|
+
const dom = document.createElement("div");
|
|
711
|
+
dom.className = "docs-toc";
|
|
712
|
+
dom.setAttribute("data-toc", "");
|
|
713
|
+
const chrome = document.createElement("div");
|
|
714
|
+
chrome.className = "docs-toc__chrome";
|
|
715
|
+
chrome.contentEditable = "false";
|
|
716
|
+
const caption = document.createElement("span");
|
|
717
|
+
caption.className = "docs-toc__caption";
|
|
718
|
+
caption.textContent = "Table of contents";
|
|
719
|
+
const refresh = document.createElement("button");
|
|
720
|
+
refresh.type = "button";
|
|
721
|
+
refresh.className = "docs-toc__refresh";
|
|
722
|
+
refresh.title = "Refresh table of contents";
|
|
723
|
+
refresh.textContent = "\u27F3";
|
|
724
|
+
chrome.appendChild(caption);
|
|
725
|
+
chrome.appendChild(refresh);
|
|
726
|
+
const contentDOM = document.createElement("div");
|
|
727
|
+
contentDOM.className = "docs-toc__entries";
|
|
728
|
+
dom.appendChild(chrome);
|
|
729
|
+
dom.appendChild(contentDOM);
|
|
730
|
+
const onRefresh = (event) => {
|
|
731
|
+
event.preventDefault();
|
|
732
|
+
regenerateToc(editor);
|
|
733
|
+
};
|
|
734
|
+
const onEntryClick = (event) => {
|
|
735
|
+
const entry = event.target?.closest?.("[data-toc-entry]");
|
|
736
|
+
if (!entry) return;
|
|
737
|
+
const pos = Number(entry.getAttribute("data-pos") ?? 0);
|
|
738
|
+
const clamped = Math.min(Math.max(0, pos), editor.state.doc.content.size);
|
|
739
|
+
editor.chain().focus().setTextSelection(clamped).scrollIntoView().run();
|
|
740
|
+
};
|
|
741
|
+
refresh.addEventListener("click", onRefresh);
|
|
742
|
+
dom.addEventListener("click", onEntryClick);
|
|
743
|
+
return {
|
|
744
|
+
dom,
|
|
745
|
+
contentDOM,
|
|
746
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
747
|
+
update(updatedNode) {
|
|
748
|
+
return updatedNode?.type?.name === "toc";
|
|
749
|
+
},
|
|
750
|
+
destroy() {
|
|
751
|
+
refresh.removeEventListener("click", onRefresh);
|
|
752
|
+
dom.removeEventListener("click", onEntryClick);
|
|
753
|
+
}
|
|
754
|
+
};
|
|
755
|
+
};
|
|
756
|
+
}
|
|
757
|
+
});
|
|
758
|
+
var tocPlugin = definePlugin13({
|
|
759
|
+
id: "toc",
|
|
760
|
+
tiptapExtensions: [TocPageNumNode, TocEntryNode, TocNode, TocCommandsExtension],
|
|
761
|
+
slashCommands: [{ name: "Table of contents", description: "Daftar isi", command: "insertToc" }],
|
|
762
|
+
commands: {
|
|
763
|
+
insertToc: (editor) => editor.commands.insertToc(),
|
|
764
|
+
refreshToc: (editor) => regenerateToc(editor)
|
|
765
|
+
}
|
|
766
|
+
});
|
|
767
|
+
|
|
315
768
|
// src/fontSize.ts
|
|
316
|
-
import { definePlugin as
|
|
317
|
-
var fontSizePlugin =
|
|
769
|
+
import { definePlugin as definePlugin14, FontSizeExtension } from "@kedataindo/docflow-core";
|
|
770
|
+
var fontSizePlugin = definePlugin14({
|
|
318
771
|
id: "font-size",
|
|
319
772
|
tiptapExtensions: [FontSizeExtension],
|
|
320
773
|
commands: {
|
|
@@ -331,6 +784,625 @@ var fontSizePlugin = definePlugin13({
|
|
|
331
784
|
}
|
|
332
785
|
});
|
|
333
786
|
|
|
787
|
+
// src/textColor.ts
|
|
788
|
+
import { definePlugin as definePlugin15 } from "@kedataindo/docflow-core";
|
|
789
|
+
import Color from "@tiptap/extension-color";
|
|
790
|
+
var textColorPlugin = definePlugin15({
|
|
791
|
+
id: "text-color",
|
|
792
|
+
tiptapExtensions: [Color],
|
|
793
|
+
toolbar: [
|
|
794
|
+
{ id: "text-color", label: "Text Color", action: "setTextColor", iconComponent: "Palette" }
|
|
795
|
+
],
|
|
796
|
+
commands: {
|
|
797
|
+
setTextColor: (editor, ...args) => {
|
|
798
|
+
const color = args[0];
|
|
799
|
+
if (!color) return false;
|
|
800
|
+
if (color === "default") {
|
|
801
|
+
return editor.chain().focus().unsetColor().run();
|
|
802
|
+
}
|
|
803
|
+
return editor.chain().focus().setColor(color).run();
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
});
|
|
807
|
+
|
|
808
|
+
// src/highlight.ts
|
|
809
|
+
import { definePlugin as definePlugin16 } from "@kedataindo/docflow-core";
|
|
810
|
+
import Highlight from "@tiptap/extension-highlight";
|
|
811
|
+
import { mergeAttributes as mergeAttributes5 } from "@tiptap/core";
|
|
812
|
+
var GoogleDocsHighlight = Highlight.extend({
|
|
813
|
+
parseHTML() {
|
|
814
|
+
return [
|
|
815
|
+
// Standard <mark> tag (default TipTap behavior)
|
|
816
|
+
{ tag: "mark" },
|
|
817
|
+
// Google Docs pastes background-color as inline span style
|
|
818
|
+
{ tag: "span", style: "background-color" }
|
|
819
|
+
];
|
|
820
|
+
},
|
|
821
|
+
renderHTML({ HTMLAttributes }) {
|
|
822
|
+
return ["mark", mergeAttributes5(this.options.HTMLAttributes, HTMLAttributes), 0];
|
|
823
|
+
}
|
|
824
|
+
});
|
|
825
|
+
var highlightPlugin = definePlugin16({
|
|
826
|
+
id: "highlight",
|
|
827
|
+
tiptapExtensions: [
|
|
828
|
+
GoogleDocsHighlight.configure({
|
|
829
|
+
multicolor: true
|
|
830
|
+
})
|
|
831
|
+
],
|
|
832
|
+
toolbar: [
|
|
833
|
+
{ id: "highlight", label: "Highlight", action: "setHighlight", iconComponent: "Highlighter" }
|
|
834
|
+
],
|
|
835
|
+
commands: {
|
|
836
|
+
setHighlight: (editor, ...args) => {
|
|
837
|
+
const color = args[0];
|
|
838
|
+
if (!color) {
|
|
839
|
+
return editor.chain().focus().toggleHighlight().run();
|
|
840
|
+
}
|
|
841
|
+
if (color === "default") {
|
|
842
|
+
return editor.chain().focus().unsetHighlight().run();
|
|
843
|
+
}
|
|
844
|
+
return editor.chain().focus().toggleHighlight({ color }).run();
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
});
|
|
848
|
+
|
|
849
|
+
// src/ai.ts
|
|
850
|
+
import { Extension as Extension2 } from "@tiptap/core";
|
|
851
|
+
import { Plugin, PluginKey } from "@tiptap/pm/state";
|
|
852
|
+
import { Decoration, DecorationSet } from "@tiptap/pm/view";
|
|
853
|
+
import { definePlugin as definePlugin17 } from "@kedataindo/docflow-core";
|
|
854
|
+
var aiPluginKey = new PluginKey("docflow-ai");
|
|
855
|
+
var CONTEXT_CHARS = 1500;
|
|
856
|
+
function getAIPreview(editor) {
|
|
857
|
+
return editor.storage.ai ? editor.storage.ai.preview ?? null : null;
|
|
858
|
+
}
|
|
859
|
+
function getAIStreamPort(editor) {
|
|
860
|
+
return editor.storage.editorContext?.aiStream;
|
|
861
|
+
}
|
|
862
|
+
function boundedContext(state, from, to) {
|
|
863
|
+
return {
|
|
864
|
+
before: state.doc.textBetween(Math.max(0, from - CONTEXT_CHARS), from, "\n", " "),
|
|
865
|
+
after: state.doc.textBetween(to, Math.min(state.doc.content.size, to + CONTEXT_CHARS), "\n", " ")
|
|
866
|
+
};
|
|
867
|
+
}
|
|
868
|
+
function runAIStream(editor, req, abort) {
|
|
869
|
+
const aiStream = getAIStreamPort(editor);
|
|
870
|
+
if (!aiStream) return;
|
|
871
|
+
void (async () => {
|
|
872
|
+
const push = (meta) => {
|
|
873
|
+
if (abort.signal.aborted || editor.isDestroyed) return;
|
|
874
|
+
editor.view.dispatch(editor.state.tr.setMeta(aiPluginKey, meta));
|
|
875
|
+
};
|
|
876
|
+
try {
|
|
877
|
+
for await (const chunk of aiStream(req, abort.signal)) {
|
|
878
|
+
if (abort.signal.aborted || editor.isDestroyed) return;
|
|
879
|
+
push({ type: "chunk", text: chunk });
|
|
880
|
+
}
|
|
881
|
+
push({ type: "done" });
|
|
882
|
+
} catch (err) {
|
|
883
|
+
push({
|
|
884
|
+
type: "error",
|
|
885
|
+
message: err instanceof Error ? err.message : "AI request failed"
|
|
886
|
+
});
|
|
887
|
+
}
|
|
888
|
+
})();
|
|
889
|
+
}
|
|
890
|
+
function stopStream(editor) {
|
|
891
|
+
const storage = editor.storage.ai;
|
|
892
|
+
storage.abort?.abort();
|
|
893
|
+
storage.abort = null;
|
|
894
|
+
}
|
|
895
|
+
var AIExtension = Extension2.create({
|
|
896
|
+
name: "ai",
|
|
897
|
+
addStorage() {
|
|
898
|
+
return {
|
|
899
|
+
preview: null,
|
|
900
|
+
abort: null
|
|
901
|
+
};
|
|
902
|
+
},
|
|
903
|
+
addCommands() {
|
|
904
|
+
return {
|
|
905
|
+
aiTransform: (options) => ({ editor, state, dispatch }) => {
|
|
906
|
+
if (aiPluginKey.getState(state)) return false;
|
|
907
|
+
if (!getAIStreamPort(editor)) {
|
|
908
|
+
console.warn("[ai] no aiStream port injected \u2014 AI actions are inert");
|
|
909
|
+
return false;
|
|
910
|
+
}
|
|
911
|
+
const { from, to, empty } = state.selection;
|
|
912
|
+
if (empty) return false;
|
|
913
|
+
const originalText = state.doc.textBetween(from, to, "\n", " ");
|
|
914
|
+
if (!originalText.trim()) return false;
|
|
915
|
+
const abort = new AbortController();
|
|
916
|
+
editor.storage.ai.abort = abort;
|
|
917
|
+
dispatch?.(
|
|
918
|
+
state.tr.setMeta(aiPluginKey, {
|
|
919
|
+
type: "start",
|
|
920
|
+
mode: "transform",
|
|
921
|
+
status: "streaming",
|
|
922
|
+
from,
|
|
923
|
+
to,
|
|
924
|
+
originalText,
|
|
925
|
+
action: options.action
|
|
926
|
+
})
|
|
927
|
+
);
|
|
928
|
+
const req = {
|
|
929
|
+
action: options.action,
|
|
930
|
+
selection: originalText,
|
|
931
|
+
context: boundedContext(state, from, to),
|
|
932
|
+
...options.prompt ? { prompt: options.prompt } : {}
|
|
933
|
+
};
|
|
934
|
+
runAIStream(editor, req, abort);
|
|
935
|
+
return true;
|
|
936
|
+
},
|
|
937
|
+
aiGenerate: () => ({ editor, state, dispatch }) => {
|
|
938
|
+
if (aiPluginKey.getState(state)) return false;
|
|
939
|
+
if (!getAIStreamPort(editor)) {
|
|
940
|
+
console.warn("[ai] no aiStream port injected \u2014 AI actions are inert");
|
|
941
|
+
return false;
|
|
942
|
+
}
|
|
943
|
+
const pos = state.selection.to;
|
|
944
|
+
dispatch?.(
|
|
945
|
+
state.tr.setMeta(aiPluginKey, {
|
|
946
|
+
type: "start",
|
|
947
|
+
mode: "generate",
|
|
948
|
+
status: "prompt",
|
|
949
|
+
from: pos,
|
|
950
|
+
to: pos,
|
|
951
|
+
originalText: "",
|
|
952
|
+
action: "generate"
|
|
953
|
+
})
|
|
954
|
+
);
|
|
955
|
+
return true;
|
|
956
|
+
},
|
|
957
|
+
aiPromptSubmit: (options) => ({ editor, state, dispatch }) => {
|
|
958
|
+
const preview = aiPluginKey.getState(state);
|
|
959
|
+
if (!preview || preview.mode !== "generate" || preview.status !== "prompt") return false;
|
|
960
|
+
const prompt = options.prompt?.trim();
|
|
961
|
+
if (!prompt) return false;
|
|
962
|
+
const abort = new AbortController();
|
|
963
|
+
editor.storage.ai.abort = abort;
|
|
964
|
+
dispatch?.(
|
|
965
|
+
state.tr.setMeta(aiPluginKey, {
|
|
966
|
+
type: "start",
|
|
967
|
+
mode: "generate",
|
|
968
|
+
status: "streaming",
|
|
969
|
+
from: preview.from,
|
|
970
|
+
to: preview.to,
|
|
971
|
+
originalText: "",
|
|
972
|
+
action: "generate"
|
|
973
|
+
})
|
|
974
|
+
);
|
|
975
|
+
const req = {
|
|
976
|
+
action: "generate",
|
|
977
|
+
prompt,
|
|
978
|
+
context: boundedContext(state, preview.from, preview.to)
|
|
979
|
+
};
|
|
980
|
+
runAIStream(editor, req, abort);
|
|
981
|
+
editor.commands.focus();
|
|
982
|
+
return true;
|
|
983
|
+
},
|
|
984
|
+
aiAccept: () => ({ editor, state, dispatch }) => {
|
|
985
|
+
const preview = aiPluginKey.getState(state);
|
|
986
|
+
if (!preview || preview.status === "prompt") return false;
|
|
987
|
+
stopStream(editor);
|
|
988
|
+
if (!dispatch) return true;
|
|
989
|
+
const tr = state.tr;
|
|
990
|
+
if (preview.text.trim()) {
|
|
991
|
+
tr.replaceWith(preview.from, preview.to, state.schema.text(preview.text));
|
|
992
|
+
}
|
|
993
|
+
tr.setMeta(aiPluginKey, { type: "clear" });
|
|
994
|
+
dispatch(tr);
|
|
995
|
+
return true;
|
|
996
|
+
},
|
|
997
|
+
aiReject: () => ({ editor, state, dispatch }) => {
|
|
998
|
+
const preview = aiPluginKey.getState(state);
|
|
999
|
+
if (!preview) return false;
|
|
1000
|
+
stopStream(editor);
|
|
1001
|
+
dispatch?.(state.tr.setMeta(aiPluginKey, { type: "clear" }));
|
|
1002
|
+
return true;
|
|
1003
|
+
}
|
|
1004
|
+
};
|
|
1005
|
+
},
|
|
1006
|
+
addProseMirrorPlugins() {
|
|
1007
|
+
const editor = this.editor;
|
|
1008
|
+
let promptWidget = null;
|
|
1009
|
+
function getPromptWidget() {
|
|
1010
|
+
if (promptWidget) return promptWidget;
|
|
1011
|
+
const wrap = document.createElement("span");
|
|
1012
|
+
wrap.className = "docs-ai-prompt";
|
|
1013
|
+
const input = document.createElement("input");
|
|
1014
|
+
input.type = "text";
|
|
1015
|
+
input.className = "docs-ai-prompt__input";
|
|
1016
|
+
input.placeholder = "Ask AI to write\u2026 (Enter \u23CE \xB7 Esc cancel)";
|
|
1017
|
+
input.addEventListener("keydown", (event) => {
|
|
1018
|
+
if (event.key === "Enter") {
|
|
1019
|
+
event.preventDefault();
|
|
1020
|
+
editor.commands.aiPromptSubmit({ prompt: input.value });
|
|
1021
|
+
} else if (event.key === "Escape") {
|
|
1022
|
+
event.preventDefault();
|
|
1023
|
+
editor.commands.aiReject();
|
|
1024
|
+
}
|
|
1025
|
+
});
|
|
1026
|
+
input.addEventListener("blur", () => {
|
|
1027
|
+
const preview = aiPluginKey.getState(editor.state);
|
|
1028
|
+
if (preview?.status === "prompt") editor.commands.aiReject();
|
|
1029
|
+
});
|
|
1030
|
+
wrap.appendChild(input);
|
|
1031
|
+
setTimeout(() => input.focus(), 0);
|
|
1032
|
+
promptWidget = { dom: wrap, stopEvent: (e) => e.target instanceof Node && wrap.contains(e.target) };
|
|
1033
|
+
return promptWidget;
|
|
1034
|
+
}
|
|
1035
|
+
function buildGhostWidget(preview) {
|
|
1036
|
+
const ghost = document.createElement("span");
|
|
1037
|
+
let actions = null;
|
|
1038
|
+
if (preview.status === "error") {
|
|
1039
|
+
ghost.className = "docs-ai-ghost docs-ai-ghost--error";
|
|
1040
|
+
ghost.textContent = ` ${preview.error ?? "AI error"} \u2014 press Esc`;
|
|
1041
|
+
} else {
|
|
1042
|
+
ghost.className = "docs-ai-ghost";
|
|
1043
|
+
const textSpan = document.createElement("span");
|
|
1044
|
+
if (!preview.text && preview.status === "streaming") {
|
|
1045
|
+
textSpan.textContent = "AI writing\u2026";
|
|
1046
|
+
textSpan.className = "docs-ai-ghost-placeholder";
|
|
1047
|
+
} else {
|
|
1048
|
+
textSpan.textContent = preview.text + (preview.status === "streaming" ? "\u258C" : "");
|
|
1049
|
+
}
|
|
1050
|
+
ghost.appendChild(textSpan);
|
|
1051
|
+
if (preview.status === "done") {
|
|
1052
|
+
actions = document.createElement("span");
|
|
1053
|
+
actions.className = "docs-ai-ghost-actions";
|
|
1054
|
+
const acceptBtn = document.createElement("button");
|
|
1055
|
+
acceptBtn.type = "button";
|
|
1056
|
+
acceptBtn.className = "docs-ai-ghost-btn docs-ai-ghost-btn--accept";
|
|
1057
|
+
acceptBtn.textContent = "\u2713 Accept";
|
|
1058
|
+
acceptBtn.title = "Accept (Enter)";
|
|
1059
|
+
acceptBtn.addEventListener("mousedown", (e) => {
|
|
1060
|
+
e.preventDefault();
|
|
1061
|
+
editor.commands.aiAccept();
|
|
1062
|
+
});
|
|
1063
|
+
const rejectBtn = document.createElement("button");
|
|
1064
|
+
rejectBtn.type = "button";
|
|
1065
|
+
rejectBtn.className = "docs-ai-ghost-btn docs-ai-ghost-btn--reject";
|
|
1066
|
+
rejectBtn.textContent = "\u2717 Reject";
|
|
1067
|
+
rejectBtn.title = "Reject (Esc)";
|
|
1068
|
+
rejectBtn.addEventListener("mousedown", (e) => {
|
|
1069
|
+
e.preventDefault();
|
|
1070
|
+
editor.commands.aiReject();
|
|
1071
|
+
});
|
|
1072
|
+
actions.appendChild(acceptBtn);
|
|
1073
|
+
actions.appendChild(rejectBtn);
|
|
1074
|
+
ghost.appendChild(actions);
|
|
1075
|
+
}
|
|
1076
|
+
}
|
|
1077
|
+
return {
|
|
1078
|
+
dom: ghost,
|
|
1079
|
+
// Keep button clicks away from ProseMirror (it would move the
|
|
1080
|
+
// selection and swallow the mousedown before our handler).
|
|
1081
|
+
stopEvent: (e) => actions !== null && e.target instanceof Node && actions.contains(e.target)
|
|
1082
|
+
};
|
|
1083
|
+
}
|
|
1084
|
+
return [
|
|
1085
|
+
new Plugin({
|
|
1086
|
+
key: aiPluginKey,
|
|
1087
|
+
state: {
|
|
1088
|
+
init: () => null,
|
|
1089
|
+
apply(tr, prev) {
|
|
1090
|
+
const meta = tr.getMeta(aiPluginKey);
|
|
1091
|
+
if (meta) {
|
|
1092
|
+
switch (meta.type) {
|
|
1093
|
+
case "start":
|
|
1094
|
+
return {
|
|
1095
|
+
mode: meta.mode,
|
|
1096
|
+
status: meta.status,
|
|
1097
|
+
from: meta.from,
|
|
1098
|
+
to: meta.to,
|
|
1099
|
+
originalText: meta.originalText,
|
|
1100
|
+
text: "",
|
|
1101
|
+
action: meta.action
|
|
1102
|
+
};
|
|
1103
|
+
case "chunk":
|
|
1104
|
+
return prev ? { ...prev, text: prev.text + meta.text } : prev;
|
|
1105
|
+
case "done":
|
|
1106
|
+
return prev ? { ...prev, status: "done" } : prev;
|
|
1107
|
+
case "error":
|
|
1108
|
+
return prev ? { ...prev, status: "error", error: meta.message } : prev;
|
|
1109
|
+
case "clear":
|
|
1110
|
+
return null;
|
|
1111
|
+
}
|
|
1112
|
+
}
|
|
1113
|
+
if (!prev) return prev;
|
|
1114
|
+
if (prev.status === "prompt" && tr.selectionSet) return null;
|
|
1115
|
+
if (tr.docChanged) {
|
|
1116
|
+
if (prev.mode === "generate") {
|
|
1117
|
+
const pos = tr.mapping.map(prev.from, 1);
|
|
1118
|
+
return { ...prev, from: pos, to: pos };
|
|
1119
|
+
}
|
|
1120
|
+
const from = tr.mapping.map(prev.from, -1);
|
|
1121
|
+
const to = tr.mapping.map(prev.to, 1);
|
|
1122
|
+
if (to <= from || tr.doc.textBetween(from, to, "\n", " ") !== prev.originalText) {
|
|
1123
|
+
return null;
|
|
1124
|
+
}
|
|
1125
|
+
return { ...prev, from, to };
|
|
1126
|
+
}
|
|
1127
|
+
return prev;
|
|
1128
|
+
}
|
|
1129
|
+
},
|
|
1130
|
+
props: {
|
|
1131
|
+
decorations(state) {
|
|
1132
|
+
const preview = aiPluginKey.getState(state);
|
|
1133
|
+
if (!preview) {
|
|
1134
|
+
promptWidget = null;
|
|
1135
|
+
return DecorationSet.empty;
|
|
1136
|
+
}
|
|
1137
|
+
const decorations = [];
|
|
1138
|
+
if (preview.mode === "transform" && preview.to > preview.from) {
|
|
1139
|
+
decorations.push(
|
|
1140
|
+
Decoration.inline(preview.from, preview.to, { class: "docs-ai-selection" })
|
|
1141
|
+
);
|
|
1142
|
+
}
|
|
1143
|
+
if (preview.status === "prompt") {
|
|
1144
|
+
const widget = getPromptWidget();
|
|
1145
|
+
decorations.push(
|
|
1146
|
+
Decoration.widget(preview.to, widget.dom, {
|
|
1147
|
+
key: "docs-ai-prompt",
|
|
1148
|
+
side: 1,
|
|
1149
|
+
stopEvent: widget.stopEvent
|
|
1150
|
+
})
|
|
1151
|
+
);
|
|
1152
|
+
} else {
|
|
1153
|
+
promptWidget = null;
|
|
1154
|
+
const ghost = buildGhostWidget(preview);
|
|
1155
|
+
decorations.push(
|
|
1156
|
+
Decoration.widget(preview.to, ghost.dom, {
|
|
1157
|
+
// The key MUST track the rendered content: prosemirror-view's
|
|
1158
|
+
// WidgetType.eq treats same-key widgets as identical and skips
|
|
1159
|
+
// the redraw — a constant key freezes the ghost at its first
|
|
1160
|
+
// paint (the "AI writing…" bug).
|
|
1161
|
+
key: `docs-ai-ghost-${preview.status}-${preview.text.length}-${(preview.error ?? "").length}`,
|
|
1162
|
+
side: 1,
|
|
1163
|
+
stopEvent: ghost.stopEvent
|
|
1164
|
+
})
|
|
1165
|
+
);
|
|
1166
|
+
}
|
|
1167
|
+
return DecorationSet.create(state.doc, decorations);
|
|
1168
|
+
},
|
|
1169
|
+
handleKeyDown(_view, event) {
|
|
1170
|
+
const preview = aiPluginKey.getState(editor.state);
|
|
1171
|
+
if (!preview) return false;
|
|
1172
|
+
if (event.key === "Escape") {
|
|
1173
|
+
editor.commands.aiReject();
|
|
1174
|
+
return true;
|
|
1175
|
+
}
|
|
1176
|
+
if (preview.status === "prompt") return false;
|
|
1177
|
+
if ((event.key === "Enter" || event.key === "Tab") && preview.status !== "error") {
|
|
1178
|
+
event.preventDefault();
|
|
1179
|
+
editor.commands.aiAccept();
|
|
1180
|
+
return true;
|
|
1181
|
+
}
|
|
1182
|
+
return false;
|
|
1183
|
+
}
|
|
1184
|
+
}
|
|
1185
|
+
})
|
|
1186
|
+
];
|
|
1187
|
+
},
|
|
1188
|
+
// Mirror the PM plugin state into extension storage so Vue components
|
|
1189
|
+
// (BubbleMenu) can read it without importing the plugin key.
|
|
1190
|
+
onTransaction() {
|
|
1191
|
+
const storage = this.editor.storage.ai;
|
|
1192
|
+
storage.preview = aiPluginKey.getState(this.editor.state) ?? null;
|
|
1193
|
+
},
|
|
1194
|
+
onDestroy() {
|
|
1195
|
+
const storage = this.editor.storage.ai;
|
|
1196
|
+
storage.abort?.abort();
|
|
1197
|
+
storage.abort = null;
|
|
1198
|
+
storage.preview = null;
|
|
1199
|
+
}
|
|
1200
|
+
});
|
|
1201
|
+
var aiPlugin = definePlugin17({
|
|
1202
|
+
id: "ai",
|
|
1203
|
+
tiptapExtensions: [AIExtension],
|
|
1204
|
+
slashCommands: [{ name: "AI", description: "Generate text with AI", command: "aiGenerate" }]
|
|
1205
|
+
});
|
|
1206
|
+
|
|
1207
|
+
// src/comment.ts
|
|
1208
|
+
import { Mark } from "@tiptap/core";
|
|
1209
|
+
var CommentMark = Mark.create({
|
|
1210
|
+
name: "comment",
|
|
1211
|
+
// Comments are inclusive (typing inside an existing comment extends
|
|
1212
|
+
// the mark range — that's the typical inline-comment UX). Excluding
|
|
1213
|
+
// would split the mark on every keystroke.
|
|
1214
|
+
inclusive: true,
|
|
1215
|
+
addAttributes() {
|
|
1216
|
+
return {
|
|
1217
|
+
threadId: {
|
|
1218
|
+
default: null,
|
|
1219
|
+
parseHTML: (el) => el.getAttribute("data-comment-thread"),
|
|
1220
|
+
renderHTML: (attrs) => {
|
|
1221
|
+
if (!attrs.threadId) return {};
|
|
1222
|
+
return { "data-comment-thread": attrs.threadId, class: "docs-comment" };
|
|
1223
|
+
}
|
|
1224
|
+
},
|
|
1225
|
+
pos: {
|
|
1226
|
+
default: null,
|
|
1227
|
+
// No HTML render — position is server-side state.
|
|
1228
|
+
renderHTML: () => ({})
|
|
1229
|
+
}
|
|
1230
|
+
};
|
|
1231
|
+
},
|
|
1232
|
+
parseHTML() {
|
|
1233
|
+
return [{ tag: "span[data-comment-thread]" }];
|
|
1234
|
+
},
|
|
1235
|
+
renderHTML({ HTMLAttributes }) {
|
|
1236
|
+
return ["span", HTMLAttributes, 0];
|
|
1237
|
+
}
|
|
1238
|
+
});
|
|
1239
|
+
var commentPlugin = {
|
|
1240
|
+
id: "comment",
|
|
1241
|
+
tiptapExtensions: [CommentMark],
|
|
1242
|
+
toolbar: [],
|
|
1243
|
+
slashCommands: [],
|
|
1244
|
+
commands: {}
|
|
1245
|
+
};
|
|
1246
|
+
|
|
1247
|
+
// src/slashMenu.ts
|
|
1248
|
+
import { Extension as Extension3 } from "@tiptap/core";
|
|
1249
|
+
import { Plugin as Plugin2, PluginKey as PluginKey2 } from "@tiptap/pm/state";
|
|
1250
|
+
import { definePlugin as definePlugin18 } from "@kedataindo/docflow-core";
|
|
1251
|
+
var slashState = {
|
|
1252
|
+
open: false,
|
|
1253
|
+
query: "",
|
|
1254
|
+
position: { top: 0, left: 0 },
|
|
1255
|
+
commands: [],
|
|
1256
|
+
selectedIndex: 0
|
|
1257
|
+
};
|
|
1258
|
+
var listeners = [];
|
|
1259
|
+
function onSlashStateChange(fn) {
|
|
1260
|
+
listeners.push(fn);
|
|
1261
|
+
return () => {
|
|
1262
|
+
listeners = listeners.filter((l) => l !== fn);
|
|
1263
|
+
};
|
|
1264
|
+
}
|
|
1265
|
+
function notifyListeners() {
|
|
1266
|
+
listeners.forEach((fn) => fn());
|
|
1267
|
+
}
|
|
1268
|
+
function closeSlashMenu() {
|
|
1269
|
+
slashState = { ...slashState, open: false, query: "" };
|
|
1270
|
+
notifyListeners();
|
|
1271
|
+
}
|
|
1272
|
+
function openSlashMenu(view, query) {
|
|
1273
|
+
const { selection } = view.state;
|
|
1274
|
+
const pos = selection.head;
|
|
1275
|
+
const coords = view.coordsAtPos(pos);
|
|
1276
|
+
const allCommands = getRegisteredCommands();
|
|
1277
|
+
const filtered = query ? allCommands.filter((c) => c.name.toLowerCase().includes(query.toLowerCase())) : allCommands;
|
|
1278
|
+
slashState = {
|
|
1279
|
+
open: true,
|
|
1280
|
+
query,
|
|
1281
|
+
position: { top: coords.bottom + 4, left: coords.left },
|
|
1282
|
+
commands: filtered.map((c) => ({ ...c, action: null })),
|
|
1283
|
+
selectedIndex: 0
|
|
1284
|
+
};
|
|
1285
|
+
notifyListeners();
|
|
1286
|
+
}
|
|
1287
|
+
var registeredCommands = /* @__PURE__ */ new Map();
|
|
1288
|
+
function registerSlashCommands(editorId, commands) {
|
|
1289
|
+
const existing = registeredCommands.get(editorId) || [];
|
|
1290
|
+
registeredCommands.set(editorId, [...existing, ...commands]);
|
|
1291
|
+
}
|
|
1292
|
+
function getRegisteredCommands() {
|
|
1293
|
+
const all = [];
|
|
1294
|
+
registeredCommands.forEach((cmds) => {
|
|
1295
|
+
all.push(...cmds);
|
|
1296
|
+
});
|
|
1297
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1298
|
+
return all.filter((c) => {
|
|
1299
|
+
if (seen.has(c.name)) return false;
|
|
1300
|
+
seen.add(c.name);
|
|
1301
|
+
return true;
|
|
1302
|
+
});
|
|
1303
|
+
}
|
|
1304
|
+
var SlashMenuExtension = Extension3.create({
|
|
1305
|
+
name: "slashMenu",
|
|
1306
|
+
addProseMirrorPlugins() {
|
|
1307
|
+
return [
|
|
1308
|
+
new Plugin2({
|
|
1309
|
+
key: new PluginKey2("slashMenu"),
|
|
1310
|
+
props: {
|
|
1311
|
+
handleTextInput(view, from, _to, text) {
|
|
1312
|
+
if (text === "/") {
|
|
1313
|
+
const $pos = view.state.doc.resolve(from);
|
|
1314
|
+
const textBefore = $pos.parent.textContent.slice(0, $pos.parentOffset);
|
|
1315
|
+
if (textBefore === "" || textBefore.endsWith(" ")) {
|
|
1316
|
+
setTimeout(() => openSlashMenu(view, ""), 10);
|
|
1317
|
+
return false;
|
|
1318
|
+
}
|
|
1319
|
+
}
|
|
1320
|
+
if (slashState.open && text.length === 1 && /[a-zA-Z]/.test(text)) {
|
|
1321
|
+
const newQuery = slashState.query + text;
|
|
1322
|
+
openSlashMenu(view, newQuery);
|
|
1323
|
+
return false;
|
|
1324
|
+
}
|
|
1325
|
+
return false;
|
|
1326
|
+
},
|
|
1327
|
+
handleKeyDown(view, event) {
|
|
1328
|
+
if (!slashState.open) return false;
|
|
1329
|
+
if (event.key === "ArrowDown") {
|
|
1330
|
+
event.preventDefault();
|
|
1331
|
+
slashState.selectedIndex = Math.min(slashState.selectedIndex + 1, slashState.commands.length - 1);
|
|
1332
|
+
notifyListeners();
|
|
1333
|
+
return true;
|
|
1334
|
+
}
|
|
1335
|
+
if (event.key === "ArrowUp") {
|
|
1336
|
+
event.preventDefault();
|
|
1337
|
+
slashState.selectedIndex = Math.max(slashState.selectedIndex - 1, 0);
|
|
1338
|
+
notifyListeners();
|
|
1339
|
+
return true;
|
|
1340
|
+
}
|
|
1341
|
+
if (event.key === "Enter") {
|
|
1342
|
+
event.preventDefault();
|
|
1343
|
+
const cmd = slashState.commands[slashState.selectedIndex];
|
|
1344
|
+
if (cmd) {
|
|
1345
|
+
const $pos = view.state.doc.resolve(view.state.selection.head);
|
|
1346
|
+
const lineStart = $pos.start();
|
|
1347
|
+
const slashPos = lineStart + $pos.parent.textContent.lastIndexOf("/");
|
|
1348
|
+
if (slashPos >= lineStart) {
|
|
1349
|
+
const tr = view.state.tr.delete(slashPos, view.state.selection.head);
|
|
1350
|
+
view.dispatch(tr);
|
|
1351
|
+
}
|
|
1352
|
+
const editor = view._tiptapEditor;
|
|
1353
|
+
if (editor) {
|
|
1354
|
+
const actionMap = editor;
|
|
1355
|
+
if (typeof actionMap[cmd.command] === "function") {
|
|
1356
|
+
actionMap[cmd.command]();
|
|
1357
|
+
}
|
|
1358
|
+
}
|
|
1359
|
+
}
|
|
1360
|
+
closeSlashMenu();
|
|
1361
|
+
return true;
|
|
1362
|
+
}
|
|
1363
|
+
if (event.key === "Escape") {
|
|
1364
|
+
closeSlashMenu();
|
|
1365
|
+
return true;
|
|
1366
|
+
}
|
|
1367
|
+
if (event.key === "Backspace") {
|
|
1368
|
+
if (slashState.query.length > 0) {
|
|
1369
|
+
const newQuery = slashState.query.slice(0, -1);
|
|
1370
|
+
if (newQuery) {
|
|
1371
|
+
openSlashMenu(view, newQuery);
|
|
1372
|
+
} else {
|
|
1373
|
+
openSlashMenu(view, "");
|
|
1374
|
+
}
|
|
1375
|
+
return false;
|
|
1376
|
+
} else {
|
|
1377
|
+
const $pos = view.state.doc.resolve(view.state.selection.head);
|
|
1378
|
+
const lineStart = $pos.start();
|
|
1379
|
+
const slashPos = $pos.parent.textContent.lastIndexOf("/");
|
|
1380
|
+
if (slashPos >= 0) {
|
|
1381
|
+
const tr = view.state.tr.delete(lineStart + slashPos, view.state.selection.head);
|
|
1382
|
+
view.dispatch(tr);
|
|
1383
|
+
}
|
|
1384
|
+
closeSlashMenu();
|
|
1385
|
+
return true;
|
|
1386
|
+
}
|
|
1387
|
+
}
|
|
1388
|
+
return false;
|
|
1389
|
+
}
|
|
1390
|
+
}
|
|
1391
|
+
})
|
|
1392
|
+
];
|
|
1393
|
+
}
|
|
1394
|
+
});
|
|
1395
|
+
var slashMenuPlugin = definePlugin18({
|
|
1396
|
+
id: "slash-menu",
|
|
1397
|
+
tiptapExtensions: [SlashMenuExtension],
|
|
1398
|
+
hooks: {
|
|
1399
|
+
onInit(_editor) {
|
|
1400
|
+
const commands = [];
|
|
1401
|
+
registerSlashCommands("default", commands);
|
|
1402
|
+
}
|
|
1403
|
+
}
|
|
1404
|
+
});
|
|
1405
|
+
|
|
334
1406
|
// src/index.ts
|
|
335
1407
|
var defaultPlugins = [
|
|
336
1408
|
formattingPlugin,
|
|
@@ -345,25 +1417,62 @@ var defaultPlugins = [
|
|
|
345
1417
|
placeholderPlugin,
|
|
346
1418
|
pageBreakPlugin,
|
|
347
1419
|
footnotePlugin,
|
|
348
|
-
|
|
1420
|
+
tocPlugin,
|
|
1421
|
+
fontSizePlugin,
|
|
1422
|
+
textColorPlugin,
|
|
1423
|
+
highlightPlugin,
|
|
1424
|
+
citationPlugin,
|
|
1425
|
+
aiPlugin,
|
|
1426
|
+
commentPlugin
|
|
349
1427
|
];
|
|
350
1428
|
export {
|
|
1429
|
+
AIExtension,
|
|
1430
|
+
BibliographyNode,
|
|
1431
|
+
CSL_LOCALE_EN_US,
|
|
1432
|
+
CSL_STYLES,
|
|
1433
|
+
CitationEngineExtension,
|
|
1434
|
+
CitationNode,
|
|
1435
|
+
CiteEngine,
|
|
1436
|
+
CommentMark as CommentMarkExtension,
|
|
1437
|
+
DEFAULT_CSL_STYLE,
|
|
351
1438
|
FontSizeExtension,
|
|
352
1439
|
FootnoteNode,
|
|
353
1440
|
PageBreak,
|
|
1441
|
+
SlashMenuExtension,
|
|
1442
|
+
TocEntryNode,
|
|
1443
|
+
TocNode,
|
|
1444
|
+
TocPageNumNode,
|
|
1445
|
+
aiPlugin,
|
|
1446
|
+
aiPluginKey,
|
|
354
1447
|
alignmentPlugin,
|
|
355
1448
|
blockquotePlugin,
|
|
1449
|
+
buildCitationNodes,
|
|
1450
|
+
citationPlugin,
|
|
356
1451
|
codeBlockPlugin,
|
|
1452
|
+
collectHeadings,
|
|
1453
|
+
commentPlugin,
|
|
357
1454
|
createPlaceholderPlugin,
|
|
358
1455
|
defaultPlugins,
|
|
359
1456
|
fontSizePlugin,
|
|
360
1457
|
footnotePlugin,
|
|
361
1458
|
formattingPlugin,
|
|
1459
|
+
getAIPreview,
|
|
1460
|
+
getCitationEngine,
|
|
362
1461
|
headingsPlugin,
|
|
1462
|
+
highlightPlugin,
|
|
363
1463
|
imagePlugin,
|
|
364
1464
|
linkPlugin,
|
|
365
1465
|
listsPlugin,
|
|
1466
|
+
nextCitationId,
|
|
1467
|
+
onSlashStateChange,
|
|
366
1468
|
pageBreakPlugin,
|
|
367
1469
|
placeholderPlugin,
|
|
368
|
-
|
|
1470
|
+
regenerateToc,
|
|
1471
|
+
registerSlashCommands,
|
|
1472
|
+
sanitizeCiteprocHtml,
|
|
1473
|
+
slashMenuPlugin,
|
|
1474
|
+
slashState,
|
|
1475
|
+
tablePlugin,
|
|
1476
|
+
textColorPlugin,
|
|
1477
|
+
tocPlugin
|
|
369
1478
|
};
|