affine-mcp-server 1.12.0 → 2.0.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/README.md +155 -502
- package/dist/edgeless/layout.js +222 -0
- package/dist/index.js +34 -62
- package/dist/markdown/parse.js +51 -10
- package/dist/toolSurface.js +322 -0
- package/dist/tools/comments.js +25 -5
- package/dist/tools/docs.js +3220 -583
- package/dist/tools/organize.js +419 -42
- package/dist/tools/workspaces.js +25 -6
- package/dist/util/mcp.js +26 -2
- package/docs/assets/edgeless-canvas-demo-advanced-dark.png +0 -0
- package/docs/assets/edgeless-canvas-demo-advanced-light.png +0 -0
- package/docs/client-setup.md +174 -0
- package/docs/configuration-and-deployment.md +265 -0
- package/docs/edgeless-canvas-cookbook.md +226 -0
- package/docs/getting-started.md +229 -0
- package/docs/tool-reference.md +186 -0
- package/docs/workflow-recipes.md +147 -0
- package/package.json +11 -2
- package/tool-manifest.json +89 -0
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
/** Pure-function edgeless-canvas layout helpers. No Y.Doc, no MCP wiring. */
|
|
2
|
+
/** Only these four positions carry tangent vectors in BlockSuite's connector model. */
|
|
3
|
+
export const SIDE_TO_NORMALIZED_POSITION = {
|
|
4
|
+
top: [0.5, 0],
|
|
5
|
+
bottom: [0.5, 1],
|
|
6
|
+
left: [0, 0.5],
|
|
7
|
+
right: [1, 0.5],
|
|
8
|
+
};
|
|
9
|
+
/** Pick connector sides for a src/tgt pair. Single-axis → that axis; diagonal →
|
|
10
|
+
* dominant by center displacement; overlap → 4×4 midpoint minimization. Ports
|
|
11
|
+
* BlockSuite's `getNearestConnectableAnchor` (`connector-manager.ts:174-190`). */
|
|
12
|
+
export function pickConnectorSides(src, tgt) {
|
|
13
|
+
const srcBottom = src.y + src.h;
|
|
14
|
+
const srcRight = src.x + src.w;
|
|
15
|
+
const tgtBottom = tgt.y + tgt.h;
|
|
16
|
+
const tgtRight = tgt.x + tgt.w;
|
|
17
|
+
const above = srcBottom <= tgt.y;
|
|
18
|
+
const below = tgtBottom <= src.y;
|
|
19
|
+
const leftOf = srcRight <= tgt.x;
|
|
20
|
+
const rightOf = tgtRight <= src.x;
|
|
21
|
+
const vSeparated = above || below;
|
|
22
|
+
const hSeparated = leftOf || rightOf;
|
|
23
|
+
if (vSeparated && !hSeparated) {
|
|
24
|
+
return above ? { from: "bottom", to: "top" } : { from: "top", to: "bottom" };
|
|
25
|
+
}
|
|
26
|
+
if (hSeparated && !vSeparated) {
|
|
27
|
+
return leftOf ? { from: "right", to: "left" } : { from: "left", to: "right" };
|
|
28
|
+
}
|
|
29
|
+
if (vSeparated && hSeparated) {
|
|
30
|
+
const dx = (tgt.x + tgt.w / 2) - (src.x + src.w / 2);
|
|
31
|
+
const dy = (tgt.y + tgt.h / 2) - (src.y + src.h / 2);
|
|
32
|
+
if (Math.abs(dx) >= Math.abs(dy)) {
|
|
33
|
+
return dx >= 0 ? { from: "right", to: "left" } : { from: "left", to: "right" };
|
|
34
|
+
}
|
|
35
|
+
return dy >= 0 ? { from: "bottom", to: "top" } : { from: "top", to: "bottom" };
|
|
36
|
+
}
|
|
37
|
+
const anchors = (b) => [
|
|
38
|
+
{ side: "top", x: b.x + b.w / 2, y: b.y },
|
|
39
|
+
{ side: "bottom", x: b.x + b.w / 2, y: b.y + b.h },
|
|
40
|
+
{ side: "left", x: b.x, y: b.y + b.h / 2 },
|
|
41
|
+
{ side: "right", x: b.x + b.w, y: b.y + b.h / 2 },
|
|
42
|
+
];
|
|
43
|
+
const srcA = anchors(src);
|
|
44
|
+
const tgtA = anchors(tgt);
|
|
45
|
+
let best = {
|
|
46
|
+
from: "bottom",
|
|
47
|
+
to: "top",
|
|
48
|
+
dist: Infinity,
|
|
49
|
+
};
|
|
50
|
+
for (const a of srcA) {
|
|
51
|
+
for (const b of tgtA) {
|
|
52
|
+
const dx = b.x - a.x;
|
|
53
|
+
const dy = b.y - a.y;
|
|
54
|
+
const dist = dx * dx + dy * dy;
|
|
55
|
+
if (dist < best.dist)
|
|
56
|
+
best = { from: a.side, to: b.side, dist };
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return { from: best.from, to: best.to };
|
|
60
|
+
}
|
|
61
|
+
/** Enclosing bound of `children`, expanded by `padding` and `titleBand` (extra on top for a frame title). */
|
|
62
|
+
export function encloseBounds(children, opts = {}) {
|
|
63
|
+
if (children.length === 0)
|
|
64
|
+
return null;
|
|
65
|
+
const padding = opts.padding ?? 40;
|
|
66
|
+
const titleBand = opts.titleBand ?? 60;
|
|
67
|
+
let minX = Infinity;
|
|
68
|
+
let minY = Infinity;
|
|
69
|
+
let maxX = -Infinity;
|
|
70
|
+
let maxY = -Infinity;
|
|
71
|
+
for (const c of children) {
|
|
72
|
+
minX = Math.min(minX, c.x);
|
|
73
|
+
minY = Math.min(minY, c.y);
|
|
74
|
+
maxX = Math.max(maxX, c.x + c.w);
|
|
75
|
+
maxY = Math.max(maxY, c.y + c.h);
|
|
76
|
+
}
|
|
77
|
+
return {
|
|
78
|
+
x: Math.floor(minX - padding),
|
|
79
|
+
y: Math.floor(minY - padding - titleBand),
|
|
80
|
+
w: Math.max(1, Math.ceil(maxX - minX + padding * 2)),
|
|
81
|
+
h: Math.max(1, Math.ceil(maxY - minY + padding * 2 + titleBand)),
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
/** Pick the bound furthest along `direction` (bottommost for `"down"`, etc). */
|
|
85
|
+
export function pickFurthestInDirection(candidates, direction) {
|
|
86
|
+
if (candidates.length === 0)
|
|
87
|
+
return null;
|
|
88
|
+
let chosen = candidates[0];
|
|
89
|
+
for (let i = 1; i < candidates.length; i++) {
|
|
90
|
+
const c = candidates[i];
|
|
91
|
+
if (direction === "down" && c.y + c.h > chosen.y + chosen.h)
|
|
92
|
+
chosen = c;
|
|
93
|
+
else if (direction === "up" && c.y < chosen.y)
|
|
94
|
+
chosen = c;
|
|
95
|
+
else if (direction === "right" && c.x + c.w > chosen.x + chosen.w)
|
|
96
|
+
chosen = c;
|
|
97
|
+
else if (direction === "left" && c.x < chosen.x)
|
|
98
|
+
chosen = c;
|
|
99
|
+
}
|
|
100
|
+
return chosen;
|
|
101
|
+
}
|
|
102
|
+
/** Parse BlockSuite's `[x,y,w,h]` string. Returns null if the input isn't well-formed. */
|
|
103
|
+
export function parseXywhString(value) {
|
|
104
|
+
if (typeof value !== "string")
|
|
105
|
+
return null;
|
|
106
|
+
const m = value.match(/^\s*\[\s*(-?\d+(?:\.\d+)?)\s*,\s*(-?\d+(?:\.\d+)?)\s*,\s*(-?\d+(?:\.\d+)?)\s*,\s*(-?\d+(?:\.\d+)?)\s*\]\s*$/);
|
|
107
|
+
if (!m)
|
|
108
|
+
return null;
|
|
109
|
+
return { x: Number(m[1]), y: Number(m[2]), width: Number(m[3]), height: Number(m[4]) };
|
|
110
|
+
}
|
|
111
|
+
/** Inverse of `parseXywhString`. */
|
|
112
|
+
export function formatXywhString(x, y, width, height) {
|
|
113
|
+
return `[${x},${y},${width},${height}]`;
|
|
114
|
+
}
|
|
115
|
+
/** Asymmetric defaults: notes default to wide/short, so equal gaps feel tight horizontally. */
|
|
116
|
+
export const DEFAULT_STACK_GAP_VERTICAL = 40;
|
|
117
|
+
export const DEFAULT_STACK_GAP_HORIZONTAL = 80;
|
|
118
|
+
/** BlockSuite's createDefaultDoc constants (packages/affine/model/src/consts/note.ts). */
|
|
119
|
+
export const DEFAULT_PAGE_BLOCK_WIDTH = 800;
|
|
120
|
+
export const DEFAULT_NOTE_HEIGHT = 92;
|
|
121
|
+
export const DEFAULT_NOTE_XYWH = `[0,0,${DEFAULT_PAGE_BLOCK_WIDTH},${DEFAULT_NOTE_HEIGHT}]`;
|
|
122
|
+
/** Position a new block relative to `ref` along `direction`. The orthogonal axis
|
|
123
|
+
* inherits from `ref` unless `preserveX` / `preserveY` is supplied. */
|
|
124
|
+
export function stackRelativeTo(ref, newSize, opts = {}) {
|
|
125
|
+
const direction = opts.direction ?? "down";
|
|
126
|
+
const isHorizontal = direction === "left" || direction === "right";
|
|
127
|
+
const gap = opts.gap ?? (isHorizontal ? DEFAULT_STACK_GAP_HORIZONTAL : DEFAULT_STACK_GAP_VERTICAL);
|
|
128
|
+
if (direction === "down") {
|
|
129
|
+
return { x: opts.preserveX ?? ref.x, y: ref.y + ref.h + gap };
|
|
130
|
+
}
|
|
131
|
+
if (direction === "up") {
|
|
132
|
+
return { x: opts.preserveX ?? ref.x, y: ref.y - gap - newSize.h };
|
|
133
|
+
}
|
|
134
|
+
if (direction === "right") {
|
|
135
|
+
return { x: ref.x + ref.w + gap, y: opts.preserveY ?? ref.y };
|
|
136
|
+
}
|
|
137
|
+
return { x: ref.x - gap - newSize.w, y: opts.preserveY ?? ref.y };
|
|
138
|
+
}
|
|
139
|
+
/** Over-estimate note height from markdown. BlockSuite's `EdgelessNoteMask`
|
|
140
|
+
* `ResizeObserver` corrects `prop:xywh.h` to the DOM-measured height on first render. */
|
|
141
|
+
export function estimateNoteHeightForMarkdown(markdown, widthPx) {
|
|
142
|
+
const NOTE_V_PADDING = 64;
|
|
143
|
+
const BODY_LINE_H = 34;
|
|
144
|
+
const CHAR_WIDTH = 8;
|
|
145
|
+
const H_PADDING = 52;
|
|
146
|
+
const H1_LINE_H = 58;
|
|
147
|
+
const H2_LINE_H = 48;
|
|
148
|
+
const H3_LINE_H = 40;
|
|
149
|
+
const CODE_LINE_H = 32;
|
|
150
|
+
const CODE_FENCE_PAD = 30;
|
|
151
|
+
const CODE_BLOCK_EXTRA = 20;
|
|
152
|
+
const BLANK_LINE_H = 14;
|
|
153
|
+
const usableWidth = Math.max(80, widthPx - H_PADDING);
|
|
154
|
+
const charsPerLine = Math.max(16, Math.floor(usableWidth / CHAR_WIDTH));
|
|
155
|
+
let total = NOTE_V_PADDING;
|
|
156
|
+
let inCode = false;
|
|
157
|
+
const lines = markdown.split("\n");
|
|
158
|
+
for (const raw of lines) {
|
|
159
|
+
const line = raw.trim();
|
|
160
|
+
if (line.startsWith("```")) {
|
|
161
|
+
inCode = !inCode;
|
|
162
|
+
total += CODE_FENCE_PAD;
|
|
163
|
+
if (inCode)
|
|
164
|
+
total += CODE_BLOCK_EXTRA;
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
if (inCode) {
|
|
168
|
+
total += CODE_LINE_H;
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
if (line === "") {
|
|
172
|
+
total += BLANK_LINE_H;
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
let lineHeight = BODY_LINE_H;
|
|
176
|
+
let prefixChars = 0;
|
|
177
|
+
if (/^#\s/.test(line)) {
|
|
178
|
+
lineHeight = H1_LINE_H;
|
|
179
|
+
prefixChars = 2;
|
|
180
|
+
}
|
|
181
|
+
else if (/^##\s/.test(line)) {
|
|
182
|
+
lineHeight = H2_LINE_H;
|
|
183
|
+
prefixChars = 3;
|
|
184
|
+
}
|
|
185
|
+
else if (/^###\s/.test(line)) {
|
|
186
|
+
lineHeight = H3_LINE_H;
|
|
187
|
+
prefixChars = 4;
|
|
188
|
+
}
|
|
189
|
+
else if (/^[-*]\s/.test(line)) {
|
|
190
|
+
prefixChars = 2;
|
|
191
|
+
}
|
|
192
|
+
else if (/^\d+\.\s/.test(line)) {
|
|
193
|
+
prefixChars = 3;
|
|
194
|
+
}
|
|
195
|
+
const contentChars = Math.max(1, line.length - prefixChars);
|
|
196
|
+
const wraps = Math.max(1, Math.ceil(contentChars / charsPerLine));
|
|
197
|
+
total += lineHeight * wraps;
|
|
198
|
+
}
|
|
199
|
+
return Math.max(120, Math.ceil(total));
|
|
200
|
+
}
|
|
201
|
+
/** Initial `labelXYWH` at source→target midpoint so BlockSuite's `hasLabel()` gate passes on first render. */
|
|
202
|
+
export function estimateConnectorLabelXYWH(labelText, fontSize, midpoint, maxWidth) {
|
|
203
|
+
const charWidth = fontSize * 0.55;
|
|
204
|
+
const estimatedW = Math.max(16, Math.ceil(labelText.length * charWidth));
|
|
205
|
+
const w = Math.min(estimatedW, maxWidth);
|
|
206
|
+
const h = Math.ceil(fontSize + 4);
|
|
207
|
+
if (!midpoint)
|
|
208
|
+
return [0, 0, Math.max(w, 16), Math.max(h, 16)];
|
|
209
|
+
return [Math.round(midpoint.x - w / 2), Math.round(midpoint.y - h / 2), w, h];
|
|
210
|
+
}
|
|
211
|
+
/** Sort by fractional `index` string ascending. Y.Map iteration order is not stable across reloads. */
|
|
212
|
+
export function sortByFractionalIndex(entries) {
|
|
213
|
+
return entries.slice().sort((a, b) => {
|
|
214
|
+
const ai = typeof a.index === "string" ? a.index : "";
|
|
215
|
+
const bi = typeof b.index === "string" ? b.index : "";
|
|
216
|
+
if (ai < bi)
|
|
217
|
+
return -1;
|
|
218
|
+
if (ai > bi)
|
|
219
|
+
return 1;
|
|
220
|
+
return 0;
|
|
221
|
+
});
|
|
222
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -18,6 +18,7 @@ import { runCli } from "./cli.js";
|
|
|
18
18
|
import { startHttpMcpServer } from "./sse.js";
|
|
19
19
|
import { existsSync } from "fs";
|
|
20
20
|
import { CONFIG_FILE } from "./config.js";
|
|
21
|
+
import { createToolFilter, toolFilterRequiresRegisterTool } from "./toolSurface.js";
|
|
21
22
|
// CLI commands: affine-mcp login|status|logout|version
|
|
22
23
|
const rawArgs = process.argv.slice(2);
|
|
23
24
|
const cliArgs = rawArgs[0] === "--" ? rawArgs.slice(1) : rawArgs;
|
|
@@ -43,21 +44,8 @@ if (subcommand) {
|
|
|
43
44
|
const config = loadConfig();
|
|
44
45
|
const transportMode = (process.env.MCP_TRANSPORT || "stdio").toLowerCase();
|
|
45
46
|
const useHttpTransport = transportMode === "sse" || transportMode === "http" || transportMode === "streamable";
|
|
46
|
-
//
|
|
47
|
-
|
|
48
|
-
// ---------------------------------------------------------------------------
|
|
49
|
-
const KNOWN_GROUPS = new Set([
|
|
50
|
-
"workspaces", "docs", "comments", "history", "organize",
|
|
51
|
-
"users", "access_tokens", "blobs", "notifications",
|
|
52
|
-
]);
|
|
53
|
-
const DISABLED_GROUPS = new Set((process.env.AFFINE_DISABLED_GROUPS || "")
|
|
54
|
-
.split(",")
|
|
55
|
-
.map((s) => s.trim().toLowerCase())
|
|
56
|
-
.filter(Boolean));
|
|
57
|
-
const DISABLED_TOOLS = new Set((process.env.AFFINE_DISABLED_TOOLS || "")
|
|
58
|
-
.split(",")
|
|
59
|
-
.map((s) => s.trim())
|
|
60
|
-
.filter(Boolean));
|
|
47
|
+
// Tool filtering is parsed once at module load (not per-session in HTTP mode).
|
|
48
|
+
const toolFilter = createToolFilter(process.env);
|
|
61
49
|
// Startup diagnostics (visible in Claude Code MCP server logs via stderr)
|
|
62
50
|
console.error(`[affine-mcp] Config: ${CONFIG_FILE} (${existsSync(CONFIG_FILE) ? 'found' : 'missing'})`);
|
|
63
51
|
console.error(`[affine-mcp] Endpoint: ${config.baseUrl}${config.graphqlPath}`);
|
|
@@ -70,12 +58,8 @@ if (hasAuth && config.baseUrl.startsWith("http://")
|
|
|
70
58
|
console.error("WARNING: Credentials configured over plain HTTP. Use HTTPS for remote servers.");
|
|
71
59
|
}
|
|
72
60
|
console.error(`[affine-mcp] Workspace: ${config.defaultWorkspaceId ? 'set' : '(none)'}`);
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
if (!KNOWN_GROUPS.has(g)) {
|
|
76
|
-
console.error(`[affine-mcp] WARNING: Unknown group "${g}" in AFFINE_DISABLED_GROUPS — ` +
|
|
77
|
-
`valid groups: ${[...KNOWN_GROUPS].join(", ")}`);
|
|
78
|
-
}
|
|
61
|
+
for (const warning of toolFilter.warnings) {
|
|
62
|
+
console.error(`[affine-mcp] WARNING: ${warning}`);
|
|
79
63
|
}
|
|
80
64
|
if (config.authMode === "oauth" && !useHttpTransport) {
|
|
81
65
|
throw new Error("AFFINE_MCP_AUTH_MODE=oauth requires MCP_TRANSPORT=http (or streamable/sse).");
|
|
@@ -155,52 +139,40 @@ async function buildServer() {
|
|
|
155
139
|
console.error("WARNING: No authentication configured. Some operations may fail.");
|
|
156
140
|
console.error("Set AFFINE_API_TOKEN or run: affine-mcp login");
|
|
157
141
|
}
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
if (typeof originalRegisterTool !== "function") {
|
|
166
|
-
console.error("[affine-mcp] WARNING: server.registerTool not found — " +
|
|
167
|
-
"AFFINE_DISABLED_TOOLS will have no effect. " +
|
|
168
|
-
"The MCP SDK API may have changed.");
|
|
169
|
-
}
|
|
170
|
-
else {
|
|
171
|
-
server.registerTool = (name, options, handler) => {
|
|
172
|
-
if (DISABLED_TOOLS.has(name))
|
|
173
|
-
return;
|
|
174
|
-
return originalRegisterTool(name, options, handler);
|
|
175
|
-
};
|
|
142
|
+
const originalRegisterTool = server.registerTool?.bind(server);
|
|
143
|
+
if (typeof originalRegisterTool !== "function") {
|
|
144
|
+
const message = "[affine-mcp] server.registerTool not found - tool filtering cannot be enforced. " +
|
|
145
|
+
"The MCP SDK API may have changed.";
|
|
146
|
+
if (toolFilterRequiresRegisterTool(toolFilter)) {
|
|
147
|
+
throw new Error(`${message} Refusing to start because AFFINE_TOOL_PROFILE is not "full" ` +
|
|
148
|
+
"or AFFINE_DISABLED_GROUPS/AFFINE_DISABLED_TOOLS is configured.");
|
|
176
149
|
}
|
|
150
|
+
console.error(`[affine-mcp] WARNING: ${message} Continuing with the full tool surface.`);
|
|
177
151
|
}
|
|
178
|
-
|
|
152
|
+
else {
|
|
153
|
+
server.registerTool = (name, options, handler) => {
|
|
154
|
+
if (!toolFilter.isEnabled(name))
|
|
155
|
+
return;
|
|
156
|
+
return originalRegisterTool(name, options, handler);
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
console.error(`[affine-mcp] Tool profile: ${toolFilter.profile}`);
|
|
179
160
|
console.error(`[affine-mcp] Disabled groups: ${process.env.AFFINE_DISABLED_GROUPS || "(none)"}`);
|
|
180
161
|
console.error(`[affine-mcp] Disabled tools: ${process.env.AFFINE_DISABLED_TOOLS || "(none)"}`);
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
if (
|
|
190
|
-
|
|
191
|
-
if (!DISABLED_GROUPS.has("users")) {
|
|
192
|
-
registerUserTools(server, gql);
|
|
193
|
-
registerUserCRUDTools(server, gql);
|
|
194
|
-
if (config.authMode !== "oauth") {
|
|
195
|
-
registerAuthTools(server, gql, config.baseUrl);
|
|
196
|
-
}
|
|
162
|
+
console.error(`[affine-mcp] Enabled tools: ${toolFilter.enabledTools.length}/${toolFilter.totalToolCount}`);
|
|
163
|
+
registerWorkspaceTools(server, gql);
|
|
164
|
+
registerDocTools(server, gql, { workspaceId: config.defaultWorkspaceId });
|
|
165
|
+
registerCommentTools(server, gql, { workspaceId: config.defaultWorkspaceId });
|
|
166
|
+
registerHistoryTools(server, gql, { workspaceId: config.defaultWorkspaceId });
|
|
167
|
+
registerOrganizeTools(server, gql, { workspaceId: config.defaultWorkspaceId });
|
|
168
|
+
registerUserTools(server, gql);
|
|
169
|
+
registerUserCRUDTools(server, gql);
|
|
170
|
+
if (config.authMode !== "oauth") {
|
|
171
|
+
registerAuthTools(server, gql, config.baseUrl);
|
|
197
172
|
}
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
registerBlobTools(server, gql);
|
|
202
|
-
if (!DISABLED_GROUPS.has("notifications"))
|
|
203
|
-
registerNotificationTools(server, gql);
|
|
173
|
+
registerAccessTokenTools(server, gql);
|
|
174
|
+
registerBlobTools(server, gql);
|
|
175
|
+
registerNotificationTools(server, gql);
|
|
204
176
|
return server;
|
|
205
177
|
}
|
|
206
178
|
async function start() {
|
package/dist/markdown/parse.js
CHANGED
|
@@ -59,6 +59,30 @@ function findMatchingInline(tokens, start, openType, closeType) {
|
|
|
59
59
|
function deltaToString(deltas) {
|
|
60
60
|
return deltas.map(delta => delta.insert).join("");
|
|
61
61
|
}
|
|
62
|
+
/**
|
|
63
|
+
* Strip deltas corresponding to the first line (up to and including the first
|
|
64
|
+
* "\n" separator). Used by callout parsing to remove the `[!NOTE]` marker
|
|
65
|
+
* line from the collected blockquote deltas.
|
|
66
|
+
*/
|
|
67
|
+
function stripFirstDeltaLine(deltas) {
|
|
68
|
+
const result = [];
|
|
69
|
+
let pastNewline = false;
|
|
70
|
+
for (const delta of deltas) {
|
|
71
|
+
if (pastNewline) {
|
|
72
|
+
result.push(delta);
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
const nlIndex = delta.insert.indexOf("\n");
|
|
76
|
+
if (nlIndex >= 0) {
|
|
77
|
+
pastNewline = true;
|
|
78
|
+
const remainder = delta.insert.slice(nlIndex + 1);
|
|
79
|
+
if (remainder.length > 0) {
|
|
80
|
+
result.push({ ...delta, insert: remainder });
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return result;
|
|
85
|
+
}
|
|
62
86
|
function renderInline(children) {
|
|
63
87
|
function applyAttrs(deltas, attrs) {
|
|
64
88
|
return deltas.map(delta => ({
|
|
@@ -234,23 +258,37 @@ function parseTable(tokens, start, end) {
|
|
|
234
258
|
}
|
|
235
259
|
function collectQuoteText(tokens, start, end) {
|
|
236
260
|
const lines = [];
|
|
261
|
+
const allDeltas = [];
|
|
262
|
+
let firstLine = true;
|
|
237
263
|
for (let i = start; i < end; i += 1) {
|
|
238
264
|
const token = tokens[i];
|
|
239
265
|
if (token.type === "inline") {
|
|
240
|
-
const
|
|
266
|
+
const lineDeltas = renderInline(token.children ?? []);
|
|
267
|
+
const line = deltaToString(lineDeltas).trim();
|
|
241
268
|
if (line) {
|
|
269
|
+
if (!firstLine) {
|
|
270
|
+
allDeltas.push({ insert: "\n" });
|
|
271
|
+
}
|
|
272
|
+
allDeltas.push(...lineDeltas);
|
|
242
273
|
lines.push(line);
|
|
274
|
+
firstLine = false;
|
|
243
275
|
}
|
|
244
276
|
continue;
|
|
245
277
|
}
|
|
246
278
|
if (token.type === "fence" || token.type === "code_block") {
|
|
247
279
|
const language = (token.info ?? "").trim();
|
|
248
280
|
const codeBody = token.content.replace(/\n$/, "");
|
|
249
|
-
|
|
281
|
+
const fenceText = `\`\`\`${language}\n${codeBody}\n\`\`\``;
|
|
282
|
+
if (!firstLine) {
|
|
283
|
+
allDeltas.push({ insert: "\n" });
|
|
284
|
+
}
|
|
285
|
+
allDeltas.push({ insert: fenceText });
|
|
286
|
+
lines.push(fenceText);
|
|
287
|
+
firstLine = false;
|
|
250
288
|
continue;
|
|
251
289
|
}
|
|
252
290
|
}
|
|
253
|
-
return lines.join("\n");
|
|
291
|
+
return { text: lines.join("\n"), deltas: allDeltas };
|
|
254
292
|
}
|
|
255
293
|
function parseCalloutAdmonition(text) {
|
|
256
294
|
const lines = text.split("\n");
|
|
@@ -361,8 +399,9 @@ function parseTokens(tokens, start, end, state) {
|
|
|
361
399
|
const levelNum = Number((token.tag ?? "h1").replace("h", ""));
|
|
362
400
|
const level = Math.max(1, Math.min(6, levelNum));
|
|
363
401
|
const inline = tokens.slice(i + 1, close).find(inner => inner.type === "inline");
|
|
364
|
-
const
|
|
365
|
-
|
|
402
|
+
const headingDeltas = inline ? renderInline(inline.children ?? []) : [];
|
|
403
|
+
const text = deltaToString(headingDeltas).trim();
|
|
404
|
+
state.operations.push({ type: "heading", level, text, deltas: headingDeltas });
|
|
366
405
|
i = close + 1;
|
|
367
406
|
break;
|
|
368
407
|
}
|
|
@@ -402,9 +441,10 @@ function parseTokens(tokens, start, end, state) {
|
|
|
402
441
|
i = close + 1;
|
|
403
442
|
break;
|
|
404
443
|
}
|
|
405
|
-
const
|
|
444
|
+
const paragraphDeltas = renderInline(children);
|
|
445
|
+
const text = deltaToString(paragraphDeltas).trim();
|
|
406
446
|
if (text.length > 0) {
|
|
407
|
-
state.operations.push({ type: "paragraph", text });
|
|
447
|
+
state.operations.push({ type: "paragraph", text, deltas: paragraphDeltas });
|
|
408
448
|
}
|
|
409
449
|
i = close + 1;
|
|
410
450
|
break;
|
|
@@ -429,13 +469,14 @@ function parseTokens(tokens, start, end, state) {
|
|
|
429
469
|
i += 1;
|
|
430
470
|
break;
|
|
431
471
|
}
|
|
432
|
-
const
|
|
472
|
+
const quoteResult = collectQuoteText(tokens, i + 1, close);
|
|
473
|
+
const quoteText = quoteResult.text.trim();
|
|
433
474
|
const calloutText = parseCalloutAdmonition(quoteText);
|
|
434
475
|
if (calloutText !== null) {
|
|
435
|
-
state.operations.push({ type: "callout", text: calloutText });
|
|
476
|
+
state.operations.push({ type: "callout", text: calloutText, deltas: stripFirstDeltaLine(quoteResult.deltas) });
|
|
436
477
|
}
|
|
437
478
|
else if (quoteText.length > 0) {
|
|
438
|
-
state.operations.push({ type: "quote", text: quoteText });
|
|
479
|
+
state.operations.push({ type: "quote", text: quoteText, deltas: quoteResult.deltas });
|
|
439
480
|
}
|
|
440
481
|
i = close + 1;
|
|
441
482
|
break;
|