All files / src/components/common CodeBlock.tsx

84% Statements 42/50
80.43% Branches 37/46
100% Functions 9/9
83.67% Lines 41/49

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207                      1x 18x     18x           18x   2x   2x     4x     4x 2x   2x 2x               16x   1x   1x     2x     2x 1x   1x 1x         15x   1x   1x     3x     3x 1x   2x 2x             18x                                                                       1x               20x   20x 2x 2x 1x 1x   1x         20x 18x 18x           20x                                                               5x                                                    
import React, { useState, useMemo } from "react";
import { CodeBlockProps } from "../../types/componentPropExports";
 
/**
 * Simple syntax highlighting for common tokens
 * No external libraries - just basic regex-based highlighting
 * 
 * Uses single-pass tokenizer approach to avoid matching keywords inside
 * comments or strings. The combined regex matches all patterns at once,
 * and the replacement function decides how to highlight each match.
 */
const highlightCode = (code: string, language?: string): string => {
  let highlighted = code;
 
  // Escape HTML
  highlighted = highlighted
    .replace(/&/g, "&")
    .replace(/</g, "&lt;")
    .replace(/>/g, "&gt;");
 
  // Apply syntax highlighting based on language using single-pass tokenizer
  if (language === "typescript" || language === "javascript" || language === "jsx" || language === "tsx") {
    // Combined pattern matches comments, strings, keywords, and numbers in one pass
    const tsPattern = /(\/\/.*$|\/\*[\s\S]*?\*\/)|("(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*'|`(?:[^`\\]|\\.)*`)|(\b(?:const|let|var|function|return|if|else|for|while|class|interface|type|import|export|from|default|async|await|try|catch|throw|new|this|extends|implements|public|private|protected|static|readonly)\b)|(\b\d+\b)/gm;
    
    highlighted = highlighted.replace(
      tsPattern,
      (match: string, comment: string, str: string, keyword: string, num: string): string => {
        Iif (comment) {
          return `<span class="text-gray-500 dark:text-gray-400 italic">${comment}</span>`;
        }
        if (str) {
          return `<span class="text-green-600 dark:text-green-400">${str}</span>`;
        }
        Eif (keyword) {
          return `<span class="text-purple-600 dark:text-purple-400">${keyword}</span>`;
        }
        if (num) {
          return `<span class="text-blue-600 dark:text-blue-400">${num}</span>`;
        }
        return match;
      }
    );
  } else if (language === "python") {
    // Combined pattern for Python
    const pythonPattern = /(#.*$)|("""[\s\S]*?"""|'''[\s\S]*?'''|"(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*')|(\b(?:def|class|if|elif|else|for|while|return|import|from|as|try|except|finally|with|lambda|yield|raise|pass|break|continue|True|False|None)\b)/gm;
    
    highlighted = highlighted.replace(
      pythonPattern,
      (match: string, comment: string, str: string, keyword: string): string => {
        Iif (comment) {
          return `<span class="text-gray-500 dark:text-gray-400 italic">${comment}</span>`;
        }
        if (str) {
          return `<span class="text-green-600 dark:text-green-400">${str}</span>`;
        }
        Eif (keyword) {
          return `<span class="text-purple-600 dark:text-purple-400">${keyword}</span>`;
        }
        return match;
      }
    );
  } else if (language === "bash" || language === "shell") {
    // Combined pattern for Bash
    const bashPattern = /(#.*$)|("(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*')|(\b(?:if|then|else|elif|fi|for|do|done|while|case|esac|function|return|exit|cd|ls|mkdir|rm|cp|mv|echo|cat|grep|sed|awk)\b)/gm;
    
    highlighted = highlighted.replace(
      bashPattern,
      (match: string, comment: string, str: string, keyword: string): string => {
        Iif (comment) {
          return `<span class="text-gray-500 dark:text-gray-400 italic">${comment}</span>`;
        }
        if (str) {
          return `<span class="text-green-600 dark:text-green-400">${str}</span>`;
        }
        Eif (keyword) {
          return `<span class="text-purple-600 dark:text-purple-400">${keyword}</span>`;
        }
        return match;
      }
    );
  }
 
  return highlighted;
};
 
/**
 * CodeBlock component - displays code with optional syntax highlighting and copy functionality
 * 
 * ## Business Perspective
 * 
 * Provides clear, readable code examples to technical teams implementing
 * security controls, improving comprehension and reducing implementation errors.
 * Critical for effective knowledge transfer and documentation. 📝
 * 
 * ## Technical Perspective
 * 
 * This component provides theme-aware code display without external dependencies.
 * It uses simple regex-based syntax highlighting for common languages (TypeScript,
 * JavaScript, Python, Bash). Supports copy-to-clipboard functionality and optional
 * line numbers.
 * 
 * **Security Note**: The 'code' prop should only contain trusted content. This
 * component uses dangerouslySetInnerHTML for syntax highlighting with HTML spans.
 * While HTML characters are escaped (&, <, >) before processing, the code input
 * should not come from untrusted user sources to prevent potential XSS risks.
 * 
 * @component
 * 
 * @example
 * ```tsx
 * <CodeBlock
 *   language="typescript"
 *   code={`const hello = "world";\nconsole.log(hello);`}
 *   showLineNumbers={true}
 *   copyable={true}
 * />
 * ```
 */
const CodeBlock: React.FC<CodeBlockProps> = ({
  language,
  code,
  showLineNumbers = false,
  copyable = true,
  className = "",
  testId = "code-block",
}) => {
  const [copied, setCopied] = useState(false);
 
  const handleCopy = async (): Promise<void> => {
    try {
      await navigator.clipboard.writeText(code);
      setCopied(true);
      setTimeout(() => setCopied(false), 2000);
    } catch (err) {
      console.error("Failed to copy code:", err);
    }
  };
 
  // Memoize highlighted code and lines to avoid re-computing on every render
  const { highlightedCode, highlightedLines } = useMemo(() => {
    const highlighted = highlightCode(code, language);
    return {
      highlightedCode: highlighted,
      highlightedLines: highlighted.split("\n"),
    };
  }, [code, language]);
 
  return (
    <div
      className={`relative rounded-md overflow-hidden ${className}`}
      data-testid={testId}
    >
      {/* Header with language and copy button */}
      <div className="flex items-center justify-between px-lg py-md bg-gray-100 dark:bg-gray-800 border-b border-gray-200 dark:border-gray-700">
        <span className="text-xs font-medium text-gray-600 dark:text-gray-400">
          {language || "code"}
        </span>
        {copyable && (
          <button
            onClick={handleCopy}
            className="text-xs px-md py-xs rounded bg-gray-200 dark:bg-gray-700 text-gray-700 dark:text-gray-300 hover:bg-gray-300 dark:hover:bg-gray-600 transition-colors"
            data-testid={`${testId}-copy-button`}
            aria-label="Copy code to clipboard"
          >
            <span aria-live="polite" aria-atomic="true">
              {copied ? "Copied!" : "Copy"}
            </span>
          </button>
        )}
      </div>
 
      {/* Code content */}
      <div className="bg-gray-50 dark:bg-gray-900 overflow-x-auto">
        <pre className="p-lg text-sm">
          <code className="font-mono">
            {showLineNumbers ? (
              <table className="w-full border-collapse">
                <tbody>
                  {highlightedLines.map((line, index) => (
                    <tr key={index}>
                      <td className="pr-lg text-right text-gray-400 dark:text-gray-600 select-none align-top">
                        {index + 1}
                      </td>
                      <td
                        className="text-gray-800 dark:text-gray-200"
                        dangerouslySetInnerHTML={{ __html: line || "&nbsp;" }}
                      />
                    </tr>
                  ))}
                </tbody>
              </table>
            ) : (
              <div
                className="text-gray-800 dark:text-gray-200"
                dangerouslySetInnerHTML={{ __html: highlightedCode }}
              />
            )}
          </code>
        </pre>
      </div>
    </div>
  );
};
 
export default CodeBlock;