@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/LICENSE +201 -0
- package/README.md +89 -0
- package/README_ja.md +69 -0
- package/dist/components/garden-explorer.d.ts +6 -0
- package/dist/components/garden-explorer.js +452 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +558 -0
- package/dist/src/garden-explorer.d.ts +32 -0
- package/dist/src/garden-explorer.server.d.ts +9 -0
- package/dist/src/search-notes.d.ts +2 -0
- package/package.json +62 -0
- package/style.css +292 -0
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(/ /g, " ").replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, '"').replace(/'/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 {};
|
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
|
+
}
|