Implement a Comprehensive Code Block Rendering System in Astro

Implement a Comprehensive Code Block Rendering System in Astro

System Overview

Create a flexible, component-based code block rendering system for Astro that enhances the default markdown code blocks with the following features:
  1. Copy-to-clipboard button for all code blocks
  2. Language indicator showing the programming language
  3. Custom language support for specialized languages (e.g., litegal, dataview)
  4. Consistent styling across all code blocks
  5. Error boundaries to prevent rendering failures
  6. Extensibility for future language-specific enhancements

Technical Requirements

Component Architecture

Implement a hierarchical component system with the following structure:
  1. BaseCodeblock.astro: Core component providing shared functionality
    • Copy button with visual feedback
    • Language indicator
    • Consistent styling wrapper
    • Slot for language-specific extensions
  2. Language-specific components: Extend BaseCodeblock with specialized rendering
    • LitegalCodeblockDisplay.astro
    • DataviewCodeblockDisplay.astro
    • Additional language components as needed
  3. Remark Plugin: Transform markdown code blocks to appropriate components
    • Map language identifiers to specific components
    • Fall back to BaseCodeblock for standard languages
    • Preserve code content and metadata

Implementation Details

1. BaseCodeblock.astro

astro
banner_image: https://img.recraft.ai/LwOZPmW3HdvCUIb2RumalT5UO3cT0Nh-EUfUsH12Ubc/rs:fit:2048:1024:0/raw:1/plain/abs://external/images/950ea127-baae-419e-952f-4a02d7665f20
---
/**
 * BaseCodeblock.astro
 * 
 * Base component for rendering code blocks with a copy button.
 * This component is used by the remark-codeblocks plugin to transform
 * standard code blocks in markdown.
 */

interface Props {
  code: string;
  lang: string;
}

const { code, lang = 'text' } = Astro.props;
---

<div class="codeblock-container">
  <div class="codeblock-header">
    <span class="codeblock-language">{lang}</span>
    <button class="copy-button" aria-label="Copy code to clipboard">
      <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
        <rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect>
        <path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path>
      </svg>
    </button>
  </div>
  
  <pre class="codeblock" data-language={lang}><code set:html={code} /></pre>
  
  <slot />
</div>

<script>
  // Find all copy buttons
  const copyButtons = document.querySelectorAll('.copy-button');
  
  // Add click event listeners
  copyButtons.forEach(button => {
    button.addEventListener('click', () => {
      // Find the closest codeblock container
      const container = button.closest('.codeblock-container');
      if (!container) return;
      
      // Get the code content
      const codeElement = container.querySelector('code');
      if (!codeElement) return;
      
      // Copy to clipboard - get the text content to avoid HTML tags
      navigator.clipboard.writeText(codeElement.textContent || '')
        .then(() => {
          // Visual feedback
          button.classList.add('copied');
          setTimeout(() => {
            button.classList.remove('copied');
          }, 2000);
        })
        .catch(err => {
          console.error('Failed to copy: ', err);
        });
    });
  });
</script>

<style>
  .codeblock-container {
    position: relative;
    margin: 1.5rem 0;
    border-radius: 0.5rem;
    overflow: hidden;
    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
  }
  
  .codeblock-header {
    display: flex;
    justify-content: space-between;
    align-items: center;
    padding: 0.5rem 1rem;
    background-color: rgba(0, 0, 0, 0.2);
    border-top-left-radius: 0.5rem;
    border-top-right-radius: 0.5rem;
    font-family: var(--ff-monospace, monospace);
    font-size: 0.8rem;
  }
  
  .codeblock-language {
    text-transform: uppercase;
    font-weight: bold;
    color: var(--clr-code-lang, #8a8a8a);
    letter-spacing: 0.05em;
  }
  
  .copy-button {
    background: transparent;
    border: none;
    color: var(--clr-code-lang, #8a8a8a);
    cursor: pointer;
    padding: 0.25rem;
    border-radius: 0.25rem;
    transition: all 0.2s ease;
    display: flex;
    align-items: center;
    justify-content: center;
  }
  
  .copy-button:hover {
    background-color: rgba(255, 255, 255, 0.1);
    color: white;
  }
  
  .copy-button.copied {
    color: var(--clr-lossless-accent--brightest, #4a9eff);
  }
  
  .codeblock {
    margin: 0;
    border-top-left-radius: 0;
    border-top-right-radius: 0;
    padding: 1em;
    overflow-x: auto;
    background-color: var(--clr-code-bg, #1e1e1e);
  }
  
  .codeblock code {
    display: block;
    white-space: pre;
    word-wrap: normal;
    overflow-x: auto;
  }
</style>

2. Language-Specific Components

Create specialized components for custom languages that extend BaseCodeblock:
astro
---
// src/components/codeblocks/LitegalCodeblockDisplay.astro
import BaseCodeblock from './BaseCodeblock.astro';

interface Props {
  code: string;
  lang?: string;
}

const { code, lang = 'litegal' } = Astro.props;
---

<BaseCodeblock code={code} lang={lang}>
  <!-- Add language-specific enhancements here -->
  <style>
    /* Add litegal-specific styles here */
    .codeblock--litegal {
      background-color: #f4f4f4;
      border-left: 4px solid #4a9eff;
    }
  </style>
</BaseCodeblock>
astro
---
// src/components/codeblocks/DataviewCodeblockDisplay.astro
import BaseCodeblock from './BaseCodeblock.astro';

interface Props {
  code: string;
  lang?: string;
}

const { code, lang = 'dataview' } = Astro.props;
---

<BaseCodeblock code={code} lang={lang}>
  <!-- Add language-specific enhancements here -->
  <style>
    /* Add dataview-specific styles here */
    .codeblock--dataview {
      background-color: #f8f8f8;
      border-left: 4px solid #50fa7b;
    }
  </style>
</BaseCodeblock>

3. Remark Plugin for AST Transformation

Create a remark plugin that transforms code blocks in the Markdown AST:
typescript
/**
 * remark-codeblocks.ts
 * 
 * A remark plugin to transform code blocks in markdown to use custom components
 * based on the language specified.
 */

import { visit } from 'unist-util-visit';
import type { Root, Parent } from 'mdast';
import type { Plugin } from 'unified';
import { astDebugger } from '../debug/ast-debugger';

// Define the structure of a code node
interface Code {
  type: 'code';
  lang?: string;
  meta?: string;
  value: string;
}

// Define the structure of an MDX JSX node for our component
interface MdxJsxAttribute {
  type: 'mdxJsxAttribute';
  name: string;
  value: string;
}

interface MdxJsxFlowElement {
  type: 'mdxJsxFlowElement';
  name: string;
  attributes: MdxJsxAttribute[];
  children: any[];
  data?: { _mdxExplicitJsx: boolean };
}

/**
 * remarkCodeblocks
 * 
 * A remark plugin that transforms code blocks in markdown to use custom Astro components
 * based on the language specified.
 * 
 * @returns A transformer function that modifies the AST
 */
const remarkCodeblocks: Plugin<[], Root> = function() {
  return function transformer(tree: Root) {
    // Track transformations for debugging
    const transformations: string[] = [];
    
    try {
      visit(tree, 'code', (node: Code, index: number, parent: Parent | null) => {
        if (!parent) return;
        
        const lang = node.lang || 'text';
        
        // Determine which component to use based on language
        let componentName = 'BaseCodeblock';
        if (lang === 'litegal') {
          componentName = 'LitegalCodeblockDisplay';
        } else if (lang === 'dataview') {
          componentName = 'DataviewCodeblockDisplay';
        }
        // Add more language-specific components as needed
        
        // Create an MDX component node
        const mdxNode: MdxJsxFlowElement = {
          type: 'mdxJsxFlowElement',
          name: componentName,
          attributes: [
            {
              type: 'mdxJsxAttribute',
              name: 'code',
              value: node.value
            },
            {
              type: 'mdxJsxAttribute',
              name: 'lang',
              value: lang
            }
          ],
          children: [],
          data: { _mdxExplicitJsx: true }
        };
        
        // Replace the original code node with our custom component
        parent.children[index] = mdxNode as any;
        transformations.push(`transformed-codeblock-${lang}-to-${componentName}`);
      });
      
      // Debug output
      if (transformations.length > 0) {
        astDebugger.writeDebugFile('remark-codeblocks-transformations', {
          phase: 'remark-codeblocks',
          transformations
        });
      }
      
      return tree;
    } catch (error) {
      console.error('Error in remark-codeblocks:', error);
      astDebugger.writeDebugFile('remark-codeblocks-error', {
        phase: 'remark-codeblocks',
        error: error.message,
        stack: error.stack
      });
      return tree;
    }
  };
};

export default remarkCodeblocks;

4. Export Components for Easy Import

Create an index.ts file to export all components:
typescript
// src/components/codeblocks/index.ts
export { default as BaseCodeblock } from './BaseCodeblock.astro';
export { default as LitegalCodeblockDisplay } from './LitegalCodeblockDisplay.astro';
export { default as DataviewCodeblockDisplay } from './DataviewCodeblockDisplay.astro';

5. Astro Configuration

Update the Astro configuration to include the remark plugin and register custom languages:
javascript
// astro.config.mjs
import { defineConfig } from 'astro/config';
import remarkCodeblocks from './src/utils/markdown/remark-codeblocks';

export default defineConfig({
  // ... other config
  markdown: {
    remarkPlugins: [
      // ... other plugins
      remarkCodeblocks,
    ],
    syntaxHighlight: 'shiki',
    shikiConfig: {
      theme: 'github-dark',
      langs: [
        {
          id: 'litegal',
          scopeName: 'source.litegal',
          grammar: {
            patterns: [
              // Litegal syntax patterns
              { match: '\\b(function|return|if|else|for|while)\\b', name: 'keyword.control.litegal' },
              { match: '\\b(true|false|null|undefined)\\b', name: 'constant.language.litegal' },
              { match: '"[^"]*"', name: 'string.quoted.double.litegal' },
              { match: '\'[^\']*\'', name: 'string.quoted.single.litegal' },
              { match: '//.*$', name: 'comment.line.double-slash.litegal' },
              { match: '/\\*[^*]*\\*+([^/*][^*]*\\*+)*/', name: 'comment.block.litegal' },
              { match: '\\b[0-9]+\\b', name: 'constant.numeric.litegal' }
            ]
          }
        },
        {
          id: 'dataview',
          scopeName: 'source.dataview',
          grammar: {
            patterns: [
              // Dataview syntax patterns
              { match: '\\b(table|list|task|from|where|sort|group by)\\b', name: 'keyword.control.dataview' },
              { match: '\\b(file|tags|outlinks|inlinks)\\b', name: 'support.function.dataview' },
              { match: '"[^"]*"', name: 'string.quoted.double.dataview' },
              { match: '\'[^\']*\'', name: 'string.quoted.single.dataview' },
              { match: '//.*$', name: 'comment.line.double-slash.dataview' },
              { match: '\\b[0-9]+\\b', name: 'constant.numeric.dataview' }
            ]
          }
        }
      ]
    }
  }
});

Implementation Sequence

Follow this sequence to implement the code block rendering system:
  1. Create the base component structure
    • Implement BaseCodeblock.astro with copy button functionality
    • Add global styles for consistent code block appearance
  2. Implement language-specific components
    • Create LitegalCodeblockDisplay.astro and DataviewCodeblockDisplay.astro
    • Add specialized styling and functionality for each language
  3. Develop the remark plugin
    • Create the AST transformation logic
    • Map languages to appropriate components
    • Add error handling and debugging
  4. Update Astro configuration
    • Register custom languages with Shiki
    • Add the remark plugin to the processing pipeline
  5. Test and refine
    • Verify rendering of standard code blocks
    • Test custom language code blocks
    • Ensure copy button works correctly
    • Validate error handling

Error Handling and Debugging

Implement robust error handling to prevent rendering failures:
  1. AST Transformation Errors
    • Catch and log errors during AST transformation
    • Preserve original code block if transformation fails
    • Write detailed error information to debug files
  2. Component Rendering Errors
    • Add error boundaries around code block components
    • Provide fallback rendering for failed components
    • Log detailed error information
  3. Debugging Tools
    • Create utility to visualize AST at different stages
    • Add debug mode to log transformation details
    • Implement feature flags for enabling/disabling components

Performance Considerations

Optimize the code block rendering system for performance:
  1. Lazy Loading
    • Consider lazy loading language-specific components
    • Use dynamic imports for rarely used languages
  2. Caching
    • Cache syntax highlighting results when possible
    • Consider memoizing component rendering
  3. Minimal DOM Manipulation
    • Optimize client-side JavaScript for minimal DOM operations
    • Use event delegation for copy button handlers

Future Enhancements

Plan for these potential future enhancements:
  1. Line Highlighting
    • Add support for highlighting specific lines
    • Implement line number display
  2. Code Folding
    • Add ability to collapse/expand code sections
    • Implement fold markers for long code blocks
  3. Theme Switching
    • Support multiple syntax highlighting themes
    • Add theme toggle functionality
  4. Interactive Code Blocks
    • Add support for editable code blocks
    • Implement code execution for supported languages

Directory Structure

Organize the code block rendering system with this structure:
text
site/src/
├── components/
│   └── codeblocks/
│       ├── BaseCodeblock.astro         # Core component
│       ├── LitegalCodeblockDisplay.astro  # Language-specific component
│       ├── DataviewCodeblockDisplay.astro # Language-specific component
│       └── index.ts                    # Exports all components
├── utils/
│   └── markdown/
│       ├── remark-codeblocks.ts        # AST transformation plugin
│       └── debug/
│           └── ast-debugger.ts         # Debugging utilities
└── styles/
    └── codeblocks.css                  # Global styles (optional)

Testing Strategy

Implement a comprehensive testing strategy:
  1. Unit Tests
    • Test AST transformation logic
    • Verify component rendering
    • Test copy button functionality
  2. Integration Tests
    • Test end-to-end rendering of markdown with code blocks
    • Verify language detection and component selection
    • Test error handling and recovery
  3. Visual Regression Tests
    • Capture screenshots of rendered code blocks
    • Compare against baseline for visual changes
    • Test across different viewport sizes

Documentation

Create thorough documentation for the code block rendering system:
  1. Component API Documentation
    • Document props and usage for each component
    • Provide examples of custom language integration
  2. Developer Guide
    • Document the process for adding new language support
    • Explain the AST transformation pipeline
  3. User Guide
    • Document markdown syntax for code blocks
    • Explain available features and how to use them

Conclusion

This comprehensive code block rendering system provides a flexible, extensible solution for enhancing markdown code blocks in Astro. By following the component architecture and implementation sequence outlined above, you can create a robust system that supports both standard and custom languages while providing a consistent user experience with features like copy buttons and language indicators.
The system is designed to be maintainable and extensible, allowing for future enhancements while maintaining backward compatibility with existing markdown content.