From 97ea44bbbfe114e659d77cdb60575d1e6a9aa880 Mon Sep 17 00:00:00 2001 From: s1gr1d <32902192+s1gr1d@users.noreply.github.com> Date: Wed, 13 May 2026 13:45:26 +0200 Subject: [PATCH 1/9] feat: Add snippets from all code tabs to copied markdown --- scripts/generate-md-exports.mjs | 67 +++++++++ scripts/generate-md-exports.test.mjs | 199 +++++++++++++++++++++++++++ src/components/codeTabs.tsx | 12 +- 3 files changed, 277 insertions(+), 1 deletion(-) create mode 100644 scripts/generate-md-exports.test.mjs diff --git a/scripts/generate-md-exports.mjs b/scripts/generate-md-exports.mjs index b4dd6007b8de62..2ad5eae117cd5a 100644 --- a/scripts/generate-md-exports.mjs +++ b/scripts/generate-md-exports.mjs @@ -26,6 +26,7 @@ import RemarkLinkRewrite from 'remark-link-rewrite'; import remarkStringify from 'remark-stringify'; import {unified} from 'unified'; import {remove} from 'unist-util-remove'; +import {visit} from 'unist-util-visit'; const DOCS_ORIGIN = process.env.NEXT_PUBLIC_DEVELOPER_DOCS ? 'https://develop.sentry.dev' @@ -957,6 +958,71 @@ function extractContentForCacheKey(html) { return title + '\0' + canonical + '\0' + normalizedMain; } +/** + * Rehype plugin that expands CodeTabs panels for markdown export. + * + * CodeTabs renders all tab panels in the DOM but hides non-active ones with + * the `hidden` attribute. Each panel carries `data-code-tab-title` and + * optionally `data-code-tab-filename`. This plugin: + * 1. Removes the `hidden` attribute so rehypeRemark processes every panel + * 2. Strips CodeBlock chrome (filename display, copy button) from the panel + * 3. Inserts a bold heading (preferring filename, falling back to tab title) + * before the
 block so readers can tell which file the snippet
+ *     belongs to
+ */
+export function rehypeExpandCodeTabs() {
+  return tree => {
+    visit(tree, 'element', node => {
+      const title = node.properties?.dataCodeTabTitle;
+      if (!title) {
+        return;
+      }
+      delete node.properties.hidden;
+      const filename = node.properties?.dataCodeTabFilename;
+      delete node.properties.dataCodeTabTitle;
+      delete node.properties.dataCodeTabFilename;
+
+      const label = filename || title;
+      const preElements = collectAll(node, el => el.tagName === 'pre');
+      if (preElements.length === 0) {
+        return;
+      }
+
+      const heading = {
+        type: 'element',
+        tagName: 'p',
+        properties: {},
+        children: [
+          {
+            type: 'element',
+            tagName: 'strong',
+            properties: {},
+            children: [{type: 'text', value: label}],
+          },
+        ],
+      };
+
+      node.children = [heading, ...preElements];
+    });
+  };
+}
+
+function collectAll(node, predicate) {
+  const results = [];
+  (function walk(n) {
+    if (n.type === 'element' && predicate(n)) {
+      results.push(n);
+      return;
+    }
+    if (n.children) {
+      for (const child of n.children) {
+        walk(child);
+      }
+    }
+  })(node);
+  return results;
+}
+
 async function genMDFromHTML(source, {cacheDir, noCache, usedCacheFiles}) {
   const rawHTML = await readFile(source, {encoding: 'utf8'});
   // Strip build-specific HTML elements for faster parsing.
@@ -1004,6 +1070,7 @@ async function genMDFromHTML(source, {cacheDir, noCache, usedCacheFiles}) {
         properties: {},
         children: tree,
       }))
+      .use(rehypeExpandCodeTabs)
       .use(rehypeRemark, {
         document: false,
         handlers: {
diff --git a/scripts/generate-md-exports.test.mjs b/scripts/generate-md-exports.test.mjs
new file mode 100644
index 00000000000000..9bce820ae703fc
--- /dev/null
+++ b/scripts/generate-md-exports.test.mjs
@@ -0,0 +1,199 @@
+import rehypeParse from 'rehype-parse';
+import rehypeRemark from 'rehype-remark';
+import remarkGfm from 'remark-gfm';
+import remarkStringify from 'remark-stringify';
+import {describe, expect, it} from 'vitest';
+import {unified} from 'unified';
+import {remove} from 'unist-util-remove';
+
+import {rehypeExpandCodeTabs} from './generate-md-exports.mjs';
+
+function htmlToMarkdown(html) {
+  return String(
+    unified()
+      .use(rehypeParse)
+      .use(rehypeExpandCodeTabs)
+      .use(rehypeRemark, {
+        document: false,
+        handlers: {
+          button() {},
+        },
+      })
+      .use(() => tree => remove(tree, {type: 'inlineCode', value: ''}))
+      .use(remarkGfm)
+      .use(remarkStringify)
+      .processSync(html)
+  );
+}
+
+/**
+ * Builds a minimal CodeTabs HTML structure matching what the component renders.
+ * Each tab panel contains the CodeBlock chrome (filename display, copy button)
+ * wrapping the actual 
 block. The first tab is visible; subsequent
+ * tabs have the `hidden` attribute.
+ */
+function buildCodeTabsHTML(tabs) {
+  const buttons = tabs
+    .map((t, i) => ``)
+    .join('');
+
+  const panels = tabs
+    .map((t, i) => {
+      const filenameAttr = t.filename
+        ? ` data-code-tab-filename="${t.filename}"`
+        : '';
+      const hiddenAttr = i > 0 ? ' hidden' : '';
+
+      return (
+        `
` + + `
` + + `
` + + `${t.filename || ''}` + + `` + + `
` + + `
Copied
` + + `
${t.code}
` + + `
` + + `
` + ); + }) + .join(''); + + return `
${buttons}
${panels}
`; +} + +describe('rehypeExpandCodeTabs', () => { + it('expands hidden tabs and labels each with a bold heading', () => { + const html = buildCodeTabsHTML([ + { + title: 'Cloudflare Workers', + lang: 'javascript', + code: 'import { sentry } from "@sentry/hono/cloudflare";', + }, + { + title: 'Node.js', + filename: 'instrument.mjs', + lang: 'javascript', + code: 'import * as Sentry from "@sentry/hono/node";', + }, + { + title: 'Bun', + lang: 'javascript', + code: 'import { sentry } from "@sentry/hono/bun";', + }, + ]); + + const md = htmlToMarkdown(html); + const codeBlocks = md.match(/```[\s\S]*?```/g); + + expect(codeBlocks).toHaveLength(3); + expect(md).toContain('**Cloudflare Workers**'); + expect(md).toContain('**instrument.mjs**'); + expect(md).toContain('**Bun**'); + expect(codeBlocks[0]).toContain('@sentry/hono/cloudflare'); + expect(codeBlocks[1]).toContain('@sentry/hono/node'); + expect(codeBlocks[2]).toContain('@sentry/hono/bun'); + }); + + it('prefers filename over tab title for the heading', () => { + const html = buildCodeTabsHTML([ + {title: 'ESM', filename: 'instrument.mjs', lang: 'javascript', code: 'init();'}, + {title: 'CommonJS', filename: 'instrument.js', lang: 'javascript', code: 'init();'}, + ]); + + const md = htmlToMarkdown(html); + + expect(md).toContain('**instrument.mjs**'); + expect(md).toContain('**instrument.js**'); + expect(md).not.toContain('**ESM**'); + expect(md).not.toContain('**CommonJS**'); + }); + + it('falls back to tab title when no filename is set', () => { + const html = buildCodeTabsHTML([ + {title: 'Cloudflare Workers', lang: 'javascript', code: 'workers();'}, + ]); + + const md = htmlToMarkdown(html); + + expect(md).toContain('**Cloudflare Workers**'); + }); + + it('strips CodeBlock chrome (filename display, copy button, copied indicator)', () => { + const html = buildCodeTabsHTML([ + {title: 'Node.js', filename: 'instrument.mjs', lang: 'javascript', code: 'init();'}, + ]); + + const md = htmlToMarkdown(html); + + expect(md).not.toContain('Copied'); + expect(md).not.toContain('`instrument.mjs`'); + }); + + it('does not affect code blocks outside of tabs', () => { + const html = + '
curl -sL https://sentry.io/get-cli/ | bash
'; + + const md = htmlToMarkdown(html); + + const codeBlocks = md.match(/```[\s\S]*?```/g); + expect(codeBlocks).toHaveLength(1); + expect(md).not.toMatch(/\*\*.*\*\*\n/); + expect(codeBlocks[0]).toContain('curl -sL'); + }); + + it('keeps standalone blocks intact when mixed with tabs on the same page', () => { + const standalone = + '
npm install @sentry/node
'; + const tabs = buildCodeTabsHTML([ + {title: 'Node.js', filename: 'instrument.mjs', lang: 'javascript', code: 'Sentry.init();'}, + {title: 'Bun', lang: 'javascript', code: 'init();'}, + ]); + const html = `
${standalone}${tabs}
`; + + const md = htmlToMarkdown(html); + + const codeBlocks = md.match(/```[\s\S]*?```/g); + expect(codeBlocks).toHaveLength(3); + expect(codeBlocks[0]).toContain('npm install'); + expect(md).toContain('**instrument.mjs**'); + expect(md).toContain('**Bun**'); + }); + + it('handles two separate tab groups on the same page', () => { + const group1 = buildCodeTabsHTML([ + {title: 'ESM', filename: 'instrument.mjs', lang: 'javascript', code: 'import init'}, + {title: 'CJS', filename: 'instrument.js', lang: 'javascript', code: 'require init'}, + ]); + const group2 = buildCodeTabsHTML([ + {title: 'Python', filename: 'main.py', lang: 'python', code: 'import sentry_sdk'}, + {title: 'Ruby', filename: 'config.rb', lang: 'ruby', code: 'require "sentry-ruby"'}, + ]); + const html = `
${group1}${group2}
`; + + const md = htmlToMarkdown(html); + + const codeBlocks = md.match(/```[\s\S]*?```/g); + expect(codeBlocks).toHaveLength(4); + expect(md).toContain('**instrument.mjs**'); + expect(md).toContain('**instrument.js**'); + expect(md).toContain('**main.py**'); + expect(md).toContain('**config.rb**'); + }); + + it('handles tab panel with no pre element gracefully', () => { + const html = + '
' + + '

Not a code block

' + + '' + + '
'; + + const md = htmlToMarkdown(html); + + expect(md).toContain('**ok**'); + expect(md).toContain('works()'); + expect(md).not.toContain('**broken**'); + }); +}); diff --git a/src/components/codeTabs.tsx b/src/components/codeTabs.tsx index bfe1a22be428b6..fa4d8784218631 100644 --- a/src/components/codeTabs.tsx +++ b/src/components/codeTabs.tsx @@ -157,7 +157,17 @@ export function CodeTabs({children}: CodeTabProps) { {showSigninNote(codeBlocks[selectedTabIndex]) && } {buttons} - {codeBlocks[selectedTabIndex]} + {codeBlocks.map((block, idx) => ( + + ))} ); } From 082c3ea743b111d053651247e20bfee1df3cef78 Mon Sep 17 00:00:00 2001 From: "getsantry[bot]" <66042841+getsantry[bot]@users.noreply.github.com> Date: Wed, 13 May 2026 13:16:50 +0000 Subject: [PATCH 2/9] [getsentry/action-github-commit] Auto commit --- scripts/generate-md-exports.test.mjs | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/scripts/generate-md-exports.test.mjs b/scripts/generate-md-exports.test.mjs index 9bce820ae703fc..e5620a79bca0aa 100644 --- a/scripts/generate-md-exports.test.mjs +++ b/scripts/generate-md-exports.test.mjs @@ -39,9 +39,7 @@ function buildCodeTabsHTML(tabs) { const panels = tabs .map((t, i) => { - const filenameAttr = t.filename - ? ` data-code-tab-filename="${t.filename}"` - : ''; + const filenameAttr = t.filename ? ` data-code-tab-filename="${t.filename}"` : ''; const hiddenAttr = i > 0 ? ' hidden' : ''; return ( @@ -146,7 +144,12 @@ describe('rehypeExpandCodeTabs', () => { const standalone = '
npm install @sentry/node
'; const tabs = buildCodeTabsHTML([ - {title: 'Node.js', filename: 'instrument.mjs', lang: 'javascript', code: 'Sentry.init();'}, + { + title: 'Node.js', + filename: 'instrument.mjs', + lang: 'javascript', + code: 'Sentry.init();', + }, {title: 'Bun', lang: 'javascript', code: 'init();'}, ]); const html = `
${standalone}${tabs}
`; From 56c2da52d75d5922e803e1463edba68ffe7c84f7 Mon Sep 17 00:00:00 2001 From: s1gr1d <32902192+s1gr1d@users.noreply.github.com> Date: Wed, 13 May 2026 15:34:34 +0200 Subject: [PATCH 3/9] externalize plugin --- scripts/generate-md-exports.mjs | 68 +--------------------------- scripts/generate-md-exports.test.mjs | 2 +- scripts/rehype-expand-code-tabs.mjs | 66 +++++++++++++++++++++++++++ 3 files changed, 69 insertions(+), 67 deletions(-) create mode 100644 scripts/rehype-expand-code-tabs.mjs diff --git a/scripts/generate-md-exports.mjs b/scripts/generate-md-exports.mjs index 2ad5eae117cd5a..95c5a67e360f16 100644 --- a/scripts/generate-md-exports.mjs +++ b/scripts/generate-md-exports.mjs @@ -26,7 +26,8 @@ import RemarkLinkRewrite from 'remark-link-rewrite'; import remarkStringify from 'remark-stringify'; import {unified} from 'unified'; import {remove} from 'unist-util-remove'; -import {visit} from 'unist-util-visit'; + +import {rehypeExpandCodeTabs} from './rehype-expand-code-tabs.mjs'; const DOCS_ORIGIN = process.env.NEXT_PUBLIC_DEVELOPER_DOCS ? 'https://develop.sentry.dev' @@ -958,71 +959,6 @@ function extractContentForCacheKey(html) { return title + '\0' + canonical + '\0' + normalizedMain; } -/** - * Rehype plugin that expands CodeTabs panels for markdown export. - * - * CodeTabs renders all tab panels in the DOM but hides non-active ones with - * the `hidden` attribute. Each panel carries `data-code-tab-title` and - * optionally `data-code-tab-filename`. This plugin: - * 1. Removes the `hidden` attribute so rehypeRemark processes every panel - * 2. Strips CodeBlock chrome (filename display, copy button) from the panel - * 3. Inserts a bold heading (preferring filename, falling back to tab title) - * before the
 block so readers can tell which file the snippet
- *     belongs to
- */
-export function rehypeExpandCodeTabs() {
-  return tree => {
-    visit(tree, 'element', node => {
-      const title = node.properties?.dataCodeTabTitle;
-      if (!title) {
-        return;
-      }
-      delete node.properties.hidden;
-      const filename = node.properties?.dataCodeTabFilename;
-      delete node.properties.dataCodeTabTitle;
-      delete node.properties.dataCodeTabFilename;
-
-      const label = filename || title;
-      const preElements = collectAll(node, el => el.tagName === 'pre');
-      if (preElements.length === 0) {
-        return;
-      }
-
-      const heading = {
-        type: 'element',
-        tagName: 'p',
-        properties: {},
-        children: [
-          {
-            type: 'element',
-            tagName: 'strong',
-            properties: {},
-            children: [{type: 'text', value: label}],
-          },
-        ],
-      };
-
-      node.children = [heading, ...preElements];
-    });
-  };
-}
-
-function collectAll(node, predicate) {
-  const results = [];
-  (function walk(n) {
-    if (n.type === 'element' && predicate(n)) {
-      results.push(n);
-      return;
-    }
-    if (n.children) {
-      for (const child of n.children) {
-        walk(child);
-      }
-    }
-  })(node);
-  return results;
-}
-
 async function genMDFromHTML(source, {cacheDir, noCache, usedCacheFiles}) {
   const rawHTML = await readFile(source, {encoding: 'utf8'});
   // Strip build-specific HTML elements for faster parsing.
diff --git a/scripts/generate-md-exports.test.mjs b/scripts/generate-md-exports.test.mjs
index e5620a79bca0aa..b51f1bd90aea5a 100644
--- a/scripts/generate-md-exports.test.mjs
+++ b/scripts/generate-md-exports.test.mjs
@@ -6,7 +6,7 @@ import {describe, expect, it} from 'vitest';
 import {unified} from 'unified';
 import {remove} from 'unist-util-remove';
 
-import {rehypeExpandCodeTabs} from './generate-md-exports.mjs';
+import {rehypeExpandCodeTabs} from './rehype-expand-code-tabs.mjs';
 
 function htmlToMarkdown(html) {
   return String(
diff --git a/scripts/rehype-expand-code-tabs.mjs b/scripts/rehype-expand-code-tabs.mjs
new file mode 100644
index 00000000000000..99a10b09726093
--- /dev/null
+++ b/scripts/rehype-expand-code-tabs.mjs
@@ -0,0 +1,66 @@
+import {visit} from 'unist-util-visit';
+
+/**
+ * Rehype plugin that expands CodeTabs panels for markdown export.
+ *
+ * CodeTabs renders all tab panels in the DOM but hides non-active ones with
+ * the `hidden` attribute. Each panel carries `data-code-tab-title` and
+ * optionally `data-code-tab-filename`. This plugin:
+ *  1. Removes the `hidden` attribute so rehypeRemark processes every panel
+ *  2. Strips CodeBlock chrome (filename display, copy button) from the panel
+ *  3. Inserts a bold heading (preferring filename, falling back to tab title)
+ *     before the 
 block so readers can tell which file the snippet
+ *     belongs to
+ */
+export function rehypeExpandCodeTabs() {
+  return tree => {
+    visit(tree, 'element', node => {
+      const title = node.properties?.dataCodeTabTitle;
+      if (!title) {
+        return;
+      }
+      delete node.properties.hidden;
+      const filename = node.properties?.dataCodeTabFilename;
+      delete node.properties.dataCodeTabTitle;
+      delete node.properties.dataCodeTabFilename;
+
+      const label = filename || title;
+      const preElements = collectAll(node, el => el.tagName === 'pre');
+      if (preElements.length === 0) {
+        return;
+      }
+
+      const heading = {
+        type: 'element',
+        tagName: 'p',
+        properties: {},
+        children: [
+          {
+            type: 'element',
+            tagName: 'strong',
+            properties: {},
+            children: [{type: 'text', value: label}],
+          },
+        ],
+      };
+
+      node.children = [heading, ...preElements];
+    });
+  };
+}
+
+function collectAll(node, predicate) {
+  const results = [];
+  (function walk(n) {
+    if (n.type === 'element' && predicate(n)) {
+      results.push(n);
+      return;
+    }
+    if (n.children) {
+      for (const child of n.children) {
+        walk(child);
+      }
+    }
+  })(node);
+  return results;
+}

From 2fa66eced61e9a93d3b74dbffd6a2b40d1924550 Mon Sep 17 00:00:00 2001
From: s1gr1d <32902192+s1gr1d@users.noreply.github.com>
Date: Wed, 13 May 2026 16:22:16 +0200
Subject: [PATCH 4/9] annother approach

---
 scripts/generate-md-exports.test.mjs | 130 +++++++++++++++------------
 scripts/rehype-expand-code-tabs.mjs  |  70 +++++++++------
 src/components/codeTabs.tsx          |  12 +--
 src/remark-code-tabs.js              |  19 ++++
 4 files changed, 134 insertions(+), 97 deletions(-)

diff --git a/scripts/generate-md-exports.test.mjs b/scripts/generate-md-exports.test.mjs
index b51f1bd90aea5a..d4b25a5feaec0b 100644
--- a/scripts/generate-md-exports.test.mjs
+++ b/scripts/generate-md-exports.test.mjs
@@ -27,56 +27,74 @@ function htmlToMarkdown(html) {
 }
 
 /**
- * Builds a minimal CodeTabs HTML structure matching what the component renders.
- * Each tab panel contains the CodeBlock chrome (filename display, copy button)
- * wrapping the actual 
 block. The first tab is visible; subsequent
- * tabs have the `hidden` attribute.
+ * Builds HTML matching what the remark-code-tabs plugin + CodeTabs component
+ * produce in the static build output:
+ *
+ * 
+ * + *
+ * + *
+ * file1 + *
code1
+ *
+ *
+ * + * + * + *
*/ function buildCodeTabsHTML(tabs) { - const buttons = tabs - .map((t, i) => ``) - .join(''); - - const panels = tabs - .map((t, i) => { - const filenameAttr = t.filename ? ` data-code-tab-filename="${t.filename}"` : ''; - const hiddenAttr = i > 0 ? ' hidden' : ''; - + const firstTab = tabs[0]; + + const codeTabsRendered = + '
' + + tabs.map((t, i) => ``).join('') + + '
' + + `${firstTab.filename || ''}` + + `
${firstTab.code}
` + + '
' + + '
'; + + const exportBlocks = tabs + .map(t => { + const filenameAttr = t.filename + ? ` data-code-tab-filename="${t.filename}"` + : ''; return ( - `
` + - `
` + - `
` + - `${t.filename || ''}` + - `` + - `
` + - `
Copied
` + - `
${t.code}
` + - `
` + - `
` + `' ); }) .join(''); - return `
${buttons}
${panels}
`; + return `
${codeTabsRendered}${exportBlocks}
`; } describe('rehypeExpandCodeTabs', () => { - it('expands hidden tabs and labels each with a bold heading', () => { + it('replaces wrapper content with all tabs, not just the active one', () => { const html = buildCodeTabsHTML([ { title: 'Cloudflare Workers', - lang: 'javascript', + filename: 'index.ts', + lang: 'typescript', code: 'import { sentry } from "@sentry/hono/cloudflare";', }, { title: 'Node.js', - filename: 'instrument.mjs', - lang: 'javascript', - code: 'import * as Sentry from "@sentry/hono/node";', + filename: 'app.ts', + lang: 'typescript', + code: 'import { sentry } from "@sentry/hono/node";', }, { title: 'Bun', - lang: 'javascript', + filename: 'index.ts', + lang: 'typescript', code: 'import { sentry } from "@sentry/hono/bun";', }, ]); @@ -85,14 +103,26 @@ describe('rehypeExpandCodeTabs', () => { const codeBlocks = md.match(/```[\s\S]*?```/g); expect(codeBlocks).toHaveLength(3); - expect(md).toContain('**Cloudflare Workers**'); - expect(md).toContain('**instrument.mjs**'); - expect(md).toContain('**Bun**'); + expect(md).toContain('**index.ts**'); + expect(md).toContain('**app.ts**'); expect(codeBlocks[0]).toContain('@sentry/hono/cloudflare'); expect(codeBlocks[1]).toContain('@sentry/hono/node'); expect(codeBlocks[2]).toContain('@sentry/hono/bun'); }); + it('removes CodeTabs-rendered content (tab buttons, filename display, active tab)', () => { + const html = buildCodeTabsHTML([ + {title: 'ESM', filename: 'instrument.mjs', lang: 'javascript', code: 'import init'}, + {title: 'CJS', filename: 'instrument.js', lang: 'javascript', code: 'require init'}, + ]); + + const md = htmlToMarkdown(html); + + expect(md).not.toContain('`instrument.mjs`'); + expect(md).not.toContain('ESM'); + expect(md).not.toContain('CJS'); + }); + it('prefers filename over tab title for the heading', () => { const html = buildCodeTabsHTML([ {title: 'ESM', filename: 'instrument.mjs', lang: 'javascript', code: 'init();'}, @@ -110,25 +140,16 @@ describe('rehypeExpandCodeTabs', () => { it('falls back to tab title when no filename is set', () => { const html = buildCodeTabsHTML([ {title: 'Cloudflare Workers', lang: 'javascript', code: 'workers();'}, + {title: 'Bun', lang: 'javascript', code: 'bun();'}, ]); const md = htmlToMarkdown(html); expect(md).toContain('**Cloudflare Workers**'); + expect(md).toContain('**Bun**'); }); - it('strips CodeBlock chrome (filename display, copy button, copied indicator)', () => { - const html = buildCodeTabsHTML([ - {title: 'Node.js', filename: 'instrument.mjs', lang: 'javascript', code: 'init();'}, - ]); - - const md = htmlToMarkdown(html); - - expect(md).not.toContain('Copied'); - expect(md).not.toContain('`instrument.mjs`'); - }); - - it('does not affect code blocks outside of tabs', () => { + it('does not affect code blocks outside of tab wrappers', () => { const html = '
curl -sL https://sentry.io/get-cli/ | bash
'; @@ -144,12 +165,7 @@ describe('rehypeExpandCodeTabs', () => { const standalone = '
npm install @sentry/node
'; const tabs = buildCodeTabsHTML([ - { - title: 'Node.js', - filename: 'instrument.mjs', - lang: 'javascript', - code: 'Sentry.init();', - }, + {title: 'Node.js', filename: 'instrument.mjs', lang: 'javascript', code: 'Sentry.init();'}, {title: 'Bun', lang: 'javascript', code: 'init();'}, ]); const html = `
${standalone}${tabs}
`; @@ -184,13 +200,12 @@ describe('rehypeExpandCodeTabs', () => { expect(md).toContain('**config.rb**'); }); - it('handles tab panel with no pre element gracefully', () => { + it('skips export block with no pre element', () => { const html = - '
' + - '

Not a code block

' + - '' + + '
' + + '
active tab
' + + '' + + '' + '
'; const md = htmlToMarkdown(html); @@ -198,5 +213,6 @@ describe('rehypeExpandCodeTabs', () => { expect(md).toContain('**ok**'); expect(md).toContain('works()'); expect(md).not.toContain('**broken**'); + expect(md).not.toContain('active tab'); }); }); diff --git a/scripts/rehype-expand-code-tabs.mjs b/scripts/rehype-expand-code-tabs.mjs index 99a10b09726093..44183cc14d0aba 100644 --- a/scripts/rehype-expand-code-tabs.mjs +++ b/scripts/rehype-expand-code-tabs.mjs @@ -1,50 +1,62 @@ import {visit} from 'unist-util-visit'; /** - * Rehype plugin that expands CodeTabs panels for markdown export. + * Rehype plugin that expands CodeTabs for markdown export. * - * CodeTabs renders all tab panels in the DOM but hides non-active ones with - * the `hidden` attribute. Each panel carries `data-code-tab-title` and - * optionally `data-code-tab-filename`. This plugin: - * 1. Removes the `hidden` attribute so rehypeRemark processes every panel - * 2. Strips CodeBlock chrome (filename display, copy button) from the panel - * 3. Inserts a bold heading (preferring filename, falling back to tab title) - * before the
 block so readers can tell which file the snippet
- *     belongs to
+ * The remark-code-tabs plugin injects hidden 
+ * blocks alongside the interactive component inside each + * .code-tabs-wrapper. These hidden blocks contain the raw code for every + * tab and are always present in the static HTML (unlike CodeTabs output, + * which may only include the active tab due to RSC serialization). + * + * This plugin: + * 1. Finds parent elements that contain [data-code-tab-title] children + * 2. Replaces ALL children with expanded export blocks (removing the + * CodeTabs-rendered content to avoid duplication) + * 3. Each export block becomes a bold heading + fenced code block, + * preferring filename over tab title for the heading */ export function rehypeExpandCodeTabs() { return tree => { visit(tree, 'element', node => { - const title = node.properties?.dataCodeTabTitle; - if (!title) { + if (!node.children) { return; } - delete node.properties.hidden; - const filename = node.properties?.dataCodeTabFilename; - delete node.properties.dataCodeTabTitle; - delete node.properties.dataCodeTabFilename; - const label = filename || title; - const preElements = collectAll(node, el => el.tagName === 'pre'); - if (preElements.length === 0) { + const exportBlocks = node.children.filter( + child => child.type === 'element' && child.properties?.dataCodeTabTitle + ); + if (exportBlocks.length === 0) { return; } - const heading = { - type: 'element', - tagName: 'p', - properties: {}, - children: [ + node.children = exportBlocks.flatMap(block => { + const title = block.properties.dataCodeTabTitle; + const filename = block.properties.dataCodeTabFilename; + const label = filename || title; + + const preElements = collectAll(block, el => el.tagName === 'pre'); + if (preElements.length === 0) { + return []; + } + + return [ { type: 'element', - tagName: 'strong', + tagName: 'p', properties: {}, - children: [{type: 'text', value: label}], + children: [ + { + type: 'element', + tagName: 'strong', + properties: {}, + children: [{type: 'text', value: label}], + }, + ], }, - ], - }; - - node.children = [heading, ...preElements]; + ...preElements, + ]; + }); }); }; } diff --git a/src/components/codeTabs.tsx b/src/components/codeTabs.tsx index fa4d8784218631..bfe1a22be428b6 100644 --- a/src/components/codeTabs.tsx +++ b/src/components/codeTabs.tsx @@ -157,17 +157,7 @@ export function CodeTabs({children}: CodeTabProps) { {showSigninNote(codeBlocks[selectedTabIndex]) && } {buttons} - {codeBlocks.map((block, idx) => ( - - ))} + {codeBlocks[selectedTabIndex]} ); } diff --git a/src/remark-code-tabs.js b/src/remark-code-tabs.js index c613776cb27ac8..2f1bdb654accab 100644 --- a/src/remark-code-tabs.js +++ b/src/remark-code-tabs.js @@ -66,6 +66,24 @@ export default function remarkCodeTabs() { [] ); + const exportBlocks = pendingCode.map(([node]) => { + const title = getTabTitle(node); + const filename = getFilename(node); + const lang = fixLanguage(node); + const hProperties = { + hidden: true, + dataCodeTabTitle: title || lang, + }; + if (filename) { + hProperties.dataCodeTabFilename = filename; + } + return { + type: 'element', + data: {hName: 'div', hProperties}, + children: [{type: 'code', lang, value: node.value}], + }; + }); + rootNode.type = 'element'; rootNode.data = { hName: 'div', @@ -79,6 +97,7 @@ export default function remarkCodeTabs() { name: 'CodeTabs', children, }, + ...exportBlocks, ]; toRemove = toRemove.concat(pendingCode.splice(1)); From 45b839ac185818f751c44a426fd758ffd3e7c5da Mon Sep 17 00:00:00 2001 From: "getsantry[bot]" <66042841+getsantry[bot]@users.noreply.github.com> Date: Wed, 13 May 2026 14:23:29 +0000 Subject: [PATCH 5/9] [getsentry/action-github-commit] Auto commit --- scripts/generate-md-exports.test.mjs | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/scripts/generate-md-exports.test.mjs b/scripts/generate-md-exports.test.mjs index d4b25a5feaec0b..722118a0b6ddc2 100644 --- a/scripts/generate-md-exports.test.mjs +++ b/scripts/generate-md-exports.test.mjs @@ -62,9 +62,7 @@ function buildCodeTabsHTML(tabs) { const exportBlocks = tabs .map(t => { - const filenameAttr = t.filename - ? ` data-code-tab-filename="${t.filename}"` - : ''; + const filenameAttr = t.filename ? ` data-code-tab-filename="${t.filename}"` : ''; return ( `