From d23d719e39171931bf06607a4332a62bbfd0998e Mon Sep 17 00:00:00 2001 From: httphypixelnet Date: Mon, 20 Jul 2026 01:24:02 -0700 Subject: [PATCH 01/14] Add type checking to JavaScript files and fix type errors in remark plugins --- .remarkrc.mjs | 4 +- src/config/sidebarConfig.ts | 6 ++ src/plugins/remark-glossary.ts | 20 ++--- src/plugins/remark-no-inline-code-fences.mjs | 6 ++ src/starlightOverrides/Header.astro | 89 ++++++++++++-------- tsconfig.json | 6 +- 6 files changed, 76 insertions(+), 55 deletions(-) diff --git a/.remarkrc.mjs b/.remarkrc.mjs index 175ab2ff..ccdabeb2 100644 --- a/.remarkrc.mjs +++ b/.remarkrc.mjs @@ -1,6 +1,8 @@ -import remarkPresetLintRecommended from 'remark-preset-lint-recommended'; +// @ts-check + import remarkFrontmatter from 'remark-frontmatter'; import remarkMdx from 'remark-mdx'; +import remarkPresetLintRecommended from 'remark-preset-lint-recommended'; import remarkNoInlineCodeFences from './src/plugins/remark-no-inline-code-fences.mjs'; export default { diff --git a/src/config/sidebarConfig.ts b/src/config/sidebarConfig.ts index 3e31facc..2088e684 100644 --- a/src/config/sidebarConfig.ts +++ b/src/config/sidebarConfig.ts @@ -13,6 +13,12 @@ export type SidebarSection = { items: SidebarItem[]; }; +export type Item = { + label: string; + href?: string; + items?: Item[]; +}; + // Define which URL paths belong to which sidebar section export const sidebarSections: Record = { // Home page - minimal sidebar or none diff --git a/src/plugins/remark-glossary.ts b/src/plugins/remark-glossary.ts index 804732dc..3889c011 100644 --- a/src/plugins/remark-glossary.ts +++ b/src/plugins/remark-glossary.ts @@ -1,5 +1,6 @@ +/// import { visit } from 'unist-util-visit'; -import type { Root, Text } from 'mdast'; +import type { Root, RootContent, Text } from 'mdast'; import type { VFile } from 'vfile'; import { glossaryTerms } from '../data/glossary'; @@ -28,17 +29,7 @@ export function remarkGlossary() { visit(tree, 'text', (node: Text, index, parent) => { if (!parent || index === undefined) return; - if ( - parent.type === 'link' || - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (parent as any).type === 'code' || - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (parent as any).type === 'inlineCode' || - (parent as unknown as { tagName?: string }).tagName === - 'abbr' || - (parent as unknown as { tagName?: string }).tagName === 'a' || - (parent as unknown as { tagName?: string }).tagName === 'code' - ) { + if (parent.type === 'link') { return; } @@ -47,12 +38,11 @@ export function remarkGlossary() { if (matches.length === 0) return; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const newNodes: any[] = []; + const newNodes: RootContent[] = []; let lastIndex = 0; matches.forEach((match) => { - const matchStart = match.index!; + const matchStart = match.index; const matchEnd = matchStart + match[0].length; const matchedTerm = match[0]; const definition = termMap.get(matchedTerm.toLowerCase()); diff --git a/src/plugins/remark-no-inline-code-fences.mjs b/src/plugins/remark-no-inline-code-fences.mjs index d123c981..f4eacb81 100644 --- a/src/plugins/remark-no-inline-code-fences.mjs +++ b/src/plugins/remark-no-inline-code-fences.mjs @@ -1,8 +1,14 @@ +// @ts-check +/// import { visit } from 'unist-util-visit'; const IGNORE_RE = /rli:\s*ignore/; export default function remarkNoInlineCodeFences() { + /** + * @param {import('mdast').Root} tree + * @param {import('vfile').VFile} file + */ return (tree, file) => { visit(tree, 'code', (node, index, parent) => { if (index !== undefined && parent) { diff --git a/src/starlightOverrides/Header.astro b/src/starlightOverrides/Header.astro index d3861413..78df9a86 100644 --- a/src/starlightOverrides/Header.astro +++ b/src/starlightOverrides/Header.astro @@ -195,9 +195,13 @@ for (const section of sidebarSections) { const items = JSON.parse(container.dataset.items || '[]'); const currentPath = container.dataset.currentPath || ''; - function renderItems(items, depth = 0) { + function renderItems( + /** @type {import('../config/sidebarConfig').Item[]} */ items, + depth = 0, + ) { let html = ''; for (const item of items) { + console.log(item); const indent = depth * 12; const isCurrentPage = item.href && @@ -246,9 +250,12 @@ for (const section of sidebarSections) { // Global image lightbox functionality const initImageLightbox = () => { // Create lightbox element if it doesn't exist - let lightbox = document.getElementById('global-image-lightbox'); - if (!lightbox) { - lightbox = document.createElement('div'); + const tmp = document.getElementById('global-image-lightbox'); + const lightbox = + tmp instanceof HTMLElement + ? tmp + : document.createElement('div'); + if (!tmp) { lightbox.id = 'global-image-lightbox'; lightbox.className = 'global-image-lightbox'; lightbox.innerHTML = ` @@ -259,38 +266,43 @@ for (const section of sidebarSections) { `; - document.body.appendChild(lightbox); - - // Close on backdrop click - lightbox.addEventListener('click', (e) => { - if ( - e.target === lightbox || - e.target.classList.contains('global-lightbox-content') - ) { - closeLightbox(); - } - }); + } + document.body.appendChild(lightbox); - // Close button - lightbox - .querySelector('.global-lightbox-close') - .addEventListener('click', closeLightbox); - - // Close on Escape key - document.addEventListener('keydown', (e) => { - if ( - e.key === 'Escape' && - lightbox.classList.contains('open') - ) { - closeLightbox(); - } - }); + // Close on backdrop click + lightbox.addEventListener('click', (e) => { + const target = e.target; + if ( + target instanceof HTMLElement && + (target === lightbox || + target.classList.contains('global-lightbox-content')) + ) { + closeLightbox(); + } + }); + const closeSelector = lightbox.querySelector( + '.global-lightbox-close', + ); + if (closeSelector) { + closeSelector.addEventListener('click', closeLightbox); } + // Close on Escape key + document.addEventListener('keydown', (e) => { + if (e.key === 'Escape' && lightbox.classList.contains('open')) { + closeLightbox(); + } + }); + /** + * @param {string} imgSrc + * @param {string} imgAlt + */ function openLightbox(imgSrc, imgAlt) { + /** @type {HTMLImageElement | null} */ const lightboxImg = lightbox.querySelector( '.global-lightbox-image', ); + if (!lightboxImg) return; lightboxImg.src = imgSrc; lightboxImg.alt = imgAlt || ''; lightbox.classList.add('open'); @@ -309,7 +321,9 @@ for (const section of sidebarSections) { const contentImages = document.querySelectorAll( '.sl-markdown-content img:not(.slides-component img):not([data-no-lightbox])', ); - contentImages.forEach((img) => { + contentImages.forEach((v) => { + if (!(v instanceof HTMLImageElement)) return; + const img = /** @type {HTMLImageElement} */ (v); // Skip if already has lightbox handler if (img.dataset.lightboxAttached) return; img.dataset.lightboxAttached = 'true'; @@ -346,8 +360,7 @@ for (const section of sidebarSections) { return null; } }; - - const setStoredTheme = (value) => { + const setStoredTheme = (/** @type {'light' | 'dark'} */ value) => { try { localStorage.setItem(storageKey, value); } catch { @@ -355,14 +368,15 @@ for (const section of sidebarSections) { } }; - const applyTheme = (theme) => { + const applyTheme = (/** @type {'light' | string} */ theme) => { const html = document.documentElement; const normalized = theme === 'light' ? 'light' : 'dark'; html.dataset.theme = normalized; setStoredTheme(normalized); - + // @ts-expect-error javascript... if (window.StarlightThemeProvider?.updatePickers) { + // @ts-expect-error javascript... window.StarlightThemeProvider.updatePickers(normalized); } }; @@ -395,9 +409,12 @@ for (const section of sidebarSections) { }); document.addEventListener('click', (e) => { + const target = e.target; if ( - !toggle.contains(e.target) && - !menu.contains(e.target) && + target instanceof Node && + target !== toggle && + !toggle.contains(target) && + !menu.contains(target) && toggle.getAttribute('aria-expanded') === 'true' ) { toggle.setAttribute('aria-expanded', 'false'); diff --git a/tsconfig.json b/tsconfig.json index 82c8c25c..6fd0f931 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -3,9 +3,9 @@ "include": [".astro/types.d.ts", "**/*"], "exclude": ["dist"], "compilerOptions": { - "baseUrl": ".", "paths": { - "@components/*": ["src/components/*"] - } + "@components/*": ["./src/components/*"] + }, + "checkJs": true } } From 9312eefbc95be79583177782f09874b4a7f7392c Mon Sep 17 00:00:00 2001 From: httphypixelnet Date: Mon, 20 Jul 2026 16:07:29 -0700 Subject: [PATCH 02/14] actual fix all type issues, set tsconfig to strictest --- scripts/validate-regions.ts | 23 ++-- src/components/Slides.astro | 2 +- src/config/sidebarConfig.ts | 17 +-- src/plugins/remark-center.ts | 31 +---- src/plugins/remark-code-region.ts | 4 +- src/plugins/remark-figure.ts | 142 ++++++++----------- src/plugins/remark-image-attributes.ts | 168 ++++++++++------------- src/plugins/remark-mdx-global-imports.ts | 2 +- tsconfig.json | 2 +- 9 files changed, 157 insertions(+), 234 deletions(-) diff --git a/scripts/validate-regions.ts b/scripts/validate-regions.ts index 560e21c8..22a929d7 100644 --- a/scripts/validate-regions.ts +++ b/scripts/validate-regions.ts @@ -61,6 +61,7 @@ function walkMdx(dir: string) { for (const line of content.split('\n')) { const m = line.match(CODEBLOCK_RE); + if (!m?.[1] || !m[2]) continue; if (!m) continue; const [, alias, definedFilePath, regionName] = m; @@ -89,7 +90,7 @@ function walkMdx(dir: string) { if (!referencedRegions.has(filePath)) { referencedRegions.set(filePath, new Set()); } - referencedRegions.get(filePath)!.add(regionName); + referencedRegions.get(filePath)?.add(regionName); } } } @@ -106,10 +107,11 @@ function validateSource(filePath: string) { const stack: { name: string; lineNum: number }[] = []; const rel = relative(EXAMPLES_DIR, filePath).replace(/\\/g, '/'); - for (let i = 0; i < lines.length; i++) { - let m = lines[i].match(START_RE); + for (const [i, line] of lines.entries()) { + let m = line.match(START_RE); if (m) { - const name = m[1]; + const name = m.at(1); + if (!name) continue; if (regions.has(name)) { errors.push(`${rel}:${i + 1}: Duplicate region "${name}"`); } @@ -118,15 +120,16 @@ function validateSource(filePath: string) { continue; } - m = lines[i].match(END_RE); + m = line.match(END_RE); if (m) { const name = m[1]; - if (stack.length === 0) { + const top = stack.at(-1); + if (top === undefined) { errors.push( `${rel}:${i + 1}: Unmatched closing tag [/${name}] — no region opened`, ); - } else if (stack[stack.length - 1].name !== name) { - const expected = stack[stack.length - 1].name; + } else if (top.name !== name) { + const expected = top.name; errors.push( `${rel}:${i + 1}: Region mismatch — expected [/${expected}] but found [/${name}]`, ); @@ -164,7 +167,7 @@ walkExamples(EXAMPLES_DIR); for (const [filePath, names] of referencedRegions) { const defs = definedRegions.get(filePath); for (const name of names) { - if (!defs || !defs.has(name)) { + if (!defs?.has(name)) { errors.push( `${filePath}: Region "${name}" referenced in MDX but not defined in examples/${filePath}`, ); @@ -175,7 +178,7 @@ for (const [filePath, names] of referencedRegions) { for (const [filePath, names] of definedRegions) { const refs = referencedRegions.get(filePath); for (const [name, lineNum] of names) { - if (!refs || !refs.has(name)) { + if (!refs?.has(name)) { errors.push( `${filePath}:${lineNum}: Orphaned region "${name}" — defined but never referenced in any .mdx file`, ); diff --git a/src/components/Slides.astro b/src/components/Slides.astro index dd3270ae..eb91364a 100644 --- a/src/components/Slides.astro +++ b/src/components/Slides.astro @@ -152,7 +152,7 @@ function parseSequentialContent(content: string): ParsedSlide[] { const srcMatch = tagAttrs.match(/src=["']([^"']+)["']/); const rawSrc = srcMatch ? srcMatch[1] : ''; const altMatch = tagAttrs.match(/alt=["']([^"']*?)["']/); - const alt = altMatch ? altMatch[1] : ''; + const alt = altMatch?.[1] ?? ''; if (!rawSrc) return; diff --git a/src/config/sidebarConfig.ts b/src/config/sidebarConfig.ts index 2088e684..9f90f5e1 100644 --- a/src/config/sidebarConfig.ts +++ b/src/config/sidebarConfig.ts @@ -301,12 +301,7 @@ export function getSidebarForPath(pathname: string): SidebarSection[] { } } - if (bestMatch) { - return sidebarSections[bestMatch]; - } - - // Default to home (empty sidebar) - return sidebarSections['/'] || []; + return sidebarSections[bestMatch || '/'] ?? sidebarSections['/'] ?? []; } /** @@ -355,12 +350,10 @@ export function getPrevNextLinks(pathname: string): { if (currentIndex === -1) { return { prev: null, next: null }; } - + const prev = allLinks.at(currentIndex - 1) ?? null; + const next = allLinks.at(currentIndex + 1) ?? null; return { - prev: currentIndex > 0 ? allLinks[currentIndex - 1] : null, - next: - currentIndex < allLinks.length - 1 - ? allLinks[currentIndex + 1] - : null, + prev, + next, }; } diff --git a/src/plugins/remark-center.ts b/src/plugins/remark-center.ts index 84a2b4ba..d5b47f2d 100644 --- a/src/plugins/remark-center.ts +++ b/src/plugins/remark-center.ts @@ -1,33 +1,16 @@ import { visit } from 'unist-util-visit'; import type { Root } from 'mdast'; -interface ContainerDirective { - type: 'containerDirective'; - name: string; - attributes?: Record; - children: unknown[]; - data?: { - hName?: string; - hProperties?: Record; - }; -} - export function remarkCenter() { return (tree: Root) => { - visit( - tree, - 'containerDirective', - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (node: any) => { - const dir = node as ContainerDirective; - if (dir.name !== 'center') return; + visit(tree, 'containerDirective', (node) => { + if (node.name !== 'center') return; - dir.data = dir.data || {}; - dir.data.hName = 'div'; - dir.data.hProperties = dir.data.hProperties || {}; - dir.data.hProperties.class = 'centered-content'; - }, - ); + node.data = node.data || {}; + node.data.hName = 'div'; + node.data.hProperties = node.data.hProperties || {}; + node.data.hProperties.class = 'centered-content'; + }); }; } diff --git a/src/plugins/remark-code-region.ts b/src/plugins/remark-code-region.ts index ae6d5de7..81e0257e 100644 --- a/src/plugins/remark-code-region.ts +++ b/src/plugins/remark-code-region.ts @@ -19,7 +19,7 @@ export default function remarkCodeRegion() { const meta: string = node.meta || ''; const token = meta.match(/^(\S+)/); - if (!token) return; + if (!token?.[1]) return; const raw = token[1]; const hashIdx = raw.indexOf('#'); @@ -107,6 +107,6 @@ function dedent(str: string): string { Math.min(min, l.match(/^[ \t]*/)?.[0].length ?? Infinity), Infinity, ); - if (indent === 0 || !isFinite(indent)) return str; + if (indent === 0 || !Number.isFinite(indent)) return str; return lines.map((l) => l.slice(indent)).join('\n'); } diff --git a/src/plugins/remark-figure.ts b/src/plugins/remark-figure.ts index c92c9349..4b85c96e 100644 --- a/src/plugins/remark-figure.ts +++ b/src/plugins/remark-figure.ts @@ -1,106 +1,80 @@ import { visit } from 'unist-util-visit'; -import type { Root } from 'mdast'; - -interface ContainerDirective { - type: 'containerDirective'; - name: string; - attributes?: Record; - children: unknown[]; - data?: { - hName?: string; - hProperties?: Record; - }; -} +import type { BlockContent, PhrasingContent, Root } from 'mdast'; export function remarkFigure() { return (tree: Root) => { - visit( - tree, - 'containerDirective', - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (node: any) => { - const dir = node as ContainerDirective; - if (dir.name !== 'figure') return; + visit(tree, 'containerDirective', (node) => { + if (node.name !== 'figure') return; - const attrs = dir.attributes || {}; + const attrs = node.attributes || {}; - let style = ''; + let style = ''; - if (attrs.width) { - style += `width: ${attrs.width};`; - } else if (attrs.w) { - style += `width: ${attrs.w}%;`; - } + if (attrs.width) { + style += `width: ${attrs.width};`; + } else if (attrs.w) { + style += `width: ${attrs.w}%;`; + } - if ('border' in attrs) { - const borderValue = attrs.border || '5px solid #ADADAD'; - style += ` --figure-border: ${borderValue.replace(/_/g, ' ')};`; - } + if ('border' in attrs) { + const borderValue = attrs.border || '5px solid #ADADAD'; + style += ` --figure-border: ${borderValue.replace(/_/g, ' ')};`; + } - dir.data = dir.data || {}; - dir.data.hName = 'figure'; - dir.data.hProperties = dir.data.hProperties || {}; - dir.data.hProperties.class = - 'md-figure' + - ('border' in attrs ? ' md-figure-border' : ''); + node.data = node.data || {}; + node.data.hName = 'figure'; + node.data.hProperties = node.data.hProperties || {}; + node.data.hProperties.class = + 'md-figure' + ('border' in attrs ? ' md-figure-border' : ''); - if (style) { - dir.data.hProperties.style = style.trim(); - } + if (style) { + node.data.hProperties.style = style.trim(); + } - const newChildren: unknown[] = []; + const newChildren = []; - for (const child of dir.children) { - const c = child as { - type: string; - children?: unknown[]; - data?: unknown; - value?: string; - }; - if (c.type === 'paragraph' && c.children) { - const images: unknown[] = []; - const textNodes: unknown[] = []; + for (const child of node.children) { + const c = child; + if (c.type === 'paragraph' && c.children) { + const images = []; + const textNodes: PhrasingContent[] = []; - for (const subChild of c.children) { - const sc = subChild as { - type: string; - value?: string; - }; - if (sc.type === 'image') { - images.push(subChild); - } else if (sc.type === 'text' && sc.value?.trim()) { - textNodes.push(subChild); - } else if (sc.type !== 'text' || sc.value?.trim()) { - textNodes.push(subChild); - } + for (const subChild of c.children) { + const sc = subChild; + if (sc.type === 'image') { + images.push(subChild); + } else if (sc.type === 'text' && sc.value?.trim()) { + textNodes.push(subChild); + } else if (sc.type !== 'text' || sc.value?.trim()) { + textNodes.push(subChild); } + } - if (images.length > 0) { - newChildren.push({ - type: 'paragraph', - children: images, - data: c.data, - }); - } + if (images.length > 0) { + newChildren.push({ + type: 'paragraph', + children: images, + data: c.data, + } as BlockContent); + } - if (textNodes.length > 0) { - newChildren.push({ - type: 'paragraph', - children: textNodes, - data: { - hName: 'figcaption', - hProperties: { class: 'md-figcaption' }, - }, - }); - } - } else { - newChildren.push(child); + if (textNodes.length > 0) { + newChildren.push({ + type: 'paragraph', + children: textNodes, + data: { + hName: 'figcaption', + hProperties: { class: 'md-figcaption' }, + }, + } as BlockContent); } + } else { + newChildren.push(child); } + } - dir.children = newChildren; - }, - ); + node.children = newChildren; + }); }; } diff --git a/src/plugins/remark-image-attributes.ts b/src/plugins/remark-image-attributes.ts index 1a846c21..c11efb21 100644 --- a/src/plugins/remark-image-attributes.ts +++ b/src/plugins/remark-image-attributes.ts @@ -16,109 +16,79 @@ import { visit } from 'unist-util-visit'; import type { Root } from 'mdast'; -interface ImageNode { - url: string; - data?: { - hName?: string; - hProperties?: Record; - }; -} - -interface ParentNode { - type: string; - children: { type: string }[]; - data?: { - hName?: string; - hProperties?: Record; - }; -} - export function remarkImageAttributes() { return (tree: Root) => { - visit( - tree, - 'paragraph', - ( - node: ParentNode, - index: number | undefined, - parent: ParentNode | undefined, - ) => { - if (!parent || index === undefined) return; - - const children = node.children; - - // Look for images with hash attributes in URL - for (let i = 0; i < children.length; i++) { - const child = children[i]; - - if (child.type === 'image') { - const imageNode = child as unknown as ImageNode; - const url = imageNode.url; - - // Check for hash in URL - const hashIndex = url.indexOf('#'); - if (hashIndex !== -1) { - const attributesStr = url.substring(hashIndex + 1); - const cleanUrl = url.substring(0, hashIndex); - - // Update image URL to remove hash - imageNode.url = cleanUrl; - - // Parse attributes - const attributes = parseAttributes(attributesStr); - - // Get alignment (default: center) - const align = attributes.align || 'center'; - // Use 'w' for width (number becomes percentage) - const width = attributes.w - ? `${attributes.w}%` - : '100%'; - const border = attributes.border || ''; - - // Build inline styles for the image - let imgStyle = `width: ${width};`; - if (border) { - imgStyle += ` border: ${border};`; - } - - // Set image properties - imageNode.data = imageNode.data || {}; - imageNode.data.hProperties = - imageNode.data.hProperties || {}; - imageNode.data.hProperties.style = imgStyle; - - // Store attributes as data-* for Slides component to read - if (attributes.w) { - imageNode.data.hProperties['data-slide-width'] = - attributes.w; - } - if (border) { - imageNode.data.hProperties[ - 'data-slide-border' - ] = border; - } - if (attributes.align) { - imageNode.data.hProperties['data-slide-align'] = - attributes.align; - } - - // Wrap the paragraph to act as a container - node.data = node.data || {}; - node.data.hName = 'div'; - node.data.hProperties = node.data.hProperties || {}; - node.data.hProperties.class = `img-wrapper img-align-${align}`; - } else if (children.length === 1) { - // Standalone image without attributes - still wrap and center - node.data = node.data || {}; - node.data.hName = 'div'; - node.data.hProperties = node.data.hProperties || {}; - node.data.hProperties.class = - 'img-wrapper img-align-center'; + visit(tree, 'paragraph', (node, index, parent) => { + if (!parent || index === undefined) return; + + const children = node.children; + + // Look for images with hash attributes in URL + for (const child of children) { + if (child.type === 'image') { + const url = child.url; + + // Check for hash in URL + const hashIndex = url.indexOf('#'); + if (hashIndex !== -1) { + const attributesStr = url.substring(hashIndex + 1); + const cleanUrl = url.substring(0, hashIndex); + + // Update image URL to remove hash + child.url = cleanUrl; + + // Parse attributes + const attributes = parseAttributes(attributesStr); + + // Get alignment (default: center) + const align = attributes.align || 'center'; + // Use 'w' for width (number becomes percentage) + const width = attributes.w + ? `${attributes.w}%` + : '100%'; + const border = attributes.border || ''; + + // Build inline styles for the image + let imgStyle = `width: ${width};`; + if (border) { + imgStyle += ` border: ${border};`; } + + // Set image properties + child.data = child.data || {}; + child.data.hProperties = child.data.hProperties || {}; + child.data.hProperties.style = imgStyle; + + // Store attributes as data-* for Slides component to read + if (attributes.w) { + child.data.hProperties['data-slide-width'] = + attributes.w; + } + if (border) { + child.data.hProperties['data-slide-border'] = + border; + } + if (attributes.align) { + child.data.hProperties['data-slide-align'] = + attributes.align; + } + + // Wrap the paragraph to act as a container + node.data = node.data || {}; + node.data.hName = 'div'; + node.data.hProperties = node.data.hProperties || {}; + node.data.hProperties.class = `img-wrapper img-align-${align}`; + } else if (children.length === 1) { + // Standalone image without attributes - still wrap and center + node.data = node.data || {}; + node.data.hName = 'div'; + node.data.hProperties = node.data.hProperties || {}; + node.data.hProperties.class = + 'img-wrapper img-align-center'; } } - }, - ); + } + }); }; } @@ -131,7 +101,7 @@ function parseAttributes(str: string): Record { for (const part of parts) { if (part.includes('=')) { const [key, ...valueParts] = part.split('='); - // Replace underscores with spaces for border values + if (!key || valueParts.length === 0) continue; const value = valueParts.join('=').replace(/_/g, ' '); attrs[key] = value; } else if (part === 'border') { diff --git a/src/plugins/remark-mdx-global-imports.ts b/src/plugins/remark-mdx-global-imports.ts index adad4fbf..26e9ad39 100644 --- a/src/plugins/remark-mdx-global-imports.ts +++ b/src/plugins/remark-mdx-global-imports.ts @@ -66,7 +66,7 @@ export function remarkMdxGlobalImports() { for (const node of tree.children) { if (node.type === 'mdxjsEsm') { const match = node.value?.match(/\bimport\s+(\w+)\s+from\b/); - if (match) existingNames.add(match[1]); + if (match?.[1]) existingNames.add(match[1]); } } diff --git a/tsconfig.json b/tsconfig.json index 6fd0f931..bb515185 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "astro/tsconfigs/strict", + "extends": "astro/tsconfigs/strictest", "include": [".astro/types.d.ts", "**/*"], "exclude": ["dist"], "compilerOptions": { From 4ef4749997b2016352c3e29aa58c0b29b28400e7 Mon Sep 17 00:00:00 2001 From: httphypixelnet Date: Mon, 20 Jul 2026 23:12:22 -0700 Subject: [PATCH 03/14] fix bad regex guard --- scripts/validate-regions.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/scripts/validate-regions.ts b/scripts/validate-regions.ts index 22a929d7..01dc1324 100644 --- a/scripts/validate-regions.ts +++ b/scripts/validate-regions.ts @@ -30,6 +30,8 @@ function parseCodeRegionSources(content: string): Map { for (let i = startIdx + 1; i < lines.length; i++) { const line = lines[i]; + if (!line) continue; + // Stop once we hit a line that isn't indented (end of the map). if (!/^\s+\S/.test(line)) break; @@ -61,8 +63,7 @@ function walkMdx(dir: string) { for (const line of content.split('\n')) { const m = line.match(CODEBLOCK_RE); - if (!m?.[1] || !m[2]) continue; - if (!m) continue; + if (!m || !m[1] || m[2] || !m[3]) continue; const [, alias, definedFilePath, regionName] = m; @@ -178,7 +179,7 @@ for (const [filePath, names] of referencedRegions) { for (const [filePath, names] of definedRegions) { const refs = referencedRegions.get(filePath); for (const [name, lineNum] of names) { - if (!refs?.has(name)) { + if (!refs || !refs.has(name)) { errors.push( `${filePath}:${lineNum}: Orphaned region "${name}" — defined but never referenced in any .mdx file`, ); From 8cf3f9844ee2e765d8e0518f2ae1c019e8253e3e Mon Sep 17 00:00:00 2001 From: httphypixelnet Date: Tue, 21 Jul 2026 23:04:56 -0700 Subject: [PATCH 04/14] make biome shut up --- scripts/validate-regions.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/validate-regions.ts b/scripts/validate-regions.ts index 01dc1324..165db68f 100644 --- a/scripts/validate-regions.ts +++ b/scripts/validate-regions.ts @@ -63,7 +63,7 @@ function walkMdx(dir: string) { for (const line of content.split('\n')) { const m = line.match(CODEBLOCK_RE); - if (!m || !m[1] || m[2] || !m[3]) continue; + if (!m?.[1] || m[2] || !m[3]) continue; const [, alias, definedFilePath, regionName] = m; @@ -179,7 +179,7 @@ for (const [filePath, names] of referencedRegions) { for (const [filePath, names] of definedRegions) { const refs = referencedRegions.get(filePath); for (const [name, lineNum] of names) { - if (!refs || !refs.has(name)) { + if (!refs?.has(name)) { errors.push( `${filePath}:${lineNum}: Orphaned region "${name}" — defined but never referenced in any .mdx file`, ); From 9776cdd1348cc7381e21b3a08c0d6d9ca15840ef Mon Sep 17 00:00:00 2001 From: httphypixelnet Date: Wed, 22 Jul 2026 15:02:52 -0700 Subject: [PATCH 05/14] fix ci --- package.json | 2 ++ pnpm-lock.yaml | 6 ++++++ 2 files changed, 8 insertions(+) diff --git a/package.json b/package.json index 1f059bea..e0696a7b 100644 --- a/package.json +++ b/package.json @@ -39,6 +39,8 @@ "eslint": "^10.5.0", "eslint-plugin-astro": "^1.7.0", "mdx2vast": "^0.3.1", + "mdast-util-mdx": "^3.0.0", + "mdast-util-mdx-jsx": "^3.2.0", "husky": "^9.1.7", "lint-staged": "^17.0.8", "prettier": "^3.8.4", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index bfd1796c..ffb05553 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -48,6 +48,12 @@ importers: lint-staged: specifier: ^17.0.8 version: 17.0.8 + mdast-util-mdx: + specifier: ^3.0.0 + version: 3.0.0 + mdast-util-mdx-jsx: + specifier: ^3.2.0 + version: 3.2.0 mdx2vast: specifier: ^0.3.1 version: 0.3.1 From 9b07fca0f4315881ef206316f771712cb7277ba5 Mon Sep 17 00:00:00 2001 From: httphypixelnet Date: Wed, 22 Jul 2026 16:45:09 -0700 Subject: [PATCH 06/14] fix unintended side effect --- src/content/docs/resources/glossary.mdx | 10 ++-- src/starlightOverrides/Header.astro | 80 +++++++++++++------------ 2 files changed, 45 insertions(+), 45 deletions(-) diff --git a/src/content/docs/resources/glossary.mdx b/src/content/docs/resources/glossary.mdx index 35d98f82..9f886c8f 100644 --- a/src/content/docs/resources/glossary.mdx +++ b/src/content/docs/resources/glossary.mdx @@ -4,19 +4,18 @@ title: Glossary {/* This file is auto-generated by scripts/generate-glossary.ts. Do not edit directly. */} + **CAN** Controller Area Network: typically yellow and green cable used to communicate with motor controllers and sensors, can be run in various topographies instead of each cable needing to connect to SystemCore **GitHub** -GitHub is like Google Drive, but for code. -It hosts git repositories and allows for improved collaboration on projects through pull requests and issues +GitHub is like Google Drive, but for code. It hosts git repositories and allows for improved collaboration on projects through pull requests and issues **Limit Switch** -Type of sensor that triggers when physically or magnetically hit. -Can be used to trigger actions on a rising or falling edge, or check state of a mechanism +Type of sensor that triggers when physically or magnetically hit. Can be used to trigger actions on a rising or falling edge, or check state of a mechanism **Magnetic Encoder** @@ -36,8 +35,7 @@ Pulse Width Modulation: A communication spec used to communicate with motor cont **Repository** -A storage location for software packages, often used in version control systems like Git. -Repositories are just folders that contain files and subfolders, and they can be hosted on platforms like GitHub to facilitate collaboration and version tracking +A storage location for software packages, often used in version control systems like Git. Repositories are just folders that contain files and subfolders, and they can be hosted on platforms like GitHub to facilitate collaboration and version tracking **Spark MAX** diff --git a/src/starlightOverrides/Header.astro b/src/starlightOverrides/Header.astro index 78df9a86..b8f772c3 100644 --- a/src/starlightOverrides/Header.astro +++ b/src/starlightOverrides/Header.astro @@ -201,7 +201,6 @@ for (const section of sidebarSections) { ) { let html = ''; for (const item of items) { - console.log(item); const indent = depth * 12; const isCurrentPage = item.href && @@ -255,44 +254,6 @@ for (const section of sidebarSections) { tmp instanceof HTMLElement ? tmp : document.createElement('div'); - if (!tmp) { - lightbox.id = 'global-image-lightbox'; - lightbox.className = 'global-image-lightbox'; - lightbox.innerHTML = ` - -
- -
- `; - } - document.body.appendChild(lightbox); - - // Close on backdrop click - lightbox.addEventListener('click', (e) => { - const target = e.target; - if ( - target instanceof HTMLElement && - (target === lightbox || - target.classList.contains('global-lightbox-content')) - ) { - closeLightbox(); - } - }); - const closeSelector = lightbox.querySelector( - '.global-lightbox-close', - ); - if (closeSelector) { - closeSelector.addEventListener('click', closeLightbox); - } - // Close on Escape key - document.addEventListener('keydown', (e) => { - if (e.key === 'Escape' && lightbox.classList.contains('open')) { - closeLightbox(); - } - }); - /** * @param {string} imgSrc * @param {string} imgAlt @@ -316,6 +277,47 @@ for (const section of sidebarSections) { document.documentElement.classList.remove('lightbox-open'); } + if (!tmp) { + lightbox.id = 'global-image-lightbox'; + lightbox.className = 'global-image-lightbox'; + lightbox.innerHTML = ` + +
+ +
+ `; + document.body.appendChild(lightbox); + + // Close on backdrop click + lightbox.addEventListener('click', (e) => { + const target = e.target; + if ( + target instanceof HTMLElement && + (target === lightbox || + target.classList.contains('global-lightbox-content')) + ) { + closeLightbox(); + } + }); + const closeSelector = lightbox.querySelector( + '.global-lightbox-close', + ); + if (closeSelector) { + closeSelector.addEventListener('click', closeLightbox); + } + // Close on Escape key + document.addEventListener('keydown', (e) => { + if ( + e.key === 'Escape' && + lightbox.classList.contains('open') + ) { + closeLightbox(); + } + }); + } + // Attach click handlers to all images in markdown content // Exclude images that are already in a Slides component or have data-no-lightbox const contentImages = document.querySelectorAll( From 2ce363ec34ab999cc16a98d73e0d2f160ea5b2d1 Mon Sep 17 00:00:00 2001 From: Zach Harel Date: Thu, 23 Jul 2026 12:05:14 -0400 Subject: [PATCH 07/14] improve type safety and formatting in glossary and header files --- src/content/docs/resources/glossary.mdx | 10 ++++++---- src/starlightOverrides/Header.astro | 4 +++- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/src/content/docs/resources/glossary.mdx b/src/content/docs/resources/glossary.mdx index 9f886c8f..35d98f82 100644 --- a/src/content/docs/resources/glossary.mdx +++ b/src/content/docs/resources/glossary.mdx @@ -4,18 +4,19 @@ title: Glossary {/* This file is auto-generated by scripts/generate-glossary.ts. Do not edit directly. */} - **CAN** Controller Area Network: typically yellow and green cable used to communicate with motor controllers and sensors, can be run in various topographies instead of each cable needing to connect to SystemCore **GitHub** -GitHub is like Google Drive, but for code. It hosts git repositories and allows for improved collaboration on projects through pull requests and issues +GitHub is like Google Drive, but for code. +It hosts git repositories and allows for improved collaboration on projects through pull requests and issues **Limit Switch** -Type of sensor that triggers when physically or magnetically hit. Can be used to trigger actions on a rising or falling edge, or check state of a mechanism +Type of sensor that triggers when physically or magnetically hit. +Can be used to trigger actions on a rising or falling edge, or check state of a mechanism **Magnetic Encoder** @@ -35,7 +36,8 @@ Pulse Width Modulation: A communication spec used to communicate with motor cont **Repository** -A storage location for software packages, often used in version control systems like Git. Repositories are just folders that contain files and subfolders, and they can be hosted on platforms like GitHub to facilitate collaboration and version tracking +A storage location for software packages, often used in version control systems like Git. +Repositories are just folders that contain files and subfolders, and they can be hosted on platforms like GitHub to facilitate collaboration and version tracking **Spark MAX** diff --git a/src/starlightOverrides/Header.astro b/src/starlightOverrides/Header.astro index b8f772c3..594ce188 100644 --- a/src/starlightOverrides/Header.astro +++ b/src/starlightOverrides/Header.astro @@ -296,7 +296,9 @@ for (const section of sidebarSections) { if ( target instanceof HTMLElement && (target === lightbox || - target.classList.contains('global-lightbox-content')) + target.classList.contains( + 'global-lightbox-content', + )) ) { closeLightbox(); } From a75619fb3944ecf3d5f3641851d8dfad8dc8570c Mon Sep 17 00:00:00 2001 From: Zach Harel Date: Thu, 23 Jul 2026 12:14:11 -0400 Subject: [PATCH 08/14] improve type safety in validate-regions.ts --- scripts/validate-regions.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/scripts/validate-regions.ts b/scripts/validate-regions.ts index 165db68f..e9eb11e3 100644 --- a/scripts/validate-regions.ts +++ b/scripts/validate-regions.ts @@ -22,7 +22,7 @@ function parseCodeRegionSources(content: string): Map { const frontmatterMatch = content.match(FRONTMATTER_RE); if (!frontmatterMatch) return sources; - const lines = frontmatterMatch[1].split('\n'); + const lines = frontmatterMatch[1]!.split('\n'); const startIdx = lines.findIndex((line) => CODE_REGION_SOURCES_KEY_RE.test(line), ); @@ -37,7 +37,7 @@ function parseCodeRegionSources(content: string): Map { const m = line.match(CODE_REGION_SOURCE_ENTRY_RE); if (m) { - sources.set(m[1], m[2]); + sources.set(m[1]!, m[2]!); } } @@ -63,7 +63,7 @@ function walkMdx(dir: string) { for (const line of content.split('\n')) { const m = line.match(CODEBLOCK_RE); - if (!m?.[1] || m[2] || !m[3]) continue; + if (!m) continue; const [, alias, definedFilePath, regionName] = m; @@ -91,7 +91,7 @@ function walkMdx(dir: string) { if (!referencedRegions.has(filePath)) { referencedRegions.set(filePath, new Set()); } - referencedRegions.get(filePath)?.add(regionName); + referencedRegions.get(filePath!)?.add(regionName!); } } } From 0197aa4734f8a646fe5d887513bbf3ed62cc6ca7 Mon Sep 17 00:00:00 2001 From: httphypixelnet Date: Thu, 23 Jul 2026 14:46:31 -0700 Subject: [PATCH 09/14] remove unnecesary optional chaining --- scripts/validate-regions.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/validate-regions.ts b/scripts/validate-regions.ts index e9eb11e3..310b7a6f 100644 --- a/scripts/validate-regions.ts +++ b/scripts/validate-regions.ts @@ -91,7 +91,7 @@ function walkMdx(dir: string) { if (!referencedRegions.has(filePath)) { referencedRegions.set(filePath, new Set()); } - referencedRegions.get(filePath!)?.add(regionName!); + referencedRegions.get(filePath)!.add(regionName!); } } } From 38e283081ea962b375a482393dea8d9ba52b2156 Mon Sep 17 00:00:00 2001 From: httphypixelnet Date: Thu, 23 Jul 2026 15:50:15 -0700 Subject: [PATCH 10/14] fix eslint from conflicts --- src/plugins/remark-glossary.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/plugins/remark-glossary.ts b/src/plugins/remark-glossary.ts index 9c3ae82c..adcd8901 100644 --- a/src/plugins/remark-glossary.ts +++ b/src/plugins/remark-glossary.ts @@ -1,4 +1,3 @@ -/// import { visit, SKIP } from 'unist-util-visit'; import type { Root, RootContent, Text } from 'mdast'; import type { VFile } from 'vfile'; From 30fd963777595fa9c2d7b033804fab36ee9efa56 Mon Sep 17 00:00:00 2001 From: httphypixelnet Date: Thu, 23 Jul 2026 15:53:12 -0700 Subject: [PATCH 11/14] remove unneeded ts specifier --- astro.config.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/astro.config.mjs b/astro.config.mjs index f6a16bf1..7f708f31 100644 --- a/astro.config.mjs +++ b/astro.config.mjs @@ -4,7 +4,7 @@ import remarkGlossary from './src/plugins/remark-glossary'; import remarkCenter from './src/plugins/remark-center'; import remarkFigure from './src/plugins/remark-figure'; import remarkImageAttributes from './src/plugins/remark-image-attributes'; -import { remarkMdxGlobalImports } from './src/plugins/remark-mdx-global-imports.ts'; +import { remarkMdxGlobalImports } from './src/plugins/remark-mdx-global-imports'; import remarkCodeRegion from './src/plugins/remark-code-region'; export default defineConfig({ From 4d3d543ca7499b37c782d26a73908dc7b9ce9792 Mon Sep 17 00:00:00 2001 From: httphypixelnet Date: Thu, 23 Jul 2026 16:24:27 -0700 Subject: [PATCH 12/14] remove edge case --- scripts/validate-regions.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/scripts/validate-regions.ts b/scripts/validate-regions.ts index 310b7a6f..d1128d03 100644 --- a/scripts/validate-regions.ts +++ b/scripts/validate-regions.ts @@ -28,10 +28,7 @@ function parseCodeRegionSources(content: string): Map { ); if (startIdx === -1) return sources; - for (let i = startIdx + 1; i < lines.length; i++) { - const line = lines[i]; - if (!line) continue; - + for (const line of lines.slice(startIdx + 1)) { // Stop once we hit a line that isn't indented (end of the map). if (!/^\s+\S/.test(line)) break; From f621e35c342d297cab90b26cd4793cd53768ca9e Mon Sep 17 00:00:00 2001 From: httphypixelnet Date: Thu, 23 Jul 2026 16:29:55 -0700 Subject: [PATCH 13/14] remove duplicate package from devdeps --- package.json | 1 - 1 file changed, 1 deletion(-) diff --git a/package.json b/package.json index da169d32..d4d1cba0 100644 --- a/package.json +++ b/package.json @@ -40,7 +40,6 @@ "eslint-plugin-astro": "^1.7.0", "mdx2vast": "^0.3.1", "mdast-util-mdx": "^3.0.0", - "mdast-util-mdx-jsx": "^3.2.0", "husky": "^9.1.7", "lint-staged": "^17.0.8", "prettier": "^3.8.4", From 466fb037d2600bffdd6fae1c9b4b6608892a19e9 Mon Sep 17 00:00:00 2001 From: httphypixelnet Date: Fri, 24 Jul 2026 22:31:59 -0700 Subject: [PATCH 14/14] fix jade's mess --- eslint.config.mjs | 4 +--- package.json | 1 - src/plugins/remark-no-html-links.mjs | 11 +++++++++-- 3 files changed, 10 insertions(+), 6 deletions(-) diff --git a/eslint.config.mjs b/eslint.config.mjs index 085833e3..553da4b3 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -18,6 +18,7 @@ export default [ { argsIgnorePattern: '^_' }, ], '@typescript-eslint/no-explicit-any': 'error', + 'no-undef': 'off', }, }, @@ -28,8 +29,5 @@ export default [ ImageMetadata: 'readonly', }, }, - rules: { - 'no-undef': 'off', - }, }, ]; diff --git a/package.json b/package.json index 487a5c99..c1eb5ed3 100644 --- a/package.json +++ b/package.json @@ -38,7 +38,6 @@ "astro-eslint-parser": "^1.4.0", "eslint": "^10.5.0", "eslint-plugin-astro": "^1.7.0", - "mdx2vast": "^0.3.1", "mdast-util-mdx": "^3.0.0", "husky": "^9.1.7", "lint-staged": "^17.0.8", diff --git a/src/plugins/remark-no-html-links.mjs b/src/plugins/remark-no-html-links.mjs index f219b3db..bcfa0970 100644 --- a/src/plugins/remark-no-html-links.mjs +++ b/src/plugins/remark-no-html-links.mjs @@ -1,10 +1,13 @@ import { visit } from 'unist-util-visit'; - /** * Forbid raw HTML/JSX anchor tags (``) in favour of markdown * link syntax (`[text](url)`). Keeps links consistent and portable. */ export default function remarkNoHtmlLinks() { + /** + * @param {import('mdast').Root} tree + * @param {import('vfile').VFile} file + */ return (tree, file) => { // MDX parses `` into JSX element nodes; `remark-mdx` never emits raw // `html` nodes, but check that type too for plain-markdown safety. @@ -13,7 +16,11 @@ export default function remarkNoHtmlLinks() { ['mdxJsxTextElement', 'mdxJsxFlowElement', 'html'], (node) => { const isAnchor = - node.name === 'a' || /]/i.test(node.value ?? ''); + ((node.type === 'mdxJsxTextElement' || + node.type === 'mdxJsxFlowElement') && + node.name === 'a') || + (node.type === 'html' && + /]/i.test(node.value ?? '')); if (!isAnchor) return; const msg = file.message(