diff --git a/packages/docs-app/src/components/blueprintDocs.tsx b/packages/docs-app/src/components/blueprintDocs.tsx index bb85c31ffed..fbc25e24092 100644 --- a/packages/docs-app/src/components/blueprintDocs.tsx +++ b/packages/docs-app/src/components/blueprintDocs.tsx @@ -21,6 +21,7 @@ import { AnchorButton, BlueprintProvider, Classes, type Intent, Tag } from "@blu import { type DocsCompleteData, type HeadingNode, npmData, type PageNode, SECTIONS } from "@blueprintjs/docs-data"; import { Banner, + CopyPageMarkdownButton, Documentation, type DocumentationProps, NavMenuItem, @@ -143,15 +144,18 @@ export class BlueprintDocs extends Component; }; - private renderPageActions = (page: { sourcePath: string }) => { + private renderPageActions = (page: { sourceMarkdown?: string; sourcePath: string }) => { return ( - + <> + + + ); }; diff --git a/packages/docs-data/compile-docs-data.mts b/packages/docs-data/compile-docs-data.mts index 450475206d9..2179c46a867 100755 --- a/packages/docs-data/compile-docs-data.mts +++ b/packages/docs-data/compile-docs-data.mts @@ -14,6 +14,7 @@ import semver from "semver"; import { Classes } from "@blueprintjs/core"; import { hooks, markedRenderer } from "./markdownRenderer.mjs"; +import { stripDocumentalistTags } from "./markdownExport.mts"; import { assignRoutes, buildNavTree, normalizeNavConfig } from "./navHelpers.mts"; import { PACKAGES, @@ -89,6 +90,10 @@ async function generateDocumentalistData(): Promise { `../{${LIBRARY_PACKAGES}}/package.json`, ); + // Attach sourceMarkdown to each page so the docs UI can offer a "Copy page" button + // that hands an LLM-friendly markdown blob to the reader. + attachSourceMarkdown(docs.pages); + // Post-process: replace documentalist's nav with one built from nav.json const rawConfig: RawNavStructure = JSON.parse(readFileSync(new URL("./nav.json", import.meta.url), "utf-8")); validateNavConfig(rawConfig); @@ -205,3 +210,18 @@ function applyNavConfig(docs: { pages: Record; nav: NavTreeNode assignRoutes(navConfig, docs.pages); docs.nav = buildNavTree(navConfig, docs.pages); } + +function attachSourceMarkdown(pages: Record): void { + for (const page of Object.values(pages)) { + const sourcePath = (page as DocPage & { sourcePath?: string }).sourcePath; + if (sourcePath == null) { + continue; + } + try { + const raw = readFileSync(resolve(monorepoRootDir, sourcePath), "utf-8"); + page.sourceMarkdown = stripDocumentalistTags(raw); + } catch { + // Source file disappeared between documentalist scan and now — non-fatal. + } + } +} diff --git a/packages/docs-data/markdownExport.mts b/packages/docs-data/markdownExport.mts new file mode 100644 index 00000000000..45b2b5d8a49 --- /dev/null +++ b/packages/docs-data/markdownExport.mts @@ -0,0 +1,36 @@ +/* ! + * (c) Copyright 2026 Palantir Technologies Inc. All rights reserved. + */ + +/** + * Replace Documentalist's `@tag value` lines with markdown-friendly placeholders so the + * exported source can be pasted into LLM/IDE tooling without unfamiliar syntax. Lines + * inside fenced code blocks are left untouched (Sass `@use`/`@import` etc.). + */ +export function stripDocumentalistTags(source: string): string { + const lines = source.split("\n"); + let inFence = false; + return lines + .map(line => { + if (/^\s*```/.test(line)) { + inFence = !inFence; + return line; + } + if (inFence) { + return line; + } + const match = /^@(reactDocs|reactExample|interface|css)\s+(.+)$/.exec(line); + if (match == null) { + return line; + } + const [, tag, value] = match; + const labels: Record = { + css: "CSS reference", + interface: "TypeScript interface", + reactDocs: "Interactive widget", + reactExample: "Interactive example", + }; + return ``; + }) + .join("\n"); +} diff --git a/packages/docs-data/markdownExport.test.ts b/packages/docs-data/markdownExport.test.ts new file mode 100644 index 00000000000..f938cd8f8af --- /dev/null +++ b/packages/docs-data/markdownExport.test.ts @@ -0,0 +1,40 @@ +/* ! + * (c) Copyright 2026 Palantir Technologies Inc. All rights reserved. + */ + +import { describe, expect, it } from "@blueprintjs/test-commons/vitest"; + +import { stripDocumentalistTags } from "./markdownExport.mts"; + +describe("stripDocumentalistTags", () => { + it("replaces top-level @reactDocs / @reactExample / @interface / @css with placeholder comments", () => { + const input = [ + "# Title", + "", + "@reactDocs Welcome", + "@reactExample ButtonExample", + "@interface ButtonProps", + "@css .bp6-button", + ].join("\n"); + expect(stripDocumentalistTags(input)).toBe( + [ + "# Title", + "", + "", + "", + "", + "", + ].join("\n"), + ); + }); + + it("does not touch lines inside fenced code blocks", () => { + const input = ["```scss", '@use "@blueprintjs/core/lib/scss/variables";', "@import 'foo';", "```"].join("\n"); + expect(stripDocumentalistTags(input)).toBe(input); + }); + + it("leaves unknown @-prefixed lines alone", () => { + const input = "@unknownTag value"; + expect(stripDocumentalistTags(input)).toBe(input); + }); +}); diff --git a/packages/docs-data/navTypes.mts b/packages/docs-data/navTypes.mts index a60303804c8..de4e39b51de 100644 --- a/packages/docs-data/navTypes.mts +++ b/packages/docs-data/navTypes.mts @@ -81,6 +81,8 @@ export interface DocPage { title: string; route: string; contents: DocContentItem[]; + /** Cleaned source markdown of the page, suitable for "Copy as markdown". */ + sourceMarkdown?: string; } /** Fields common to all nav tree nodes. */ diff --git a/packages/docs-theme/src/components/copyPageMarkdownButton.tsx b/packages/docs-theme/src/components/copyPageMarkdownButton.tsx new file mode 100644 index 00000000000..945c8de72cd --- /dev/null +++ b/packages/docs-theme/src/components/copyPageMarkdownButton.tsx @@ -0,0 +1,51 @@ +/* ! + * (c) Copyright 2026 Palantir Technologies Inc. All rights reserved. + */ + +import { useCallback, useEffect, useRef, useState } from "react"; + +import { AnchorButton, Tooltip } from "@blueprintjs/core"; + +export interface CopyPageMarkdownButtonProps { + /** Markdown source of the current page. The button is hidden if undefined. */ + sourceMarkdown?: string; +} + +/** + * Action button for copying the current docs page as markdown to the clipboard. + * Useful for handing the page to an LLM, IDE, or codegen tool. + */ +export const CopyPageMarkdownButton: React.FC = ({ sourceMarkdown }) => { + const [copied, setCopied] = useState(false); + const timeoutRef = useRef>(); + + useEffect(() => { + return () => clearTimeout(timeoutRef.current); + }, []); + + const handleClick = useCallback(() => { + if (sourceMarkdown == null) { + return; + } + void navigator.clipboard.writeText(sourceMarkdown); + setCopied(true); + clearTimeout(timeoutRef.current); + timeoutRef.current = setTimeout(() => setCopied(false), 1500); + }, [sourceMarkdown]); + + if (sourceMarkdown == null) { + return null; + } + + return ( + + + + ); +}; diff --git a/packages/docs-theme/src/index.ts b/packages/docs-theme/src/index.ts index 2b2af5a7c01..78e4109cbc2 100644 --- a/packages/docs-theme/src/index.ts +++ b/packages/docs-theme/src/index.ts @@ -18,6 +18,7 @@ export * from "./components/banner"; export * from "./components/documentation"; export * from "./components/example"; export * from "./components/codeExample"; +export * from "./components/copyPageMarkdownButton"; export * from "./components/navMenuItem"; export * from "./components/navButton"; export * from "./common";