@riebeckite/plugin-excalibrain 0.0.1

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/index.js ADDED
@@ -0,0 +1,785 @@
1
+ // index.ts
2
+ import {
3
+ createClientEntry,
4
+ createStyleAsset,
5
+ definePlugin,
6
+ isPublished
7
+ } from "@riebeckite/core";
8
+
9
+ // src/ontology.ts
10
+ var DEFAULT_ONTOLOGY = {
11
+ parents: ["parent", "parents", "up", "u", "north", "origin", "inception"],
12
+ children: [
13
+ "children",
14
+ "child",
15
+ "down",
16
+ "d",
17
+ "south",
18
+ "leads to",
19
+ "contributes to"
20
+ ],
21
+ leftFriends: [
22
+ "friends",
23
+ "friend",
24
+ "similar",
25
+ "supports",
26
+ "alternatives",
27
+ "advantages"
28
+ ],
29
+ rightFriends: ["opposes", "disadvantages", "missing", "cons"],
30
+ previous: ["previous", "prev", "west", "w", "before"],
31
+ next: ["next", "n", "east", "e", "after"],
32
+ hidden: ["hidden"]
33
+ };
34
+ var FIELD_ROLE_ORDER = [
35
+ "parents",
36
+ "children",
37
+ "leftFriends",
38
+ "rightFriends",
39
+ "previous",
40
+ "next",
41
+ "hidden"
42
+ ];
43
+ var FIELD_TO_NODE_ROLE = {
44
+ parents: "parent",
45
+ children: "child",
46
+ leftFriends: "leftFriend",
47
+ rightFriends: "rightFriend",
48
+ previous: "previous",
49
+ next: "next"
50
+ };
51
+ function normalizeFieldName(value) {
52
+ return value.trim().toLowerCase().replace(/\s+/g, "-");
53
+ }
54
+ function resolveOntology(overrides = {}) {
55
+ const merged = {
56
+ ...DEFAULT_ONTOLOGY,
57
+ ...overrides
58
+ };
59
+ const fieldRole = /* @__PURE__ */ new Map();
60
+ for (const role of FIELD_ROLE_ORDER) {
61
+ for (const field of merged[role] ?? []) {
62
+ const normalized = normalizeFieldName(field);
63
+ if (normalized && !fieldRole.has(normalized)) {
64
+ fieldRole.set(normalized, role);
65
+ }
66
+ }
67
+ }
68
+ return { fieldRole };
69
+ }
70
+ function fieldRoleOf(field, ontology) {
71
+ return ontology.fieldRole.get(normalizeFieldName(field)) ?? null;
72
+ }
73
+ function collectDefinedRelations(input) {
74
+ const relations = [];
75
+ const inlineFields = extractInlineFields(input.markdown);
76
+ for (const role of FIELD_ROLE_ORDER) {
77
+ if (role === "hidden") continue;
78
+ const nodeRole = FIELD_TO_NODE_ROLE[role];
79
+ for (const [field, value] of Object.entries(input.frontmatter)) {
80
+ if (fieldRoleOf(field, input.ontology) !== role) continue;
81
+ for (const target of extractTargets(value)) {
82
+ relations.push({ role: nodeRole, target });
83
+ }
84
+ }
85
+ for (const [field, value] of inlineFields) {
86
+ if (fieldRoleOf(field, input.ontology) !== role) continue;
87
+ for (const target of extractTargets(value)) {
88
+ relations.push({ role: nodeRole, target });
89
+ }
90
+ }
91
+ }
92
+ return relations;
93
+ }
94
+ function isNoteHidden(input) {
95
+ for (const field of Object.keys(input.frontmatter)) {
96
+ if (fieldRoleOf(field, input.ontology) === "hidden") return true;
97
+ }
98
+ for (const [field] of extractInlineFields(input.markdown)) {
99
+ if (fieldRoleOf(field, input.ontology) === "hidden") return true;
100
+ }
101
+ return false;
102
+ }
103
+ var WIKILINK_PATTERN = /\[\[([^\]|#^]+)(?:[#^][^\]|]+)?(?:\|[^\]]+)?\]\]/g;
104
+ function extractWikilinkTargets(value) {
105
+ WIKILINK_PATTERN.lastIndex = 0;
106
+ const targets = [];
107
+ for (let match = WIKILINK_PATTERN.exec(value); match !== null; match = WIKILINK_PATTERN.exec(value)) {
108
+ const target = match[1]?.trim();
109
+ if (target) targets.push(target);
110
+ }
111
+ return targets;
112
+ }
113
+ function extractTargets(value) {
114
+ if (Array.isArray(value)) {
115
+ return value.flatMap((item) => extractTargets(item));
116
+ }
117
+ if (typeof value !== "string") return [];
118
+ const links = extractWikilinkTargets(value);
119
+ if (links.length > 0) return links;
120
+ const trimmed = value.trim();
121
+ if (!trimmed) return [];
122
+ if (/^(true|false|null|~|\d+(?:\.\d+)?)$/i.test(trimmed)) return [];
123
+ return [trimmed];
124
+ }
125
+ var INLINE_BRACKET_FIELD = /\[([A-Za-z][A-Za-z0-9 _-]*)::\s*([^\]]+)\]/g;
126
+ var INLINE_LINE_FIELD = /^[ \t>*+-]*([A-Za-z][A-Za-z0-9 _-]*)::[ \t]*(.+)$/gm;
127
+ var FENCED_CODE = /```[\s\S]*?```|~~~[\s\S]*?~~~/g;
128
+ function extractInlineFields(markdown) {
129
+ const body = stripFrontmatter(markdown).replace(FENCED_CODE, "");
130
+ const fields = [];
131
+ for (let match = INLINE_BRACKET_FIELD.exec(body); match !== null; match = INLINE_BRACKET_FIELD.exec(body)) {
132
+ fields.push([match[1] ?? "", (match[2] ?? "").trim()]);
133
+ }
134
+ for (let match = INLINE_LINE_FIELD.exec(body); match !== null; match = INLINE_LINE_FIELD.exec(body)) {
135
+ fields.push([match[1] ?? "", (match[2] ?? "").trim()]);
136
+ }
137
+ return fields;
138
+ }
139
+ function stripFrontmatter(markdown) {
140
+ if (!markdown.startsWith("---")) return markdown;
141
+ const end = markdown.indexOf("\n---", 3);
142
+ if (end === -1) return markdown;
143
+ const closing = markdown.indexOf("\n", end + 1);
144
+ return closing === -1 ? "" : markdown.slice(closing + 1);
145
+ }
146
+
147
+ // src/options.ts
148
+ function resolveExcaliBrainOptions(options = {}) {
149
+ return {
150
+ render: options.render ?? "build",
151
+ auto: options.auto ?? true,
152
+ heading: options.heading ?? true,
153
+ headingText: options.headingText ?? "ExcaliBrain",
154
+ className: options.className ?? "rb-excalibrain",
155
+ maxPerRegion: options.maxPerRegion ?? 8,
156
+ infer: options.infer ?? true,
157
+ siblings: options.siblings ?? true,
158
+ ontology: resolveOntology(options.ontology),
159
+ showHidden: options.showHidden ?? false,
160
+ width: options.width ?? 720,
161
+ height: options.height ?? 480,
162
+ language: options.language ?? "excalibrain"
163
+ };
164
+ }
165
+ function validateExcaliBrainOptions(options) {
166
+ if (!options) return [];
167
+ const issues = [];
168
+ if (options.render !== void 0 && !isRenderMode(options.render)) {
169
+ issues.push({
170
+ path: "render",
171
+ message: 'Expected "build", "client", or "both".'
172
+ });
173
+ }
174
+ for (const key of ["auto", "heading", "infer", "siblings", "showHidden"]) {
175
+ if (options[key] !== void 0 && typeof options[key] !== "boolean") {
176
+ issues.push({ path: key, message: "Expected a boolean." });
177
+ }
178
+ }
179
+ for (const key of ["headingText", "className", "language"]) {
180
+ if (options[key] !== void 0 && (typeof options[key] !== "string" || options[key].trim() === "")) {
181
+ issues.push({ path: key, message: "Expected a non-empty string." });
182
+ }
183
+ }
184
+ if (options.maxPerRegion !== void 0 && (!Number.isInteger(options.maxPerRegion) || options.maxPerRegion < 0)) {
185
+ issues.push({
186
+ path: "maxPerRegion",
187
+ message: "Expected a non-negative integer."
188
+ });
189
+ }
190
+ for (const key of ["width", "height"]) {
191
+ if (options[key] !== void 0 && (!Number.isFinite(options[key]) || options[key] <= 0)) {
192
+ issues.push({ path: key, message: "Expected a positive number." });
193
+ }
194
+ }
195
+ if (options.ontology !== void 0 && !isOntology(options.ontology)) {
196
+ issues.push({
197
+ path: "ontology",
198
+ message: "Expected an object of field-name arrays."
199
+ });
200
+ }
201
+ return issues;
202
+ }
203
+ function isRenderMode(value) {
204
+ return value === "build" || value === "client" || value === "both";
205
+ }
206
+ function isOntology(value) {
207
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
208
+ return false;
209
+ }
210
+ return Object.values(value).every(
211
+ (fields) => fields === void 0 || Array.isArray(fields) && fields.every((field) => typeof field === "string")
212
+ );
213
+ }
214
+
215
+ // src/graph.ts
216
+ function buildExcaliBrainGraph(input) {
217
+ const options = resolveExcaliBrainOptions(input.options);
218
+ const { slug, manifest } = input;
219
+ const centerEntry = manifest.bySlug.get(slug) ?? null;
220
+ const center = {
221
+ id: slug,
222
+ slug,
223
+ title: resolveTitle(centerEntry, input.frontmatter, slug),
224
+ permalink: centerEntry?.permalink ?? null,
225
+ virtual: false,
226
+ role: "center",
227
+ relationType: "defined"
228
+ };
229
+ if (!options.showHidden && isNoteHidden({
230
+ frontmatter: input.frontmatter,
231
+ markdown: input.markdown,
232
+ ontology: options.ontology
233
+ })) {
234
+ return { center, nodes: [], links: [] };
235
+ }
236
+ const relations = /* @__PURE__ */ new Map();
237
+ const add = (target, role, relationType) => {
238
+ if (role === "center") return;
239
+ const node = resolveNode(target, slug, manifest);
240
+ if (!node) return;
241
+ const existing = relations.get(node.id);
242
+ if (existing) {
243
+ if (existing.relationType === "inferred" && relationType === "defined") {
244
+ existing.role = role;
245
+ existing.relationType = "defined";
246
+ existing.node.role = role;
247
+ existing.node.relationType = "defined";
248
+ }
249
+ return;
250
+ }
251
+ node.role = role;
252
+ node.relationType = relationType;
253
+ relations.set(node.id, { node, role, relationType });
254
+ };
255
+ for (const relation of collectDefinedRelations({
256
+ frontmatter: input.frontmatter,
257
+ markdown: input.markdown,
258
+ ontology: options.ontology
259
+ })) {
260
+ add(relation.target, relation.role, "defined");
261
+ }
262
+ const outgoing = noteLinkSlugs(
263
+ manifest.outgoingLinks.get(slug) ?? [],
264
+ slug
265
+ );
266
+ const incoming = unique(
267
+ (manifest.incomingLinks.get(slug) ?? []).filter(
268
+ (candidate) => candidate !== slug
269
+ )
270
+ );
271
+ const outgoingSet = new Set(outgoing);
272
+ const mutual = incoming.filter((candidate) => outgoingSet.has(candidate));
273
+ const mutualSet = new Set(mutual);
274
+ if (options.infer) {
275
+ for (const target of outgoing) {
276
+ if (!mutualSet.has(target)) add(target, "child", "inferred");
277
+ }
278
+ for (const target of incoming) {
279
+ if (!mutualSet.has(target)) add(target, "parent", "inferred");
280
+ }
281
+ for (const target of mutual) add(target, "leftFriend", "inferred");
282
+ }
283
+ if (options.siblings) {
284
+ const parents = [...relations.values()].filter(
285
+ (relation) => relation.role === "parent"
286
+ );
287
+ for (const parent of parents) {
288
+ const parentSlug = parent.node.slug;
289
+ if (!parentSlug) continue;
290
+ const siblings = noteLinkSlugs(
291
+ manifest.outgoingLinks.get(parentSlug) ?? [],
292
+ slug
293
+ );
294
+ for (const siblingSlug of siblings) {
295
+ add(siblingSlug, "sibling", "inferred");
296
+ }
297
+ }
298
+ }
299
+ const nodes = [...relations.values()].map((relation) => relation.node);
300
+ const links = [...relations.values()].map(
301
+ (relation) => ({
302
+ from: slug,
303
+ to: relation.node.id,
304
+ role: relation.role,
305
+ relationType: relation.relationType
306
+ })
307
+ );
308
+ return { center, nodes, links };
309
+ }
310
+ function resolveNode(target, selfSlug, manifest) {
311
+ const raw = target.trim();
312
+ if (!raw) return null;
313
+ const targetSlug = resolveSlug(raw, manifest);
314
+ if (targetSlug && targetSlug !== selfSlug) {
315
+ const entry = manifest.bySlug.get(targetSlug);
316
+ if (entry) {
317
+ return {
318
+ id: targetSlug,
319
+ slug: targetSlug,
320
+ title: entry.title,
321
+ permalink: entry.permalink,
322
+ virtual: false,
323
+ role: "parent",
324
+ relationType: "defined"
325
+ };
326
+ }
327
+ }
328
+ return {
329
+ id: `virtual:${raw.toLowerCase()}`,
330
+ slug: null,
331
+ title: raw,
332
+ permalink: null,
333
+ virtual: true,
334
+ role: "parent",
335
+ relationType: "defined"
336
+ };
337
+ }
338
+ function resolveSlug(target, manifest) {
339
+ const direct = manifest.bySlug.get(target);
340
+ if (direct) return direct.slug;
341
+ const indexed = manifest.contentIndex.get(target.toLowerCase());
342
+ if (indexed) return indexed;
343
+ const permalink = manifest.byPermalink.get(target);
344
+ if (permalink) return permalink.slug;
345
+ return null;
346
+ }
347
+ function noteLinkSlugs(links, selfSlug) {
348
+ return unique(
349
+ links.filter(
350
+ (link) => link.kind === "note" && link.slug !== null && link.slug !== selfSlug
351
+ ).map((link) => link.slug)
352
+ );
353
+ }
354
+ function resolveTitle(entry, frontmatter, slug) {
355
+ if (entry?.title) return entry.title;
356
+ const title = frontmatter.title;
357
+ return typeof title === "string" && title.trim() ? title : slug;
358
+ }
359
+ function unique(values) {
360
+ return [...new Set(values)];
361
+ }
362
+
363
+ // src/layout.ts
364
+ var EXCALIBRAIN_REGION_ORDER = [
365
+ "parents",
366
+ "children",
367
+ "left-friends",
368
+ "right-friends",
369
+ "previous",
370
+ "next",
371
+ "siblings"
372
+ ];
373
+ var ROLE_ORDER = [
374
+ "parent",
375
+ "child",
376
+ "leftFriend",
377
+ "rightFriend",
378
+ "previous",
379
+ "next",
380
+ "sibling"
381
+ ];
382
+ var ROLE_REGION = {
383
+ parent: "parents",
384
+ child: "children",
385
+ leftFriend: "left-friends",
386
+ rightFriend: "right-friends",
387
+ previous: "previous",
388
+ next: "next",
389
+ sibling: "siblings",
390
+ center: "center"
391
+ };
392
+ var ROLE_ANCHOR = {
393
+ parent: { angle: -90, radius: "near" },
394
+ child: { angle: 90, radius: "near" },
395
+ leftFriend: { angle: 180, radius: "near" },
396
+ rightFriend: { angle: 0, radius: "near" },
397
+ previous: { angle: 180, radius: "far" },
398
+ next: { angle: 0, radius: "far" },
399
+ sibling: { angle: 0, radius: "far" }
400
+ };
401
+ var SIBLING_ANGLES = [-135, -45, 135, 45];
402
+ var NODE_HEIGHT = 36;
403
+ var CENTER_HEIGHT = 40;
404
+ var MAX_NODE_WIDTH = 150;
405
+ var MAX_CENTER_WIDTH = 180;
406
+ function layoutExcaliBrain(graph, options = {}) {
407
+ const resolved = resolveExcaliBrainOptions(options);
408
+ const width = resolved.width;
409
+ const height = resolved.height;
410
+ const cx = width / 2;
411
+ const cy = height / 2;
412
+ const minDimension = Math.min(width, height);
413
+ const nearRadius = minDimension * 0.3;
414
+ const farRadius = Math.max(
415
+ nearRadius,
416
+ Math.min(nearRadius * 1.5, minDimension / 2 - 28)
417
+ );
418
+ const positioned = [
419
+ positionCenter(graph.center, cx, cy, width, height)
420
+ ];
421
+ const groups = /* @__PURE__ */ new Map();
422
+ for (const role of ROLE_ORDER) groups.set(role, []);
423
+ for (const node of graph.nodes) {
424
+ if (node.role === "center") continue;
425
+ groups.get(node.role)?.push(node);
426
+ }
427
+ const maxPerRegion = Math.max(0, Math.trunc(resolved.maxPerRegion));
428
+ for (const role of ROLE_ORDER) {
429
+ const group = [...groups.get(role) ?? []].sort(compareNodes).slice(0, maxPerRegion);
430
+ if (group.length === 0) continue;
431
+ if (role === "sibling") {
432
+ const buckets = [[], [], [], []];
433
+ group.forEach((node, index) => {
434
+ buckets[index % SIBLING_ANGLES.length]?.push(node);
435
+ });
436
+ buckets.forEach((bucket, anchorIndex) => {
437
+ bucket.forEach((node, index) => {
438
+ const angle = spreadAngle(
439
+ SIBLING_ANGLES[anchorIndex] ?? -135,
440
+ index,
441
+ bucket.length
442
+ );
443
+ positioned.push(
444
+ positionNode(node, angle, farRadius * 0.92, cx, cy, width, height)
445
+ );
446
+ });
447
+ });
448
+ continue;
449
+ }
450
+ const anchor = ROLE_ANCHOR[role];
451
+ const radius = anchor.radius === "far" ? farRadius : nearRadius;
452
+ group.forEach((node, index) => {
453
+ const angle = spreadAngle(anchor.angle, index, group.length);
454
+ positioned.push(
455
+ positionNode(node, angle, radius, cx, cy, width, height)
456
+ );
457
+ });
458
+ }
459
+ const ids = new Set(positioned.map((node) => node.id));
460
+ const links = [];
461
+ const seen = /* @__PURE__ */ new Set();
462
+ for (const link of graph.links) {
463
+ if (!ids.has(link.from) || !ids.has(link.to)) continue;
464
+ const key = `${link.from}\0${link.to}\0${link.role}`;
465
+ if (seen.has(key)) continue;
466
+ seen.add(key);
467
+ links.push({
468
+ from: link.from,
469
+ to: link.to,
470
+ role: link.role,
471
+ relationType: link.relationType
472
+ });
473
+ }
474
+ return { width, height, nodes: positioned, links };
475
+ }
476
+ function positionCenter(center, cx, cy, width, height) {
477
+ const boxWidth = Math.min(
478
+ MAX_CENTER_WIDTH,
479
+ Math.max(72, Math.round(center.title.length * 7 + 24))
480
+ );
481
+ return {
482
+ id: center.id,
483
+ role: "center",
484
+ region: "center",
485
+ relationType: center.relationType,
486
+ title: center.title,
487
+ slug: center.slug,
488
+ permalink: center.permalink,
489
+ virtual: center.virtual,
490
+ x: clamp(Math.round(cx - boxWidth / 2), 4, width - boxWidth - 4),
491
+ y: clamp(Math.round(cy - CENTER_HEIGHT / 2), 4, height - CENTER_HEIGHT - 4),
492
+ width: boxWidth,
493
+ height: CENTER_HEIGHT
494
+ };
495
+ }
496
+ function positionNode(node, angle, radius, cx, cy, width, height) {
497
+ const point = polar(cx, cy, angle, radius);
498
+ const boxWidth = Math.min(
499
+ MAX_NODE_WIDTH,
500
+ Math.max(56, Math.round(node.title.length * 6.6 + 20))
501
+ );
502
+ return {
503
+ id: node.id,
504
+ role: node.role,
505
+ region: ROLE_REGION[node.role],
506
+ relationType: node.relationType,
507
+ title: node.title,
508
+ slug: node.slug,
509
+ permalink: node.permalink,
510
+ virtual: node.virtual,
511
+ x: clamp(Math.round(point.x - boxWidth / 2), 4, width - boxWidth - 4),
512
+ y: clamp(
513
+ Math.round(point.y - NODE_HEIGHT / 2),
514
+ 4,
515
+ height - NODE_HEIGHT - 4
516
+ ),
517
+ width: boxWidth,
518
+ height: NODE_HEIGHT
519
+ };
520
+ }
521
+ function polar(cx, cy, angleDegrees, radius) {
522
+ const radians = angleDegrees * Math.PI / 180;
523
+ return {
524
+ x: cx + radius * Math.cos(radians),
525
+ y: cy + radius * Math.sin(radians)
526
+ };
527
+ }
528
+ function spreadAngle(anchor, index, count) {
529
+ if (count <= 1) return anchor;
530
+ const step = Math.min(16, 64 / count);
531
+ return anchor + (index - (count - 1) / 2) * step;
532
+ }
533
+ function compareNodes(a, b) {
534
+ const left = a.title.toLowerCase();
535
+ const right = b.title.toLowerCase();
536
+ if (left < right) return -1;
537
+ if (left > right) return 1;
538
+ if (a.id < b.id) return -1;
539
+ if (a.id > b.id) return 1;
540
+ return 0;
541
+ }
542
+ function clamp(value, min, max) {
543
+ const upper = Math.max(min, max);
544
+ return Math.min(Math.max(value, min), upper);
545
+ }
546
+
547
+ // src/placeholder.ts
548
+ var EXCALIBRAIN_PLACEHOLDER_ATTRIBUTE = "data-rb-excalibrain";
549
+ function createExcaliBrainPlaceholder() {
550
+ return `<div ${EXCALIBRAIN_PLACEHOLDER_ATTRIBUTE}=""></div>`;
551
+ }
552
+ function createExcaliBrainPlaceholderPattern() {
553
+ return new RegExp(
554
+ `<div\\b[^>]*\\b${EXCALIBRAIN_PLACEHOLDER_ATTRIBUTE}(?:="[^"]*")?[^>]*>\\s*</div>`,
555
+ "g"
556
+ );
557
+ }
558
+ function hasExcaliBrainPlaceholder(html) {
559
+ return html.includes(EXCALIBRAIN_PLACEHOLDER_ATTRIBUTE);
560
+ }
561
+ function replaceExcaliBrainPlaceholders(html, replacement) {
562
+ return html.replace(createExcaliBrainPlaceholderPattern(), replacement);
563
+ }
564
+
565
+ // src/render.ts
566
+ var ROLE_ATTRIBUTE = {
567
+ parent: "parent",
568
+ child: "child",
569
+ leftFriend: "left-friend",
570
+ rightFriend: "right-friend",
571
+ previous: "previous",
572
+ next: "next",
573
+ sibling: "sibling",
574
+ center: "center"
575
+ };
576
+ function renderExcaliBrainSvg(graph, layout, options = {}) {
577
+ const className = options.className ?? "rb-excalibrain";
578
+ const byId = new Map(layout.nodes.map((node) => [node.id, node]));
579
+ const parts = [];
580
+ parts.push(
581
+ `<svg class="${className}__svg" viewBox="0 0 ${layout.width} ${layout.height}" role="img" aria-label="${escapeAttribute(`${graph.center.title} ExcaliBrain`)}" xmlns="http://www.w3.org/2000/svg" width="${layout.width}" height="${layout.height}">`
582
+ );
583
+ const links = layout.links.map((link) => renderLink(link, byId, className)).join("");
584
+ parts.push(`<g class="${className}__links">${links}</g>`);
585
+ for (const region of EXCALIBRAIN_REGION_ORDER) {
586
+ const regionNodes = layout.nodes.filter((node) => node.region === region);
587
+ if (regionNodes.length === 0) continue;
588
+ const nodes = regionNodes.map((node) => renderNode(node, className)).join("");
589
+ parts.push(
590
+ `<g class="${className}__region" data-region="${region}">${nodes}</g>`
591
+ );
592
+ }
593
+ const center = layout.nodes.find((node) => node.role === "center");
594
+ if (center) parts.push(renderNode(center, className));
595
+ parts.push("</svg>");
596
+ return parts.join("");
597
+ }
598
+ function renderExcaliBrainSection(input) {
599
+ const options = input.options ?? {};
600
+ const renderMode = input.render ?? "build";
601
+ const className = options.className ?? "rb-excalibrain";
602
+ const heading = options.heading !== false;
603
+ const headingText = options.headingText ?? "ExcaliBrain";
604
+ const centerSlug = input.graph.center.slug ?? input.graph.center.id;
605
+ const renderAtBuild = renderMode === "build" || renderMode === "both";
606
+ const renderAtClient = renderMode === "client" || renderMode === "both";
607
+ const svg = renderAtBuild ? renderExcaliBrainSvg(input.graph, input.layout, options) : "";
608
+ const payload = renderAtClient ? ` data-excalibrain-payload="${escapeAttribute(
609
+ JSON.stringify({
610
+ graph: input.graph,
611
+ layout: input.layout,
612
+ className
613
+ })
614
+ )}"` : "";
615
+ return [
616
+ `<section class="${className}" data-excalibrain data-excalibrain-render="${renderMode}" data-excalibrain-center="${escapeAttribute(centerSlug)}">`,
617
+ heading ? `<h2 class="${className}__heading">${escapeHtml(headingText)}</h2>` : "",
618
+ `<div class="${className}__canvas"${payload}>${svg}</div>`,
619
+ "</section>"
620
+ ].join("");
621
+ }
622
+ function renderNode(node, className) {
623
+ const box = `<rect class="${className}__node-box" x="${node.x}" y="${node.y}" width="${node.width}" height="${node.height}" rx="4" />`;
624
+ const label = `<text class="${className}__node-label" x="${round(
625
+ node.x + node.width / 2
626
+ )}" y="${round(node.y + node.height / 2)}" text-anchor="middle" dominant-baseline="middle">${escapeHtml(node.title)}</text>`;
627
+ const inner = `${box}${label}`;
628
+ const content = node.permalink ? `<a class="${className}__node-link" href="${escapeAttribute(node.permalink)}">${inner}</a>` : inner;
629
+ const attributes = [
630
+ `class="${className}__node"`,
631
+ `data-node-role="${ROLE_ATTRIBUTE[node.role]}"`,
632
+ `data-node-slug="${escapeAttribute(node.slug ?? node.title)}"`,
633
+ node.virtual ? 'data-node-virtual="true"' : "",
634
+ `data-relation-type="${node.relationType}"`
635
+ ].filter(Boolean);
636
+ return `<g ${attributes.join(" ")}>${content}</g>`;
637
+ }
638
+ function renderLink(link, byId, className) {
639
+ const from = byId.get(link.from);
640
+ const to = byId.get(link.to);
641
+ if (!from || !to) return "";
642
+ const d = `M ${round(from.x + from.width / 2)} ${round(from.y + from.height / 2)} L ${round(to.x + to.width / 2)} ${round(to.y + to.height / 2)}`;
643
+ return `<path class="${className}__link" data-link-role="${ROLE_ATTRIBUTE[link.role]}" data-relation-type="${link.relationType}" d="${d}" />`;
644
+ }
645
+ function round(value) {
646
+ return Math.round(value * 100) / 100;
647
+ }
648
+ function escapeHtml(value) {
649
+ return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
650
+ }
651
+ function escapeAttribute(value) {
652
+ return escapeHtml(value).replace(/"/g, "&quot;").replace(/'/g, "&#39;");
653
+ }
654
+
655
+ // src/rehype.ts
656
+ function rehypeExcaliBrain(options = {}) {
657
+ const language = options.language ?? "excalibrain";
658
+ return (tree) => {
659
+ replaceBlocks(tree, language);
660
+ };
661
+ }
662
+ function replaceBlocks(node, language) {
663
+ const children = getChildren(node);
664
+ if (!children) return;
665
+ for (let index = 0; index < children.length; index += 1) {
666
+ const child = children[index];
667
+ if (child === void 0) continue;
668
+ if (isExcaliBrainBlock(child, language)) {
669
+ children[index] = {
670
+ type: "raw",
671
+ value: createExcaliBrainPlaceholder()
672
+ };
673
+ continue;
674
+ }
675
+ replaceBlocks(child, language);
676
+ }
677
+ }
678
+ function isExcaliBrainBlock(node, language) {
679
+ const element = asElement(node);
680
+ if (element?.tagName !== "pre") return false;
681
+ const code = getChildren(element)?.find(
682
+ (child) => asElement(child)?.tagName === "code"
683
+ );
684
+ if (!code) return false;
685
+ return classList(code).includes(`language-${language}`);
686
+ }
687
+ function asElement(node) {
688
+ return node.type === "element" ? node : null;
689
+ }
690
+ function getChildren(node) {
691
+ const children = node.children;
692
+ return Array.isArray(children) ? children : null;
693
+ }
694
+ function classList(node) {
695
+ const value = node.properties?.className;
696
+ if (Array.isArray(value)) return value.map(String);
697
+ if (typeof value === "string") return value.split(/\s+/);
698
+ return [];
699
+ }
700
+
701
+ // index.ts
702
+ var SECTION_ATTRIBUTE = "data-excalibrain";
703
+ function createExcaliBrainRuntime(options) {
704
+ const notes = /* @__PURE__ */ new Map();
705
+ const resolved = resolveExcaliBrainOptions(options);
706
+ return {
707
+ track(slug, markdown, content) {
708
+ notes.set(slug, { markdown, content });
709
+ },
710
+ resolve(manifest, config) {
711
+ for (const entry of manifest.entries) {
712
+ if (config && !isPublished(config, entry.frontmatter)) continue;
713
+ const note = notes.get(entry.slug);
714
+ const markdown = note?.markdown ?? "";
715
+ const graph = buildExcaliBrainGraph({
716
+ slug: entry.slug,
717
+ frontmatter: entry.frontmatter,
718
+ markdown,
719
+ manifest,
720
+ options
721
+ });
722
+ const layout = layoutExcaliBrain(graph, options);
723
+ const hasNeighbour = graph.nodes.length > 0;
724
+ const hasFence = hasExcaliBrainPlaceholder(entry.html);
725
+ if (!hasFence && !(resolved.auto && hasNeighbour)) continue;
726
+ if (!hasFence && entry.html.includes(SECTION_ATTRIBUTE)) continue;
727
+ const section = renderExcaliBrainSection({
728
+ graph,
729
+ layout,
730
+ options,
731
+ render: resolved.render
732
+ });
733
+ if (hasFence) {
734
+ entry.html = replaceExcaliBrainPlaceholders(entry.html, section);
735
+ if (note) {
736
+ note.content.html = replaceExcaliBrainPlaceholders(
737
+ note.content.html,
738
+ section
739
+ );
740
+ }
741
+ } else {
742
+ entry.html = `${entry.html}${section}`;
743
+ if (note) note.content.html = `${note.content.html}${section}`;
744
+ }
745
+ }
746
+ }
747
+ };
748
+ }
749
+ function excaliBrain(options = {}) {
750
+ const runtime = createExcaliBrainRuntime(options);
751
+ const language = options.language ?? "excalibrain";
752
+ return definePlugin({
753
+ name: "excalibrain",
754
+ order: -10,
755
+ options,
756
+ validateOptions: validateExcaliBrainOptions,
757
+ extendHtmlPipeline: (pipeline) => {
758
+ pipeline.use(rehypeExcaliBrain, { language });
759
+ },
760
+ onPostParsed: (context) => {
761
+ runtime.track(context.slug, context.markdown, context.content);
762
+ },
763
+ onManifestCreated: (context) => {
764
+ runtime.resolve(context.manifest, context.config);
765
+ },
766
+ assets: [createStyleAsset("excalibrain")],
767
+ clientEntries: [createClientEntry("excalibrain", "initExcaliBrain")]
768
+ });
769
+ }
770
+ var excaliBrainPlugin = excaliBrain;
771
+ export {
772
+ DEFAULT_ONTOLOGY,
773
+ buildExcaliBrainGraph,
774
+ collectDefinedRelations,
775
+ excaliBrain,
776
+ excaliBrainPlugin,
777
+ isNoteHidden,
778
+ layoutExcaliBrain,
779
+ normalizeFieldName,
780
+ renderExcaliBrainSection,
781
+ renderExcaliBrainSvg,
782
+ resolveExcaliBrainOptions,
783
+ resolveOntology,
784
+ validateExcaliBrainOptions
785
+ };