starlight-cannoli-plugins 0.1.0 → 1.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/README.md +59 -9
- package/dist/chunk-NTIYGHZG.js +246 -0
- package/dist/{chunk-WOOF7XZX.js → chunk-TXRBCETT.js} +43 -6
- package/dist/index.js +2 -2
- package/dist/plugins/rehype-validate-links.d.ts +4 -1
- package/dist/plugins/rehype-validate-links.js +1 -1
- package/dist/plugins/starlight-index-only-sidebar.d.ts +6 -7
- package/dist/plugins/starlight-index-only-sidebar.js +1 -1
- package/package.json +26 -20
- package/dist/chunk-6XXMT37V.js +0 -162
package/README.md
CHANGED
|
@@ -6,13 +6,22 @@ A collection of powerful plugins for [Astro Starlight](https://starlight.astro.b
|
|
|
6
6
|
|
|
7
7
|
### Starlight Index-Only Sidebar
|
|
8
8
|
|
|
9
|
-
Automatically generates
|
|
9
|
+
Automatically generates a nested Starlight sidebar by recursively scanning directories for `index.md`/`index.mdx` files. Only directories with index files appear in the sidebar, creating a clean, minimal navigation structure.
|
|
10
10
|
|
|
11
11
|
**Features:**
|
|
12
|
-
-
|
|
12
|
+
- Recursively scans directories for `index.md` or `index.mdx` files
|
|
13
|
+
- Creates sidebar entries only for pages with index files
|
|
13
14
|
- Respects frontmatter: `draft: true` and `sidebar.hidden: true` hide entries
|
|
14
|
-
-
|
|
15
|
-
-
|
|
15
|
+
- Automatically collapses single-child groups (no intermediate wrappers)
|
|
16
|
+
- Configurable depth limiting to flatten deeply nested content
|
|
17
|
+
- Two labeling modes: directory names or frontmatter titles
|
|
18
|
+
- Ignores `assets` directories entirely
|
|
19
|
+
|
|
20
|
+
**Options:**
|
|
21
|
+
|
|
22
|
+
- `directories` (required): Array of directory names to scan (e.g., `["guides", "api"]`)
|
|
23
|
+
- `maxDepthNesting` (optional, default: `100`): Maximum nesting depth. Root is level 0. At max depth, deeper index files are flattened as sibling items.
|
|
24
|
+
- `dirnameDeterminesLabels` (optional, default: `true`): When `true`, all labels use raw directory names. When `false`, slug item labels come from frontmatter `title` field; group labels still use directory names.
|
|
16
25
|
|
|
17
26
|
**Usage:**
|
|
18
27
|
|
|
@@ -28,11 +37,9 @@ export default defineConfig({
|
|
|
28
37
|
title: "My Docs",
|
|
29
38
|
plugins: [
|
|
30
39
|
starlightIndexOnlySidebar({
|
|
31
|
-
directories: [
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
],
|
|
35
|
-
dirnameDeterminesLabel: false, // optional: use directory names as labels
|
|
40
|
+
directories: ["guides", "api", "tutorials"],
|
|
41
|
+
maxDepthNesting: 2, // optional
|
|
42
|
+
dirnameDeterminesLabels: false, // optional
|
|
36
43
|
}),
|
|
37
44
|
],
|
|
38
45
|
}),
|
|
@@ -50,6 +57,7 @@ A rehype plugin that validates all internal links in your Markdown/MDX files at
|
|
|
50
57
|
- Auto-expands extensionless links to match `.md` or `.mdx` files
|
|
51
58
|
- Converts internal links to site-absolute paths
|
|
52
59
|
- Throws build errors for broken links
|
|
60
|
+
- Skip validation for forward-reference links using multiple approaches
|
|
53
61
|
|
|
54
62
|
**Usage:**
|
|
55
63
|
|
|
@@ -73,6 +81,48 @@ Or import directly:
|
|
|
73
81
|
import { rehypeValidateLinks } from "cannoli-starlight-plugins/rehype-validate-links";
|
|
74
82
|
```
|
|
75
83
|
|
|
84
|
+
**Skipping Link Validation:**
|
|
85
|
+
|
|
86
|
+
There are three ways to skip validation for specific links:
|
|
87
|
+
|
|
88
|
+
**1. Question Mark Prefix** (Per-link, in markdown)
|
|
89
|
+
|
|
90
|
+
Prepend a `?` to the link href to skip validation:
|
|
91
|
+
|
|
92
|
+
```mdx
|
|
93
|
+
[Grade Calculator](?csci-320-331-obrenic/grade-calculator)
|
|
94
|
+
[Grade Calculator](?./csci-320-331-obrenic/grade-calculator)
|
|
95
|
+
[Grade Calculator](?/csci-320-331-obrenic/grade-calculator)
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
**2. HTML Data Attribute** (Per-link, requires HTML syntax)
|
|
99
|
+
|
|
100
|
+
Use the `data-no-link-check` attribute on anchor tags:
|
|
101
|
+
|
|
102
|
+
```mdx
|
|
103
|
+
<a href="csci-320-331-obrenic/grade-calculator" data-no-link-check>Grade Calculator</a>
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
**3. Global Skip Patterns** (Configuration-based)
|
|
107
|
+
|
|
108
|
+
Use the `skipPatterns` option to exclude links matching glob patterns:
|
|
109
|
+
|
|
110
|
+
```ts
|
|
111
|
+
// astro.config.mjs
|
|
112
|
+
export default defineConfig({
|
|
113
|
+
markdown: {
|
|
114
|
+
rehypePlugins: [
|
|
115
|
+
[rehypeValidateLinks, {
|
|
116
|
+
skipPatterns: [
|
|
117
|
+
'/csci-320-331-obrenic/grade-calculator', // exact match
|
|
118
|
+
'**/draft-*', // glob pattern
|
|
119
|
+
]
|
|
120
|
+
}],
|
|
121
|
+
],
|
|
122
|
+
},
|
|
123
|
+
});
|
|
124
|
+
```
|
|
125
|
+
|
|
76
126
|
## Installation
|
|
77
127
|
|
|
78
128
|
```bash
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
// src/plugins/starlight-index-only-sidebar.ts
|
|
2
|
+
import { join as join2 } from "path";
|
|
3
|
+
|
|
4
|
+
// src/plugins/utils/sidebar-builder-utils.ts
|
|
5
|
+
import * as fs from "fs";
|
|
6
|
+
import * as path from "path";
|
|
7
|
+
import { parse as parseYaml } from "yaml";
|
|
8
|
+
function isDir(filePath) {
|
|
9
|
+
try {
|
|
10
|
+
return fs.statSync(filePath).isDirectory();
|
|
11
|
+
} catch {
|
|
12
|
+
return false;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
function findIndexMd(dirPath) {
|
|
16
|
+
const indexMd = path.join(dirPath, "index.md");
|
|
17
|
+
const indexMdx = path.join(dirPath, "index.mdx");
|
|
18
|
+
try {
|
|
19
|
+
if (fs.statSync(indexMd).isFile()) return indexMd;
|
|
20
|
+
if (fs.statSync(indexMdx).isFile()) return indexMdx;
|
|
21
|
+
} catch {
|
|
22
|
+
}
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
function parseFrontmatter(filePath) {
|
|
26
|
+
try {
|
|
27
|
+
const content = fs.readFileSync(filePath, "utf-8");
|
|
28
|
+
const match = content.match(/^---\n([\s\S]*?)\n---/);
|
|
29
|
+
if (!match) return {};
|
|
30
|
+
const frontmatter = parseYaml(match[1]);
|
|
31
|
+
return typeof frontmatter === "object" && frontmatter ? frontmatter : {};
|
|
32
|
+
} catch {
|
|
33
|
+
return {};
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
function getSlug(dirPath, rootDir) {
|
|
37
|
+
const normalized = path.normalize(dirPath).replace(/\\/g, "/");
|
|
38
|
+
const rootParent = path.normalize(path.dirname(rootDir)).replace(/\\/g, "/");
|
|
39
|
+
const slug = normalized.startsWith(rootParent) ? normalized.slice(rootParent.length + 1) : normalized;
|
|
40
|
+
return slug + "/index";
|
|
41
|
+
}
|
|
42
|
+
function hasIndexMdInSubtree(dirPath) {
|
|
43
|
+
const indexFile = findIndexMd(dirPath);
|
|
44
|
+
if (indexFile) {
|
|
45
|
+
const fm = parseFrontmatter(indexFile);
|
|
46
|
+
if (fm.draft || fm.sidebar?.hidden) return false;
|
|
47
|
+
return true;
|
|
48
|
+
}
|
|
49
|
+
try {
|
|
50
|
+
const entries = fs.readdirSync(dirPath);
|
|
51
|
+
for (const entry of entries) {
|
|
52
|
+
if (entry === "assets") continue;
|
|
53
|
+
const fullPath = path.join(dirPath, entry);
|
|
54
|
+
if (isDir(fullPath) && hasIndexMdInSubtree(fullPath)) {
|
|
55
|
+
return true;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
} catch {
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
function isGroupItem(item) {
|
|
64
|
+
return "items" in item;
|
|
65
|
+
}
|
|
66
|
+
function pathSegmentToLabel(pathSegment) {
|
|
67
|
+
let label = pathSegment.split("-").map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
|
|
68
|
+
const match = label.match(/^([a-z]+) [0-9]{2,}/i);
|
|
69
|
+
if (match) {
|
|
70
|
+
const firstGroup = match[1];
|
|
71
|
+
const uppercasedFirstGroup = firstGroup.toUpperCase();
|
|
72
|
+
label = uppercasedFirstGroup + label.substring(firstGroup.length);
|
|
73
|
+
}
|
|
74
|
+
return label;
|
|
75
|
+
}
|
|
76
|
+
function buildSidebarItems(dirPath, rootDir, currentDepth, maxDepth, dirnameDeterminesLabels, parentDirName = "") {
|
|
77
|
+
const items = [];
|
|
78
|
+
const indexFile = findIndexMd(dirPath);
|
|
79
|
+
if (indexFile) {
|
|
80
|
+
const fm = parseFrontmatter(indexFile);
|
|
81
|
+
if (!fm.draft && !fm.sidebar?.hidden) {
|
|
82
|
+
let label;
|
|
83
|
+
if (currentDepth >= maxDepth && parentDirName) {
|
|
84
|
+
label = parentDirName;
|
|
85
|
+
} else if (dirnameDeterminesLabels) {
|
|
86
|
+
label = "Overview";
|
|
87
|
+
} else {
|
|
88
|
+
label = fm.title || "Overview";
|
|
89
|
+
}
|
|
90
|
+
items.push({
|
|
91
|
+
label,
|
|
92
|
+
slug: getSlug(dirPath, rootDir)
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
if (currentDepth >= maxDepth) {
|
|
97
|
+
const flattenedItems = flattenIndexFilesAtDepth(
|
|
98
|
+
dirPath,
|
|
99
|
+
rootDir,
|
|
100
|
+
dirnameDeterminesLabels
|
|
101
|
+
);
|
|
102
|
+
items.push(...flattenedItems);
|
|
103
|
+
return items;
|
|
104
|
+
}
|
|
105
|
+
try {
|
|
106
|
+
const entries = fs.readdirSync(dirPath).sort();
|
|
107
|
+
for (const entry of entries) {
|
|
108
|
+
if (entry === "assets") continue;
|
|
109
|
+
const fullPath = path.join(dirPath, entry);
|
|
110
|
+
if (!isDir(fullPath)) continue;
|
|
111
|
+
if (!hasIndexMdInSubtree(fullPath)) continue;
|
|
112
|
+
const subItems = buildSidebarItems(
|
|
113
|
+
fullPath,
|
|
114
|
+
rootDir,
|
|
115
|
+
currentDepth + 1,
|
|
116
|
+
maxDepth,
|
|
117
|
+
dirnameDeterminesLabels,
|
|
118
|
+
entry
|
|
119
|
+
);
|
|
120
|
+
const hasIndex = findIndexMd(fullPath) !== null;
|
|
121
|
+
if (!hasIndex) {
|
|
122
|
+
const allGroups = subItems.length > 0 && subItems.every(isGroupItem);
|
|
123
|
+
if (allGroups || subItems.length === 1 && isGroupItem(subItems[0])) {
|
|
124
|
+
items.push(...subItems);
|
|
125
|
+
} else {
|
|
126
|
+
items.push({
|
|
127
|
+
label: pathSegmentToLabel(entry),
|
|
128
|
+
items: subItems
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
} else {
|
|
132
|
+
items.push({
|
|
133
|
+
label: pathSegmentToLabel(entry),
|
|
134
|
+
items: subItems
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
} catch {
|
|
139
|
+
}
|
|
140
|
+
return items;
|
|
141
|
+
}
|
|
142
|
+
function flattenIndexFilesAtDepth(dirPath, rootDir, dirnameDeterminesLabels) {
|
|
143
|
+
const items = [];
|
|
144
|
+
try {
|
|
145
|
+
const entries = fs.readdirSync(dirPath);
|
|
146
|
+
for (const entry of entries) {
|
|
147
|
+
if (entry === "assets") continue;
|
|
148
|
+
const fullPath = path.join(dirPath, entry);
|
|
149
|
+
if (!isDir(fullPath)) continue;
|
|
150
|
+
const indexFile = findIndexMd(fullPath);
|
|
151
|
+
if (indexFile) {
|
|
152
|
+
const fm = parseFrontmatter(indexFile);
|
|
153
|
+
if (!fm.draft && !fm.sidebar?.hidden) {
|
|
154
|
+
items.push({
|
|
155
|
+
label: pathSegmentToLabel(entry),
|
|
156
|
+
slug: getSlug(fullPath, rootDir)
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
items.push(
|
|
161
|
+
...flattenIndexFilesAtDepth(fullPath, rootDir, dirnameDeterminesLabels)
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
} catch {
|
|
165
|
+
}
|
|
166
|
+
return items;
|
|
167
|
+
}
|
|
168
|
+
function getIndexMdSidebarItems(directory, options = {}) {
|
|
169
|
+
const maxDepth = options.maxDepthNesting ?? 100;
|
|
170
|
+
const dirnameDeterminesLabels = options.dirnameDeterminesLabels ?? true;
|
|
171
|
+
const rootDirName = pathSegmentToLabel(path.basename(directory));
|
|
172
|
+
const items = buildSidebarItems(
|
|
173
|
+
directory,
|
|
174
|
+
directory,
|
|
175
|
+
0,
|
|
176
|
+
maxDepth,
|
|
177
|
+
dirnameDeterminesLabels
|
|
178
|
+
);
|
|
179
|
+
return [
|
|
180
|
+
{
|
|
181
|
+
label: rootDirName,
|
|
182
|
+
items
|
|
183
|
+
}
|
|
184
|
+
];
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// src/plugins/starlight-index-only-sidebar.ts
|
|
188
|
+
var SITE_DOCS_ROOT = "./src/content/docs";
|
|
189
|
+
function normalizeSlug(slug) {
|
|
190
|
+
return slug.endsWith("/index") ? slug.slice(0, -6) : slug;
|
|
191
|
+
}
|
|
192
|
+
function normalizeItems(items) {
|
|
193
|
+
return items.map((item) => {
|
|
194
|
+
if ("items" in item) {
|
|
195
|
+
return {
|
|
196
|
+
label: item.label,
|
|
197
|
+
items: normalizeItems(item.items)
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
if ("slug" in item) {
|
|
201
|
+
return {
|
|
202
|
+
label: item.label,
|
|
203
|
+
slug: normalizeSlug(item.slug)
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
return item;
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
function starlightIndexOnlySidebar(pluginOptions) {
|
|
210
|
+
return {
|
|
211
|
+
name: "index-only-sidebar",
|
|
212
|
+
hooks: {
|
|
213
|
+
"config:setup": (hookOptions) => {
|
|
214
|
+
const { updateConfig } = hookOptions;
|
|
215
|
+
const { directories, maxDepthNesting, dirnameDeterminesLabels } = pluginOptions;
|
|
216
|
+
const functionOptions = {
|
|
217
|
+
maxDepthNesting,
|
|
218
|
+
dirnameDeterminesLabels
|
|
219
|
+
};
|
|
220
|
+
const sidebarItems = directories.map((directory) => {
|
|
221
|
+
const dirPath = join2(SITE_DOCS_ROOT, directory);
|
|
222
|
+
const rawItems = getIndexMdSidebarItems(dirPath, functionOptions);
|
|
223
|
+
const rootGroup = rawItems[0];
|
|
224
|
+
if (!rootGroup || !("items" in rootGroup)) {
|
|
225
|
+
return void 0;
|
|
226
|
+
}
|
|
227
|
+
const items = rootGroup.items || [];
|
|
228
|
+
const normalizedItems = normalizeItems(items);
|
|
229
|
+
return {
|
|
230
|
+
label: rootGroup.label,
|
|
231
|
+
items: normalizedItems
|
|
232
|
+
};
|
|
233
|
+
}).filter(
|
|
234
|
+
(group) => group !== void 0 && group.items.length > 0
|
|
235
|
+
);
|
|
236
|
+
updateConfig({
|
|
237
|
+
sidebar: sidebarItems
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
export {
|
|
245
|
+
starlightIndexOnlySidebar
|
|
246
|
+
};
|
|
@@ -1,19 +1,32 @@
|
|
|
1
1
|
// src/plugins/rehype-validate-links.ts
|
|
2
2
|
import { existsSync } from "fs";
|
|
3
3
|
import { sync as globSync } from "glob";
|
|
4
|
+
import { minimatch } from "minimatch";
|
|
4
5
|
import { dirname, join, relative, resolve } from "path";
|
|
5
6
|
import { visit } from "unist-util-visit";
|
|
6
7
|
var PROJECT_DOCS_DIR = "src/content/docs";
|
|
8
|
+
function matchesSkipPattern(path, patterns) {
|
|
9
|
+
if (!patterns || patterns.length === 0) {
|
|
10
|
+
return false;
|
|
11
|
+
}
|
|
12
|
+
return patterns.some((pattern) => minimatch(path, pattern));
|
|
13
|
+
}
|
|
7
14
|
function getResolvedLink(href, currentFilePath) {
|
|
15
|
+
let skipValidation = false;
|
|
16
|
+
let processedHref = href;
|
|
17
|
+
if (href.startsWith("?")) {
|
|
18
|
+
skipValidation = true;
|
|
19
|
+
processedHref = href.slice(1);
|
|
20
|
+
}
|
|
8
21
|
try {
|
|
9
|
-
new URL(
|
|
22
|
+
new URL(processedHref);
|
|
10
23
|
return null;
|
|
11
24
|
} catch {
|
|
12
25
|
}
|
|
13
|
-
if (!
|
|
26
|
+
if (!processedHref) {
|
|
14
27
|
return null;
|
|
15
28
|
}
|
|
16
|
-
const fragmentMatch =
|
|
29
|
+
const fragmentMatch = processedHref.split("#");
|
|
17
30
|
const withoutFragment = fragmentMatch[0];
|
|
18
31
|
const fragment = fragmentMatch[1] || "";
|
|
19
32
|
if (!withoutFragment) {
|
|
@@ -46,7 +59,8 @@ function getResolvedLink(href, currentFilePath) {
|
|
|
46
59
|
original_href: href,
|
|
47
60
|
project_absolute_href: finalProjectAbsoluteHref,
|
|
48
61
|
site_absolute_href: siteAbsoluteHref,
|
|
49
|
-
fragment
|
|
62
|
+
fragment,
|
|
63
|
+
skipValidation
|
|
50
64
|
};
|
|
51
65
|
}
|
|
52
66
|
function validateLink(link) {
|
|
@@ -73,7 +87,7 @@ function validateLink(link) {
|
|
|
73
87
|
}
|
|
74
88
|
}
|
|
75
89
|
}
|
|
76
|
-
function rehypeValidateLinks() {
|
|
90
|
+
function rehypeValidateLinks(options) {
|
|
77
91
|
return (tree, file) => {
|
|
78
92
|
const filePath = file.path;
|
|
79
93
|
if (!filePath) {
|
|
@@ -82,7 +96,7 @@ function rehypeValidateLinks() {
|
|
|
82
96
|
);
|
|
83
97
|
return;
|
|
84
98
|
}
|
|
85
|
-
visit(tree, "element", (node) => {
|
|
99
|
+
visit(tree, "element", (node, index, parent) => {
|
|
86
100
|
let resourcePath;
|
|
87
101
|
let attributeName = null;
|
|
88
102
|
if (node.tagName === "a") {
|
|
@@ -95,6 +109,29 @@ function rehypeValidateLinks() {
|
|
|
95
109
|
if (!resourcePath || !attributeName) return;
|
|
96
110
|
const link = getResolvedLink(resourcePath, filePath);
|
|
97
111
|
if (!link) return;
|
|
112
|
+
if (link.skipValidation) {
|
|
113
|
+
node.properties = node.properties || {};
|
|
114
|
+
node.properties[attributeName] = link.site_absolute_href;
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
if (node.properties?.["data-no-link-check"] !== void 0) {
|
|
118
|
+
node.properties = node.properties || {};
|
|
119
|
+
node.properties[attributeName] = link.site_absolute_href;
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
if (matchesSkipPattern(link.site_absolute_href, options?.skipPatterns)) {
|
|
123
|
+
node.properties = node.properties || {};
|
|
124
|
+
node.properties[attributeName] = link.site_absolute_href;
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
if (index !== void 0 && parent && "children" in parent && Array.isArray(parent.children)) {
|
|
128
|
+
const nextNode = parent.children[index + 1];
|
|
129
|
+
if (nextNode && "type" in nextNode && nextNode.type === "comment" && "value" in nextNode && typeof nextNode.value === "string" && nextNode.value.includes("no-link-check")) {
|
|
130
|
+
node.properties = node.properties || {};
|
|
131
|
+
node.properties[attributeName] = link.site_absolute_href;
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
98
135
|
validateLink(link);
|
|
99
136
|
node.properties = node.properties || {};
|
|
100
137
|
node.properties[attributeName] = link.site_absolute_href;
|
package/dist/index.js
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
import { Root } from 'hast';
|
|
2
2
|
import { VFile } from 'vfile';
|
|
3
3
|
|
|
4
|
+
type TRehypeValidateLinksOptions = {
|
|
5
|
+
skipPatterns?: string[];
|
|
6
|
+
};
|
|
4
7
|
/**
|
|
5
8
|
* Rehype plugin to validate all internal links and convert them to absolute paths
|
|
6
9
|
*/
|
|
7
|
-
declare function rehypeValidateLinks(): (tree: Root, file: VFile) => void;
|
|
10
|
+
declare function rehypeValidateLinks(options?: TRehypeValidateLinksOptions): (tree: Root, file: VFile) => void;
|
|
8
11
|
|
|
9
12
|
export { rehypeValidateLinks as default, rehypeValidateLinks };
|
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
import { HookParameters } from '@astrojs/starlight/types';
|
|
2
2
|
|
|
3
|
-
type
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
type TOptions = {
|
|
4
|
+
maxDepthNesting?: number;
|
|
5
|
+
dirnameDeterminesLabels?: boolean;
|
|
6
6
|
};
|
|
7
|
-
type TIndexOnlySidebarPluginOptions = {
|
|
8
|
-
directories:
|
|
9
|
-
dirnameDeterminesLabel?: boolean;
|
|
7
|
+
type TIndexOnlySidebarPluginOptions = TOptions & {
|
|
8
|
+
directories: string[];
|
|
10
9
|
};
|
|
11
|
-
declare function starlightIndexOnlySidebar(
|
|
10
|
+
declare function starlightIndexOnlySidebar(pluginOptions: TIndexOnlySidebarPluginOptions): {
|
|
12
11
|
name: string;
|
|
13
12
|
hooks: {
|
|
14
13
|
"config:setup": (hookOptions: HookParameters<"config:setup">) => void;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "starlight-cannoli-plugins",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.1
|
|
4
|
+
"version": "1.0.1",
|
|
5
5
|
"description": "Starlight plugins for automatic sidebar generation and link validation",
|
|
6
6
|
"license": "ISC",
|
|
7
7
|
"main": "./dist/index.js",
|
|
@@ -36,16 +36,20 @@
|
|
|
36
36
|
"url": "https://github.com/OccasionalCoderByTrade/astro-starlight-plugins"
|
|
37
37
|
},
|
|
38
38
|
"scripts": {
|
|
39
|
-
"
|
|
40
|
-
"
|
|
41
|
-
"
|
|
42
|
-
"
|
|
43
|
-
"
|
|
44
|
-
"
|
|
45
|
-
"prepublishOnly": "npm run build
|
|
39
|
+
"app:dev": "node scripts/sync-docs-to-public.js && rm -rf .astro/ && astro dev --port 5600",
|
|
40
|
+
"app:build": "rm -rf .astro/ && node scripts/sync-docs-to-public.js && astro build",
|
|
41
|
+
"app:preview": "astro build && astro preview --port 5600",
|
|
42
|
+
"app:help": "astro --help",
|
|
43
|
+
"app:check": "astro check",
|
|
44
|
+
"lib:build": "tsup",
|
|
45
|
+
"prepublishOnly": "npm run lib:build",
|
|
46
|
+
"tree:index": "tree -P index.md src/content/docs",
|
|
47
|
+
"tree:depth": "bash scripts/tree-depth.sh",
|
|
48
|
+
"clean:empty-dirs": "find src/content/docs -type d -empty -delete"
|
|
46
49
|
},
|
|
47
50
|
"dependencies": {
|
|
48
51
|
"glob": "^13.0.6",
|
|
52
|
+
"minimatch": "^10.2.4",
|
|
49
53
|
"unist-util-visit": "^5.0.0",
|
|
50
54
|
"yaml": "^2.4.0"
|
|
51
55
|
},
|
|
@@ -64,29 +68,31 @@
|
|
|
64
68
|
"devDependencies": {
|
|
65
69
|
"@astrojs/check": "^0.9.6",
|
|
66
70
|
"@astrojs/starlight": "^0.37.6",
|
|
71
|
+
"@eslint/js": "^10.0.1",
|
|
72
|
+
"@eslint/markdown": "^7.5.1",
|
|
67
73
|
"@expressive-code/plugin-line-numbers": "^0.41.7",
|
|
68
74
|
"@hpcc-js/wasm": "^2.33.1",
|
|
75
|
+
"@trivago/prettier-plugin-sort-imports": "^6.0.2",
|
|
69
76
|
"@types/hast": "^2.3.10",
|
|
77
|
+
"@typescript-eslint/eslint-plugin": "^8.56.1",
|
|
78
|
+
"@typescript-eslint/parser": "^8.56.1",
|
|
70
79
|
"astro": "^5.6.1",
|
|
80
|
+
"eslint": "^10.0.2",
|
|
81
|
+
"eslint-md-cannoli-plugins": "^1.0.2",
|
|
82
|
+
"globals": "^17.4.0",
|
|
71
83
|
"hast": "^1.0.0",
|
|
84
|
+
"jiti": "^2.6.1",
|
|
85
|
+
"lodash": "^4.17.23",
|
|
72
86
|
"rehype-graphviz": "^0.3.0",
|
|
73
87
|
"rehype-mathjax": "^7.1.0",
|
|
74
88
|
"rehype-raw": "^7.0.0",
|
|
75
89
|
"remark-math": "^6.0.0",
|
|
90
|
+
"sass-embedded": "^1.97.3",
|
|
76
91
|
"sharp": "^0.34.2",
|
|
77
92
|
"tsup": "^8.3.0",
|
|
93
|
+
"tsx": "^4.21.0",
|
|
78
94
|
"typescript": "^5.9.3",
|
|
79
|
-
"
|
|
80
|
-
"
|
|
81
|
-
"@eslint/markdown": "^7.5.1",
|
|
82
|
-
"@trivago/prettier-plugin-sort-imports": "^6.0.2",
|
|
83
|
-
"@typescript-eslint/eslint-plugin": "^8.56.1",
|
|
84
|
-
"@typescript-eslint/parser": "^8.56.1",
|
|
85
|
-
"eslint": "^10.0.2",
|
|
86
|
-
"eslint-md-cannoli-plugins": "^1.0.2",
|
|
87
|
-
"globals": "^17.4.0",
|
|
88
|
-
"jiti": "^2.6.1",
|
|
89
|
-
"sass-embedded": "^1.97.3",
|
|
90
|
-
"typescript-eslint": "^8.56.1"
|
|
95
|
+
"typescript-eslint": "^8.56.1",
|
|
96
|
+
"vfile": "^6.0.0"
|
|
91
97
|
}
|
|
92
98
|
}
|
package/dist/chunk-6XXMT37V.js
DELETED
|
@@ -1,162 +0,0 @@
|
|
|
1
|
-
// src/plugins/starlight-index-only-sidebar.ts
|
|
2
|
-
import { readFileSync, readdirSync, statSync } from "fs";
|
|
3
|
-
import { join } from "path";
|
|
4
|
-
import { parse as parseYaml } from "yaml";
|
|
5
|
-
var SITE_DOCS_ROOT = "./src/content/docs";
|
|
6
|
-
function getFrontmatterMetadata(mdFilePath) {
|
|
7
|
-
const defaultMetadata = {
|
|
8
|
-
title: null,
|
|
9
|
-
draft: false,
|
|
10
|
-
sidebarHidden: false
|
|
11
|
-
};
|
|
12
|
-
try {
|
|
13
|
-
const content = readFileSync(mdFilePath, "utf-8");
|
|
14
|
-
const match = content.match(/^---\n([\s\S]*?)\n---/);
|
|
15
|
-
if (!match) return defaultMetadata;
|
|
16
|
-
const frontmatterText = match[1];
|
|
17
|
-
const frontmatter = parseYaml(frontmatterText);
|
|
18
|
-
if (!frontmatter || typeof frontmatter !== "object") {
|
|
19
|
-
return defaultMetadata;
|
|
20
|
-
}
|
|
21
|
-
const title = typeof frontmatter.title === "string" ? frontmatter.title : null;
|
|
22
|
-
const draft = frontmatter.draft === true;
|
|
23
|
-
let sidebarHidden = false;
|
|
24
|
-
if (frontmatter.sidebar) {
|
|
25
|
-
if (typeof frontmatter.sidebar === "object" && frontmatter.sidebar !== null) {
|
|
26
|
-
sidebarHidden = frontmatter.sidebar.hidden === true;
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
return {
|
|
30
|
-
title,
|
|
31
|
-
draft,
|
|
32
|
-
sidebarHidden
|
|
33
|
-
};
|
|
34
|
-
} catch {
|
|
35
|
-
return defaultMetadata;
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
function pathSegmentToLabel(pathSegment) {
|
|
39
|
-
let label = pathSegment.split("-").map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
|
|
40
|
-
const match = label.match(/^([a-z]+) [0-9]{2,}/i);
|
|
41
|
-
if (match) {
|
|
42
|
-
const firstGroup = match[1];
|
|
43
|
-
const uppercasedFirstGroup = firstGroup.toUpperCase();
|
|
44
|
-
label = uppercasedFirstGroup + label.substring(firstGroup.length);
|
|
45
|
-
}
|
|
46
|
-
return label;
|
|
47
|
-
}
|
|
48
|
-
function processRootIndex(dir, basePath, dirDeterminesLabel, items) {
|
|
49
|
-
if (!basePath || basePath.includes("/")) {
|
|
50
|
-
return;
|
|
51
|
-
}
|
|
52
|
-
const indexPath = join(dir, "index.md");
|
|
53
|
-
try {
|
|
54
|
-
statSync(indexPath);
|
|
55
|
-
} catch {
|
|
56
|
-
return;
|
|
57
|
-
}
|
|
58
|
-
const metadata = getFrontmatterMetadata(indexPath);
|
|
59
|
-
if (metadata.draft || metadata.sidebarHidden) {
|
|
60
|
-
return;
|
|
61
|
-
}
|
|
62
|
-
if (dirDeterminesLabel) {
|
|
63
|
-
const lastSegment = basePath.split("/").pop() || basePath;
|
|
64
|
-
items.push({
|
|
65
|
-
label: pathSegmentToLabel(lastSegment),
|
|
66
|
-
slug: basePath
|
|
67
|
-
});
|
|
68
|
-
} else {
|
|
69
|
-
items.push(basePath);
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
function buildIndexSidebar(dir, basePath = "", docsRoot, dirDeterminesLabel = false) {
|
|
73
|
-
const items = [];
|
|
74
|
-
try {
|
|
75
|
-
processRootIndex(dir, basePath, dirDeterminesLabel, items);
|
|
76
|
-
const entries = readdirSync(dir).sort();
|
|
77
|
-
for (const entry of entries) {
|
|
78
|
-
if (entry.startsWith(".")) continue;
|
|
79
|
-
const fullPath = join(dir, entry);
|
|
80
|
-
const stat = statSync(fullPath);
|
|
81
|
-
if (stat.isDirectory()) {
|
|
82
|
-
const indexPath = join(fullPath, "index.md");
|
|
83
|
-
const slug = basePath ? `${basePath}/${entry}` : entry;
|
|
84
|
-
try {
|
|
85
|
-
statSync(indexPath);
|
|
86
|
-
const metadata = getFrontmatterMetadata(indexPath);
|
|
87
|
-
if (metadata.draft || metadata.sidebarHidden) {
|
|
88
|
-
continue;
|
|
89
|
-
}
|
|
90
|
-
const subItems = buildIndexSidebar(fullPath, slug, docsRoot, dirDeterminesLabel);
|
|
91
|
-
if (subItems.length > 0) {
|
|
92
|
-
let groupLabel;
|
|
93
|
-
if (dirDeterminesLabel) {
|
|
94
|
-
groupLabel = pathSegmentToLabel(entry);
|
|
95
|
-
} else {
|
|
96
|
-
const title = metadata.title || entry;
|
|
97
|
-
groupLabel = title;
|
|
98
|
-
}
|
|
99
|
-
items.push({
|
|
100
|
-
label: groupLabel,
|
|
101
|
-
items: [dirDeterminesLabel ? { label: groupLabel, slug } : slug, ...subItems]
|
|
102
|
-
});
|
|
103
|
-
} else {
|
|
104
|
-
if (dirDeterminesLabel) {
|
|
105
|
-
items.push({
|
|
106
|
-
label: pathSegmentToLabel(entry),
|
|
107
|
-
slug
|
|
108
|
-
});
|
|
109
|
-
} else {
|
|
110
|
-
items.push(slug);
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
} catch {
|
|
114
|
-
const subItems = buildIndexSidebar(fullPath, slug, docsRoot, dirDeterminesLabel);
|
|
115
|
-
if (subItems.length > 0) {
|
|
116
|
-
items.push(...subItems);
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
} catch (error) {
|
|
122
|
-
console.error(`Error reading directory ${dir}:`, error);
|
|
123
|
-
}
|
|
124
|
-
return items.sort((a, b) => {
|
|
125
|
-
const aIsGroup = typeof a === "object" && "items" in a;
|
|
126
|
-
const bIsGroup = typeof b === "object" && "items" in b;
|
|
127
|
-
if (!aIsGroup && bIsGroup) return -1;
|
|
128
|
-
if (aIsGroup && !bIsGroup) return 1;
|
|
129
|
-
return 0;
|
|
130
|
-
});
|
|
131
|
-
}
|
|
132
|
-
function starlightIndexOnlySidebar(options) {
|
|
133
|
-
return {
|
|
134
|
-
name: "index-only-sidebar",
|
|
135
|
-
hooks: {
|
|
136
|
-
"config:setup": (hookOptions) => {
|
|
137
|
-
const { updateConfig } = hookOptions;
|
|
138
|
-
const dirnameDeterminesLabel = options.dirnameDeterminesLabel ?? false;
|
|
139
|
-
const sidebarItems = options.directories.map((dirConfig) => {
|
|
140
|
-
const dirItems = buildIndexSidebar(
|
|
141
|
-
join(SITE_DOCS_ROOT, dirConfig.directory),
|
|
142
|
-
dirConfig.directory,
|
|
143
|
-
SITE_DOCS_ROOT,
|
|
144
|
-
dirnameDeterminesLabel
|
|
145
|
-
);
|
|
146
|
-
const label = dirnameDeterminesLabel ? pathSegmentToLabel(dirConfig.directory) : dirConfig.label;
|
|
147
|
-
return {
|
|
148
|
-
label,
|
|
149
|
-
items: dirItems
|
|
150
|
-
};
|
|
151
|
-
}).filter((group) => group.items.length > 0);
|
|
152
|
-
updateConfig({
|
|
153
|
-
sidebar: sidebarItems
|
|
154
|
-
});
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
};
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
export {
|
|
161
|
-
starlightIndexOnlySidebar
|
|
162
|
-
};
|