Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 17 additions & 4 deletions astro.config.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import starlight from '@astrojs/starlight';
import { pluginCollapsibleSections } from '@expressive-code/plugin-collapsible-sections';
import { defineConfig, sharpImageService } from 'astro/config';
import rehypeSlug from 'rehype-slug';
import remarkSmartypants from 'remark-smartypants';
Expand All @@ -11,6 +10,9 @@ import { starlightPluginLlmsTxt } from './config/plugins/llms-txt';
import { starlightPluginSmokeTest } from './config/plugins/smoke-test';
import { rehypeTasklistEnhancer } from './config/plugins/rehype-tasklist-enhancer';
import { remarkFallbackLang } from './config/plugins/remark-fallback-lang';
import { tasklistEnhancerPlugin } from './config/plugins/satteri-tasklist-enhancer';
import { fallbackLangPlugin } from './config/plugins/satteri-fallback-lang';
import mdx from '@astrojs/mdx';

/* https://docs.netlify.com/configure-builds/environment-variables/#read-only-variables */
const NETLIFY_PREVIEW_SITE = process.env.CONTEXT !== 'production' && process.env.DEPLOY_PRIME_URL;
Expand All @@ -28,9 +30,7 @@ export default defineConfig({
]),
starlight({
title: 'Docs',
expressiveCode: {
plugins: [pluginCollapsibleSections()],
},
expressiveCode: false,
components: {
EditLink: './src/components/starlight/EditLink.astro',
Hero: './src/components/starlight/Hero.astro',
Expand Down Expand Up @@ -72,6 +72,9 @@ export default defineConfig({
plugins: [starlightPluginSmokeTest(), starlightPluginLlmsTxt()],
}),
sitemap(),
mdx({
optimize: true,
}),
],
trailingSlash: 'always',
scopedStyleStrategy: 'where',
Expand All @@ -91,6 +94,16 @@ export default defineConfig({
rehypeTasklistEnhancer(),
],
},
experimental: {
rustCompiler: true,
nativeMarkdown: {
mdastPlugins: [fallbackLangPlugin()],
hastPlugins: [tasklistEnhancerPlugin()],
features: {
directive: true,
},
},
},
image: {
domains: ['avatars.githubusercontent.com'],
service: sharpImageService(),
Expand Down
58 changes: 58 additions & 0 deletions config/plugins/satteri-fallback-lang.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import fs from 'node:fs';
import path from 'node:path';
import { defineMdastPlugin } from 'satteri';

const pageSourceDir = path.resolve('./src/content/docs');
const baseUrl = 'https://docs.astro.build/';

export function fallbackLangPlugin() {
return defineMdastPlugin({
name: 'fallback-lang',
link(node, context) {
const pageUrl = mdFilePathToUrl(context.filename, pageSourceDir, baseUrl);
const pageLang = getLanguageCodeFromPathname(pageUrl.pathname);

// Ignore pages without language prefix and English pages
if (!pageLang || pageLang === 'en') return;

const linkUrl = new URL(node.url, pageUrl);

// Ignore external links
if (pageUrl.host !== linkUrl.host) return;

// Ignore link targets without language prefix
const linkLang = getLanguageCodeFromPathname(linkUrl.pathname);
if (!linkLang) return;

// Ignore link targets that have a valid source file
const linkSourceFileName = tryFindSourceFileForPathname(linkUrl.pathname, pageSourceDir);
if (linkSourceFileName) return;

context.appendChild(node, {
type: 'text',
value: '\u00A0(EN)',
});
},
});
}

function mdFilePathToUrl(mdFilePath: string, pageSourceDir: string, baseUrl: string) {
const pathBelowRoot = path.relative(pageSourceDir, mdFilePath);
const pathname = pathBelowRoot.replace(/\\/g, '/').replace(/\.mdx?$/i, '/');
return new URL(pathname, baseUrl);
}

function getLanguageCodeFromPathname(pathname: string) {
const firstPathPart = pathname.split('/')[1];
if (firstPathPart?.match(/^[a-z]{2}(-[a-zA-Z]{2})?$/)) return firstPathPart;
}

function tryFindSourceFileForPathname(pathname: string, pageSourceDir: string) {
const possibleSourceFilePaths = [
path.join(pageSourceDir, pathname, '.') + '.md',
path.join(pageSourceDir, pathname, 'index.md'),
path.join(pageSourceDir, pathname, '.') + '.mdx',
path.join(pageSourceDir, pathname, 'index.mdx'),
];
return possibleSourceFilePaths.find((possiblePath) => fs.existsSync(possiblePath));
}
101 changes: 101 additions & 0 deletions config/plugins/satteri-tasklist-enhancer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import { defineHastPlugin } from 'satteri';

import type { Element, ElementContent } from 'hast';

/**
* Satteri HAST plugin to enhance the output of GitHub-Flavored Markdown's task lists.
* This improves possibilities for our `<Checklist>` component.
*
* 1. Wraps checkboxes and siblings in a `<label>` to associate them.
* 2. Wraps sibling nodes after checkboxes in `<span>` to ease styling `:checked ~ *`.
*/
export function tasklistEnhancerPlugin() {
return defineHastPlugin({
name: 'tasklist-enhancer',
element: [
{
filter: ['ul'],
visit(node, ctx) {
if (hasCheckboxChild(node)) {
ctx.setProperty(node, 'className', ['contains-task-list']);
}
},
},
{
filter: ['li'],
visit(node) {
const children = node.children;

const result = findCheckboxInSubtree(node);
if (!result) return;

const { parent, index } = result;
const parentChildren = parent.children;

const head = parentChildren.slice(0, index + 1);
const tail = parentChildren.slice(index + 1);
const label = h('label', {}, [...head, h('span', {}, tail)]);

// Build new li with restructured children
let newChildren: ElementContent[];
if (parent === node) {
// Input was a direct child of the li
newChildren = [label];
} else {
// Input was inside a child element (e.g. <p>) — clone that child
newChildren = children.map((child) =>
child === parent ? h(parent.tagName, parent.properties ?? {}, [label]) : child
);
}

return {
type: 'element',
tagName: node.tagName,
properties: {
...node.properties,
className: ['task-list-item'],
},
children: newChildren,
};
},
},
],
});
}

/** Check if a <ul> has any direct <li> children containing a checkbox. */
function hasCheckboxChild(ul: Element): boolean {
return ul.children.some(
(li) => li.type === 'element' && li.tagName === 'li' && findCheckboxInSubtree(li) !== undefined
);
}

/** Depth-first search for a checkbox `<input>`, returning its direct parent and index. */
function findCheckboxInSubtree(node: Element): { parent: Element; index: number } | undefined {
for (let i = 0; i < node.children.length; i++) {
const child = node.children[i]!;
if (
child.type === 'element' &&
child.tagName === 'input' &&
child.properties?.['type'] === 'checkbox'
) {
return { parent: node, index: i };
}
}

for (const child of node.children) {
if (child.type === 'element') {
const result = findCheckboxInSubtree(child);
if (result) return result;
}
}
}

function h(tag: string, properties: Element['properties'], children: ElementContent[]): Element {
return {
type: 'element',
tagName: tag,
properties,
children,
};
}
6 changes: 5 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
"@types/mdast": "^4.0.4",
"@types/node": "^22.18.1",
"@typescript-eslint/parser": "^8.51.0",
"astro": "^6.0.2",
"astro": "https://pkg.pr.new/astro@16149",
"astro-eslint-parser": "^1.2.2",
"astro-og-canvas": "^0.10.0",
"canvaskit-wasm": "^0.40.0",
Expand All @@ -58,6 +58,9 @@
},
"dependencies": {
"@astrojs/check": "^0.9.7",
"@astrojs/compiler-rs": "^0.1.6",
"@astrojs/markdown-remark": "https://pkg.pr.new/@astrojs/markdown-remark@16149",
"@astrojs/mdx": "https://pkg.pr.new/@astrojs/mdx@16149?fdsfsd",
"@astrojs/sitemap": "^3.7.1",
"@astrojs/starlight": "^0.38.0",
"@expressive-code/plugin-collapsible-sections": "^0.41.6",
Expand All @@ -66,6 +69,7 @@
"jsdoc-api": "^9.3.5",
"rehype-slug": "^6.0.0",
"remark-smartypants": "^3.0.2",
"satteri": "^0.2.5",
"sharp": "^0.34.3",
"starlight-llms-txt": "^0.6.0"
},
Expand Down
Loading
Loading