diff --git a/.remarkrc.mjs b/.remarkrc.mjs index be0807a2..b24df3ef 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'; import remarkNoHtmlLinks from './src/plugins/remark-no-html-links.mjs'; import remarkLintNoDeadUrls from 'remark-lint-no-dead-urls'; diff --git a/astro.config.mjs b/astro.config.mjs index ac00065e..c7796950 100644 --- a/astro.config.mjs +++ b/astro.config.mjs @@ -5,7 +5,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({ 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 340beddc..c1eb5ed3 100644 --- a/package.json +++ b/package.json @@ -38,6 +38,7 @@ "astro-eslint-parser": "^1.4.0", "eslint": "^10.5.0", "eslint-plugin-astro": "^1.7.0", + "mdast-util-mdx": "^3.0.0", "husky": "^9.1.7", "lint-staged": "^17.0.8", "mdast-util-mdx-jsx": "^3.2.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index fd4b6316..7ca00108 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -48,6 +48,9 @@ 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 diff --git a/scripts/validate-regions.ts b/scripts/validate-regions.ts index 560e21c8..d1128d03 100644 --- a/scripts/validate-regions.ts +++ b/scripts/validate-regions.ts @@ -22,20 +22,19 @@ 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), ); if (startIdx === -1) return sources; - for (let i = startIdx + 1; i < lines.length; i++) { - const line = lines[i]; + 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; const m = line.match(CODE_REGION_SOURCE_ENTRY_RE); if (m) { - sources.set(m[1], m[2]); + sources.set(m[1]!, m[2]!); } } @@ -89,7 +88,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 +105,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 +118,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 +165,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 +176,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 400bb706..bc56f1ed 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 @@ -341,12 +347,7 @@ export function getSidebarForPath(pathname: string): SidebarSection[] { } } - if (bestMatch) { - return sidebarSections[bestMatch]; - } - - // Default to home (empty sidebar) - return sidebarSections['/'] || []; + return sidebarSections[bestMatch || '/'] ?? sidebarSections['/'] ?? []; } /** @@ -395,12 +396,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 6b68c71e..b43f26fa 100644 --- a/src/plugins/remark-mdx-global-imports.ts +++ b/src/plugins/remark-mdx-global-imports.ts @@ -67,7 +67,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/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( 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 d8ce178d..1ceca959 100644 --- a/src/starlightOverrides/Header.astro +++ b/src/starlightOverrides/Header.astro @@ -196,7 +196,10 @@ 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) { const indent = depth * 12; @@ -247,9 +250,35 @@ 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'); + /** + * @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'); + document.body.style.overflow = 'hidden'; + document.documentElement.classList.add('lightbox-open'); + } + + function closeLightbox() { + lightbox.classList.remove('open'); + document.body.style.overflow = ''; + document.documentElement.classList.remove('lightbox-open'); + } + + if (!tmp) { lightbox.id = 'global-image-lightbox'; lightbox.className = 'global-image-lightbox'; lightbox.innerHTML = ` @@ -264,19 +293,23 @@ for (const section of sidebarSections) { // Close on backdrop click lightbox.addEventListener('click', (e) => { + const target = e.target; if ( - e.target === lightbox || - e.target.classList.contains('global-lightbox-content') + target instanceof HTMLElement && + (target === lightbox || + target.classList.contains( + 'global-lightbox-content', + )) ) { closeLightbox(); } }); - - // Close button - lightbox - .querySelector('.global-lightbox-close') - .addEventListener('click', closeLightbox); - + const closeSelector = lightbox.querySelector( + '.global-lightbox-close', + ); + if (closeSelector) { + closeSelector.addEventListener('click', closeLightbox); + } // Close on Escape key document.addEventListener('keydown', (e) => { if ( @@ -288,29 +321,14 @@ for (const section of sidebarSections) { }); } - function openLightbox(imgSrc, imgAlt) { - const lightboxImg = lightbox.querySelector( - '.global-lightbox-image', - ); - lightboxImg.src = imgSrc; - lightboxImg.alt = imgAlt || ''; - lightbox.classList.add('open'); - document.body.style.overflow = 'hidden'; - document.documentElement.classList.add('lightbox-open'); - } - - function closeLightbox() { - lightbox.classList.remove('open'); - document.body.style.overflow = ''; - document.documentElement.classList.remove('lightbox-open'); - } - // 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( '.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'; @@ -347,8 +365,7 @@ for (const section of sidebarSections) { return null; } }; - - const setStoredTheme = (value) => { + const setStoredTheme = (/** @type {'light' | 'dark'} */ value) => { try { localStorage.setItem(storageKey, value); } catch { @@ -356,14 +373,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); } }; @@ -396,9 +414,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..bb515185 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,11 +1,11 @@ { - "extends": "astro/tsconfigs/strict", + "extends": "astro/tsconfigs/strictest", "include": [".astro/types.d.ts", "**/*"], "exclude": ["dist"], "compilerOptions": { - "baseUrl": ".", "paths": { - "@components/*": ["src/components/*"] - } + "@components/*": ["./src/components/*"] + }, + "checkJs": true } }