🛠️ Create rehypeCopyBtn & rehypeExternalLinks with custom types

This commit is contained in:
pheralb
2025-09-08 17:14:42 +01:00
parent 5c88b29387
commit a05e849ddb
4 changed files with 173 additions and 2 deletions
+12 -2
View File
@@ -4,9 +4,13 @@ import { z } from "zod";
import { compileMarkdown } from "@content-collections/markdown";
import { defineCollection, defineConfig } from "@content-collections/core";
// Shiki:
// Plugings:
import rehypeSlug from "rehype-slug";
import rehypeAutolinkHeadings from "rehype-autolink-headings";
import rehypeShiki from "@shikijs/rehype/core";
import { shikiHighlighter, rehypeShikiOptions } from "./src/utils/shiki";
import { rehypeCopyBtn } from "./src/markdown/rehypeCopyBtn";
import { rehypeExternalLinks } from "./src/markdown/rehypeExternalLinks";
const docs = defineCollection({
name: "docs",
@@ -19,7 +23,13 @@ const docs = defineCollection({
transform: async (document, context) => {
const highlighter = await shikiHighlighter();
const html = await compileMarkdown(context, document, {
rehypePlugins: [[rehypeShiki, highlighter, rehypeShikiOptions]],
rehypePlugins: [
rehypeSlug,
rehypeAutolinkHeadings,
[rehypeShiki, highlighter, rehypeShikiOptions],
rehypeExternalLinks,
rehypeCopyBtn,
],
});
return {
...document,
+125
View File
@@ -0,0 +1,125 @@
import type { UnistNode, UnistTree } from "@/types/unist";
import { visit } from "unist-util-visit";
import { cn } from "@/utils/cn";
export const rehypeCopyBtn = () => {
return (tree: UnistTree) => {
visit(tree, "element", (node: UnistNode, index, parent) => {
if (node.tagName === "pre" && parent && typeof index === "number") {
const copyIcon = {
type: "element",
tagName: "svg",
properties: {
xmlns: "http://www.w3.org/2000/svg",
width: "14",
height: "14",
viewBox: "0 0 24 24",
fill: "none",
stroke: "currentColor",
strokeWidth: "2",
strokeLinecap: "round",
strokeLinejoin: "round",
style: "display: inline;",
},
children: [
{
type: "element",
tagName: "rect",
properties: {
width: "14",
height: "14",
x: "8",
y: "8",
rx: "2",
ry: "2",
},
children: [],
},
{
type: "element",
tagName: "path",
properties: {
d: "M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2",
},
children: [],
},
],
};
const successIcon = {
type: "element",
tagName: "svg",
properties: {
xmlns: "http://www.w3.org/2000/svg",
width: "14",
height: "14",
viewBox: "0 0 24 24",
fill: "none",
stroke: "currentColor",
strokeWidth: "2",
strokeLinecap: "round",
strokeLinejoin: "round",
style: "display: none;",
},
children: [
{
type: "element",
tagName: "path",
properties: {
d: "M18 6 7 17l-5-5",
},
children: [],
},
{
type: "element",
tagName: "path",
properties: {
d: "m22 10-7.5 7.5L13 16",
},
children: [],
},
],
};
const copyButton = {
type: "element",
tagName: "button",
title: "Copy code to clipboard",
"aria-label": "Copy code to clipboard",
properties: {
type: "button",
title: "Copy code to clipboard",
class: cn(
"absolute top-2 right-2 px-1.5 py-0.5 rounded-md",
"bg-transparent hover:bg-neutral-200 dark:hover:bg-neutral-800",
"transition-colors",
),
onclick: `
const button = this;
const copyIcon = button.querySelector('svg:first-child');
const successIcon = button.querySelector('svg:last-child');
const codeBlock = button.nextElementSibling;
navigator.clipboard.writeText(codeBlock.innerText).then(() => {
copyIcon.style.display = 'none';
successIcon.style.display = 'inline';
setTimeout(() => {
copyIcon.style.display = 'inline';
successIcon.style.display = 'none';
}, 2000);
}).catch((err) => {
console.error('Error copying:', err);
});
`,
},
children: [copyIcon, successIcon],
};
const wrapper = {
type: "element",
tagName: "div",
properties: { class: "relative" },
children: [copyButton, node],
};
parent.children[index] = wrapper;
}
});
};
};
+18
View File
@@ -0,0 +1,18 @@
import type { UnistNode, UnistTree } from "@/types/unist";
import { visit } from "unist-util-visit";
const APP_DOMAIN = "svgl.app";
export const rehypeExternalLinks = () => {
return (tree: UnistTree) => {
visit(tree, "element", (node: UnistNode) => {
if (node.tagName === "a" && node.properties?.href) {
const href = String(node.properties.href);
if (!href.includes(APP_DOMAIN)) {
node.properties.target = "_blank";
node.properties.rel = "noopener noreferrer";
}
}
});
};
};
+18
View File
@@ -0,0 +1,18 @@
export interface UnistNode {
type: string;
name?: string;
tagName?: string;
value?: string;
properties?: Record<string, unknown>;
attributes?: {
name: string;
value: unknown;
type?: string;
}[];
children?: UnistNode[];
}
export interface UnistTree {
type: string;
children: UnistNode[];
}