From 2a8cb58a82a5a4238fa36520f6fd112d9a3faaaf Mon Sep 17 00:00:00 2001
From: q <787025321@qq.com>
Date: Thu, 7 Aug 2025 15:29:06 +0800
Subject: [PATCH 1/2] feat: add copy page functions
---
i18n/en/code.json | 24 ++++
i18n/zh/code.json | 24 ++++
src/components/CopyPageButton/index.tsx | 117 ++++++++++++++++++
.../CopyPageButton/styles.module.scss | 18 +++
src/css/markdown.scss | 1 -
src/theme/DocItem/Content/index.tsx | 48 +++++++
src/theme/DocItem/Content/styles.module.css | 15 +++
.../DocRoot/Layout/Main/styles.module.css | 1 -
static/icons/copied.svg | 1 +
static/icons/copy.svg | 1 +
static/icons/down.svg | 1 +
static/icons/markdown.svg | 1 +
12 files changed, 250 insertions(+), 2 deletions(-)
create mode 100644 src/components/CopyPageButton/index.tsx
create mode 100644 src/components/CopyPageButton/styles.module.scss
create mode 100644 src/theme/DocItem/Content/index.tsx
create mode 100644 src/theme/DocItem/Content/styles.module.css
create mode 100644 static/icons/copied.svg
create mode 100644 static/icons/copy.svg
create mode 100644 static/icons/down.svg
create mode 100644 static/icons/markdown.svg
diff --git a/i18n/en/code.json b/i18n/en/code.json
index de4dd2f009..cbf0cef42d 100644
--- a/i18n/en/code.json
+++ b/i18n/en/code.json
@@ -780,5 +780,29 @@
"Enterprise Features & Licensing": {
"message": "Enterprise Features & Licensing",
"description": "Enterprise Features & Licensing"
+ },
+ "Copy Page": {
+ "message": "Copy Page",
+ "description": "Copy Page"
+ },
+ "Copy page as Markdown for LLMs": {
+ "message": "Copy page as Markdown for LLMs",
+ "description": "Copy page as Markdown for LLMs"
+ },
+ "View as Markdown": {
+ "message": "View as Markdown",
+ "description": "View as Markdown"
+ },
+ "View this page as plain text": {
+ "message": "View this page as plain text",
+ "description": "View this page as plain text"
+ },
+ "Failed to copy markdown": {
+ "message": "Failed to copy markdown",
+ "description": "Failed to copy markdown"
+ },
+ "Copying...": {
+ "message": "Copying...",
+ "description": "Copying..."
}
}
diff --git a/i18n/zh/code.json b/i18n/zh/code.json
index ff69649140..784e0e3318 100644
--- a/i18n/zh/code.json
+++ b/i18n/zh/code.json
@@ -804,5 +804,29 @@
"Enterprise Features & Licensing": {
"message": "企业功能与许可",
"description": "Enterprise Features & Licensing"
+ },
+ "Copy Page": {
+ "message": "复制页面",
+ "description": "Copy Page"
+ },
+ "Copy page as Markdown for LLMs": {
+ "message": "复制为 Markdown 格式,供大语言模型使用",
+ "description": "Copy page as Markdown for LLMs"
+ },
+ "View as Markdown": {
+ "message": "查看 Markdown 格式",
+ "description": "View as Markdown"
+ },
+ "View this page as plain text": {
+ "message": "查看纯文本",
+ "description": "View this page as plain text"
+ },
+ "Failed to copy markdown": {
+ "message": "无法复制 Markdown",
+ "description": "Failed to copy markdown"
+ },
+ "Copying...": {
+ "message": "正在复制...",
+ "description": "Copying..."
}
}
diff --git a/src/components/CopyPageButton/index.tsx b/src/components/CopyPageButton/index.tsx
new file mode 100644
index 0000000000..ef8805d75f
--- /dev/null
+++ b/src/components/CopyPageButton/index.tsx
@@ -0,0 +1,117 @@
+import React, { useState, useMemo, useCallback } from "react";
+import { Button, Dropdown, Flex, Spin } from "antd";
+import styles from "./styles.module.scss";
+import DownArrow from "@site/static/icons/down.svg";
+import MarkdownSvg from "@site/static/icons/markdown.svg";
+import CopySvg from "@site/static/icons/copy.svg";
+import CopiedSvg from "@site/static/icons/copied.svg";
+import { useDoc } from "@docusaurus/plugin-content-docs/client";
+import axios from "axios";
+import $t from "@site/src/utils/tools";
+// mark
+// import TurndownService from "turndown";
+// const turndownService = new TurndownService();
+// const getPageContentAsHtml = (): string | null => {
+// const contentElement = document.querySelector("article");
+// return contentElement ? contentElement.innerHTML : null;
+// };
+
+// const convertHtmlToMarkdown = (html: string): string => {
+// return turndownService.turndown(html);
+// };
+const CopyDropdownButton: React.FC = () => {
+ const [loading, setLoading] = useState(false);
+ const [isCopied, setIsCopied] = useState(false);
+ const { metadata } = useDoc();
+ const sourceUrl = useMemo(() => {
+ return (
+ metadata?.source?.replace(
+ "@site",
+ "https://raw.githubusercontent.com/databendlabs/databend-docs/refs/heads/main"
+ ) || ""
+ );
+ }, [metadata]);
+ const handleCopy = useCallback((url: string) => {
+ if (!url) return;
+ setLoading(true);
+ axios
+ .get(url)
+ .then((response) => {
+ setIsCopied(true);
+ if (response.status === 200) {
+ navigator.clipboard.writeText(response.data);
+ } else {
+ alert($t("Failed to copy markdown"));
+ }
+ })
+ .finally(() => {
+ setLoading(false);
+ setTimeout(() => {
+ setIsCopied(false);
+ }, 3000);
+ });
+ }, []);
+ const menu = useMemo(() => {
+ const items = [
+ {
+ key: "copy",
+ icon: ,
+ label: $t("Copy Page"),
+ description: $t("Copy page as Markdown for LLMs"),
+ },
+ {
+ key: "markdown",
+ icon: ,
+ label: $t("View as Markdown"),
+ description: $t("View this page as plain text"),
+ },
+ ];
+
+ return {
+ items: items.map(({ key, icon, label, description }) => ({
+ key,
+ label: (
+
+
{label}
+
{description}
+
+ ),
+ icon: ,
+ })),
+ onClick: ({ key }: { key: string }) => {
+ if (key === "copy") handleCopy(sourceUrl);
+ if (key === "markdown") window.open(sourceUrl, "_blank");
+ },
+ };
+ }, [sourceUrl, handleCopy]);
+ const renderButtonContent = useMemo(
+ () => (
+
+ {loading ? (
+
+ ) : isCopied ? (
+
+ ) : (
+
+ )}
+ {loading ? $t("Copying...") : $t("Copy Page")}
+
+ ),
+ [loading, isCopied]
+ );
+
+ return (
+ handleCopy(sourceUrl)}
+ menu={menu}
+ placement="bottomRight"
+ icon={}
+ className={styles.buttonCainter}
+ trigger={["click"]}
+ >
+ {renderButtonContent}
+
+ );
+};
+
+export default CopyDropdownButton;
diff --git a/src/components/CopyPageButton/styles.module.scss b/src/components/CopyPageButton/styles.module.scss
new file mode 100644
index 0000000000..df51154bd6
--- /dev/null
+++ b/src/components/CopyPageButton/styles.module.scss
@@ -0,0 +1,18 @@
+.buttonCainter {
+ max-width: 140px;
+ button {
+ font-weight: 500;
+ border: 1px solid var(--color-border) !important;
+ background-color: var(--color-bg-1) !important;
+ color: var(--color-text-0) !important;
+ &:hover {
+ background-color: var(--color-fill-1) !important;
+ }
+ &:last-child {
+ svg {
+ position: relative;
+ top: 2px;
+ }
+ }
+ }
+}
diff --git a/src/css/markdown.scss b/src/css/markdown.scss
index a33d9a3bbe..17930c1308 100644
--- a/src/css/markdown.scss
+++ b/src/css/markdown.scss
@@ -222,7 +222,6 @@
&:hover {
background-color: var(--bg-hover-copy);
opacity: 1;
- color: #fff !important;
}
}
.theme-code-block:hover {
diff --git a/src/theme/DocItem/Content/index.tsx b/src/theme/DocItem/Content/index.tsx
new file mode 100644
index 0000000000..c076ecd35c
--- /dev/null
+++ b/src/theme/DocItem/Content/index.tsx
@@ -0,0 +1,48 @@
+import React, { type ReactNode } from "react";
+import clsx from "clsx";
+import { ThemeClassNames } from "@docusaurus/theme-common";
+import { useDoc } from "@docusaurus/plugin-content-docs/client";
+import Heading from "@theme/Heading";
+import MDXContent from "@theme/MDXContent";
+import type { Props } from "@theme/DocItem/Content";
+import styles from "./styles.module.css";
+import CopyPageButton from "@site/src/components/CopyPageButton";
+
+/**
+ Title can be declared inside md content or declared through
+ front matter and added manually. To make both cases consistent,
+ the added title is added under the same div.markdown block
+ See https://github.com/facebook/docusaurus/pull/4882#issuecomment-853021120
+
+ We render a "synthetic title" if:
+ - user doesn't ask to hide it with front matter
+ - the markdown content does not already contain a top-level h1 heading
+*/
+function useSyntheticTitle(): string | null {
+ const { metadata, frontMatter, contentTitle } = useDoc();
+ const shouldRender =
+ !frontMatter.hide_title && typeof contentTitle === "undefined";
+ if (!shouldRender) {
+ return null;
+ }
+ return metadata.title;
+}
+
+export default function DocItemContent({ children }: Props): ReactNode {
+ const syntheticTitle = useSyntheticTitle();
+ return (
+
+
+ {syntheticTitle ? (
+
+ ) : (
+
+ )}
+
+
+ {children}
+
+ );
+}
diff --git a/src/theme/DocItem/Content/styles.module.css b/src/theme/DocItem/Content/styles.module.css
new file mode 100644
index 0000000000..6c883490fa
--- /dev/null
+++ b/src/theme/DocItem/Content/styles.module.css
@@ -0,0 +1,15 @@
+.headerSection {
+ display: flex;
+ justify-content: space-between;
+ align-items: "flex-start";
+ width: 100%;
+ padding-right: 12px;
+}
+
+@media (max-width: 900px) {
+ .headerSection {
+ flex-direction: column-reverse;
+ align-items: flex-start;
+ justify-conten: flex-start;
+ }
+}
diff --git a/src/theme/DocRoot/Layout/Main/styles.module.css b/src/theme/DocRoot/Layout/Main/styles.module.css
index 096eb06490..224ab937bc 100644
--- a/src/theme/DocRoot/Layout/Main/styles.module.css
+++ b/src/theme/DocRoot/Layout/Main/styles.module.css
@@ -2,7 +2,6 @@
display: flex;
width: 100%;
}
-
@media (min-width: 997px) {
.docMainContainer {
flex-grow: 1;
diff --git a/static/icons/copied.svg b/static/icons/copied.svg
new file mode 100644
index 0000000000..1742fd0e3a
--- /dev/null
+++ b/static/icons/copied.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/static/icons/copy.svg b/static/icons/copy.svg
new file mode 100644
index 0000000000..6c755bb849
--- /dev/null
+++ b/static/icons/copy.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/static/icons/down.svg b/static/icons/down.svg
new file mode 100644
index 0000000000..9b9537721f
--- /dev/null
+++ b/static/icons/down.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/static/icons/markdown.svg b/static/icons/markdown.svg
new file mode 100644
index 0000000000..8a7df5b37d
--- /dev/null
+++ b/static/icons/markdown.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
From 319cf3b404703c970dc9cf418e3ca50a4e1aa894 Mon Sep 17 00:00:00 2001
From: q <787025321@qq.com>
Date: Thu, 7 Aug 2025 16:09:03 +0800
Subject: [PATCH 2/2] fix: special urls
---
package.json | 2 +
src/components/CopyPageButton/index.tsx | 74 ++++++++++++-------
.../CopyPageButton/styles.module.scss | 1 +
src/css/custom.scss | 4 +
yarn.lock | 17 +++++
5 files changed, 73 insertions(+), 25 deletions(-)
diff --git a/package.json b/package.json
index eb26cfd591..b12d4287f8 100644
--- a/package.json
+++ b/package.json
@@ -36,6 +36,7 @@
"@docusaurus/preset-classic": "^3.8.1",
"@docusaurus/theme-mermaid": "^3.8.1",
"@mdx-js/react": "^3.0.0",
+ "@types/turndown": "^5.0.5",
"ahooks": "^3.8.0",
"antd": "^5.24.8",
"axios": "^1.7.2",
@@ -54,6 +55,7 @@
"react-scroll-progress-bar": "^2.0.3",
"sass": "^1.77.8",
"sass-resources-loader": "^2.2.5",
+ "turndown": "^7.2.0",
"vanilla-cookieconsent": "^3.1.0",
"xml2js": "^0.6.2"
},
diff --git a/src/components/CopyPageButton/index.tsx b/src/components/CopyPageButton/index.tsx
index ef8805d75f..4449503cc7 100644
--- a/src/components/CopyPageButton/index.tsx
+++ b/src/components/CopyPageButton/index.tsx
@@ -8,31 +8,38 @@ import CopiedSvg from "@site/static/icons/copied.svg";
import { useDoc } from "@docusaurus/plugin-content-docs/client";
import axios from "axios";
import $t from "@site/src/utils/tools";
-// mark
-// import TurndownService from "turndown";
-// const turndownService = new TurndownService();
-// const getPageContentAsHtml = (): string | null => {
-// const contentElement = document.querySelector("article");
-// return contentElement ? contentElement.innerHTML : null;
-// };
+import TurndownService from "turndown";
+const SPECIAL_LINKS = [
+ "/guides/",
+ "/guides/products/dc/platforms",
+ "/guides/products/dc/pricing",
+ "guides/deploy/deploy/non-production/deploying-databend",
+ "/guides/cloud/new-account",
+];
-// const convertHtmlToMarkdown = (html: string): string => {
-// return turndownService.turndown(html);
-// };
+const getPageContentAsHtml = (): string | null => {
+ const contentElement = document.querySelector("article");
+ return contentElement ? contentElement.innerHTML : null;
+};
+
+const convertHtmlToMarkdown = (html: string): string => {
+ const turndownService = new TurndownService();
+ return turndownService.turndown(html);
+};
const CopyDropdownButton: React.FC = () => {
const [loading, setLoading] = useState(false);
const [isCopied, setIsCopied] = useState(false);
const { metadata } = useDoc();
- const sourceUrl = useMemo(() => {
- return (
- metadata?.source?.replace(
- "@site",
- "https://raw.githubusercontent.com/databendlabs/databend-docs/refs/heads/main"
- ) || ""
- );
- }, [metadata]);
- const handleCopy = useCallback((url: string) => {
- if (!url) return;
+ function copyHtml() {
+ setIsCopied(true);
+ const htmlContent = getPageContentAsHtml();
+ const markdownContent = convertHtmlToMarkdown(htmlContent);
+ navigator.clipboard.writeText(markdownContent?.replace("Copy Page", ""));
+ setTimeout(() => {
+ setIsCopied(false);
+ }, 3000);
+ }
+ function copyMarkdown(url: string) {
setLoading(true);
axios
.get(url)
@@ -50,18 +57,35 @@ const CopyDropdownButton: React.FC = () => {
setIsCopied(false);
}, 3000);
});
+ }
+ const sourceUrl = useMemo(() => {
+ return (
+ metadata?.source?.replace(
+ "@site",
+ "https://raw.githubusercontent.com/databendlabs/databend-docs/refs/heads/main"
+ ) || ""
+ );
+ }, [metadata]);
+ const handleCopy = useCallback((url: string) => {
+ const nowLink = metadata?.permalink;
+ if (SPECIAL_LINKS?.some((link) => link === nowLink)) {
+ copyHtml();
+ return;
+ }
+ if (!url) return;
+ copyMarkdown(url);
}, []);
const menu = useMemo(() => {
const items = [
{
key: "copy",
- icon: ,
+ icon: ,
label: $t("Copy Page"),
description: $t("Copy page as Markdown for LLMs"),
},
{
key: "markdown",
- icon: ,
+ icon: ,
label: $t("View as Markdown"),
description: $t("View this page as plain text"),
},
@@ -90,9 +114,9 @@ const CopyDropdownButton: React.FC = () => {
{loading ? (
) : isCopied ? (
-
+
) : (
-
+
)}
{loading ? $t("Copying...") : $t("Copy Page")}
@@ -105,7 +129,7 @@ const CopyDropdownButton: React.FC = () => {
onClick={() => handleCopy(sourceUrl)}
menu={menu}
placement="bottomRight"
- icon={}
+ icon={}
className={styles.buttonCainter}
trigger={["click"]}
>
diff --git a/src/components/CopyPageButton/styles.module.scss b/src/components/CopyPageButton/styles.module.scss
index df51154bd6..af17652384 100644
--- a/src/components/CopyPageButton/styles.module.scss
+++ b/src/components/CopyPageButton/styles.module.scss
@@ -1,5 +1,6 @@
.buttonCainter {
max-width: 140px;
+ max-height: 33px;
button {
font-weight: 500;
border: 1px solid var(--color-border) !important;
diff --git a/src/css/custom.scss b/src/css/custom.scss
index b7d5589248..4287afb1fe 100644
--- a/src/css/custom.scss
+++ b/src/css/custom.scss
@@ -368,3 +368,7 @@ textarea {
#cc-main .cm {
border: 1px solid var(--color-border);
}
+.ant-space-compact-block {
+ display: flex;
+ width: 100%;
+}
diff --git a/yarn.lock b/yarn.lock
index 18dc47a805..0a4ac56411 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -3268,6 +3268,11 @@
dependencies:
langium "3.3.1"
+"@mixmark-io/domino@^2.2.0":
+ version "2.2.0"
+ resolved "https://registry.npmmirror.com/@mixmark-io/domino/-/domino-2.2.0.tgz#4e8ec69bf1afeb7a14f0628b7e2c0f35bdb336c3"
+ integrity sha512-Y28PR25bHXUg88kCV7nivXrP2Nj2RueZ3/l/jdx6J9f8J4nsEGcgX0Qe6lt7Pa+J79+kPiJU3LguR6O/6zrLOw==
+
"@module-federation/error-codes@0.16.0":
version "0.16.0"
resolved "https://registry.yarnpkg.com/@module-federation/error-codes/-/error-codes-0.16.0.tgz#e375b2d10405cf24bae9a798337e4805cbcd69ee"
@@ -4381,6 +4386,11 @@
resolved "https://registry.yarnpkg.com/@types/trusted-types/-/trusted-types-2.0.7.tgz#baccb07a970b91707df3a3e8ba6896c57ead2d11"
integrity sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==
+"@types/turndown@^5.0.5":
+ version "5.0.5"
+ resolved "https://registry.npmmirror.com/@types/turndown/-/turndown-5.0.5.tgz#614de24fc9ace4d8c0d9483ba81dc8c1976dd26f"
+ integrity sha512-TL2IgGgc7B5j78rIccBtlYAnkuv8nUQqhQc+DSYV5j9Be9XOcm/SKOVRuA47xAVI3680Tk9B1d8flK2GWT2+4w==
+
"@types/unist@*", "@types/unist@^3.0.0":
version "3.0.2"
resolved "https://registry.yarnpkg.com/@types/unist/-/unist-3.0.2.tgz#6dd61e43ef60b34086287f83683a5c1b2dc53d20"
@@ -11890,6 +11900,13 @@ tslib@^2.4.0:
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.8.1.tgz#612efe4ed235d567e8aba5f2a5fab70280ade83f"
integrity sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==
+turndown@^7.2.0:
+ version "7.2.0"
+ resolved "https://registry.npmmirror.com/turndown/-/turndown-7.2.0.tgz#67d614fe8371fb511079a93345abfd156c0ffcf4"
+ integrity sha512-eCZGBN4nNNqM9Owkv9HAtWRYfLA4h909E/WGAWWBpmB275ehNhZyk87/Tpvjbp0jjNl9XwCsbe6bm6CqFsgD+A==
+ dependencies:
+ "@mixmark-io/domino" "^2.2.0"
+
type-fest@^0.21.3:
version "0.21.3"
resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.21.3.tgz#d260a24b0198436e133fa26a524a6d65fa3b2e37"