@riebeckite/plugin-garden-explorer 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,558 @@
1
+ // index.ts
2
+ import { definePlugin } from "@riebeckite/core";
3
+
4
+ // components/garden-explorer.tsx
5
+ import { layoutRadialGraph } from "@riebeckite/core";
6
+ import { useEffect, useMemo, useState } from "hono/jsx";
7
+
8
+ // src/search-notes.ts
9
+ var FIELD_WEIGHTS = {
10
+ slug: 64,
11
+ title: 56,
12
+ tags: 44,
13
+ headings: 32,
14
+ body: 10
15
+ };
16
+ function searchGardenExplorerNotes(notes, query) {
17
+ const normalizedQuery = normalizeSearchQuery(query);
18
+ if (normalizedQuery.length === 0) return [];
19
+ return notes.map((note) => scoreNote(note, normalizedQuery)).filter(
20
+ (result) => result !== null
21
+ ).sort(
22
+ (a, b) => b.score - a.score || a.note.title.localeCompare(b.note.title, "ja")
23
+ ).map(({ note }) => note);
24
+ }
25
+ function normalizeSearchQuery(value) {
26
+ return normalizeSearchText(value).replace(/^#+/, "");
27
+ }
28
+ function normalizeSearchText(value) {
29
+ return value.toLocaleLowerCase().normalize("NFKC").replace(
30
+ /[ァ-ン]/g,
31
+ (char) => String.fromCharCode(char.charCodeAt(0) - 96)
32
+ );
33
+ }
34
+ function scoreNote(note, normalizedQuery) {
35
+ const matches = [
36
+ scoreField("title", note.title, normalizedQuery),
37
+ scoreField("slug", note.slug, normalizedQuery),
38
+ ...note.tags.map((tag) => scoreField("tags", tag, normalizedQuery)),
39
+ ...note.headings.map(
40
+ (heading) => scoreField("headings", heading, normalizedQuery)
41
+ ),
42
+ scoreField("body", note.body, normalizedQuery)
43
+ ].filter((match) => match.score > 0);
44
+ if (matches.length === 0) return null;
45
+ return {
46
+ note,
47
+ score: matches.reduce((total, match) => total + match.score, 0)
48
+ };
49
+ }
50
+ function scoreField(field, value, normalizedQuery) {
51
+ const normalizedValue = normalizeSearchText(value);
52
+ const index = normalizedValue.indexOf(normalizedQuery);
53
+ const weight = FIELD_WEIGHTS[field];
54
+ if (normalizedValue === normalizedQuery) return { score: weight * 3 };
55
+ if (index !== -1) return { score: weight * (index === 0 ? 2 : 1) };
56
+ const fuzzyScore = scoreFuzzyMatch(normalizedValue, normalizedQuery);
57
+ return { score: fuzzyScore > 0 ? Math.round(weight * fuzzyScore) : 0 };
58
+ }
59
+ function scoreFuzzyMatch(value, query) {
60
+ if (query.length < 2) return 0;
61
+ let valueIndex = 0;
62
+ let matched = 0;
63
+ let gaps = 0;
64
+ for (const queryCharacter of query) {
65
+ const nextIndex = value.indexOf(queryCharacter, valueIndex);
66
+ if (nextIndex === -1) return 0;
67
+ gaps += nextIndex - valueIndex;
68
+ valueIndex = nextIndex + 1;
69
+ matched += 1;
70
+ }
71
+ const coverage = matched / Math.max(value.length, query.length);
72
+ const continuityPenalty = Math.min(gaps / Math.max(value.length, 1), 0.8);
73
+ return Math.max(0, coverage * (1 - continuityPenalty));
74
+ }
75
+
76
+ // components/garden-explorer.tsx
77
+ import { jsx, jsxs } from "hono/jsx/jsx-runtime";
78
+ var GRAPH_WIDTH = 900;
79
+ var GRAPH_HEIGHT = 620;
80
+ var MIN_SCALE = 0.6;
81
+ var MAX_SCALE = 2.4;
82
+ function GardenExplorer(props) {
83
+ const noteBySlug = useMemo(
84
+ () => new Map(props.data.notes.map((note) => [note.slug, note])),
85
+ [props.data.notes]
86
+ );
87
+ const [query, setQuery] = useState("");
88
+ const [selectedTag, setSelectedTag] = useState(null);
89
+ const [selectedFolder, setSelectedFolder] = useState(null);
90
+ const [selectedSlug, setSelectedSlug] = useState(
91
+ () => props.data.notes[0]?.slug ?? ""
92
+ );
93
+ const [mobilePanel, setMobilePanel] = useState("graph");
94
+ const [view, setView] = useState({ x: 0, y: 0, scale: 1 });
95
+ useEffect(() => {
96
+ const params = new URLSearchParams(window.location.search);
97
+ const note = params.get("note");
98
+ const tag = params.get("tag");
99
+ const folder = params.get("folder");
100
+ if (note && noteBySlug.has(note)) setSelectedSlug(note);
101
+ if (tag) setSelectedTag(tag);
102
+ if (folder) setSelectedFolder(folder);
103
+ }, [noteBySlug]);
104
+ useEffect(() => {
105
+ const params = new URLSearchParams();
106
+ if (selectedSlug) params.set("note", selectedSlug);
107
+ if (selectedTag) params.set("tag", selectedTag);
108
+ if (selectedFolder) params.set("folder", selectedFolder);
109
+ const next = params.toString() ? `/explore?${params}` : "/explore";
110
+ window.history.replaceState(null, "", next);
111
+ }, [selectedSlug, selectedTag, selectedFolder]);
112
+ const filteredNotes = useMemo(() => {
113
+ const base = props.data.notes.filter((note) => {
114
+ if (selectedTag && !note.tags.includes(selectedTag)) return false;
115
+ if (selectedFolder && note.folder !== selectedFolder) return false;
116
+ return true;
117
+ });
118
+ const searched = query ? searchGardenExplorerNotes(base, query) : base;
119
+ return searched.slice(0, 80);
120
+ }, [props.data.notes, query, selectedTag, selectedFolder]);
121
+ const visibleSlugSet = useMemo(
122
+ () => new Set(filteredNotes.map((note) => note.slug)),
123
+ [filteredNotes]
124
+ );
125
+ const visibleEdges = useMemo(
126
+ () => props.data.edges.filter(
127
+ (edge) => visibleSlugSet.has(edge.source) && visibleSlugSet.has(edge.target)
128
+ ),
129
+ [props.data.edges, visibleSlugSet]
130
+ );
131
+ const graphNodes = useMemo(
132
+ () => layoutNodes(filteredNotes, selectedSlug),
133
+ [filteredNotes, selectedSlug]
134
+ );
135
+ const selectedNote = noteBySlug.get(selectedSlug) ?? filteredNotes[0] ?? null;
136
+ const relatedNotes = useMemo(
137
+ () => selectedNote ? getRelatedNotes(selectedNote, props.data.notes) : [],
138
+ [selectedNote, props.data.notes]
139
+ );
140
+ const selectNote = (slug) => {
141
+ setSelectedSlug(slug);
142
+ setMobilePanel("details");
143
+ };
144
+ const clearFilters = () => {
145
+ setQuery("");
146
+ setSelectedTag(null);
147
+ setSelectedFolder(null);
148
+ };
149
+ const zoom = (delta) => {
150
+ setView((current) => ({
151
+ ...current,
152
+ scale: clamp(current.scale + delta, MIN_SCALE, MAX_SCALE)
153
+ }));
154
+ };
155
+ return /* @__PURE__ */ jsxs("div", { class: "garden-explorer rr-garden-explorer", "data-garden-explorer": true, children: [
156
+ /* @__PURE__ */ jsxs("fieldset", { class: "garden-explorer__mobile-tabs", children: [
157
+ /* @__PURE__ */ jsx("legend", { class: "sr-only", children: "Explorer panels" }),
158
+ ["graph", "explorer", "details"].map((panel) => /* @__PURE__ */ jsx(
159
+ "button",
160
+ {
161
+ type: "button",
162
+ class: "garden-explorer__mobile-tab",
163
+ "aria-pressed": mobilePanel === panel,
164
+ onClick: () => setMobilePanel(panel),
165
+ children: panel
166
+ }
167
+ ))
168
+ ] }),
169
+ /* @__PURE__ */ jsxs("aside", { class: panelClass("explorer", mobilePanel), children: [
170
+ /* @__PURE__ */ jsxs("div", { class: "garden-explorer__section", children: [
171
+ /* @__PURE__ */ jsx("p", { class: "garden-explorer__eyebrow", children: "Explorer" }),
172
+ /* @__PURE__ */ jsxs("label", { class: "garden-explorer__search", children: [
173
+ /* @__PURE__ */ jsx("span", { class: "sr-only", children: "Search notes" }),
174
+ /* @__PURE__ */ jsx(
175
+ "input",
176
+ {
177
+ type: "search",
178
+ value: query,
179
+ placeholder: "Search notes...",
180
+ onInput: (event) => setQuery(event.currentTarget.value)
181
+ }
182
+ )
183
+ ] }),
184
+ /* @__PURE__ */ jsx(
185
+ "button",
186
+ {
187
+ type: "button",
188
+ class: "garden-explorer__text-button",
189
+ onClick: clearFilters,
190
+ children: "Clear filters"
191
+ }
192
+ )
193
+ ] }),
194
+ /* @__PURE__ */ jsx(
195
+ FilterList,
196
+ {
197
+ title: "Tags",
198
+ items: props.data.tags.map((tag) => ({
199
+ key: tag.name,
200
+ label: `#${tag.name}`,
201
+ count: tag.count
202
+ })),
203
+ selectedKey: selectedTag,
204
+ onSelect: (key) => setSelectedTag(key === selectedTag ? null : key)
205
+ }
206
+ ),
207
+ /* @__PURE__ */ jsx(
208
+ FilterList,
209
+ {
210
+ title: "Folders",
211
+ items: props.data.folders.map((folder) => ({
212
+ key: folder.path,
213
+ label: folder.path,
214
+ count: folder.count
215
+ })),
216
+ selectedKey: selectedFolder,
217
+ onSelect: (key) => setSelectedFolder(key === selectedFolder ? null : key)
218
+ }
219
+ ),
220
+ /* @__PURE__ */ jsxs("div", { class: "garden-explorer__section", children: [
221
+ /* @__PURE__ */ jsx("p", { class: "garden-explorer__eyebrow", children: "Notes" }),
222
+ /* @__PURE__ */ jsx(
223
+ NoteList,
224
+ {
225
+ notes: filteredNotes,
226
+ selectedSlug,
227
+ onSelect: selectNote
228
+ }
229
+ )
230
+ ] })
231
+ ] }),
232
+ /* @__PURE__ */ jsxs("section", { class: panelClass("graph", mobilePanel), "aria-label": "Note graph", children: [
233
+ /* @__PURE__ */ jsxs("div", { class: "garden-explorer__graph-toolbar", children: [
234
+ /* @__PURE__ */ jsxs("span", { children: [
235
+ filteredNotes.length,
236
+ " notes"
237
+ ] }),
238
+ /* @__PURE__ */ jsxs("div", { children: [
239
+ /* @__PURE__ */ jsx(
240
+ "button",
241
+ {
242
+ type: "button",
243
+ onClick: () => zoom(-0.2),
244
+ "aria-label": "Zoom out",
245
+ children: "−"
246
+ }
247
+ ),
248
+ /* @__PURE__ */ jsx(
249
+ "button",
250
+ {
251
+ type: "button",
252
+ onClick: () => zoom(0.2),
253
+ "aria-label": "Zoom in",
254
+ children: "+"
255
+ }
256
+ ),
257
+ /* @__PURE__ */ jsx(
258
+ "button",
259
+ {
260
+ type: "button",
261
+ onClick: () => setView({ x: 0, y: 0, scale: 1 }),
262
+ children: "Reset"
263
+ }
264
+ )
265
+ ] })
266
+ ] }),
267
+ /* @__PURE__ */ jsx(
268
+ "svg",
269
+ {
270
+ class: "garden-graph",
271
+ viewBox: `0 0 ${GRAPH_WIDTH} ${GRAPH_HEIGHT}`,
272
+ role: "img",
273
+ "aria-label": "Interactive graph of notes and internal links",
274
+ onWheel: (event) => {
275
+ event.preventDefault();
276
+ zoom(event.deltaY > 0 ? -0.1 : 0.1);
277
+ },
278
+ onPointerMove: (event) => {
279
+ if (event.buttons !== 1) return;
280
+ setView((current) => ({
281
+ ...current,
282
+ x: current.x + event.movementX / current.scale,
283
+ y: current.y + event.movementY / current.scale
284
+ }));
285
+ },
286
+ children: /* @__PURE__ */ jsxs("g", { transform: `translate(${view.x} ${view.y}) scale(${view.scale})`, children: [
287
+ visibleEdges.map((edge) => {
288
+ const source = graphNodes.get(edge.source);
289
+ const target = graphNodes.get(edge.target);
290
+ if (!source || !target) return null;
291
+ return /* @__PURE__ */ jsx(
292
+ "line",
293
+ {
294
+ class: "garden-graph__edge",
295
+ x1: source.x,
296
+ y1: source.y,
297
+ x2: target.x,
298
+ y2: target.y
299
+ },
300
+ `${edge.source}-${edge.target}`
301
+ );
302
+ }),
303
+ filteredNotes.map((note) => {
304
+ const node = graphNodes.get(note.slug);
305
+ if (!node) return null;
306
+ const selected = note.slug === selectedSlug;
307
+ return /* @__PURE__ */ jsx("g", { class: "garden-graph__node", children: /* @__PURE__ */ jsxs(
308
+ "a",
309
+ {
310
+ href: `/explore?note=${encodeURIComponent(note.slug)}`,
311
+ "aria-label": `Select ${note.title}`,
312
+ onClick: (event) => {
313
+ event.preventDefault();
314
+ selectNote(note.slug);
315
+ },
316
+ children: [
317
+ /* @__PURE__ */ jsx(
318
+ "circle",
319
+ {
320
+ cx: node.x,
321
+ cy: node.y,
322
+ r: selected ? 15 : node.radius,
323
+ "data-selected": selected ? "true" : "false"
324
+ }
325
+ ),
326
+ /* @__PURE__ */ jsx("text", { x: node.x + 18, y: node.y + 5, children: note.title })
327
+ ]
328
+ }
329
+ ) }, note.slug);
330
+ })
331
+ ] })
332
+ }
333
+ )
334
+ ] }),
335
+ /* @__PURE__ */ jsx("aside", { class: panelClass("details", mobilePanel), children: selectedNote ? /* @__PURE__ */ jsx(
336
+ NoteDetails,
337
+ {
338
+ note: selectedNote,
339
+ noteBySlug,
340
+ relatedNotes,
341
+ onSelect: selectNote
342
+ }
343
+ ) : /* @__PURE__ */ jsx("p", { class: "garden-explorer__empty", children: "Select a note to inspect links." }) })
344
+ ] });
345
+ }
346
+ function FilterList(props) {
347
+ return /* @__PURE__ */ jsxs("div", { class: "garden-explorer__section", children: [
348
+ /* @__PURE__ */ jsx("p", { class: "garden-explorer__eyebrow", children: props.title }),
349
+ /* @__PURE__ */ jsx("div", { class: "garden-explorer__chips", children: props.items.slice(0, 28).map((item) => /* @__PURE__ */ jsxs(
350
+ "button",
351
+ {
352
+ type: "button",
353
+ class: "garden-explorer__chip",
354
+ "aria-pressed": props.selectedKey === item.key,
355
+ onClick: () => props.onSelect(item.key),
356
+ children: [
357
+ /* @__PURE__ */ jsx("span", { children: item.label }),
358
+ /* @__PURE__ */ jsx("span", { children: item.count })
359
+ ]
360
+ },
361
+ item.key
362
+ )) })
363
+ ] });
364
+ }
365
+ function NoteList(props) {
366
+ return /* @__PURE__ */ jsx("ul", { class: "garden-explorer__note-list", children: props.notes.map((note) => /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsxs(
367
+ "button",
368
+ {
369
+ type: "button",
370
+ "aria-pressed": props.selectedSlug === note.slug,
371
+ onClick: () => props.onSelect(note.slug),
372
+ children: [
373
+ /* @__PURE__ */ jsx("span", { children: note.title }),
374
+ /* @__PURE__ */ jsx("small", { children: note.folder })
375
+ ]
376
+ }
377
+ ) }, note.slug)) });
378
+ }
379
+ function NoteDetails(props) {
380
+ return /* @__PURE__ */ jsxs("div", { class: "garden-explorer__details", children: [
381
+ /* @__PURE__ */ jsx("p", { class: "garden-explorer__eyebrow", children: "Selected Note" }),
382
+ /* @__PURE__ */ jsx("h2", { children: /* @__PURE__ */ jsx("a", { href: props.note.permalink, children: props.note.title }) }),
383
+ /* @__PURE__ */ jsx("p", { class: "garden-explorer__path", children: props.note.slug }),
384
+ props.note.excerpt && /* @__PURE__ */ jsx("p", { children: props.note.excerpt }),
385
+ /* @__PURE__ */ jsx("div", { class: "garden-explorer__tag-row", children: props.note.tags.map((tag) => /* @__PURE__ */ jsxs("span", { children: [
386
+ "#",
387
+ tag
388
+ ] }, tag)) }),
389
+ /* @__PURE__ */ jsx(
390
+ LinkedNoteSection,
391
+ {
392
+ title: "Outgoing Links",
393
+ slugs: props.note.outgoing,
394
+ noteBySlug: props.noteBySlug,
395
+ onSelect: props.onSelect
396
+ }
397
+ ),
398
+ /* @__PURE__ */ jsx(
399
+ LinkedNoteSection,
400
+ {
401
+ title: "Backlinks",
402
+ slugs: props.note.backlinks,
403
+ noteBySlug: props.noteBySlug,
404
+ onSelect: props.onSelect
405
+ }
406
+ ),
407
+ /* @__PURE__ */ jsxs("div", { class: "garden-explorer__section garden-explorer__section--flush", children: [
408
+ /* @__PURE__ */ jsx("p", { class: "garden-explorer__eyebrow", children: "Related Notes" }),
409
+ /* @__PURE__ */ jsx(
410
+ NoteList,
411
+ {
412
+ notes: props.relatedNotes,
413
+ selectedSlug: props.note.slug,
414
+ onSelect: props.onSelect
415
+ }
416
+ )
417
+ ] })
418
+ ] });
419
+ }
420
+ function LinkedNoteSection(props) {
421
+ const notes = props.slugs.map((slug) => props.noteBySlug.get(slug)).filter((note) => note !== void 0);
422
+ return /* @__PURE__ */ jsxs("div", { class: "garden-explorer__section garden-explorer__section--flush", children: [
423
+ /* @__PURE__ */ jsx("p", { class: "garden-explorer__eyebrow", children: props.title }),
424
+ notes.length > 0 ? /* @__PURE__ */ jsx(NoteList, { notes, selectedSlug: "", onSelect: props.onSelect }) : /* @__PURE__ */ jsx("p", { class: "garden-explorer__empty", children: "No notes." })
425
+ ] });
426
+ }
427
+ function layoutNodes(notes, selectedSlug) {
428
+ return layoutRadialGraph(notes, {
429
+ width: GRAPH_WIDTH,
430
+ height: GRAPH_HEIGHT,
431
+ centerSlug: selectedSlug
432
+ });
433
+ }
434
+ function getRelatedNotes(selectedNote, notes) {
435
+ const directSlugs = /* @__PURE__ */ new Set([
436
+ ...selectedNote.outgoing,
437
+ ...selectedNote.backlinks
438
+ ]);
439
+ const selectedTags = new Set(selectedNote.tags);
440
+ return notes.filter((note) => note.slug !== selectedNote.slug).map((note) => ({
441
+ note,
442
+ score: (directSlugs.has(note.slug) ? 8 : 0) + note.tags.filter((tag) => selectedTags.has(tag)).length * 3
443
+ })).filter((item) => item.score > 0).sort(
444
+ (a, b) => b.score - a.score || a.note.title.localeCompare(b.note.title, "ja")
445
+ ).slice(0, 8).map((item) => item.note);
446
+ }
447
+ function panelClass(panel, activePanel) {
448
+ return `garden-explorer__panel garden-explorer__panel--${panel}${panel === activePanel ? " garden-explorer__panel--active" : ""}`;
449
+ }
450
+ function clamp(value, min, max) {
451
+ return Math.min(Math.max(value, min), max);
452
+ }
453
+
454
+ // src/garden-explorer.server.ts
455
+ import {
456
+ buildGraphEdges,
457
+ isPublished
458
+ } from "@riebeckite/core";
459
+ var MAX_BODY_LENGTH = 4e3;
460
+ function getGardenExplorerData(args) {
461
+ const graph = args.manifest.graph;
462
+ const publishedEntries = graph.nodes().filter((entry) => isPublished(args.config, entry.frontmatter));
463
+ const publishedSlugs = new Set(publishedEntries.map((entry) => entry.slug));
464
+ const notes = publishedEntries.map(
465
+ (entry) => toGardenNote(entry, graph, publishedSlugs, args.resolveTitle)
466
+ ).sort((a, b) => a.title.localeCompare(b.title, "ja"));
467
+ return {
468
+ notes,
469
+ edges: buildGraphEdges(notes, publishedSlugs),
470
+ tags: buildTags(notes),
471
+ folders: buildFolders(notes)
472
+ };
473
+ }
474
+ function toGardenNote(entry, graph, publishedSlugs, resolveTitle) {
475
+ const { outgoingSlugs, incomingSlugs } = graph.filterNeighbors(
476
+ entry.slug,
477
+ (slug) => publishedSlugs.has(slug)
478
+ );
479
+ return {
480
+ slug: entry.slug,
481
+ permalink: entry.permalink,
482
+ title: resolveTitle(entry.slug, entry.frontmatter.title),
483
+ headings: extractHeadings(entry.html),
484
+ body: toPlainText(entry.html).slice(0, MAX_BODY_LENGTH),
485
+ excerpt: createExcerpt(entry),
486
+ tags: entry.tags,
487
+ date: getEntryDate(entry.frontmatter),
488
+ folder: getFolder(entry.slug),
489
+ outgoing: outgoingSlugs,
490
+ backlinks: incomingSlugs
491
+ };
492
+ }
493
+ function buildTags(notes) {
494
+ const counts = /* @__PURE__ */ new Map();
495
+ for (const note of notes) {
496
+ for (const tag of note.tags) counts.set(tag, (counts.get(tag) ?? 0) + 1);
497
+ }
498
+ return Array.from(counts, ([name, count]) => ({ name, count })).sort(
499
+ (a, b) => b.count - a.count || a.name.localeCompare(b.name, "ja")
500
+ );
501
+ }
502
+ function buildFolders(notes) {
503
+ const counts = /* @__PURE__ */ new Map();
504
+ for (const note of notes)
505
+ counts.set(note.folder, (counts.get(note.folder) ?? 0) + 1);
506
+ return Array.from(counts, ([path, count]) => ({ path, count })).sort(
507
+ (a, b) => a.path.localeCompare(b.path, "ja")
508
+ );
509
+ }
510
+ function getFolder(slug) {
511
+ const segments = slug.split("/");
512
+ if (segments.length <= 1) return "Root";
513
+ return segments.slice(0, -1).join("/");
514
+ }
515
+ function createExcerpt(entry) {
516
+ const description = entry.frontmatter.description;
517
+ const text = typeof description === "string" && description.trim() ? description.trim() : toPlainText(entry.html);
518
+ return text.slice(0, 180);
519
+ }
520
+ function extractHeadings(html) {
521
+ const headings = [];
522
+ const headingPattern = /<h([1-4])\b[^>]*>([\s\S]*?)<\/h\1>/g;
523
+ for (const match of html.matchAll(headingPattern)) {
524
+ const heading = toPlainText(match[2] ?? "");
525
+ if (heading) headings.push(heading);
526
+ }
527
+ return headings;
528
+ }
529
+ function getEntryDate(frontmatter) {
530
+ const value = frontmatter.published ?? frontmatter.date ?? frontmatter.created;
531
+ if (value instanceof Date && !Number.isNaN(value.getTime()))
532
+ return value.toISOString();
533
+ if (typeof value !== "string" || !value.trim()) return null;
534
+ const date = new Date(value);
535
+ return Number.isNaN(date.getTime()) ? null : date.toISOString();
536
+ }
537
+ function toPlainText(html) {
538
+ return html.replace(/<script\b[^>]*>[\s\S]*?<\/script>/gi, " ").replace(/<style\b[^>]*>[\s\S]*?<\/style>/gi, " ").replace(/<[^>]+>/g, " ").replace(/&nbsp;/g, " ").replace(/&amp;/g, "&").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&quot;/g, '"').replace(/&#39;/g, "'").replace(/\s+/g, " ").trim();
539
+ }
540
+
541
+ // index.ts
542
+ function gardenExplorerPlugin() {
543
+ return definePlugin({
544
+ name: "garden-explorer",
545
+ assets: [
546
+ {
547
+ pluginName: "garden-explorer",
548
+ kind: "style",
549
+ moduleSpecifier: "@riebeckite/plugin-garden-explorer/style.css"
550
+ }
551
+ ]
552
+ });
553
+ }
554
+ export {
555
+ GardenExplorer,
556
+ gardenExplorerPlugin,
557
+ getGardenExplorerData
558
+ };
@@ -0,0 +1,32 @@
1
+ import type { GraphEdge } from "@riebeckite/core";
2
+ type GardenExplorerSearchFields = {
3
+ slug: string;
4
+ permalink: string;
5
+ title: string;
6
+ headings: string[];
7
+ body: string;
8
+ excerpt: string;
9
+ tags: string[];
10
+ date: string | null;
11
+ };
12
+ export type GardenExplorerNote = GardenExplorerSearchFields & {
13
+ folder: string;
14
+ outgoing: string[];
15
+ backlinks: string[];
16
+ };
17
+ export type GardenExplorerEdge = GraphEdge;
18
+ export type GardenExplorerTag = {
19
+ name: string;
20
+ count: number;
21
+ };
22
+ export type GardenExplorerFolder = {
23
+ path: string;
24
+ count: number;
25
+ };
26
+ export type GardenExplorerData = {
27
+ notes: GardenExplorerNote[];
28
+ edges: GardenExplorerEdge[];
29
+ tags: GardenExplorerTag[];
30
+ folders: GardenExplorerFolder[];
31
+ };
32
+ export {};
@@ -0,0 +1,9 @@
1
+ import { type ContentManifest, type ResolvedRiebeckiteConfig } from "@riebeckite/core";
2
+ import type { GardenExplorerData } from "./garden-explorer.js";
3
+ type TitleResolver = (slug: string, title: unknown) => string;
4
+ export declare function getGardenExplorerData(args: {
5
+ manifest: ContentManifest;
6
+ config: ResolvedRiebeckiteConfig;
7
+ resolveTitle: TitleResolver;
8
+ }): GardenExplorerData;
9
+ export {};
@@ -0,0 +1,2 @@
1
+ import type { GardenExplorerNote } from "./garden-explorer.js";
2
+ export declare function searchGardenExplorerNotes(notes: GardenExplorerNote[], query: string): GardenExplorerNote[];
package/package.json ADDED
@@ -0,0 +1,62 @@
1
+ {
2
+ "name": "@riebeckite/plugin-garden-explorer",
3
+ "version": "0.0.1",
4
+ "license": "Apache-2.0",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.js",
12
+ "source": "./index.ts",
13
+ "default": "./dist/index.js"
14
+ },
15
+ "./components": {
16
+ "types": "./dist/components/garden-explorer.d.ts",
17
+ "import": "./dist/components/garden-explorer.js",
18
+ "source": "./components/garden-explorer.tsx",
19
+ "default": "./dist/components/garden-explorer.js"
20
+ },
21
+ "./style.css": "./style.css"
22
+ },
23
+ "dependencies": {
24
+ "hono": "^4.12.25",
25
+ "@riebeckite/core": "0.0.1"
26
+ },
27
+ "publishConfig": {
28
+ "access": "public"
29
+ },
30
+ "files": [
31
+ "LICENSE",
32
+ "README.md",
33
+ "README_ja.md",
34
+ "dist",
35
+ "style.css"
36
+ ],
37
+ "repository": {
38
+ "type": "git",
39
+ "url": "https://github.com/Rerurate514/riebeckite.git",
40
+ "directory": "packages/plugins/garden-explorer"
41
+ },
42
+ "homepage": "https://github.com/Rerurate514/riebeckite#readme",
43
+ "bugs": {
44
+ "url": "https://github.com/Rerurate514/riebeckite/issues"
45
+ },
46
+ "description": "Interactive graph and search explorer for Riebeckite notes.",
47
+ "keywords": [
48
+ "riebeckite",
49
+ "plugin",
50
+ "knowledge-graph",
51
+ "search"
52
+ ],
53
+ "engines": {
54
+ "node": "^20.19.0 || >=22.12.0"
55
+ },
56
+ "sideEffects": [
57
+ "./style.css"
58
+ ],
59
+ "scripts": {
60
+ "build": "node ../../../scripts/build_package.mjs"
61
+ }
62
+ }