starlight-cannoli-plugins 1.0.0 → 1.0.2

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 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 your Starlight sidebar by scanning for `index.md` files in specified directories. Only directories containing an `index.md` file will appear in the sidebar.
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
- - Scans directories recursively for `index.md` files
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
- - Option to use directory names as sidebar labels with automatic formatting (e.g., `csci-316` → `CSCI 316`)
15
- - No manual sidebar configuration needed
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
- { label: "Guides", directory: "guides" },
33
- { label: "API Docs", directory: "api" },
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
@@ -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(href);
22
+ new URL(processedHref);
10
23
  return null;
11
24
  } catch {
12
25
  }
13
- if (!href) {
26
+ if (!processedHref) {
14
27
  return null;
15
28
  }
16
- const fragmentMatch = href.split("#");
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
@@ -3,7 +3,7 @@ import {
3
3
  } from "./chunk-NTIYGHZG.js";
4
4
  import {
5
5
  rehypeValidateLinks
6
- } from "./chunk-WOOF7XZX.js";
6
+ } from "./chunk-TXRBCETT.js";
7
7
  export {
8
8
  rehypeValidateLinks,
9
9
  starlightIndexOnlySidebar
@@ -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,7 +1,7 @@
1
1
  import {
2
2
  rehypeValidateLinks,
3
3
  rehype_validate_links_default
4
- } from "../chunk-WOOF7XZX.js";
4
+ } from "../chunk-TXRBCETT.js";
5
5
  export {
6
6
  rehype_validate_links_default as default,
7
7
  rehypeValidateLinks
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "starlight-cannoli-plugins",
3
3
  "type": "module",
4
- "version": "1.0.0",
4
+ "version": "1.0.2",
5
5
  "description": "Starlight plugins for automatic sidebar generation and link validation",
6
6
  "license": "ISC",
7
7
  "main": "./dist/index.js",
@@ -18,10 +18,13 @@
18
18
  "./rehype-validate-links": {
19
19
  "import": "./dist/plugins/rehype-validate-links.js",
20
20
  "types": "./dist/plugins/rehype-validate-links.d.ts"
21
- }
21
+ },
22
+ "./styles": "./src/styles/",
23
+ "./styles/*": "./src/styles/*"
22
24
  },
23
25
  "files": [
24
- "dist"
26
+ "dist",
27
+ "src/styles"
25
28
  ],
26
29
  "keywords": [
27
30
  "astro",
@@ -49,6 +52,7 @@
49
52
  },
50
53
  "dependencies": {
51
54
  "glob": "^13.0.6",
55
+ "minimatch": "^10.2.4",
52
56
  "unist-util-visit": "^5.0.0",
53
57
  "yaml": "^2.4.0"
54
58
  },
@@ -0,0 +1,393 @@
1
+ $info-color: #3b82f6;
2
+ $warning-color: #f59e0b;
3
+
4
+ html {
5
+ scroll-behavior: smooth;
6
+ }
7
+
8
+ // invert img.note-svg when on dark mode
9
+ img.note-svg {
10
+ color-scheme: light dark;
11
+ padding-top: 1em;
12
+ padding-bottom: 1em;
13
+
14
+ html[data-theme="light"] & {
15
+ filter: none;
16
+ }
17
+
18
+ html:not([data-theme="light"]) & {
19
+ filter: invert(1) hue-rotate(180deg);
20
+ }
21
+ }
22
+
23
+ /************ Starlight Additions/Overrides ************/
24
+
25
+ .sl-container:where(.astro-7nkwcw3z) {
26
+ max-width: 50rem;
27
+ }
28
+
29
+ .main-pane {
30
+ table > thead > tr > th {
31
+ border-bottom: 1px solid hsl(228, 6.2%, 47.3%);
32
+ }
33
+ }
34
+
35
+ starlight-toc {
36
+ a {
37
+ transition: transform 0.2s ease;
38
+ transform-origin: left;
39
+
40
+ span {
41
+ position: relative;
42
+ display: inline-block;
43
+ transition: color 0.2s ease;
44
+
45
+ &::before {
46
+ content: "";
47
+ position: absolute;
48
+ left: -8px;
49
+ top: 0;
50
+ bottom: 0;
51
+ width: 3px;
52
+ background-color: transparent;
53
+ border-radius: 2px;
54
+ transition: background-color 0.2s ease;
55
+ }
56
+ }
57
+ }
58
+
59
+ a[aria-current="true"] {
60
+ transform: scale(1.04);
61
+
62
+ span {
63
+ &::before {
64
+ background-color: var(--sl-color-text-accent);
65
+ }
66
+ }
67
+ }
68
+ }
69
+
70
+ #starlight__search mark {
71
+ color: hsl(339.8, 63.4%, 60.4%);
72
+ }
73
+
74
+ #starlight__mobile-toc {
75
+ > .dropdown {
76
+ border-bottom: 1px solid hsla(0, 0%, 100%, 0.6);
77
+ }
78
+ }
79
+
80
+ .expressive-code .copy > button {
81
+ height: 2rem;
82
+ width: 2rem;
83
+ }
84
+
85
+ /************ Details/Summary Styles ************/
86
+ .main-pane details {
87
+ background: var(--sl-color-bg-nav);
88
+ border-radius: 4px;
89
+ padding: 0.3rem 0.6rem;
90
+ display: block;
91
+
92
+ &[open] {
93
+ max-width: 100%;
94
+
95
+ > div.details-wrapper {
96
+ overflow: auto;
97
+ max-height: 67vh;
98
+ padding-right: 0.8rem;
99
+ padding-left: 0.8rem;
100
+ }
101
+
102
+ p > img {
103
+ height: auto;
104
+ width: auto;
105
+ }
106
+ }
107
+ }
108
+
109
+ /********** MathJax/LaTeX Styling **********/
110
+ mjx-container[jax="SVG"] {
111
+ overflow-x: auto;
112
+
113
+ &[display="true"] {
114
+ margin-top: 0.7em !important;
115
+ margin-bottom: 0.7em !important;
116
+ padding-top: 0.3em;
117
+ padding-bottom: 0.3em;
118
+ }
119
+
120
+ > svg {
121
+ display: unset;
122
+ max-width: unset;
123
+ height: unset;
124
+ }
125
+ }
126
+
127
+ /********** TikZ SVG Styling **********/
128
+ script[type="text/tikz"] {
129
+ display: block;
130
+ }
131
+
132
+ div > div[class="page"] > svg {
133
+ color-scheme: light dark;
134
+
135
+ html[data-theme="light"] & {
136
+ stroke: currentColor;
137
+ fill: currentColor;
138
+ }
139
+
140
+ html:not([data-theme="light"]) & {
141
+ stroke: white;
142
+ fill: white;
143
+ filter: invert(1) hue-rotate(180deg);
144
+ }
145
+ }
146
+
147
+ /********** Mimic Bootstrap Class Utilities **********/
148
+ .visually-hidden {
149
+ display: none !important;
150
+ }
151
+
152
+ .visibility-hidden {
153
+ visibility: hidden !important;
154
+ }
155
+
156
+ .visible {
157
+ display: block !important;
158
+ }
159
+
160
+ .text-center {
161
+ text-align: center !important;
162
+ }
163
+
164
+ .d-block {
165
+ display: block !important;
166
+ }
167
+
168
+ .d-flex {
169
+ display: flex !important;
170
+ }
171
+
172
+ .flex-row {
173
+ flex-direction: row !important;
174
+ }
175
+
176
+ .flex-column {
177
+ flex-direction: column !important;
178
+ }
179
+
180
+ .justify-content-center {
181
+ justify-content: center !important;
182
+ }
183
+
184
+ .justify-content-between {
185
+ justify-content: space-between !important;
186
+ }
187
+
188
+ .justify-content-around {
189
+ justify-content: space-around !important;
190
+ }
191
+
192
+ .justify-content-evenly {
193
+ justify-content: space-evenly !important;
194
+ }
195
+
196
+ .align-items-center {
197
+ align-items: center !important;
198
+ }
199
+
200
+ .align-items-start {
201
+ align-items: flex-start !important;
202
+ }
203
+
204
+ .flex-wrap {
205
+ flex-wrap: wrap !important;
206
+
207
+ .flex-fill {
208
+ flex: 1 1 auto;
209
+ margin-top: unset;
210
+ }
211
+
212
+ .flex-fill.flex-code {
213
+ min-width: 0;
214
+ }
215
+ }
216
+
217
+ .flex-grow-1 {
218
+ flex-grow: 1 !important;
219
+ }
220
+
221
+ /* Overflow utilities */
222
+ .overflow-auto {
223
+ overflow: auto !important;
224
+ }
225
+
226
+ .overflow-y-auto {
227
+ overflow-y: auto !important;
228
+ }
229
+
230
+ .overflow-x-hidden {
231
+ overflow-x: hidden !important;
232
+ }
233
+
234
+ /* Padding and Margin Utilities */
235
+ /* Exposes utility classes like .p-0, .p-1, .pt-2, .mb-3, ..., .m-6, .gap-6 */
236
+
237
+ $spacings: (
238
+ p: padding,
239
+ pt: padding-top,
240
+ pb: padding-bottom,
241
+ ps: padding-left,
242
+ pe: padding-right,
243
+ m: margin,
244
+ mt: margin-top,
245
+ mb: margin-bottom,
246
+ ms: margin-left,
247
+ me: margin-right,
248
+ gap: gap,
249
+ gap-row: row-gap,
250
+ gap-col: column-gap,
251
+ );
252
+
253
+ $axis-spacings: (
254
+ px: (
255
+ padding-left,
256
+ padding-right,
257
+ ),
258
+ py: (
259
+ padding-top,
260
+ padding-bottom,
261
+ ),
262
+ mx: (
263
+ margin-left,
264
+ margin-right,
265
+ ),
266
+ my: (
267
+ margin-top,
268
+ margin-bottom,
269
+ ),
270
+ );
271
+
272
+ @each $class, $property in $spacings {
273
+ .#{$class}-0 {
274
+ #{$property}: 0 !important;
275
+ }
276
+
277
+ $scalar: 0.125;
278
+ $accumulated: 0;
279
+
280
+ @for $i from 1 through 6 {
281
+ $scalar: $scalar + ($i % 2) * $scalar;
282
+ $accumulated: $accumulated + $scalar;
283
+
284
+ .#{$class}-#{$i} {
285
+ #{$property}: #{$accumulated}rem !important;
286
+ }
287
+ }
288
+ }
289
+
290
+ /* Axis-specific spacing utilities (-x and -y) */
291
+ @each $class, $properties in $axis-spacings {
292
+ .#{$class}-0 {
293
+ @each $property in $properties {
294
+ #{$property}: 0 !important;
295
+ }
296
+ }
297
+
298
+ $scalar: 0.125;
299
+ $accumulated: 0;
300
+
301
+ @for $i from 1 through 6 {
302
+ $scalar: $scalar + ($i % 2) * $scalar;
303
+ $accumulated: $accumulated + $scalar;
304
+
305
+ .#{$class}-#{$i} {
306
+ @each $property in $properties {
307
+ #{$property}: #{$accumulated}rem !important;
308
+ }
309
+ }
310
+ }
311
+ }
312
+
313
+ /************ Custom Styles ************/
314
+
315
+ .highlight-green,
316
+ .highlight-red {
317
+ padding: 5px;
318
+ }
319
+
320
+ .highlight-green {
321
+ background: #7de37d34;
322
+ }
323
+
324
+ .highlight-red {
325
+ background: #f7535b46;
326
+ }
327
+
328
+ .admin-only {
329
+ display: none;
330
+ }
331
+
332
+ .note-indicator {
333
+ display: inline-block;
334
+ width: 8px;
335
+ height: 8px;
336
+ border-radius: 50%;
337
+ background-color: $info-color;
338
+ margin-right: 6px;
339
+ vertical-align: middle;
340
+ opacity: 0.6;
341
+ }
342
+
343
+ div.note {
344
+ display: none;
345
+ border: 3px solid $warning-color;
346
+ border-left: 6px solid $warning-color;
347
+ padding: 12px;
348
+ border-radius: 4px;
349
+ margin: 10px 0;
350
+
351
+ &::before {
352
+ content: "Personal Note";
353
+ display: block;
354
+ font-weight: bold;
355
+ color: $info-color;
356
+ margin-bottom: 8px;
357
+ font-size: 0.95em;
358
+ }
359
+ }
360
+
361
+ /********** Toggle Checkbox Styles **********/
362
+ #toggle-all-details-btn {
363
+ display: flex;
364
+ align-items: center;
365
+ gap: 0.5rem;
366
+ cursor: pointer;
367
+
368
+ input[type="checkbox"] {
369
+ position: relative;
370
+ appearance: none;
371
+ width: 1.2em;
372
+ height: 1.2em;
373
+ border: 2px solid currentColor;
374
+ border-radius: 2px;
375
+ cursor: pointer;
376
+ display: flex;
377
+ align-items: center;
378
+ justify-content: center;
379
+ transition: background-color 0.2s ease;
380
+
381
+ &:checked {
382
+ background-color: var(--sl-color-accent);
383
+ border-color: var(--sl-color-accent);
384
+
385
+ &::after {
386
+ content: "✓";
387
+ color: white;
388
+ font-size: 0.8em;
389
+ font-weight: bold;
390
+ }
391
+ }
392
+ }
393
+ }