@tayacrystals/lore 0.1.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.
@@ -0,0 +1,229 @@
1
+ import type { Element, ElementContent, Text } from "hast";
2
+ import { h } from "hastscript";
3
+
4
+ /** SVG icon for a file */
5
+ function fileIcon(): Element {
6
+ return h(
7
+ "svg",
8
+ {
9
+ xmlns: "http://www.w3.org/2000/svg",
10
+ width: "16",
11
+ height: "16",
12
+ viewBox: "0 0 24 24",
13
+ fill: "none",
14
+ stroke: "currentColor",
15
+ strokeWidth: "2",
16
+ strokeLinecap: "round",
17
+ strokeLinejoin: "round",
18
+ class: "ft-icon",
19
+ },
20
+ [
21
+ h("path", { d: "M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z" }),
22
+ h("path", { d: "M14 2v4a2 2 0 0 0 2 2h4" }),
23
+ ],
24
+ );
25
+ }
26
+
27
+ /** SVG icon for a folder */
28
+ function folderIcon(): Element {
29
+ return h(
30
+ "svg",
31
+ {
32
+ xmlns: "http://www.w3.org/2000/svg",
33
+ width: "16",
34
+ height: "16",
35
+ viewBox: "0 0 24 24",
36
+ fill: "none",
37
+ stroke: "currentColor",
38
+ strokeWidth: "2",
39
+ strokeLinecap: "round",
40
+ strokeLinejoin: "round",
41
+ class: "ft-icon",
42
+ },
43
+ [
44
+ h("path", {
45
+ d: "M20 20a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-7.9a2 2 0 0 1-1.69-.9L9.6 3.9A2 2 0 0 0 7.93 3H4a2 2 0 0 0-2 2v13a2 2 0 0 0 2 2Z",
46
+ }),
47
+ ],
48
+ );
49
+ }
50
+
51
+ /** Check if a node is an element with a given tag */
52
+ function isElement(node: ElementContent, tag: string): node is Element {
53
+ return node.type === "element" && node.tagName === tag;
54
+ }
55
+
56
+ /** Check if text looks like a placeholder (... or ellipsis) */
57
+ function isPlaceholder(text: string): boolean {
58
+ const trimmed = text.trim();
59
+ return trimmed === "..." || trimmed === "\u2026";
60
+ }
61
+
62
+ /** Get text content of a node (shallow) */
63
+ function getTextContent(node: ElementContent): string {
64
+ if (node.type === "text") return node.value;
65
+ if (node.type === "element") {
66
+ return node.children.map((c: ElementContent) => getTextContent(c)).join("");
67
+ }
68
+ return "";
69
+ }
70
+
71
+ /** Check if a name ends with / (directory marker) */
72
+ function isDirectoryName(name: string): boolean {
73
+ return name.endsWith("/");
74
+ }
75
+
76
+ /**
77
+ * Process a single <li> into a file tree entry.
78
+ * Returns the transformed element.
79
+ */
80
+ function processListItem(li: Element): Element {
81
+ const children = li.children.filter(
82
+ (c: ElementContent) => !(c.type === "text" && (c as Text).value.trim() === ""),
83
+ );
84
+
85
+ // Separate nested <ul> from inline content
86
+ const inlineContent: ElementContent[] = [];
87
+ let nestedList: Element | null = null;
88
+
89
+ for (const child of children) {
90
+ if (isElement(child, "ul")) {
91
+ nestedList = child;
92
+ } else {
93
+ inlineContent.push(child);
94
+ }
95
+ }
96
+
97
+ // Extract the file/folder name and detect features
98
+ let isHighlighted = false;
99
+ let isDirectory = nestedList !== null;
100
+ const nameNodes: ElementContent[] = [];
101
+ const commentNodes: ElementContent[] = [];
102
+ let foundName = false;
103
+
104
+ for (const node of inlineContent) {
105
+ if (!foundName) {
106
+ if (isElement(node, "strong")) {
107
+ // Highlighted entry
108
+ isHighlighted = true;
109
+ nameNodes.push(...node.children);
110
+ foundName = true;
111
+ } else if (isElement(node, "a")) {
112
+ nameNodes.push(node);
113
+ foundName = true;
114
+ } else if (node.type === "text") {
115
+ const text = node.value;
116
+ // The name is the first word/path segment
117
+ if (!text.trim()) continue;
118
+ nameNodes.push(node);
119
+ foundName = true;
120
+ } else if (isElement(node, "code")) {
121
+ nameNodes.push(node);
122
+ foundName = true;
123
+ } else {
124
+ nameNodes.push(node);
125
+ foundName = true;
126
+ }
127
+ } else {
128
+ commentNodes.push(node);
129
+ }
130
+ }
131
+
132
+ // Check if the name text implies a directory
133
+ const nameText = nameNodes.map((n) => getTextContent(n)).join("").trim();
134
+ if (isDirectoryName(nameText)) {
135
+ isDirectory = true;
136
+ }
137
+
138
+ // Check for placeholder
139
+ if (isPlaceholder(nameText)) {
140
+ return h("li", { class: "ft-entry ft-placeholder" }, [
141
+ h("span", { class: "ft-entry-inner" }, [
142
+ h("span", { class: "ft-name ft-placeholder-text" }, [
143
+ { type: "text", value: "\u2026" } as Text,
144
+ ]),
145
+ ]),
146
+ ]);
147
+ }
148
+
149
+ // Clean trailing slash from directory names for display
150
+ const displayNameNodes: ElementContent[] = nameNodes.map((n) => {
151
+ if (n.type === "text" && n.value.endsWith("/")) {
152
+ return { ...n, value: n.value.slice(0, -1) } as Text;
153
+ }
154
+ return n;
155
+ });
156
+
157
+ // Build the icon
158
+ const icon = isDirectory ? folderIcon() : fileIcon();
159
+
160
+ // Build name span
161
+ const nameSpan = h(
162
+ "span",
163
+ { class: isHighlighted ? "ft-name ft-highlight" : "ft-name" },
164
+ displayNameNodes,
165
+ );
166
+
167
+ // Build comment span if present
168
+ const commentSpan =
169
+ commentNodes.length > 0
170
+ ? h("span", { class: "ft-comment" }, commentNodes)
171
+ : null;
172
+
173
+ const entryInner: ElementContent[] = [icon, nameSpan];
174
+ if (commentSpan) entryInner.push(commentSpan);
175
+
176
+ if (isDirectory && nestedList) {
177
+ // Process nested list recursively
178
+ const processedList = processFileList(nestedList);
179
+
180
+ // Directory with children: collapsible
181
+ const summary = h("summary", { class: "ft-entry-inner ft-dir-summary" }, entryInner);
182
+ const details = h("details", { open: true }, [summary, processedList]);
183
+
184
+ return h("li", { class: "ft-entry ft-directory" }, [details]);
185
+ } else if (isDirectory) {
186
+ // Empty directory (name ended with / but no children)
187
+ return h("li", { class: "ft-entry ft-directory" }, [
188
+ h("span", { class: "ft-entry-inner" }, entryInner),
189
+ ]);
190
+ } else {
191
+ // Regular file
192
+ return h("li", { class: "ft-entry ft-file" }, [
193
+ h("span", { class: "ft-entry-inner" }, entryInner),
194
+ ]);
195
+ }
196
+ }
197
+
198
+ /** Process a <ul> element, transforming all its <li> children */
199
+ function processFileList(ul: Element): Element {
200
+ const processed = ul.children
201
+ .filter((child: ElementContent): child is Element => isElement(child, "li"))
202
+ .map((li: Element) => processListItem(li));
203
+
204
+ return h("ul", { class: "ft-list", role: "tree" }, processed);
205
+ }
206
+
207
+ /**
208
+ * Transform raw HTML (from markdown list) into file tree HTML.
209
+ * Takes the HTML string from the slot and returns transformed HTML.
210
+ */
211
+ export async function transformFileTree(html: string): Promise<string> {
212
+ const { rehype } = await import("rehype");
213
+ const { toHtml } = await import("hast-util-to-html");
214
+ const { visit } = await import("unist-util-visit");
215
+
216
+ const processor = rehype().data("settings", { fragment: true });
217
+ const tree = processor.parse(html);
218
+
219
+ // Find the top-level <ul> and process it
220
+ visit(tree, "element", (node, index, parent) => {
221
+ if (node.tagName === "ul" && parent && index !== undefined) {
222
+ const processed = processFileList(node);
223
+ (parent.children as ElementContent[])[index] = processed;
224
+ return "skip";
225
+ }
226
+ });
227
+
228
+ return toHtml(tree);
229
+ }
@@ -0,0 +1,97 @@
1
+ import type { CollectionEntry } from "astro:content";
2
+ import type { SidebarGroupConfig } from "../config";
3
+
4
+ const BASE_URL = import.meta.env.BASE_URL || "/";
5
+
6
+ export interface SidebarItem {
7
+ type: "link";
8
+ label: string;
9
+ href: string;
10
+ order: number;
11
+ icon?: string;
12
+ }
13
+
14
+ export interface SidebarGroup {
15
+ type: "group";
16
+ label: string;
17
+ order: number;
18
+ slug: string;
19
+ items: SidebarEntry[];
20
+ }
21
+
22
+ export type SidebarEntry = SidebarItem | SidebarGroup;
23
+
24
+ function titleCase(str: string): string {
25
+ return str
26
+ .replace(/[-_]/g, " ")
27
+ .replace(/\b\w/g, (c) => c.toUpperCase());
28
+ }
29
+
30
+ export function buildSidebar(
31
+ entries: CollectionEntry<"docs">[],
32
+ sidebarConfig: Record<string, SidebarGroupConfig>,
33
+ ): SidebarEntry[] {
34
+ const published = entries.filter((e) => !e.data.draft);
35
+ return buildLevel(published, sidebarConfig, "", 0);
36
+ }
37
+
38
+ function buildLevel(
39
+ entries: CollectionEntry<"docs">[],
40
+ configAtLevel: Record<string, SidebarGroupConfig>,
41
+ prefix: string,
42
+ depth: number,
43
+ ): SidebarEntry[] {
44
+ const groups = new Map<string, CollectionEntry<"docs">[]>();
45
+ const topLevel: SidebarItem[] = [];
46
+
47
+ for (const entry of entries) {
48
+ const relativePath = prefix ? entry.id.slice(prefix.length + 1) : entry.id;
49
+ const parts = relativePath.split("/");
50
+
51
+ if (parts.length > 1) {
52
+ const groupSlug = parts[0];
53
+ if (!groups.has(groupSlug)) {
54
+ groups.set(groupSlug, []);
55
+ }
56
+ groups.get(groupSlug)!.push(entry);
57
+ } else {
58
+ const href = entry.id === "index" ? `${BASE_URL}docs` : `${BASE_URL}docs/${entry.id}`;
59
+ topLevel.push({
60
+ type: "link",
61
+ label: entry.data.title,
62
+ href,
63
+ order: entry.data.order,
64
+ icon: entry.data.icon,
65
+ });
66
+ }
67
+ }
68
+
69
+ const result: SidebarEntry[] = [];
70
+
71
+ // Add top-level items (excluding index at root level only)
72
+ for (const item of topLevel) {
73
+ if (depth === 0 && item.href === "/docs") continue;
74
+ result.push(item);
75
+ }
76
+
77
+ // Add groups (recursively)
78
+ for (const [slug, groupEntries] of groups) {
79
+ const fullSlug = prefix ? `${prefix}/${slug}` : slug;
80
+ const config = configAtLevel[slug];
81
+ const childConfig = config?.children ?? {};
82
+
83
+ const items = buildLevel(groupEntries, childConfig, fullSlug, depth + 1);
84
+ items.sort((a, b) => a.order - b.order);
85
+
86
+ result.push({
87
+ type: "group",
88
+ label: config?.label ?? titleCase(slug),
89
+ order: config?.order ?? 999,
90
+ slug: fullSlug,
91
+ items,
92
+ });
93
+ }
94
+
95
+ result.sort((a, b) => a.order - b.order);
96
+ return result;
97
+ }
package/utils/toc.ts ADDED
@@ -0,0 +1,28 @@
1
+ export interface TocItem {
2
+ depth: number;
3
+ slug: string;
4
+ text: string;
5
+ children: TocItem[];
6
+ }
7
+
8
+ export interface MarkdownHeading {
9
+ depth: number;
10
+ slug: string;
11
+ text: string;
12
+ }
13
+
14
+ export function buildToc(headings: MarkdownHeading[]): TocItem[] {
15
+ const filtered = headings.filter((h) => h.depth >= 2 && h.depth <= 3);
16
+ const result: TocItem[] = [];
17
+
18
+ for (const heading of filtered) {
19
+ const item: TocItem = { ...heading, children: [] };
20
+ if (heading.depth === 2) {
21
+ result.push(item);
22
+ } else if (result.length > 0) {
23
+ result[result.length - 1].children.push(item);
24
+ }
25
+ }
26
+
27
+ return result;
28
+ }
package/virtual.d.ts ADDED
@@ -0,0 +1,9 @@
1
+ declare module "virtual:lore/config" {
2
+ import type { LoreConfig } from "lore/config";
3
+ const config: LoreConfig;
4
+ export default config;
5
+ }
6
+
7
+ declare module "virtual:lore/user-css" {
8
+ // Side-effect imports only
9
+ }
package/vite-plugin.ts ADDED
@@ -0,0 +1,28 @@
1
+ import type { Plugin } from "vite";
2
+ import type { LoreConfig } from "./config";
3
+
4
+ const VIRTUAL_CONFIG = "virtual:lore/config";
5
+ const VIRTUAL_USER_CSS = "virtual:lore/user-css";
6
+ const RESOLVED_CONFIG = "\0" + VIRTUAL_CONFIG;
7
+ const RESOLVED_USER_CSS = "\0" + VIRTUAL_USER_CSS;
8
+
9
+ export function vitePluginLore(config: LoreConfig): Plugin {
10
+ return {
11
+ name: "lore:virtual-modules",
12
+ resolveId(id: string) {
13
+ if (id === VIRTUAL_CONFIG) return RESOLVED_CONFIG;
14
+ if (id === VIRTUAL_USER_CSS) return RESOLVED_USER_CSS;
15
+ },
16
+ load(id: string) {
17
+ if (id === RESOLVED_CONFIG) {
18
+ return `export default ${JSON.stringify(config)};`;
19
+ }
20
+ if (id === RESOLVED_USER_CSS) {
21
+ const imports = config.customCss
22
+ .map((css) => `import "${css}";`)
23
+ .join("\n");
24
+ return imports || "// No custom CSS";
25
+ }
26
+ },
27
+ };
28
+ }