Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 12 additions & 8 deletions packages/docs-app/src/components/blueprintDocs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -143,15 +144,18 @@ export class BlueprintDocs extends Component<BlueprintDocsProps, { themeName: st
return <NavMenuItem {...props} />;
};

private renderPageActions = (page: { sourcePath: string }) => {
private renderPageActions = (page: { sourceMarkdown?: string; sourcePath: string }) => {
return (
<AnchorButton
href={`${GITHUB_SOURCE_URL}/${page.sourcePath}`}
icon="edit"
target="_blank"
text="Edit this page"
variant="minimal"
/>
<>
<CopyPageMarkdownButton sourceMarkdown={page.sourceMarkdown} />
<AnchorButton
href={`${GITHUB_SOURCE_URL}/${page.sourcePath}`}
icon="edit"
target="_blank"
text="Edit this page"
variant="minimal"
/>
</>
);
};

Expand Down
20 changes: 20 additions & 0 deletions packages/docs-data/compile-docs-data.mts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -89,6 +90,10 @@ async function generateDocumentalistData(): Promise<void> {
`../{${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);
Expand Down Expand Up @@ -205,3 +210,18 @@ function applyNavConfig(docs: { pages: Record<string, DocPage>; nav: NavTreeNode
assignRoutes(navConfig, docs.pages);
docs.nav = buildNavTree(navConfig, docs.pages);
}

function attachSourceMarkdown(pages: Record<string, DocPage>): 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.
}
}
}
36 changes: 36 additions & 0 deletions packages/docs-data/markdownExport.mts
Original file line number Diff line number Diff line change
@@ -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<string, string> = {
css: "CSS reference",
interface: "TypeScript interface",
reactDocs: "Interactive widget",
reactExample: "Interactive example",
};
return `<!-- ${labels[tag]}: ${value.trim()} (see online docs) -->`;
})
.join("\n");
}
40 changes: 40 additions & 0 deletions packages/docs-data/markdownExport.test.ts
Original file line number Diff line number Diff line change
@@ -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",
"",
"<!-- Interactive widget: Welcome (see online docs) -->",
"<!-- Interactive example: ButtonExample (see online docs) -->",
"<!-- TypeScript interface: ButtonProps (see online docs) -->",
"<!-- CSS reference: .bp6-button (see online docs) -->",
].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);
});
});
2 changes: 2 additions & 0 deletions packages/docs-data/navTypes.mts
Original file line number Diff line number Diff line change
Expand Up @@ -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. */
Expand Down
51 changes: 51 additions & 0 deletions packages/docs-theme/src/components/copyPageMarkdownButton.tsx
Original file line number Diff line number Diff line change
@@ -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<CopyPageMarkdownButtonProps> = ({ sourceMarkdown }) => {
const [copied, setCopied] = useState(false);
const timeoutRef = useRef<ReturnType<typeof setTimeout>>();

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 (
<Tooltip content={copied ? "Copied!" : "Copy this page as markdown"} hoverOpenDelay={300} position="top">
<AnchorButton
aria-label="Copy page as markdown"
icon={copied ? "tick" : "clipboard"}
onClick={handleClick}
text="Copy page"
variant="minimal"
/>
</Tooltip>
);
};
1 change: 1 addition & 0 deletions packages/docs-theme/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down