From 40668b1167855ae0a7086fbe9c4fa6e76a76b3a8 Mon Sep 17 00:00:00 2001 From: Devon White Date: Sun, 9 Nov 2025 18:13:09 -0500 Subject: [PATCH 1/3] Retain lanaguage identifer in code blocks --- .../plugins/code-block-handler.ts | 125 +++++ .../transformation/plugins/plugin-registry.ts | 6 +- website/docs/test-mdx-tabs.mdx | 520 ++++++++++++++++++ 3 files changed, 650 insertions(+), 1 deletion(-) create mode 100644 packages/docusaurus-plugin-llms-txt/src/transformation/plugins/code-block-handler.ts create mode 100644 website/docs/test-mdx-tabs.mdx diff --git a/packages/docusaurus-plugin-llms-txt/src/transformation/plugins/code-block-handler.ts b/packages/docusaurus-plugin-llms-txt/src/transformation/plugins/code-block-handler.ts new file mode 100644 index 0000000..da9d421 --- /dev/null +++ b/packages/docusaurus-plugin-llms-txt/src/transformation/plugins/code-block-handler.ts @@ -0,0 +1,125 @@ +/** + * Copyright (c) SignalWire, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import type { Element } from 'hast'; +import type { Code } from 'mdast'; + +/** + * Extracts language identifier from className array. + * Looks for classes starting with 'language-' and extracts the language part. + * + * @param className - Array of class names from the element + * @returns Language identifier or null if not found + */ +function extractLanguage(className: unknown): string | null { + if (!Array.isArray(className)) { + return null; + } + + for (const cls of className) { + if (typeof cls === 'string' && cls.startsWith('language-')) { + return cls.replace('language-', ''); + } + } + + return null; +} + +/** + * Extracts text content from a hast node and its children, + * converting
elements to newlines. + * + * @param node - The hast node to extract text from + * @returns The text content with preserved line breaks + */ +function extractText(node: Element | { type: string; value?: string }): string { + // Text nodes have a value property + if ('value' in node && typeof node.value === 'string') { + return node.value; + } + + // Element nodes - check if it's a
tag + if ('tagName' in node && node.tagName === 'br') { + return '\n'; + } + + // Recursively extract text from children + if ('children' in node && Array.isArray(node.children)) { + return node.children + .map((child: Element | { type: string; value?: string }) => + extractText(child) + ) + .join(''); + } + + return ''; +} + +/** + * Custom handler for
 elements to preserve code block
+ * language identifiers.
+ *
+ * Docusaurus places language classes on 
 and parent 
elements, + * but not on elements. The default rehype-remark handler only + * checks elements, causing language identifiers to be lost. + * + * This handler: + * 1. Checks if
 contains a  child
+ * 2. Extracts language from 
 element's className
+ * 3. Falls back to checking parent element if needed
+ * 4. Extracts code content from  element
+ * 5. Returns proper mdast code node with language preserved
+ *
+ * @param h - Handler context from hast-util-to-mdast
+ * @param node - The 
 element from the hast tree
+ * @returns An mdast code node with language identifier
+ */
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
+export function handlePreElement(h: any, node: Element): Code | void {
+  // Verify this is a pre element
+  if (node.tagName !== 'pre') {
+    return undefined;
+  }
+
+  // Find the code element child
+  const codeElement = node.children?.find(
+    (child): child is Element =>
+      typeof child === 'object' &&
+      child !== null &&
+      'type' in child &&
+      child.type === 'element' &&
+      'tagName' in child &&
+      child.tagName === 'code'
+  );
+
+  if (!codeElement) {
+    // No code element found, let default handler process it
+    return undefined;
+  }
+
+  // Try to extract language from pre element first
+  let lang = extractLanguage(node.properties?.className);
+
+  // If not found on pre, check parent element (Docusaurus wrapper div)
+  if (!lang && h.augment && 'parent' in h && h.parent) {
+    const parent = h.parent as Element | undefined;
+    if (parent && 'properties' in parent) {
+      lang = extractLanguage(parent.properties?.className);
+    }
+  }
+
+  // Extract the code content from the code element
+  const value = extractText(codeElement);
+
+  // Return mdast code node with language (or null if not found)
+  return {
+    type: 'code',
+    lang,
+    meta: null,
+    value,
+  };
+}
diff --git a/packages/docusaurus-plugin-llms-txt/src/transformation/plugins/plugin-registry.ts b/packages/docusaurus-plugin-llms-txt/src/transformation/plugins/plugin-registry.ts
index 960062d..ff3c5e9 100644
--- a/packages/docusaurus-plugin-llms-txt/src/transformation/plugins/plugin-registry.ts
+++ b/packages/docusaurus-plugin-llms-txt/src/transformation/plugins/plugin-registry.ts
@@ -11,6 +11,7 @@ import remarkGfm from 'remark-gfm';
 import remarkStringify from 'remark-stringify';
 import { unified } from 'unified';
 
+import { handlePreElement } from './code-block-handler';
 import rehypeLinks from './rehype-links';
 import rehypeTables from './rehype-tables';
 
@@ -97,7 +98,10 @@ export class PluginRegistry {
 
     // Always last - converts HTML AST to Markdown AST
     processor.use(rehypeRemark, {
-      handlers: { br: () => ({ type: 'html', value: '
' }) }, + handlers: { + br: () => ({ type: 'html', value: '
' }), + pre: handlePreElement, + }, }); } diff --git a/website/docs/test-mdx-tabs.mdx b/website/docs/test-mdx-tabs.mdx new file mode 100644 index 0000000..769078f --- /dev/null +++ b/website/docs/test-mdx-tabs.mdx @@ -0,0 +1,520 @@ +--- +sidebar_position: 100 +title: Test MDX Tabs +--- + +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +# Testing Code Blocks in MDX Components + +This page tests whether code blocks inside MDX components (Tabs) preserve their language identifiers and formatting. + +## Web Languages + + + + +```javascript +function fetchUserData(userId) { + return fetch(`/api/users/${userId}`) + .then(response => response.json()) + .then(data => { + console.log('User data:', data); + return data; + }) + .catch(error => console.error('Error:', error)); +} +``` + + + + +```typescript +interface User { + id: number; + name: string; + email: string; +} + +async function fetchUserData(userId: number): Promise { + const response = await fetch(`/api/users/${userId}`); + const data: User = await response.json(); + return data; +} +``` + + + + +```html + + + + + Test Page + + +

Hello World

+

This is a test page.

+ + +``` + +
+ + +```css +.container { + display: flex; + justify-content: center; + align-items: center; + padding: 2rem; + background-color: #f0f0f0; +} + +.container h1 { + color: #333; + font-size: 2rem; +} +``` + + +
+ +## Backend Languages + + + + +```python +def calculate_fibonacci(n): + if n <= 1: + return n + + fib = [0, 1] + for i in range(2, n + 1): + fib.append(fib[i-1] + fib[i-2]) + + return fib[n] + +result = calculate_fibonacci(10) +print(f"Fibonacci(10) = {result}") +``` + + + + +```java +public class FibonacciCalculator { + public static int calculateFibonacci(int n) { + if (n <= 1) { + return n; + } + + int[] fib = new int[n + 1]; + fib[0] = 0; + fib[1] = 1; + + for (int i = 2; i <= n; i++) { + fib[i] = fib[i-1] + fib[i-2]; + } + + return fib[n]; + } +} +``` + + + + +```go +package main + +import "fmt" + +func calculateFibonacci(n int) int { + if n <= 1 { + return n + } + + fib := make([]int, n+1) + fib[0] = 0 + fib[1] = 1 + + for i := 2; i <= n; i++ { + fib[i] = fib[i-1] + fib[i-2] + } + + return fib[n] +} + +func main() { + fmt.Printf("Fibonacci(10) = %d\n", calculateFibonacci(10)) +} +``` + + + + +```rust +fn calculate_fibonacci(n: usize) -> u64 { + if n <= 1 { + return n as u64; + } + + let mut fib = vec![0u64; n + 1]; + fib[0] = 0; + fib[1] = 1; + + for i in 2..=n { + fib[i] = fib[i-1] + fib[i-2]; + } + + fib[n] +} + +fn main() { + println!("Fibonacci(10) = {}", calculate_fibonacci(10)); +} +``` + + + + +```php + +``` + + + + +## Shell and Configuration + + + + +```bash +#!/bin/bash + +# Function to calculate factorial +factorial() { + local n=$1 + if [ $n -le 1 ]; then + echo 1 + else + local prev=$(factorial $((n-1))) + echo $((n * prev)) + fi +} + +result=$(factorial 5) +echo "Factorial of 5 is: $result" +``` + + + + +```shell +#!/bin/sh + +# Deploy script +echo "Starting deployment..." + +npm install +npm run build +npm run test + +if [ $? -eq 0 ]; then + echo "Build successful, deploying..." + rsync -avz build/ user@server:/var/www/ +else + echo "Build failed, aborting deployment" + exit 1 +fi +``` + + + + +```yaml +name: CI/CD Pipeline +on: + push: + branches: [main] + pull_request: + branches: [main] + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Setup Node + uses: actions/setup-node@v2 + with: + node-version: '18' + - run: npm install + - run: npm test +``` + + + + +```json +{ + "name": "my-app", + "version": "1.0.0", + "scripts": { + "start": "node index.js", + "test": "jest", + "build": "webpack --mode production" + }, + "dependencies": { + "express": "^4.18.0", + "react": "^18.2.0" + } +} +``` + + + + +```toml +[package] +name = "my-app" +version = "1.0.0" +edition = "2021" + +[dependencies] +serde = { version = "1.0", features = ["derive"] } +tokio = { version = "1.0", features = ["full"] } + +[dev-dependencies] +criterion = "0.5" +``` + + + + +## Data and Query Languages + + + + +```sql +SELECT + u.id, + u.name, + u.email, + COUNT(o.id) as order_count, + SUM(o.total) as total_spent +FROM users u +LEFT JOIN orders o ON u.id = o.user_id +WHERE u.created_at > '2024-01-01' +GROUP BY u.id, u.name, u.email +HAVING COUNT(o.id) > 5 +ORDER BY total_spent DESC +LIMIT 10; +``` + + + + +```graphql +query GetUserWithOrders($userId: ID!) { + user(id: $userId) { + id + name + email + orders(first: 10, orderBy: CREATED_AT_DESC) { + edges { + node { + id + total + createdAt + items { + productName + quantity + } + } + } + } + } +} +``` + + + + +## Mobile Languages + + + + +```swift +struct ContentView: View { + @State private var count = 0 + + var body: some View { + VStack { + Text("Count: \(count)") + .font(.largeTitle) + .padding() + + Button(action: { + count += 1 + }) { + Text("Increment") + .padding() + .background(Color.blue) + .foregroundColor(.white) + .cornerRadius(8) + } + } + } +} +``` + + + + +```kotlin +class MainActivity : AppCompatActivity() { + private var count = 0 + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_main) + + val button = findViewById