@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.
- package/components/docs/Breadcrumbs.astro +41 -0
- package/components/docs/PrevNext.astro +50 -0
- package/components/docs/Sidebar.astro +28 -0
- package/components/docs/SidebarGroup.astro +55 -0
- package/components/docs/SidebarItem.astro +26 -0
- package/components/docs/TableOfContents.astro +82 -0
- package/components/global/SearchModal.astro +159 -0
- package/components/mdx/Accordion.astro +20 -0
- package/components/mdx/Callout.astro +53 -0
- package/components/mdx/Card.astro +26 -0
- package/components/mdx/CardGrid.astro +16 -0
- package/components/mdx/CodeTabs.astro +129 -0
- package/components/mdx/FileTree.astro +117 -0
- package/components/mdx/Step.astro +18 -0
- package/components/mdx/Steps.astro +6 -0
- package/components/mdx/Tab.astro +11 -0
- package/components/mdx/Tabs.astro +73 -0
- package/components.ts +11 -0
- package/config.ts +42 -0
- package/index.ts +2 -0
- package/integration.ts +68 -0
- package/layouts/DocsLayout.astro +277 -0
- package/loaders.ts +5 -0
- package/package.json +51 -0
- package/routes/docs.astro +201 -0
- package/schema.ts +13 -0
- package/styles/global.css +78 -0
- package/styles/prose.css +148 -0
- package/utils/navigation.ts +32 -0
- package/utils/rehype-file-tree.ts +229 -0
- package/utils/sidebar.ts +97 -0
- package/utils/toc.ts +28 -0
- package/virtual.d.ts +9 -0
- package/vite-plugin.ts +28 -0
|
@@ -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
|
+
}
|
package/utils/sidebar.ts
ADDED
|
@@ -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
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
|
+
}
|