fantsec-docmost-cli 2.2.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/LICENSE +21 -0
- package/README.md +137 -0
- package/build/__tests__/cli-utils.test.js +287 -0
- package/build/__tests__/client-pagination.test.js +103 -0
- package/build/__tests__/discovery.test.js +40 -0
- package/build/__tests__/envelope.test.js +91 -0
- package/build/__tests__/filters.test.js +235 -0
- package/build/__tests__/integration/comment.test.js +48 -0
- package/build/__tests__/integration/discovery.test.js +24 -0
- package/build/__tests__/integration/file.test.js +33 -0
- package/build/__tests__/integration/group.test.js +48 -0
- package/build/__tests__/integration/helpers/global-setup.js +80 -0
- package/build/__tests__/integration/helpers/run-cli.js +163 -0
- package/build/__tests__/integration/invite.test.js +34 -0
- package/build/__tests__/integration/page.test.js +69 -0
- package/build/__tests__/integration/search.test.js +45 -0
- package/build/__tests__/integration/share.test.js +49 -0
- package/build/__tests__/integration/space.test.js +56 -0
- package/build/__tests__/integration/user.test.js +15 -0
- package/build/__tests__/integration/workspace.test.js +42 -0
- package/build/__tests__/markdown-converter.test.js +445 -0
- package/build/__tests__/mcp-tooling.test.js +58 -0
- package/build/__tests__/page-mentions.test.js +65 -0
- package/build/__tests__/tiptap-extensions.test.js +135 -0
- package/build/client.js +715 -0
- package/build/commands/comment.js +54 -0
- package/build/commands/discovery.js +21 -0
- package/build/commands/file.js +36 -0
- package/build/commands/group.js +91 -0
- package/build/commands/invite.js +67 -0
- package/build/commands/page.js +227 -0
- package/build/commands/search.js +33 -0
- package/build/commands/share.js +65 -0
- package/build/commands/space.js +154 -0
- package/build/commands/user.js +38 -0
- package/build/commands/workspace.js +77 -0
- package/build/index.js +19 -0
- package/build/lib/auth-utils.js +53 -0
- package/build/lib/cli-utils.js +293 -0
- package/build/lib/collaboration.js +126 -0
- package/build/lib/filters.js +137 -0
- package/build/lib/markdown-converter.js +187 -0
- package/build/lib/mcp-tooling.js +295 -0
- package/build/lib/page-mentions.js +162 -0
- package/build/lib/tiptap-extensions.js +86 -0
- package/build/mcp.js +186 -0
- package/build/program.js +60 -0
- package/package.json +64 -0
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
import { format } from "util";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { getSafeOutput, isCommanderHelpExit, normalizeError, printError, } from "./cli-utils.js";
|
|
4
|
+
import { createProgram } from "../program.js";
|
|
5
|
+
const EXCLUDED_COMMANDS = new Set(["commands"]);
|
|
6
|
+
const FILE_OUTPUT_COMMANDS = new Set(["file-download", "page-export", "space-export"]);
|
|
7
|
+
let executionQueue = Promise.resolve();
|
|
8
|
+
function toBuffer(chunk, encoding) {
|
|
9
|
+
if (typeof chunk === "string") {
|
|
10
|
+
return Buffer.from(chunk, encoding);
|
|
11
|
+
}
|
|
12
|
+
return Buffer.from(chunk);
|
|
13
|
+
}
|
|
14
|
+
function captureWrite(target) {
|
|
15
|
+
return (chunk, encoding, cb) => {
|
|
16
|
+
const normalizedEncoding = typeof encoding === "string" ? encoding : undefined;
|
|
17
|
+
const callback = typeof encoding === "function" ? encoding : cb;
|
|
18
|
+
target.push(toBuffer(chunk, normalizedEncoding));
|
|
19
|
+
callback?.(null);
|
|
20
|
+
return true;
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
async function withCapturedStdio(run) {
|
|
24
|
+
const output = { stdout: [], stderr: [] };
|
|
25
|
+
const originalStdoutWrite = process.stdout.write.bind(process.stdout);
|
|
26
|
+
const originalStderrWrite = process.stderr.write.bind(process.stderr);
|
|
27
|
+
const originalConsoleLog = console.log;
|
|
28
|
+
const originalConsoleError = console.error;
|
|
29
|
+
process.stdout.write = captureWrite(output.stdout);
|
|
30
|
+
process.stderr.write = captureWrite(output.stderr);
|
|
31
|
+
console.log = (...args) => {
|
|
32
|
+
output.stdout.push(Buffer.from(`${format(...args)}\n`, "utf-8"));
|
|
33
|
+
};
|
|
34
|
+
console.error = (...args) => {
|
|
35
|
+
output.stderr.push(Buffer.from(`${format(...args)}\n`, "utf-8"));
|
|
36
|
+
};
|
|
37
|
+
try {
|
|
38
|
+
const result = await run();
|
|
39
|
+
return {
|
|
40
|
+
result,
|
|
41
|
+
stdout: Buffer.concat(output.stdout).toString("utf-8"),
|
|
42
|
+
stderr: Buffer.concat(output.stderr).toString("utf-8"),
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
finally {
|
|
46
|
+
process.stdout.write = originalStdoutWrite;
|
|
47
|
+
process.stderr.write = originalStderrWrite;
|
|
48
|
+
console.log = originalConsoleLog;
|
|
49
|
+
console.error = originalConsoleError;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
function isNativeParseInt(option) {
|
|
53
|
+
return option.parseArg === parseInt || option.parseArg?.name === "parseInt";
|
|
54
|
+
}
|
|
55
|
+
function makeBooleanSchema(description, required, defaultValue) {
|
|
56
|
+
let schema = z.boolean().describe(description);
|
|
57
|
+
if (defaultValue !== undefined) {
|
|
58
|
+
schema = schema.default(defaultValue);
|
|
59
|
+
}
|
|
60
|
+
else if (!required) {
|
|
61
|
+
schema = schema.optional();
|
|
62
|
+
}
|
|
63
|
+
return schema;
|
|
64
|
+
}
|
|
65
|
+
function makeNumberSchema(description, required, defaultValue) {
|
|
66
|
+
let schema = z.number().int().describe(description);
|
|
67
|
+
if (defaultValue !== undefined) {
|
|
68
|
+
schema = schema.default(defaultValue);
|
|
69
|
+
}
|
|
70
|
+
else if (!required) {
|
|
71
|
+
schema = schema.optional();
|
|
72
|
+
}
|
|
73
|
+
return schema;
|
|
74
|
+
}
|
|
75
|
+
function makeStringSchema(description, required, choices, defaultValue) {
|
|
76
|
+
let schema = z.string().describe(description);
|
|
77
|
+
if (choices && choices.length > 0) {
|
|
78
|
+
schema = schema.refine((value) => typeof value === "string" && choices.includes(value), {
|
|
79
|
+
message: `Expected one of: ${choices.join(", ")}`,
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
if (defaultValue !== undefined) {
|
|
83
|
+
schema = schema.default(defaultValue);
|
|
84
|
+
}
|
|
85
|
+
else if (!required) {
|
|
86
|
+
schema = schema.optional();
|
|
87
|
+
}
|
|
88
|
+
return schema;
|
|
89
|
+
}
|
|
90
|
+
function createOptionSchema(option, commandName) {
|
|
91
|
+
const name = option.attributeName();
|
|
92
|
+
const long = option.long;
|
|
93
|
+
if (!long) {
|
|
94
|
+
throw new Error(`Option '${option.flags}' on command '${commandName}' is missing a long flag.`);
|
|
95
|
+
}
|
|
96
|
+
const required = FILE_OUTPUT_COMMANDS.has(commandName) && name === "output"
|
|
97
|
+
? true
|
|
98
|
+
: option.mandatory === true;
|
|
99
|
+
const optionDescription = option.description || option.flags;
|
|
100
|
+
const description = FILE_OUTPUT_COMMANDS.has(commandName) && name === "output"
|
|
101
|
+
? `${optionDescription} Required in MCP mode so binary output is written to a local file.`
|
|
102
|
+
: optionDescription;
|
|
103
|
+
if (option.isBoolean()) {
|
|
104
|
+
return {
|
|
105
|
+
name,
|
|
106
|
+
long,
|
|
107
|
+
description,
|
|
108
|
+
required,
|
|
109
|
+
schema: makeBooleanSchema(description, required, option.defaultValue),
|
|
110
|
+
serialize: (value) => (value ? [long] : []),
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
if (option.argChoices?.length === 2 && option.argChoices.includes("true") && option.argChoices.includes("false")) {
|
|
114
|
+
return {
|
|
115
|
+
name,
|
|
116
|
+
long,
|
|
117
|
+
description,
|
|
118
|
+
required,
|
|
119
|
+
schema: makeBooleanSchema(description, required, option.defaultValue),
|
|
120
|
+
serialize: (value) => (value === undefined ? [] : [long, String(value)]),
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
if (isNativeParseInt(option)) {
|
|
124
|
+
return {
|
|
125
|
+
name,
|
|
126
|
+
long,
|
|
127
|
+
description,
|
|
128
|
+
required,
|
|
129
|
+
schema: makeNumberSchema(description, required, option.defaultValue),
|
|
130
|
+
serialize: (value) => (value === undefined ? [] : [long, String(value)]),
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
return {
|
|
134
|
+
name,
|
|
135
|
+
long,
|
|
136
|
+
description,
|
|
137
|
+
required,
|
|
138
|
+
schema: makeStringSchema(description, required, option.argChoices, option.defaultValue),
|
|
139
|
+
serialize: (value) => (value === undefined ? [] : [long, String(value)]),
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
function getAnnotations(commandName) {
|
|
143
|
+
const readOnlyPrefixes = [
|
|
144
|
+
"page-list",
|
|
145
|
+
"page-info",
|
|
146
|
+
"page-history",
|
|
147
|
+
"page-breadcrumbs",
|
|
148
|
+
"page-tree",
|
|
149
|
+
"page-trash",
|
|
150
|
+
"space-list",
|
|
151
|
+
"space-info",
|
|
152
|
+
"space-member-list",
|
|
153
|
+
"workspace-info",
|
|
154
|
+
"workspace-public",
|
|
155
|
+
"member-list",
|
|
156
|
+
"user-me",
|
|
157
|
+
"group-list",
|
|
158
|
+
"group-info",
|
|
159
|
+
"group-member-list",
|
|
160
|
+
"comment-list",
|
|
161
|
+
"comment-info",
|
|
162
|
+
"share-list",
|
|
163
|
+
"share-info",
|
|
164
|
+
"share-for-page",
|
|
165
|
+
"invite-list",
|
|
166
|
+
"invite-info",
|
|
167
|
+
"invite-link",
|
|
168
|
+
"search",
|
|
169
|
+
"search-suggest",
|
|
170
|
+
];
|
|
171
|
+
const destructiveTokens = ["delete", "remove", "revoke"];
|
|
172
|
+
const readOnlyHint = readOnlyPrefixes.some((prefix) => commandName.startsWith(prefix));
|
|
173
|
+
const destructiveHint = destructiveTokens.some((token) => commandName.includes(token));
|
|
174
|
+
const idempotentHint = readOnlyHint || commandName.endsWith("-update") || commandName.endsWith("-role");
|
|
175
|
+
return {
|
|
176
|
+
readOnlyHint: readOnlyHint || undefined,
|
|
177
|
+
destructiveHint: destructiveHint || undefined,
|
|
178
|
+
idempotentHint: idempotentHint || undefined,
|
|
179
|
+
openWorldHint: true,
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
export function listMcpTools() {
|
|
183
|
+
const program = createProgram();
|
|
184
|
+
return program.commands
|
|
185
|
+
.filter((command) => !EXCLUDED_COMMANDS.has(command.name()))
|
|
186
|
+
.map((command) => {
|
|
187
|
+
const commandName = command.name();
|
|
188
|
+
const options = command.options.map((option) => createOptionSchema(option, commandName));
|
|
189
|
+
const inputSchema = Object.fromEntries(options.map((option) => [option.name, option.schema]));
|
|
190
|
+
const toolName = commandName.replace(/-/g, "_");
|
|
191
|
+
const baseDescription = command.description();
|
|
192
|
+
const description = FILE_OUTPUT_COMMANDS.has(commandName)
|
|
193
|
+
? `${baseDescription}. This MCP wrapper requires \`output\` so exported/downloaded bytes are saved to a local file instead of streamed over stdio.`
|
|
194
|
+
: baseDescription;
|
|
195
|
+
return {
|
|
196
|
+
commandName,
|
|
197
|
+
toolName,
|
|
198
|
+
description,
|
|
199
|
+
inputSchema,
|
|
200
|
+
options,
|
|
201
|
+
annotations: getAnnotations(commandName),
|
|
202
|
+
requiresOutputPath: FILE_OUTPUT_COMMANDS.has(commandName),
|
|
203
|
+
};
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
function buildArgv(tool, args, auth) {
|
|
207
|
+
const argv = ["node", "docmost", "--format", "json"];
|
|
208
|
+
if (auth?.apiUrl) {
|
|
209
|
+
argv.push("--api-url", auth.apiUrl);
|
|
210
|
+
}
|
|
211
|
+
if (auth?.token) {
|
|
212
|
+
argv.push("--token", auth.token);
|
|
213
|
+
}
|
|
214
|
+
else if (auth?.email && auth?.password) {
|
|
215
|
+
argv.push("--email", auth.email, "--password", auth.password);
|
|
216
|
+
}
|
|
217
|
+
argv.push(tool.commandName);
|
|
218
|
+
for (const option of tool.options) {
|
|
219
|
+
argv.push(...option.serialize(args[option.name]));
|
|
220
|
+
}
|
|
221
|
+
return argv;
|
|
222
|
+
}
|
|
223
|
+
async function executeToolInternal(tool, args, auth) {
|
|
224
|
+
const program = createProgram();
|
|
225
|
+
const argv = buildArgv(tool, args, auth);
|
|
226
|
+
const { stdout, stderr } = await withCapturedStdio(async () => {
|
|
227
|
+
try {
|
|
228
|
+
await program.parseAsync(argv);
|
|
229
|
+
}
|
|
230
|
+
catch (error) {
|
|
231
|
+
if (isCommanderHelpExit(error)) {
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
const normalized = normalizeError(error);
|
|
235
|
+
printError(normalized, getSafeOutput(program));
|
|
236
|
+
}
|
|
237
|
+
});
|
|
238
|
+
const combinedStdout = stdout.trim();
|
|
239
|
+
const combinedStderr = stderr.trim();
|
|
240
|
+
const parsed = combinedStdout
|
|
241
|
+
? safeJsonParse(combinedStdout)
|
|
242
|
+
: (combinedStderr ? safeJsonParse(combinedStderr) : undefined);
|
|
243
|
+
const ok = isSuccessResult(parsed, stderr);
|
|
244
|
+
return {
|
|
245
|
+
ok,
|
|
246
|
+
stdout,
|
|
247
|
+
stderr,
|
|
248
|
+
parsed,
|
|
249
|
+
outputPath: typeof args.output === "string" ? args.output : undefined,
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
function safeJsonParse(value) {
|
|
253
|
+
try {
|
|
254
|
+
return JSON.parse(value);
|
|
255
|
+
}
|
|
256
|
+
catch {
|
|
257
|
+
return undefined;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
function isSuccessResult(parsed, stderr) {
|
|
261
|
+
if (!parsed || typeof parsed !== "object") {
|
|
262
|
+
return stderr.trim().length === 0;
|
|
263
|
+
}
|
|
264
|
+
const envelope = parsed;
|
|
265
|
+
if (typeof envelope.ok === "boolean") {
|
|
266
|
+
return envelope.ok;
|
|
267
|
+
}
|
|
268
|
+
return stderr.trim().length === 0;
|
|
269
|
+
}
|
|
270
|
+
export function parseDocmostBearer(bearerToken, defaultApiUrl = process.env.DOCMOST_API_URL) {
|
|
271
|
+
const trimmed = bearerToken?.trim();
|
|
272
|
+
if (!defaultApiUrl) {
|
|
273
|
+
throw new Error("DOCMOST_API_URL is not set.");
|
|
274
|
+
}
|
|
275
|
+
if (!trimmed) {
|
|
276
|
+
return { apiUrl: defaultApiUrl };
|
|
277
|
+
}
|
|
278
|
+
const separatorIndex = trimmed.indexOf(":");
|
|
279
|
+
if (separatorIndex === -1) {
|
|
280
|
+
return {
|
|
281
|
+
apiUrl: defaultApiUrl,
|
|
282
|
+
token: trimmed,
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
return {
|
|
286
|
+
apiUrl: defaultApiUrl,
|
|
287
|
+
email: trimmed.slice(0, separatorIndex),
|
|
288
|
+
password: trimmed.slice(separatorIndex + 1),
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
export function executeTool(tool, args, auth) {
|
|
292
|
+
const run = executionQueue.then(() => executeToolInternal(tool, args, auth));
|
|
293
|
+
executionQueue = run.then(() => undefined, () => undefined);
|
|
294
|
+
return run;
|
|
295
|
+
}
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import { randomUUID } from "crypto";
|
|
2
|
+
import { marked } from "marked";
|
|
3
|
+
import { generateJSON } from "@tiptap/html";
|
|
4
|
+
import { JSDOM } from "jsdom";
|
|
5
|
+
import { tiptapExtensions } from "./tiptap-extensions.js";
|
|
6
|
+
const EXCLUDED_PARENT_TAGS = new Set(["A", "CODE", "PRE", "SCRIPT", "STYLE"]);
|
|
7
|
+
function ensureDomEnvironment(dom) {
|
|
8
|
+
if (typeof window === "undefined") {
|
|
9
|
+
global.window = dom.window;
|
|
10
|
+
}
|
|
11
|
+
if (typeof document === "undefined") {
|
|
12
|
+
global.document = dom.window.document;
|
|
13
|
+
}
|
|
14
|
+
if (typeof Element === "undefined") {
|
|
15
|
+
global.Element = dom.window.Element;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
function isAsciiWordChar(char) {
|
|
19
|
+
return /[A-Za-z0-9_]/.test(char);
|
|
20
|
+
}
|
|
21
|
+
function isMentionBoundary(text, atIndex) {
|
|
22
|
+
if (atIndex === 0) {
|
|
23
|
+
return true;
|
|
24
|
+
}
|
|
25
|
+
return !isAsciiWordChar(text[atIndex - 1]);
|
|
26
|
+
}
|
|
27
|
+
function isPlainMentionStopChar(char) {
|
|
28
|
+
return /\s/.test(char) || /[\\/`'".,!?;:()[\]{}<>|~^&*=+#$%,。!?;:、()【】《》]/.test(char);
|
|
29
|
+
}
|
|
30
|
+
function extractMentionTokens(text) {
|
|
31
|
+
const tokens = [];
|
|
32
|
+
for (let index = 0; index < text.length; index += 1) {
|
|
33
|
+
if (text[index] !== "@" || !isMentionBoundary(text, index)) {
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
if (text[index + 1] === "[") {
|
|
37
|
+
const closingIndex = text.indexOf("]", index + 2);
|
|
38
|
+
if (closingIndex === -1) {
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
const label = text.slice(index + 2, closingIndex).trim();
|
|
42
|
+
if (!label) {
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
tokens.push({
|
|
46
|
+
start: index,
|
|
47
|
+
end: closingIndex + 1,
|
|
48
|
+
raw: text.slice(index, closingIndex + 1),
|
|
49
|
+
label,
|
|
50
|
+
});
|
|
51
|
+
index = closingIndex;
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
let end = index + 1;
|
|
55
|
+
while (end < text.length && !isPlainMentionStopChar(text[end])) {
|
|
56
|
+
end += 1;
|
|
57
|
+
}
|
|
58
|
+
const label = text.slice(index + 1, end).trim();
|
|
59
|
+
if (!label) {
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
tokens.push({
|
|
63
|
+
start: index,
|
|
64
|
+
end,
|
|
65
|
+
raw: text.slice(index, end),
|
|
66
|
+
label,
|
|
67
|
+
});
|
|
68
|
+
index = end - 1;
|
|
69
|
+
}
|
|
70
|
+
return tokens;
|
|
71
|
+
}
|
|
72
|
+
async function injectPageMentionsIntoHtml(html, resolvePageMention, creatorId) {
|
|
73
|
+
const dom = new JSDOM(`<!DOCTYPE html><html><body>${html}</body></html>`);
|
|
74
|
+
ensureDomEnvironment(dom);
|
|
75
|
+
const { document, NodeFilter } = dom.window;
|
|
76
|
+
const walker = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT, {
|
|
77
|
+
acceptNode: (node) => {
|
|
78
|
+
const text = node.textContent ?? "";
|
|
79
|
+
if (!text.includes("@")) {
|
|
80
|
+
return NodeFilter.FILTER_REJECT;
|
|
81
|
+
}
|
|
82
|
+
const parentTag = node.parentElement?.tagName;
|
|
83
|
+
if (parentTag && EXCLUDED_PARENT_TAGS.has(parentTag)) {
|
|
84
|
+
return NodeFilter.FILTER_REJECT;
|
|
85
|
+
}
|
|
86
|
+
return NodeFilter.FILTER_ACCEPT;
|
|
87
|
+
},
|
|
88
|
+
});
|
|
89
|
+
const textNodes = [];
|
|
90
|
+
let currentNode = walker.nextNode();
|
|
91
|
+
while (currentNode) {
|
|
92
|
+
textNodes.push(currentNode);
|
|
93
|
+
currentNode = walker.nextNode();
|
|
94
|
+
}
|
|
95
|
+
const labels = new Set();
|
|
96
|
+
const nodeTokens = new Map();
|
|
97
|
+
for (const textNode of textNodes) {
|
|
98
|
+
const tokens = extractMentionTokens(textNode.textContent ?? "");
|
|
99
|
+
if (tokens.length === 0) {
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
nodeTokens.set(textNode, tokens);
|
|
103
|
+
for (const token of tokens) {
|
|
104
|
+
labels.add(token.label);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
if (labels.size === 0) {
|
|
108
|
+
return html;
|
|
109
|
+
}
|
|
110
|
+
const resolvedMentions = new Map();
|
|
111
|
+
for (const label of labels) {
|
|
112
|
+
resolvedMentions.set(label, await resolvePageMention(label));
|
|
113
|
+
}
|
|
114
|
+
for (const [textNode, tokens] of nodeTokens.entries()) {
|
|
115
|
+
const originalText = textNode.textContent ?? "";
|
|
116
|
+
const fragment = document.createDocumentFragment();
|
|
117
|
+
let cursor = 0;
|
|
118
|
+
let replaced = false;
|
|
119
|
+
for (const token of tokens) {
|
|
120
|
+
if (token.start > cursor) {
|
|
121
|
+
fragment.append(document.createTextNode(originalText.slice(cursor, token.start)));
|
|
122
|
+
}
|
|
123
|
+
const resolved = resolvedMentions.get(token.label);
|
|
124
|
+
if (resolved) {
|
|
125
|
+
const mention = document.createElement("span");
|
|
126
|
+
mention.setAttribute("data-type", "mention");
|
|
127
|
+
mention.setAttribute("data-id", randomUUID());
|
|
128
|
+
mention.setAttribute("data-entity-type", "page");
|
|
129
|
+
mention.setAttribute("data-entity-id", resolved.id);
|
|
130
|
+
mention.setAttribute("data-label", resolved.title);
|
|
131
|
+
mention.setAttribute("data-slug-id", resolved.slugId);
|
|
132
|
+
if (creatorId) {
|
|
133
|
+
mention.setAttribute("data-creator-id", creatorId);
|
|
134
|
+
}
|
|
135
|
+
mention.textContent = resolved.title;
|
|
136
|
+
fragment.append(mention);
|
|
137
|
+
replaced = true;
|
|
138
|
+
}
|
|
139
|
+
else {
|
|
140
|
+
fragment.append(document.createTextNode(token.raw));
|
|
141
|
+
}
|
|
142
|
+
cursor = token.end;
|
|
143
|
+
}
|
|
144
|
+
if (cursor < originalText.length) {
|
|
145
|
+
fragment.append(document.createTextNode(originalText.slice(cursor)));
|
|
146
|
+
}
|
|
147
|
+
if (replaced) {
|
|
148
|
+
textNode.parentNode?.replaceChild(fragment, textNode);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
return document.body.innerHTML;
|
|
152
|
+
}
|
|
153
|
+
export async function markdownToProseMirrorJson(markdown, options = {}) {
|
|
154
|
+
const html = await marked.parse(markdown);
|
|
155
|
+
const resolvedHtml = options.resolvePageMention
|
|
156
|
+
? await injectPageMentionsIntoHtml(html, options.resolvePageMention, options.creatorId)
|
|
157
|
+
: html;
|
|
158
|
+
return generateJSON(resolvedHtml, tiptapExtensions);
|
|
159
|
+
}
|
|
160
|
+
export function __internal_extractMentionTokens(text) {
|
|
161
|
+
return extractMentionTokens(text);
|
|
162
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { mergeAttributes, Node } from "@tiptap/core";
|
|
2
|
+
import StarterKit from "@tiptap/starter-kit";
|
|
3
|
+
import Image from "@tiptap/extension-image";
|
|
4
|
+
import Link from "@tiptap/extension-link";
|
|
5
|
+
import { Table } from "@tiptap/extension-table";
|
|
6
|
+
import { TableRow } from "@tiptap/extension-table-row";
|
|
7
|
+
import { TableCell } from "@tiptap/extension-table-cell";
|
|
8
|
+
import { TableHeader } from "@tiptap/extension-table-header";
|
|
9
|
+
const Mention = Node.create({
|
|
10
|
+
name: "mention",
|
|
11
|
+
group: "inline",
|
|
12
|
+
inline: true,
|
|
13
|
+
atom: true,
|
|
14
|
+
selectable: true,
|
|
15
|
+
addAttributes() {
|
|
16
|
+
return {
|
|
17
|
+
id: {
|
|
18
|
+
default: null,
|
|
19
|
+
parseHTML: (element) => element.getAttribute("data-id"),
|
|
20
|
+
renderHTML: (attributes) => attributes.id ? { "data-id": attributes.id } : {},
|
|
21
|
+
},
|
|
22
|
+
label: {
|
|
23
|
+
default: null,
|
|
24
|
+
parseHTML: (element) => element.getAttribute("data-label"),
|
|
25
|
+
renderHTML: (attributes) => attributes.label ? { "data-label": attributes.label } : {},
|
|
26
|
+
},
|
|
27
|
+
entityType: {
|
|
28
|
+
default: null,
|
|
29
|
+
parseHTML: (element) => element.getAttribute("data-entity-type"),
|
|
30
|
+
renderHTML: (attributes) => attributes.entityType ? { "data-entity-type": attributes.entityType } : {},
|
|
31
|
+
},
|
|
32
|
+
entityId: {
|
|
33
|
+
default: null,
|
|
34
|
+
parseHTML: (element) => element.getAttribute("data-entity-id"),
|
|
35
|
+
renderHTML: (attributes) => attributes.entityId ? { "data-entity-id": attributes.entityId } : {},
|
|
36
|
+
},
|
|
37
|
+
slugId: {
|
|
38
|
+
default: null,
|
|
39
|
+
parseHTML: (element) => element.getAttribute("data-slug-id"),
|
|
40
|
+
renderHTML: (attributes) => attributes.slugId ? { "data-slug-id": attributes.slugId } : {},
|
|
41
|
+
},
|
|
42
|
+
creatorId: {
|
|
43
|
+
default: null,
|
|
44
|
+
parseHTML: (element) => element.getAttribute("data-creator-id"),
|
|
45
|
+
renderHTML: (attributes) => attributes.creatorId ? { "data-creator-id": attributes.creatorId } : {},
|
|
46
|
+
},
|
|
47
|
+
anchorId: {
|
|
48
|
+
default: null,
|
|
49
|
+
parseHTML: (element) => element.getAttribute("data-anchor-id"),
|
|
50
|
+
renderHTML: (attributes) => attributes.anchorId ? { "data-anchor-id": attributes.anchorId } : {},
|
|
51
|
+
},
|
|
52
|
+
};
|
|
53
|
+
},
|
|
54
|
+
parseHTML() {
|
|
55
|
+
return [{ tag: 'span[data-type="mention"]' }];
|
|
56
|
+
},
|
|
57
|
+
renderHTML({ node, HTMLAttributes }) {
|
|
58
|
+
return [
|
|
59
|
+
"span",
|
|
60
|
+
mergeAttributes({ "data-type": "mention" }, HTMLAttributes),
|
|
61
|
+
node.attrs.label ?? node.attrs.entityId ?? "",
|
|
62
|
+
];
|
|
63
|
+
},
|
|
64
|
+
});
|
|
65
|
+
// Define extensions compatible with standard Markdown features
|
|
66
|
+
// We use the default Tiptap extensions to handle basic content
|
|
67
|
+
export const tiptapExtensions = [
|
|
68
|
+
StarterKit.configure({
|
|
69
|
+
// Explicitly enable features that might be disabled in some contexts
|
|
70
|
+
codeBlock: {},
|
|
71
|
+
heading: {},
|
|
72
|
+
}),
|
|
73
|
+
Image.configure({
|
|
74
|
+
inline: true,
|
|
75
|
+
}),
|
|
76
|
+
Link.configure({
|
|
77
|
+
openOnClick: false,
|
|
78
|
+
}),
|
|
79
|
+
Mention,
|
|
80
|
+
Table.configure({
|
|
81
|
+
resizable: false,
|
|
82
|
+
}),
|
|
83
|
+
TableRow,
|
|
84
|
+
TableCell,
|
|
85
|
+
TableHeader,
|
|
86
|
+
];
|