Rouge Syntax Highlighter Demo
Rouge Syntax Highlighter Demo
This page demonstrates the MIJUG workspace’s accessible code syntax highlighting using Rouge with WCAG 2.1 AA compliance. All code blocks are keyboard navigable, screen reader friendly, and include copy functionality.
Web Technologies
HTML
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>MIJUG Accessible Code Example</title>
<meta name="description" content="WCAG 2.1 AA compliant code examples" />
</head>
<body>
<header role="banner">
<h1>Accessible Code Samples</h1>
</header>
<main id="content" role="main">
<section aria-labelledby="section-heading">
<h2 id="section-heading">WCAG Compliance</h2>
<p>This example follows accessibility best practices.</p>
</section>
</main>
<footer role="contentinfo">
<p>© 2025 MIJUG Workspace</p>
</footer>
</body>
</html>
CSS
/* MIJUG Theme Accessible Styles */
:root {
--mijug-blue: #1a3660;
--mijug-green: #1e8449;
--mijug-background: #f8f9fa;
--mijug-text: #333333;
}
/* Accessible contrast ratios (WCAG 2.1 AA) */
body {
font-family: "Inter", system-ui, sans-serif;
line-height: 1.5;
color: var(--mijug-text);
background-color: var(--mijug-background);
}
.code-container {
border: 2px solid var(--mijug-blue);
border-radius: 4px;
margin: 1.5rem 0;
position: relative;
}
/* Focus styles for keyboard navigation */
.highlight:focus {
outline: 3px solid var(--mijug-green);
outline-offset: 2px;
}
@media (prefers-reduced-motion: reduce) {
* {
transition: none !important;
}
}
JavaScript
// MIJUG Accessible Code Features
class AccessibilityHelper {
constructor() {
this.focusableElements = 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])';
}
enableKeyboardTrap(element) {
const focusable = Array.from(element.querySelectorAll(this.focusableElements));
element.addEventListener("keydown", (e) => {
if (e.key === "Tab") {
if (e.shiftKey && document.activeElement === focusable[0]) {
e.preventDefault();
focusable[focusable.length - 1].focus();
} else if (!e.shiftKey && document.activeElement === focusable[focusable.length - 1]) {
e.preventDefault();
focusable[0].focus();
}
}
});
}
announceToScreenReader(message) {
const announcer = document.getElementById("a11y-announcer") || this.createAnnouncer();
announcer.textContent = message;
}
createAnnouncer() {
const announcer = document.createElement("div");
announcer.id = "a11y-announcer";
announcer.setAttribute("aria-live", "polite");
announcer.setAttribute("aria-atomic", "true");
announcer.className = "sr-only";
document.body.appendChild(announcer);
return announcer;
}
}
// Initialize accessibility features
document.addEventListener("DOMContentLoaded", () => {
const a11yHelper = new AccessibilityHelper();
// Enable keyboard trap for modal dialogs
const modals = document.querySelectorAll('[role="dialog"]');
modals.forEach((modal) => a11yHelper.enableKeyboardTrap(modal));
// Announce when content changes
const loadingStates = document.querySelectorAll("[data-loading]");
loadingStates.forEach((element) => {
element.addEventListener("statechange", (e) => {
a11yHelper.announceToScreenReader(`Content ${e.detail.state}`);
});
});
});
TypeScript
/**
* MIJUG Code Highlighter with Accessibility Features
* Provides copy functionality and keyboard navigation for code blocks
* WCAG 2.1 AA compliant with proper focus management
*/
interface CodeBlock extends HTMLElement {
querySelector(selector: string): HTMLElement | null;
}
class CodeHighlighter {
private readonly copyButtonClass = "copy-button";
private readonly highlightSelector = ".highlight";
private readonly containerSelector = ".highlighter-rouge";
private readonly copySuccessDelay = 2000;
constructor() {
this.init();
}
private init(): void {
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", () => this.setupCodeBlocks());
} else {
this.setupCodeBlocks();
}
}
private setupCodeBlocks(): void {
const codeBlocks = document.querySelectorAll<CodeBlock>(this.highlightSelector);
codeBlocks.forEach((block) => {
this.addCopyButton(block);
this.addKeyboardNavigation(block);
});
}
private addCopyButton(block: CodeBlock): void {
const button = this.createCopyButton();
const container = block.closest(this.containerSelector) as HTMLElement;
if (container) {
container.appendChild(button);
button.addEventListener("click", () => this.handleCopyClick(button, block));
button.addEventListener("keydown", (e) => this.handleCopyKeydown(e, button));
}
}
private createCopyButton(): HTMLButtonElement {
const button = document.createElement("button");
button.className = this.copyButtonClass;
button.textContent = "Copy";
button.setAttribute("aria-label", "Copy code to clipboard");
button.setAttribute("type", "button");
button.setAttribute("tabindex", "0");
return button;
}
private async handleCopyClick(button: HTMLButtonElement, block: CodeBlock): Promise<void> {
try {
const code = block.querySelector("code");
if (!code?.textContent) {
throw new Error("No code content found to copy");
}
const text = code.textContent.trim();
if (!navigator?.clipboard?.writeText) {
throw new Error("Clipboard API not supported in this browser");
}
await navigator.clipboard.writeText(text);
this.showCopySuccess(button);
} catch (error) {
console.error("Copy failed:", error);
this.showCopyError(button);
this.fallbackCopy(block, button);
}
}
private fallbackCopy(block: CodeBlock, button: HTMLButtonElement): void {
try {
const code = block.querySelector("code");
if (!code?.textContent) return;
const textarea = document.createElement("textarea");
textarea.value = code.textContent.trim();
textarea.style.position = "fixed";
textarea.style.opacity = "0";
textarea.style.pointerEvents = "none";
document.body.appendChild(textarea);
textarea.select();
const success = document.execCommand("copy");
document.body.removeChild(textarea);
if (success) {
this.showCopySuccess(button);
} else {
throw new Error("Fallback copy failed");
}
} catch (error) {
console.error("Fallback copy failed:", error);
this.showCopyError(button);
}
}
private handleCopyKeydown(event: KeyboardEvent, button: HTMLButtonElement): void {
if (event.key === "Enter" || event.key === " ") {
event.preventDefault();
button.click();
}
if (event.key === "Escape") {
const codeBlock = button.closest(this.containerSelector)?.querySelector(this.highlightSelector) as HTMLElement;
if (codeBlock) {
codeBlock.focus();
}
}
}
private addKeyboardNavigation(block: CodeBlock): void {
if (!block.hasAttribute("tabindex")) {
block.setAttribute("tabindex", "0");
}
block.addEventListener("keydown", (event) => {
if (event.key === "Tab" && !event.shiftKey) {
const copyButton = block.parentElement?.querySelector(`.${this.copyButtonClass}`) as HTMLElement;
if (copyButton) {
event.preventDefault();
copyButton.focus();
}
}
if ((event.key === "Enter" || event.key === " ") && event.ctrlKey) {
event.preventDefault();
const copyButton = block.parentElement?.querySelector(`.${this.copyButtonClass}`) as HTMLButtonElement;
if
My Info Just Under Glass