From a05e849ddb3918e209e2c99a424ce76a0df3a79e Mon Sep 17 00:00:00 2001 From: pheralb Date: Mon, 8 Sep 2025 17:14:42 +0100 Subject: [PATCH] =?UTF-8?q?=F0=9F=9B=A0=EF=B8=8F=20Create=20rehypeCopyBtn?= =?UTF-8?q?=20&=20rehypeExternalLinks=20with=20custom=20types?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- content-collections.ts | 14 +++- src/markdown/rehypeCopyBtn.ts | 125 ++++++++++++++++++++++++++++ src/markdown/rehypeExternalLinks.ts | 18 ++++ src/types/unist.ts | 18 ++++ 4 files changed, 173 insertions(+), 2 deletions(-) create mode 100644 src/markdown/rehypeCopyBtn.ts create mode 100644 src/markdown/rehypeExternalLinks.ts create mode 100644 src/types/unist.ts diff --git a/content-collections.ts b/content-collections.ts index 5deee57..ae3c8c0 100644 --- a/content-collections.ts +++ b/content-collections.ts @@ -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, diff --git a/src/markdown/rehypeCopyBtn.ts b/src/markdown/rehypeCopyBtn.ts new file mode 100644 index 0000000..1c72d39 --- /dev/null +++ b/src/markdown/rehypeCopyBtn.ts @@ -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; + } + }); + }; +}; diff --git a/src/markdown/rehypeExternalLinks.ts b/src/markdown/rehypeExternalLinks.ts new file mode 100644 index 0000000..ccd399e --- /dev/null +++ b/src/markdown/rehypeExternalLinks.ts @@ -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"; + } + } + }); + }; +}; diff --git a/src/types/unist.ts b/src/types/unist.ts new file mode 100644 index 0000000..ea5f38f --- /dev/null +++ b/src/types/unist.ts @@ -0,0 +1,18 @@ +export interface UnistNode { + type: string; + name?: string; + tagName?: string; + value?: string; + properties?: Record; + attributes?: { + name: string; + value: unknown; + type?: string; + }[]; + children?: UnistNode[]; +} + +export interface UnistTree { + type: string; + children: UnistNode[]; +}