From 28e8710267871e938f0ea9798cb8353b0258909a Mon Sep 17 00:00:00 2001 From: aradhyacp Date: Wed, 15 Jul 2026 10:16:01 +0530 Subject: [PATCH 1/5] feat(streamdown): add code download configuration to StreamdownProps and context - Introduced CodeDownloadConfig interface to define optional baseFileName for code downloads. - Updated StreamdownProps to include codeDownload property. - Enhanced StreamdownContextType to support codeDownload configuration. - Ensured default values are set for new properties in the context. --- packages/streamdown/index.tsx | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/packages/streamdown/index.tsx b/packages/streamdown/index.tsx index be32d9fd..6a6b5dd7 100644 --- a/packages/streamdown/index.tsx +++ b/packages/streamdown/index.tsx @@ -182,6 +182,10 @@ export interface MermaidOptions { errorComponent?: React.ComponentType; } +export interface CodeDownloadConfig { + baseFileName?: string; +} + export type AllowedTags = Record; export type StreamdownProps = Options & { @@ -233,6 +237,7 @@ export type StreamdownProps = Options & { onAnimationStart?: () => void; /** Called when isAnimating transitions from true to false. Suppressed in mode="static". */ onAnimationEnd?: () => void; + codeDownload?: CodeDownloadConfig; }; const defaultSanitizeSchema = { @@ -278,6 +283,7 @@ const carets = { // Combined context for better performance - reduces React tree depth from 5 nested providers to 1 export interface StreamdownContextType { + codeDownload?: CodeDownloadConfig; controls: ControlsConfig; isAnimating: boolean; /** Show line numbers in code blocks. @default true */ @@ -298,6 +304,7 @@ const defaultLinkSafetyConfig: LinkSafetyConfig = { }; const defaultStreamdownContext: StreamdownContextType = { + codeDownload: undefined, shikiTheme: defaultShikiTheme, controls: true, isAnimating: false, @@ -441,6 +448,7 @@ export const Streamdown = memo( shikiTheme = defaultShikiTheme, mermaid, controls = true, + codeDownload, isAnimating = false, animated, BlockComponent = Block, @@ -611,6 +619,7 @@ export const Streamdown = memo( () => ({ shikiTheme: plugins?.code?.getThemes() ?? shikiTheme, controls, + codeDownload, isAnimating, lineNumbers, mode, @@ -621,6 +630,7 @@ export const Streamdown = memo( shikiTheme, controls, isAnimating, + codeDownload, lineNumbers, mode, mermaid, From 638cbac466f863fadca9f2723feff4234c3a1a11 Mon Sep 17 00:00:00 2001 From: aradhyacp Date: Wed, 15 Jul 2026 10:16:07 +0530 Subject: [PATCH 2/5] fix(streamdown): update filename generation in download button to use codeDownload configuration - Modified filename logic in CodeBlockDownloadButton to utilize baseFileName from StreamdownContext. - Ensured fallback to default filename if baseFileName is not provided. --- packages/streamdown/lib/code-block/download-button.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/streamdown/lib/code-block/download-button.tsx b/packages/streamdown/lib/code-block/download-button.tsx index 4a315d1f..cc4a2b96 100644 --- a/packages/streamdown/lib/code-block/download-button.tsx +++ b/packages/streamdown/lib/code-block/download-button.tsx @@ -334,7 +334,7 @@ export const CodeBlockDownloadButton = ({ }) => { const cn = useCn(); const { code: contextCode } = useCodeBlockContext(); - const { isAnimating } = useContext(StreamdownContext); + const { isAnimating, codeDownload } = useContext(StreamdownContext); const t = useTranslations(); const icons = useIcons(); const code = propCode ?? contextCode; @@ -342,7 +342,7 @@ export const CodeBlockDownloadButton = ({ language && language in languageExtensionMap ? languageExtensionMap[language] : "txt"; - const filename = `file.${extension}`; + const filename = `${codeDownload?.baseFileName ?? "file"}.${extension}`; const mimeType = "text/plain"; const downloadCode = () => { From 441cbc33878862aac2e948e2239fe74353d97cfa Mon Sep 17 00:00:00 2001 From: aradhyacp Date: Wed, 15 Jul 2026 10:31:33 +0530 Subject: [PATCH 3/5] test(streamdown): enhance CodeBlockDownloadButton tests for custom filename scenarios - Added tests to verify the functionality of custom baseFileName in the download button. - Included cases for handling undefined codeDownload, unknown languages, and special characters in filenames. - Ensured that the button is enabled and the correct filename is used during the download process. --- .../__tests__/code-block-download.test.tsx | 210 ++++++++++++++++++ 1 file changed, 210 insertions(+) diff --git a/packages/streamdown/__tests__/code-block-download.test.tsx b/packages/streamdown/__tests__/code-block-download.test.tsx index 74b2e860..96efd9f1 100644 --- a/packages/streamdown/__tests__/code-block-download.test.tsx +++ b/packages/streamdown/__tests__/code-block-download.test.tsx @@ -137,4 +137,214 @@ describe("CodeBlockDownloadButton", () => { ); expect(button?.hasAttribute("disabled")).toBe(true); }); + + it("should use custom baseFileName from context", async () => { + const { save } = await import("../lib/utils"); + + const { container } = render( + + + + + + ); + + await waitFor(() => { + const button = container.querySelector( + '[data-streamdown="code-block-download-button"]' + ); + expect(button?.hasAttribute("disabled")).toBe(false); + }); + + const button = container.querySelector( + '[data-streamdown="code-block-download-button"]' + ); + // biome-ignore lint/style/noNonNullAssertion: test assertion + fireEvent.click(button!); + + expect(save).toHaveBeenCalledWith( + "myScript.js", + "console.log('test');", + "text/plain" + ); + }); + + it("should use custom baseFileName with unknown language", async () => { + const { save } = await import("../lib/utils"); + + const { container } = render( + + + + + + ); + + await waitFor(() => { + const button = container.querySelector( + '[data-streamdown="code-block-download-button"]' + ); + expect(button?.hasAttribute("disabled")).toBe(false); + }); + + const button = container.querySelector( + '[data-streamdown="code-block-download-button"]' + ); + // biome-ignore lint/style/noNonNullAssertion: test assertion + fireEvent.click(button!); + + expect(save).toHaveBeenCalledWith( + "output.txt", + "some data", + "text/plain" + ); + }); + + it("should fall back to default filename when codeDownload is undefined", async () => { + const { save } = await import("../lib/utils"); + + const { container } = render( + + + + + + ); + + await waitFor(() => { + const button = container.querySelector( + '[data-streamdown="code-block-download-button"]' + ); + expect(button?.hasAttribute("disabled")).toBe(false); + }); + + const button = container.querySelector( + '[data-streamdown="code-block-download-button"]' + ); + // biome-ignore lint/style/noNonNullAssertion: test assertion + fireEvent.click(button!); + + expect(save).toHaveBeenCalledWith( + "file.py", + "python code", + "text/plain" + ); + }); + + it("should fall back to default filename when baseFileName is not set", async () => { + const { save } = await import("../lib/utils"); + + const { container } = render( + + + + + + ); + + await waitFor(() => { + const button = container.querySelector( + '[data-streamdown="code-block-download-button"]' + ); + expect(button?.hasAttribute("disabled")).toBe(false); + }); + + const button = container.querySelector( + '[data-streamdown="code-block-download-button"]' + ); + // biome-ignore lint/style/noNonNullAssertion: test assertion + fireEvent.click(button!); + + expect(save).toHaveBeenCalledWith( + "file.rs", + "rust code", + "text/plain" + ); + }); + + it("should handle special characters in custom baseFileName", async () => { + const { save } = await import("../lib/utils"); + + const { container } = render( + + + + + + ); + + await waitFor(() => { + const button = container.querySelector( + '[data-streamdown="code-block-download-button"]' + ); + expect(button?.hasAttribute("disabled")).toBe(false); + }); + + const button = container.querySelector( + '[data-streamdown="code-block-download-button"]' + ); + // biome-ignore lint/style/noNonNullAssertion: test assertion + fireEvent.click(button!); + + expect(save).toHaveBeenCalledWith( + "my-config.backup.json", + "config data", + "text/plain" + ); + }); }); From 0c21d53dff68b699b7a284459bd584f0b0c20b76 Mon Sep 17 00:00:00 2001 From: aradhyacp Date: Wed, 15 Jul 2026 10:32:04 +0530 Subject: [PATCH 4/5] chore: add changeset for configurable code download filenames --- .changeset/full-donuts-help.md | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 .changeset/full-donuts-help.md diff --git a/.changeset/full-donuts-help.md b/.changeset/full-donuts-help.md new file mode 100644 index 00000000..f62cf082 --- /dev/null +++ b/.changeset/full-donuts-help.md @@ -0,0 +1,8 @@ +--- +"streamdown": minor +--- + +- Add `codeDownload.baseFileName` to customize downloaded code filenames +- Preserve automatic language-to-extension mapping for downloaded files +- Keep existing `file.` behavior as the default when not configured +- Expose the configuration through `StreamdownContext` without prop drilling From 9259126b314d6f6b1c40e5860cb1e93833087300 Mon Sep 17 00:00:00 2001 From: aradhyacp Date: Wed, 15 Jul 2026 10:43:45 +0530 Subject: [PATCH 5/5] refactor(tests): streamline CodeBlockDownloadButton test assertions and component usage - Simplified the rendering of CodeBlockDownloadButton by consolidating props into a single line. - Updated test assertions for expected save calls to improve readability and maintainability. --- .../__tests__/code-block-download.test.tsx | 38 ++++--------------- 1 file changed, 7 insertions(+), 31 deletions(-) diff --git a/packages/streamdown/__tests__/code-block-download.test.tsx b/packages/streamdown/__tests__/code-block-download.test.tsx index 96efd9f1..874f4bd5 100644 --- a/packages/streamdown/__tests__/code-block-download.test.tsx +++ b/packages/streamdown/__tests__/code-block-download.test.tsx @@ -194,10 +194,7 @@ describe("CodeBlockDownloadButton", () => { }} > - + ); @@ -215,11 +212,7 @@ describe("CodeBlockDownloadButton", () => { // biome-ignore lint/style/noNonNullAssertion: test assertion fireEvent.click(button!); - expect(save).toHaveBeenCalledWith( - "output.txt", - "some data", - "text/plain" - ); + expect(save).toHaveBeenCalledWith("output.txt", "some data", "text/plain"); }); it("should fall back to default filename when codeDownload is undefined", async () => { @@ -236,10 +229,7 @@ describe("CodeBlockDownloadButton", () => { }} > - + ); @@ -257,11 +247,7 @@ describe("CodeBlockDownloadButton", () => { // biome-ignore lint/style/noNonNullAssertion: test assertion fireEvent.click(button!); - expect(save).toHaveBeenCalledWith( - "file.py", - "python code", - "text/plain" - ); + expect(save).toHaveBeenCalledWith("file.py", "python code", "text/plain"); }); it("should fall back to default filename when baseFileName is not set", async () => { @@ -278,10 +264,7 @@ describe("CodeBlockDownloadButton", () => { }} > - + ); @@ -299,11 +282,7 @@ describe("CodeBlockDownloadButton", () => { // biome-ignore lint/style/noNonNullAssertion: test assertion fireEvent.click(button!); - expect(save).toHaveBeenCalledWith( - "file.rs", - "rust code", - "text/plain" - ); + expect(save).toHaveBeenCalledWith("file.rs", "rust code", "text/plain"); }); it("should handle special characters in custom baseFileName", async () => { @@ -320,10 +299,7 @@ describe("CodeBlockDownloadButton", () => { }} > - + );