mirror of
https://seed.radicle.dev/rad:z4V1sjrXqjvFdnCUbxPFqd5p4DtH5.git
synced 2026-10-18 19:30:27 +08:00
The FAQ shipped here was a verbatim copy of the one on radicle.dev, carrying a canonical tag that already pointed search engines at that original. Drop the duplicate and redirect /faq to radicle.dev/faq so existing links still reach the content.
134 lines
3.8 KiB
JavaScript
134 lines
3.8 KiB
JavaScript
import rehypeSlug from "rehype-slug";
|
|
import remarkUnwrapImages from "remark-unwrap-images";
|
|
import { getSingletonHighlighter } from "shiki";
|
|
import { mdsvex, escapeSvelte } from "mdsvex";
|
|
import { vitePreprocess } from "@sveltejs/vite-plugin-svelte";
|
|
|
|
// Add an id and a leading anchor link to linkable doc entries so they can be
|
|
// deep-linked and copied: glossary term paragraphs (a bold term followed by an
|
|
// em-dash, e.g. `**Term** — definition`). Each linkable element also gets a
|
|
// `term` class that drives the shared styling and copy interaction in
|
|
// Docs.svelte.
|
|
function slugify(text) {
|
|
return text
|
|
.toLowerCase()
|
|
.replace(/[^a-z0-9]+/g, "-")
|
|
.replace(/^-+|-+$/g, "");
|
|
}
|
|
|
|
function textOf(node) {
|
|
if (node.type === "text") {
|
|
return node.value;
|
|
}
|
|
if (Array.isArray(node.children)) {
|
|
return node.children.map(textOf).join("");
|
|
}
|
|
return "";
|
|
}
|
|
|
|
function addTermAnchor(node, id, label) {
|
|
node.properties = node.properties || {};
|
|
node.properties.className = [...(node.properties.className || []), "term"];
|
|
node.children.unshift({
|
|
type: "element",
|
|
tagName: "a",
|
|
properties: {
|
|
className: ["term-link"],
|
|
href: `#${id}`,
|
|
ariaLabel: `Link to ${label}`,
|
|
},
|
|
children: [],
|
|
});
|
|
}
|
|
|
|
function rehypeTermAnchors() {
|
|
return tree => {
|
|
const seen = new Map();
|
|
|
|
const walk = nodes => {
|
|
for (const node of nodes) {
|
|
// Glossary: a bold term immediately followed by an em-dash. This is
|
|
// pattern-based, so it never matches guide paragraphs.
|
|
if (
|
|
node.type === "element" &&
|
|
node.tagName === "p" &&
|
|
Array.isArray(node.children)
|
|
) {
|
|
const firstIndex = node.children.findIndex(
|
|
child => !(child.type === "text" && !child.value.trim()),
|
|
);
|
|
const first = node.children[firstIndex];
|
|
const after = node.children[firstIndex + 1];
|
|
const isTerm =
|
|
first &&
|
|
first.type === "element" &&
|
|
first.tagName === "strong" &&
|
|
after &&
|
|
after.type === "text" &&
|
|
/^\s*[—–-]/.test(after.value);
|
|
|
|
if (isTerm) {
|
|
const text = textOf(first).trim();
|
|
let slug = slugify(text);
|
|
const count = seen.get(slug) || 0;
|
|
seen.set(slug, count + 1);
|
|
if (count) {
|
|
slug = `${slug}-${count}`;
|
|
}
|
|
node.properties = node.properties || {};
|
|
node.properties.id = slug;
|
|
addTermAnchor(node, slug, text);
|
|
// Drop the em-dash separator from the rendered output, keeping just
|
|
// the bold term followed by its definition.
|
|
after.value = after.value.replace(/^\s*[—–-]\s*/, " ");
|
|
}
|
|
}
|
|
if (Array.isArray(node.children)) {
|
|
walk(node.children);
|
|
}
|
|
}
|
|
};
|
|
walk(tree.children);
|
|
};
|
|
}
|
|
|
|
const mdsvexOptions = {
|
|
extensions: [".md"],
|
|
highlight: {
|
|
highlighter: async (code, lang = "text") => {
|
|
const highlighter = await getSingletonHighlighter({
|
|
themes: ["github-light", "github-dark"],
|
|
langs: [
|
|
"bash",
|
|
"sh",
|
|
"toml",
|
|
"ini",
|
|
"rust",
|
|
"javascript",
|
|
"typescript",
|
|
"json",
|
|
"yaml",
|
|
"markdown",
|
|
"html",
|
|
"css",
|
|
],
|
|
});
|
|
const html = escapeSvelte(
|
|
highlighter.codeToHtml(code, {
|
|
lang,
|
|
themes: { light: "github-light", dark: "github-dark" },
|
|
defaultColor: false,
|
|
}),
|
|
);
|
|
return `{@html \`${html}\` }`;
|
|
},
|
|
},
|
|
remarkPlugins: [remarkUnwrapImages],
|
|
rehypePlugins: [rehypeSlug, rehypeTermAnchors],
|
|
};
|
|
|
|
export default {
|
|
extensions: [".svelte", ".md"],
|
|
preprocess: [vitePreprocess(), mdsvex(mdsvexOptions)],
|
|
};
|