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,126 @@
|
|
|
1
|
+
import { HocuspocusProvider } from "@hocuspocus/provider";
|
|
2
|
+
import { TiptapTransformer } from "@hocuspocus/transformer";
|
|
3
|
+
import * as Y from "yjs";
|
|
4
|
+
import WebSocket from "ws";
|
|
5
|
+
import { JSDOM } from "jsdom";
|
|
6
|
+
import { tiptapExtensions } from "./tiptap-extensions.js";
|
|
7
|
+
const debug = (...args) => {
|
|
8
|
+
if (process.env.DEBUG)
|
|
9
|
+
console.error(...args);
|
|
10
|
+
};
|
|
11
|
+
let domSetup = false;
|
|
12
|
+
function setupDomEnvironment() {
|
|
13
|
+
if (domSetup)
|
|
14
|
+
return;
|
|
15
|
+
const dom = new JSDOM("<!DOCTYPE html><html><body></body></html>");
|
|
16
|
+
global.window = dom.window;
|
|
17
|
+
global.document = dom.window.document;
|
|
18
|
+
// @ts-ignore
|
|
19
|
+
global.Element = dom.window.Element;
|
|
20
|
+
// @ts-ignore
|
|
21
|
+
global.WebSocket = WebSocket;
|
|
22
|
+
domSetup = true;
|
|
23
|
+
}
|
|
24
|
+
export async function updatePageContentRealtime(pageId, tiptapJson, collabToken, baseUrl) {
|
|
25
|
+
setupDomEnvironment();
|
|
26
|
+
debug(`Starting realtime update for page ${pageId}`);
|
|
27
|
+
debug(`Collab token: ${collabToken ? "present" : "missing"}`);
|
|
28
|
+
// 1. Setup Hocuspocus Provider
|
|
29
|
+
const ydoc = new Y.Doc();
|
|
30
|
+
// Construct WebSocket URL
|
|
31
|
+
// Replace protocol
|
|
32
|
+
let wsUrl = baseUrl.replace(/^http/, "ws");
|
|
33
|
+
const urlObj = new URL(wsUrl);
|
|
34
|
+
// Remove /api suffix if present, as the websocket is mounted on root /collab
|
|
35
|
+
if (urlObj.pathname.endsWith("/api") || urlObj.pathname.endsWith("/api/")) {
|
|
36
|
+
urlObj.pathname = urlObj.pathname.replace(/\/api\/?$/, "");
|
|
37
|
+
}
|
|
38
|
+
// Set correct path to /collab
|
|
39
|
+
urlObj.pathname = urlObj.pathname.replace(/\/$/, "") + "/collab";
|
|
40
|
+
wsUrl = urlObj.toString();
|
|
41
|
+
debug(`Connecting to WebSocket: ${wsUrl}`);
|
|
42
|
+
return new Promise((resolve, reject) => {
|
|
43
|
+
let synced = false;
|
|
44
|
+
let settled = false;
|
|
45
|
+
const fail = (error) => {
|
|
46
|
+
if (settled)
|
|
47
|
+
return;
|
|
48
|
+
settled = true;
|
|
49
|
+
clearTimeout(timer);
|
|
50
|
+
reject(error);
|
|
51
|
+
};
|
|
52
|
+
// Safety timeout
|
|
53
|
+
const timer = setTimeout(() => {
|
|
54
|
+
if (provider)
|
|
55
|
+
provider.destroy();
|
|
56
|
+
fail(new Error("Connection timeout to collaboration server"));
|
|
57
|
+
}, 25000);
|
|
58
|
+
const provider = new HocuspocusProvider({
|
|
59
|
+
url: wsUrl,
|
|
60
|
+
name: `page.${pageId}`,
|
|
61
|
+
document: ydoc,
|
|
62
|
+
token: collabToken,
|
|
63
|
+
// @ts-ignore - Required for Node.js environment
|
|
64
|
+
WebSocketPolyfill: WebSocket,
|
|
65
|
+
onConnect: () => debug("WS Connect"),
|
|
66
|
+
onDisconnect: () => {
|
|
67
|
+
debug("WS Disconnect");
|
|
68
|
+
if (!synced) {
|
|
69
|
+
provider.destroy();
|
|
70
|
+
fail(new Error("WebSocket disconnected before sync completed"));
|
|
71
|
+
}
|
|
72
|
+
},
|
|
73
|
+
onClose: () => {
|
|
74
|
+
debug("WS Close");
|
|
75
|
+
if (!synced) {
|
|
76
|
+
fail(new Error("WebSocket closed before sync completed"));
|
|
77
|
+
}
|
|
78
|
+
},
|
|
79
|
+
onSynced: () => {
|
|
80
|
+
synced = true;
|
|
81
|
+
debug("Connected and synced!");
|
|
82
|
+
try {
|
|
83
|
+
// Prepare the new content in a separate doc
|
|
84
|
+
const tempDoc = TiptapTransformer.toYdoc(tiptapJson, "default", tiptapExtensions);
|
|
85
|
+
// Clear existing content
|
|
86
|
+
ydoc.transact(() => {
|
|
87
|
+
const fragment = ydoc.getXmlFragment("default");
|
|
88
|
+
if (fragment.length > 0) {
|
|
89
|
+
fragment.delete(0, fragment.length);
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
// Apply new content from tempDoc (outside transact to avoid nested transactions)
|
|
93
|
+
const update = Y.encodeStateAsUpdate(tempDoc);
|
|
94
|
+
Y.applyUpdate(ydoc, update);
|
|
95
|
+
debug("Content replaced. Background persistence in progress (server saves after ~10s debounce)...");
|
|
96
|
+
// Clear safety timeout as we are successful
|
|
97
|
+
clearTimeout(timer);
|
|
98
|
+
settled = true;
|
|
99
|
+
// Resolve immediately so the user doesn't have to wait
|
|
100
|
+
resolve();
|
|
101
|
+
// Keep connection open in background for save/sync (Docmost has 10s debounce)
|
|
102
|
+
// The node process will keep running this timeout even after the tool returns
|
|
103
|
+
const bgTimer = setTimeout(() => {
|
|
104
|
+
try {
|
|
105
|
+
debug(`Closing background connection for page ${pageId}`);
|
|
106
|
+
provider.destroy();
|
|
107
|
+
}
|
|
108
|
+
catch (err) {
|
|
109
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
110
|
+
process.stderr.write(`Warning: failed to close WebSocket: ${msg}\n`);
|
|
111
|
+
}
|
|
112
|
+
}, 15000);
|
|
113
|
+
bgTimer.unref();
|
|
114
|
+
}
|
|
115
|
+
catch (e) {
|
|
116
|
+
provider.destroy();
|
|
117
|
+
fail(e instanceof Error ? e : new Error(String(e)));
|
|
118
|
+
}
|
|
119
|
+
},
|
|
120
|
+
onAuthenticationFailed: () => {
|
|
121
|
+
provider.destroy();
|
|
122
|
+
fail(new Error("Authentication failed for collaboration connection"));
|
|
123
|
+
},
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Filter functions to extract only relevant information from API responses
|
|
3
|
+
* for better agent consumption
|
|
4
|
+
*/
|
|
5
|
+
export function filterWorkspace(data) {
|
|
6
|
+
return {
|
|
7
|
+
id: data.id,
|
|
8
|
+
name: data.name,
|
|
9
|
+
description: data.description,
|
|
10
|
+
defaultSpaceId: data.defaultSpaceId,
|
|
11
|
+
createdAt: data.createdAt,
|
|
12
|
+
updatedAt: data.updatedAt,
|
|
13
|
+
deletedAt: data.deletedAt,
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
export function filterSpace(space) {
|
|
17
|
+
return {
|
|
18
|
+
id: space.id,
|
|
19
|
+
name: space.name,
|
|
20
|
+
description: space.description,
|
|
21
|
+
slug: space.slug,
|
|
22
|
+
visibility: space.visibility,
|
|
23
|
+
createdAt: space.createdAt,
|
|
24
|
+
updatedAt: space.updatedAt,
|
|
25
|
+
deletedAt: space.deletedAt,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
export function filterGroup(group) {
|
|
29
|
+
return {
|
|
30
|
+
id: group.id,
|
|
31
|
+
name: group.name,
|
|
32
|
+
description: group.description,
|
|
33
|
+
workspaceId: group.workspaceId,
|
|
34
|
+
createdAt: group.createdAt,
|
|
35
|
+
updatedAt: group.updatedAt,
|
|
36
|
+
deletedAt: group.deletedAt,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
export function filterPage(page, content, subpages) {
|
|
40
|
+
return {
|
|
41
|
+
id: page.id,
|
|
42
|
+
title: page.title,
|
|
43
|
+
parentPageId: page.parentPageId,
|
|
44
|
+
spaceId: page.spaceId,
|
|
45
|
+
isLocked: page.isLocked,
|
|
46
|
+
createdAt: page.createdAt,
|
|
47
|
+
updatedAt: page.updatedAt,
|
|
48
|
+
deletedAt: page.deletedAt,
|
|
49
|
+
// Include converted markdown content if valid string (even empty)
|
|
50
|
+
...(typeof content === "string" && { content }),
|
|
51
|
+
// Include subpages if provided
|
|
52
|
+
...(subpages &&
|
|
53
|
+
subpages.length > 0 && {
|
|
54
|
+
subpages: subpages.map((p) => ({ id: p.id, title: p.title })),
|
|
55
|
+
}),
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
export function filterSearchResult(result) {
|
|
59
|
+
return {
|
|
60
|
+
id: result.id,
|
|
61
|
+
title: result.title,
|
|
62
|
+
parentPageId: result.parentPageId,
|
|
63
|
+
createdAt: result.createdAt,
|
|
64
|
+
updatedAt: result.updatedAt,
|
|
65
|
+
rank: result.rank,
|
|
66
|
+
highlight: result.highlight,
|
|
67
|
+
spaceId: result.space?.id,
|
|
68
|
+
spaceName: result.space?.name,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
export function filterHistoryEntry(entry) {
|
|
72
|
+
return {
|
|
73
|
+
id: entry.id,
|
|
74
|
+
pageId: entry.pageId,
|
|
75
|
+
title: entry.title,
|
|
76
|
+
version: entry.version,
|
|
77
|
+
createdAt: entry.createdAt,
|
|
78
|
+
lastUpdatedBy: entry.lastUpdatedBy?.name || entry.lastUpdatedById,
|
|
79
|
+
contributors: entry.contributors?.map((c) => c.name) || [],
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
export function filterMember(member) {
|
|
83
|
+
return {
|
|
84
|
+
id: member.id,
|
|
85
|
+
name: member.name,
|
|
86
|
+
email: member.email,
|
|
87
|
+
role: member.role,
|
|
88
|
+
createdAt: member.createdAt,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
export function filterInvite(invite) {
|
|
92
|
+
return {
|
|
93
|
+
id: invite.id,
|
|
94
|
+
email: invite.email,
|
|
95
|
+
role: invite.role,
|
|
96
|
+
status: invite.status,
|
|
97
|
+
invitedById: invite.invitedById,
|
|
98
|
+
createdAt: invite.createdAt,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
export function filterUser(user) {
|
|
102
|
+
return {
|
|
103
|
+
id: user.id,
|
|
104
|
+
name: user.name,
|
|
105
|
+
email: user.email,
|
|
106
|
+
role: user.role,
|
|
107
|
+
locale: user.locale,
|
|
108
|
+
createdAt: user.createdAt,
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
export function filterComment(comment) {
|
|
112
|
+
return {
|
|
113
|
+
id: comment.id,
|
|
114
|
+
pageId: comment.pageId,
|
|
115
|
+
content: comment.content,
|
|
116
|
+
selection: comment.selection,
|
|
117
|
+
parentCommentId: comment.parentCommentId,
|
|
118
|
+
creatorId: comment.creatorId,
|
|
119
|
+
createdAt: comment.createdAt,
|
|
120
|
+
updatedAt: comment.updatedAt,
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
export function filterShare(share) {
|
|
124
|
+
return {
|
|
125
|
+
id: share.id,
|
|
126
|
+
pageId: share.pageId,
|
|
127
|
+
includeSubPages: share.includeSubPages,
|
|
128
|
+
searchIndexing: share.searchIndexing,
|
|
129
|
+
createdAt: share.createdAt,
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
export function filterHistoryDetail(entry, content) {
|
|
133
|
+
return {
|
|
134
|
+
...filterHistoryEntry(entry),
|
|
135
|
+
...(typeof content === "string" && { content }),
|
|
136
|
+
};
|
|
137
|
+
}
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Convert ProseMirror/TipTap JSON content to Markdown
|
|
3
|
+
* Supports all Docmost-specific node types and extensions
|
|
4
|
+
*/
|
|
5
|
+
function escapeHtmlAttr(value) {
|
|
6
|
+
return value
|
|
7
|
+
.replace(/&/g, "&")
|
|
8
|
+
.replace(/"/g, """)
|
|
9
|
+
.replace(/'/g, "'")
|
|
10
|
+
.replace(/</g, "<")
|
|
11
|
+
.replace(/>/g, ">");
|
|
12
|
+
}
|
|
13
|
+
function sanitizeUrl(url) {
|
|
14
|
+
const trimmed = url.trim();
|
|
15
|
+
if (/^javascript:/i.test(trimmed) || /^data:/i.test(trimmed) || /^vbscript:/i.test(trimmed)) {
|
|
16
|
+
return "";
|
|
17
|
+
}
|
|
18
|
+
return trimmed;
|
|
19
|
+
}
|
|
20
|
+
export function convertProseMirrorToMarkdown(content) {
|
|
21
|
+
if (!content || !content.content)
|
|
22
|
+
return "";
|
|
23
|
+
const processNode = (node) => {
|
|
24
|
+
const type = node.type;
|
|
25
|
+
const nodeContent = node.content || [];
|
|
26
|
+
switch (type) {
|
|
27
|
+
case "doc":
|
|
28
|
+
return nodeContent.map(processNode).join("\n\n");
|
|
29
|
+
case "paragraph":
|
|
30
|
+
const text = nodeContent.map(processNode).join("");
|
|
31
|
+
const align = node.attrs?.textAlign;
|
|
32
|
+
if (align && align !== "left") {
|
|
33
|
+
return `<div align="${escapeHtmlAttr(align)}">${text}</div>`;
|
|
34
|
+
}
|
|
35
|
+
return text || "";
|
|
36
|
+
case "heading":
|
|
37
|
+
const level = node.attrs?.level || 1;
|
|
38
|
+
const headingText = nodeContent.map(processNode).join("");
|
|
39
|
+
return "#".repeat(level) + " " + headingText;
|
|
40
|
+
case "text":
|
|
41
|
+
let textContent = node.text || "";
|
|
42
|
+
// Apply marks (bold, italic, code, etc.)
|
|
43
|
+
if (node.marks) {
|
|
44
|
+
for (const mark of node.marks) {
|
|
45
|
+
switch (mark.type) {
|
|
46
|
+
case "bold":
|
|
47
|
+
textContent = `**${textContent}**`;
|
|
48
|
+
break;
|
|
49
|
+
case "italic":
|
|
50
|
+
textContent = `*${textContent}*`;
|
|
51
|
+
break;
|
|
52
|
+
case "code":
|
|
53
|
+
textContent = `\`${textContent}\``;
|
|
54
|
+
break;
|
|
55
|
+
case "link":
|
|
56
|
+
textContent = `[${textContent}](${sanitizeUrl(mark.attrs?.href || "")})`;
|
|
57
|
+
break;
|
|
58
|
+
case "strike":
|
|
59
|
+
textContent = `~~${textContent}~~`;
|
|
60
|
+
break;
|
|
61
|
+
case "underline":
|
|
62
|
+
textContent = `<u>${textContent}</u>`;
|
|
63
|
+
break;
|
|
64
|
+
case "subscript":
|
|
65
|
+
textContent = `<sub>${textContent}</sub>`;
|
|
66
|
+
break;
|
|
67
|
+
case "superscript":
|
|
68
|
+
textContent = `<sup>${textContent}</sup>`;
|
|
69
|
+
break;
|
|
70
|
+
case "highlight":
|
|
71
|
+
const color = mark.attrs?.color || "yellow";
|
|
72
|
+
textContent = `<mark style="background-color: ${escapeHtmlAttr(color)}">${textContent}</mark>`;
|
|
73
|
+
break;
|
|
74
|
+
case "textStyle":
|
|
75
|
+
if (mark.attrs?.color) {
|
|
76
|
+
textContent = `<span style="color: ${escapeHtmlAttr(mark.attrs.color)}">${textContent}</span>`;
|
|
77
|
+
}
|
|
78
|
+
break;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return textContent;
|
|
83
|
+
case "codeBlock":
|
|
84
|
+
const language = node.attrs?.language || "";
|
|
85
|
+
const code = nodeContent.map(processNode).join("");
|
|
86
|
+
return "```" + language + "\n" + code + "\n```";
|
|
87
|
+
case "bulletList":
|
|
88
|
+
return nodeContent
|
|
89
|
+
.map((item) => processListItem(item, "-"))
|
|
90
|
+
.join("\n");
|
|
91
|
+
case "orderedList":
|
|
92
|
+
return nodeContent
|
|
93
|
+
.map((item, index) => processListItem(item, `${index + 1}.`))
|
|
94
|
+
.join("\n");
|
|
95
|
+
case "taskList":
|
|
96
|
+
return nodeContent.map((item) => processTaskItem(item)).join("\n");
|
|
97
|
+
case "taskItem":
|
|
98
|
+
const checked = node.attrs?.checked || false;
|
|
99
|
+
const checkbox = checked ? "[x]" : "[ ]";
|
|
100
|
+
return `- ${checkbox} ${nodeContent.map(processNode).join("\n")}`;
|
|
101
|
+
case "listItem":
|
|
102
|
+
return nodeContent.map(processNode).join("\n");
|
|
103
|
+
case "blockquote":
|
|
104
|
+
return nodeContent.map((n) => "> " + processNode(n)).join("\n");
|
|
105
|
+
case "horizontalRule":
|
|
106
|
+
return "---";
|
|
107
|
+
case "hardBreak":
|
|
108
|
+
return "\n";
|
|
109
|
+
case "image":
|
|
110
|
+
const imgAlt = node.attrs?.alt || "";
|
|
111
|
+
const imgSrc = sanitizeUrl(node.attrs?.src || "");
|
|
112
|
+
const imgCaption = node.attrs?.caption || "";
|
|
113
|
+
return `${imgCaption ? `\n*${imgCaption}*` : ""}`;
|
|
114
|
+
case "video":
|
|
115
|
+
const videoSrc = sanitizeUrl(node.attrs?.src || "");
|
|
116
|
+
return `🎥 [Video](${videoSrc})`;
|
|
117
|
+
case "youtube":
|
|
118
|
+
const youtubeUrl = sanitizeUrl(node.attrs?.src || "");
|
|
119
|
+
return `📺 [YouTube Video](${youtubeUrl})`;
|
|
120
|
+
case "table":
|
|
121
|
+
const rows = nodeContent.map(processNode);
|
|
122
|
+
if (rows.length > 0) {
|
|
123
|
+
const colCount = (nodeContent[0]?.content || []).length || 1;
|
|
124
|
+
const separator = "|" + " --- |".repeat(colCount);
|
|
125
|
+
rows.splice(1, 0, separator);
|
|
126
|
+
}
|
|
127
|
+
return rows.join("\n");
|
|
128
|
+
case "tableRow":
|
|
129
|
+
return "| " + nodeContent.map(processNode).join(" | ") + " |";
|
|
130
|
+
case "tableCell":
|
|
131
|
+
case "tableHeader":
|
|
132
|
+
return nodeContent.map(processNode).join("");
|
|
133
|
+
case "callout":
|
|
134
|
+
const calloutType = node.attrs?.type || "info";
|
|
135
|
+
const calloutContent = nodeContent.map(processNode).join("\n");
|
|
136
|
+
return `:::${calloutType.toLowerCase()}\n${calloutContent}\n:::`;
|
|
137
|
+
case "details":
|
|
138
|
+
return nodeContent.map(processNode).join("\n");
|
|
139
|
+
case "detailsSummary":
|
|
140
|
+
const summaryText = nodeContent.map(processNode).join("");
|
|
141
|
+
return `<details>\n<summary>${summaryText}</summary>\n`;
|
|
142
|
+
case "detailsContent":
|
|
143
|
+
const detailsText = nodeContent.map(processNode).join("\n");
|
|
144
|
+
return `${detailsText}\n</details>`;
|
|
145
|
+
case "mathInline":
|
|
146
|
+
const inlineMath = node.attrs?.text || "";
|
|
147
|
+
return `$${inlineMath}$`;
|
|
148
|
+
case "mathBlock":
|
|
149
|
+
const blockMath = node.attrs?.text || "";
|
|
150
|
+
return `$$\n${blockMath}\n$$`;
|
|
151
|
+
case "mention":
|
|
152
|
+
const mentionLabel = node.attrs?.label || node.attrs?.id || "";
|
|
153
|
+
return `@${mentionLabel}`;
|
|
154
|
+
case "attachment":
|
|
155
|
+
const attachmentName = node.attrs?.fileName || "attachment";
|
|
156
|
+
const attachmentUrl = sanitizeUrl(node.attrs?.src || "");
|
|
157
|
+
return `📎 [${attachmentName}](${attachmentUrl})`;
|
|
158
|
+
case "drawio":
|
|
159
|
+
return `📊 [Draw.io Diagram]`;
|
|
160
|
+
case "excalidraw":
|
|
161
|
+
return `✏️ [Excalidraw Drawing]`;
|
|
162
|
+
case "embed":
|
|
163
|
+
const embedUrl = sanitizeUrl(node.attrs?.src || "");
|
|
164
|
+
return `🔗 [Embedded Content](${embedUrl})`;
|
|
165
|
+
case "subpages":
|
|
166
|
+
return "{{SUBPAGES}}";
|
|
167
|
+
default:
|
|
168
|
+
process.stderr.write(`Warning: unknown node type '${type}', rendering as plain text.\n`);
|
|
169
|
+
return nodeContent.map(processNode).join("");
|
|
170
|
+
}
|
|
171
|
+
};
|
|
172
|
+
const processListItem = (item, prefix) => {
|
|
173
|
+
const itemContent = item.content || [];
|
|
174
|
+
const lines = itemContent.map(processNode);
|
|
175
|
+
return lines
|
|
176
|
+
.map((line, i) => i === 0 ? `${prefix} ${line}` : ` ${line}`)
|
|
177
|
+
.join("\n");
|
|
178
|
+
};
|
|
179
|
+
const processTaskItem = (item) => {
|
|
180
|
+
const checked = item.attrs?.checked || false;
|
|
181
|
+
const checkbox = checked ? "[x]" : "[ ]";
|
|
182
|
+
const itemContent = item.content || [];
|
|
183
|
+
const text = itemContent.map(processNode).join("");
|
|
184
|
+
return `- ${checkbox} ${text}`;
|
|
185
|
+
};
|
|
186
|
+
return processNode(content).trim();
|
|
187
|
+
}
|