|
| 1 | +/** |
| 2 | + * Client-side C syntax highlighter for RDoc |
| 3 | + */ |
| 4 | + |
| 5 | +(function() { |
| 6 | + 'use strict'; |
| 7 | + |
| 8 | + // C control flow and storage class keywords |
| 9 | + const C_KEYWORDS = new Set([ |
| 10 | + 'auto', 'break', 'case', 'continue', 'default', 'do', 'else', 'extern', |
| 11 | + 'for', 'goto', 'if', 'inline', 'register', 'return', 'sizeof', 'static', |
| 12 | + 'switch', 'while', |
| 13 | + '_Alignas', '_Alignof', '_Generic', '_Noreturn', '_Static_assert', '_Thread_local' |
| 14 | + ]); |
| 15 | + |
| 16 | + // C type keywords and type qualifiers |
| 17 | + const C_TYPE_KEYWORDS = new Set([ |
| 18 | + 'bool', 'char', 'const', 'double', 'enum', 'float', 'int', 'long', |
| 19 | + 'restrict', 'short', 'signed', 'struct', 'typedef', 'union', 'unsigned', |
| 20 | + 'void', 'volatile', '_Atomic', '_Bool', '_Complex', '_Imaginary' |
| 21 | + ]); |
| 22 | + |
| 23 | + // Library-defined types (typedef'd in headers, not language keywords) |
| 24 | + // Includes: Ruby C API types (VALUE, ID), POSIX types (size_t, ssize_t), |
| 25 | + // fixed-width integer types (uint32_t, int64_t), and standard I/O types (FILE) |
| 26 | + const C_TYPES = new Set([ |
| 27 | + 'VALUE', 'ID', 'size_t', 'ssize_t', 'ptrdiff_t', 'uintptr_t', 'intptr_t', |
| 28 | + 'uint8_t', 'uint16_t', 'uint32_t', 'uint64_t', |
| 29 | + 'int8_t', 'int16_t', 'int32_t', 'int64_t', |
| 30 | + 'FILE', 'DIR', 'va_list' |
| 31 | + ]); |
| 32 | + |
| 33 | + // Common Ruby VALUE macros and boolean literals |
| 34 | + const RUBY_MACROS = new Set([ |
| 35 | + 'Qtrue', 'Qfalse', 'Qnil', 'Qundef', 'NULL', 'TRUE', 'FALSE', 'true', 'false' |
| 36 | + ]); |
| 37 | + |
| 38 | + const OPERATORS = new Set([ |
| 39 | + '==', '!=', '<=', '>=', '&&', '||', '<<', '>>', '++', '--', |
| 40 | + '+=', '-=', '*=', '/=', '%=', '&=', '|=', '^=', '->', |
| 41 | + '+', '-', '*', '/', '%', '<', '>', '=', '!', '&', '|', '^', '~' |
| 42 | + ]); |
| 43 | + |
| 44 | + // Single character that can start an operator |
| 45 | + const OPERATOR_CHARS = new Set('+-*/%<>=!&|^~'); |
| 46 | + |
| 47 | + function isMacro(word) { |
| 48 | + return RUBY_MACROS.has(word) || /^[A-Z][A-Z0-9_]*$/.test(word); |
| 49 | + } |
| 50 | + |
| 51 | + function isType(word) { |
| 52 | + return C_TYPE_KEYWORDS.has(word) || C_TYPES.has(word) || /_t$/.test(word); |
| 53 | + } |
| 54 | + |
| 55 | + /** |
| 56 | + * Escape HTML special characters |
| 57 | + */ |
| 58 | + function escapeHtml(text) { |
| 59 | + return text |
| 60 | + .replace(/&/g, '&') |
| 61 | + .replace(/</g, '<') |
| 62 | + .replace(/>/g, '>') |
| 63 | + .replace(/"/g, '"') |
| 64 | + .replace(/'/g, '''); |
| 65 | + } |
| 66 | + |
| 67 | + /** |
| 68 | + * Check if position is at line start (only whitespace before it) |
| 69 | + */ |
| 70 | + function isLineStart(code, pos) { |
| 71 | + if (pos === 0) return true; |
| 72 | + for (let i = pos - 1; i >= 0; i--) { |
| 73 | + const ch = code[i]; |
| 74 | + if (ch === '\n') return true; |
| 75 | + if (ch !== ' ' && ch !== '\t') return false; |
| 76 | + } |
| 77 | + return true; |
| 78 | + } |
| 79 | + |
| 80 | + /** |
| 81 | + * Highlight C source code |
| 82 | + */ |
| 83 | + function highlightC(code) { |
| 84 | + const tokens = []; |
| 85 | + let i = 0; |
| 86 | + const len = code.length; |
| 87 | + |
| 88 | + while (i < len) { |
| 89 | + const char = code[i]; |
| 90 | + |
| 91 | + // Multi-line comment |
| 92 | + if (char === '/' && code[i + 1] === '*') { |
| 93 | + let end = code.indexOf('*/', i + 2); |
| 94 | + end = (end === -1) ? len : end + 2; |
| 95 | + const comment = code.substring(i, end); |
| 96 | + tokens.push('<span class="c-comment">', escapeHtml(comment), '</span>'); |
| 97 | + i = end; |
| 98 | + continue; |
| 99 | + } |
| 100 | + |
| 101 | + // Single-line comment |
| 102 | + if (char === '/' && code[i + 1] === '/') { |
| 103 | + const end = code.indexOf('\n', i); |
| 104 | + const commentEnd = (end === -1) ? len : end; |
| 105 | + const comment = code.substring(i, commentEnd); |
| 106 | + tokens.push('<span class="c-comment">', escapeHtml(comment), '</span>'); |
| 107 | + i = commentEnd; |
| 108 | + continue; |
| 109 | + } |
| 110 | + |
| 111 | + // Preprocessor directive (must be at line start) |
| 112 | + if (char === '#' && isLineStart(code, i)) { |
| 113 | + let end = i + 1; |
| 114 | + while (end < len && code[end] !== '\n') { |
| 115 | + if (code[end] === '\\' && end + 1 < len && code[end + 1] === '\n') { |
| 116 | + end += 2; // Handle line continuation |
| 117 | + } else { |
| 118 | + end++; |
| 119 | + } |
| 120 | + } |
| 121 | + const preprocessor = code.substring(i, end); |
| 122 | + tokens.push('<span class="c-preprocessor">', escapeHtml(preprocessor), '</span>'); |
| 123 | + i = end; |
| 124 | + continue; |
| 125 | + } |
| 126 | + |
| 127 | + // String literal |
| 128 | + if (char === '"') { |
| 129 | + let end = i + 1; |
| 130 | + while (end < len && code[end] !== '"') { |
| 131 | + if (code[end] === '\\' && end + 1 < len) { |
| 132 | + end += 2; // Skip escaped character |
| 133 | + } else { |
| 134 | + end++; |
| 135 | + } |
| 136 | + } |
| 137 | + if (end < len) end++; // Include closing quote |
| 138 | + const string = code.substring(i, end); |
| 139 | + tokens.push('<span class="c-string">', escapeHtml(string), '</span>'); |
| 140 | + i = end; |
| 141 | + continue; |
| 142 | + } |
| 143 | + |
| 144 | + // Character literal |
| 145 | + if (char === "'") { |
| 146 | + let end = i + 1; |
| 147 | + // Handle escape sequences like '\n', '\\', '\'' |
| 148 | + if (end < len && code[end] === '\\' && end + 1 < len) { |
| 149 | + end += 2; // Skip backslash and escaped char |
| 150 | + } else if (end < len) { |
| 151 | + end++; // Single character |
| 152 | + } |
| 153 | + if (end < len && code[end] === "'") end++; // Closing quote |
| 154 | + const charLit = code.substring(i, end); |
| 155 | + tokens.push('<span class="c-value">', escapeHtml(charLit), '</span>'); |
| 156 | + i = end; |
| 157 | + continue; |
| 158 | + } |
| 159 | + |
| 160 | + // Number (integer or float) |
| 161 | + if (char >= '0' && char <= '9') { |
| 162 | + let end = i; |
| 163 | + |
| 164 | + // Hexadecimal |
| 165 | + if (char === '0' && (code[i + 1] === 'x' || code[i + 1] === 'X')) { |
| 166 | + end = i + 2; |
| 167 | + while (end < len) { |
| 168 | + const ch = code[end]; |
| 169 | + if ((ch >= '0' && ch <= '9') || (ch >= 'a' && ch <= 'f') || (ch >= 'A' && ch <= 'F')) { |
| 170 | + end++; |
| 171 | + } else { |
| 172 | + break; |
| 173 | + } |
| 174 | + } |
| 175 | + } |
| 176 | + // Octal |
| 177 | + else if (char === '0' && code[i + 1] >= '0' && code[i + 1] <= '7') { |
| 178 | + end = i + 1; |
| 179 | + while (end < len && code[end] >= '0' && code[end] <= '7') end++; |
| 180 | + } |
| 181 | + // Decimal/Float |
| 182 | + else { |
| 183 | + while (end < len) { |
| 184 | + const ch = code[end]; |
| 185 | + if ((ch >= '0' && ch <= '9') || ch === '.') { |
| 186 | + end++; |
| 187 | + } else { |
| 188 | + break; |
| 189 | + } |
| 190 | + } |
| 191 | + // Scientific notation |
| 192 | + if (end < len && (code[end] === 'e' || code[end] === 'E')) { |
| 193 | + end++; |
| 194 | + if (end < len && (code[end] === '+' || code[end] === '-')) end++; |
| 195 | + while (end < len && code[end] >= '0' && code[end] <= '9') end++; |
| 196 | + } |
| 197 | + } |
| 198 | + |
| 199 | + // Suffix (u, l, f, etc.) |
| 200 | + while (end < len) { |
| 201 | + const ch = code[end]; |
| 202 | + if (ch === 'u' || ch === 'U' || ch === 'l' || ch === 'L' || ch === 'f' || ch === 'F') { |
| 203 | + end++; |
| 204 | + } else { |
| 205 | + break; |
| 206 | + } |
| 207 | + } |
| 208 | + |
| 209 | + const number = code.substring(i, end); |
| 210 | + tokens.push('<span class="c-value">', escapeHtml(number), '</span>'); |
| 211 | + i = end; |
| 212 | + continue; |
| 213 | + } |
| 214 | + |
| 215 | + // Identifier or keyword |
| 216 | + if ((char >= 'a' && char <= 'z') || (char >= 'A' && char <= 'Z') || char === '_') { |
| 217 | + let end = i + 1; |
| 218 | + while (end < len) { |
| 219 | + const ch = code[end]; |
| 220 | + if ((ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') || |
| 221 | + (ch >= '0' && ch <= '9') || ch === '_') { |
| 222 | + end++; |
| 223 | + } else { |
| 224 | + break; |
| 225 | + } |
| 226 | + } |
| 227 | + const word = code.substring(i, end); |
| 228 | + |
| 229 | + if (C_KEYWORDS.has(word)) { |
| 230 | + tokens.push('<span class="c-keyword">', escapeHtml(word), '</span>'); |
| 231 | + } else if (isType(word)) { |
| 232 | + // Check types before macros (VALUE, ID are types, not macros) |
| 233 | + tokens.push('<span class="c-type">', escapeHtml(word), '</span>'); |
| 234 | + } else if (isMacro(word)) { |
| 235 | + tokens.push('<span class="c-macro">', escapeHtml(word), '</span>'); |
| 236 | + } else { |
| 237 | + // Check if followed by '(' -> function name |
| 238 | + let nextCharIdx = end; |
| 239 | + while (nextCharIdx < len && (code[nextCharIdx] === ' ' || code[nextCharIdx] === '\t')) { |
| 240 | + nextCharIdx++; |
| 241 | + } |
| 242 | + if (nextCharIdx < len && code[nextCharIdx] === '(') { |
| 243 | + tokens.push('<span class="c-function">', escapeHtml(word), '</span>'); |
| 244 | + } else { |
| 245 | + tokens.push('<span class="c-identifier">', escapeHtml(word), '</span>'); |
| 246 | + } |
| 247 | + } |
| 248 | + i = end; |
| 249 | + continue; |
| 250 | + } |
| 251 | + |
| 252 | + // Operators |
| 253 | + if (OPERATOR_CHARS.has(char)) { |
| 254 | + let op = char; |
| 255 | + // Check for two-character operators |
| 256 | + if (i + 1 < len) { |
| 257 | + const twoChar = char + code[i + 1]; |
| 258 | + if (OPERATORS.has(twoChar)) { |
| 259 | + op = twoChar; |
| 260 | + } |
| 261 | + } |
| 262 | + tokens.push('<span class="c-operator">', escapeHtml(op), '</span>'); |
| 263 | + i += op.length; |
| 264 | + continue; |
| 265 | + } |
| 266 | + |
| 267 | + // Everything else (punctuation, whitespace) |
| 268 | + tokens.push(escapeHtml(char)); |
| 269 | + i++; |
| 270 | + } |
| 271 | + |
| 272 | + return tokens.join(''); |
| 273 | + } |
| 274 | + |
| 275 | + /** |
| 276 | + * Initialize C syntax highlighting on page load |
| 277 | + */ |
| 278 | + function initHighlighting() { |
| 279 | + const codeBlocks = document.querySelectorAll('pre[data-language="c"]'); |
| 280 | + |
| 281 | + codeBlocks.forEach(block => { |
| 282 | + if (block.getAttribute('data-highlighted') === 'true') { |
| 283 | + return; |
| 284 | + } |
| 285 | + |
| 286 | + const code = block.textContent; |
| 287 | + const highlighted = highlightC(code); |
| 288 | + |
| 289 | + block.innerHTML = highlighted; |
| 290 | + block.setAttribute('data-highlighted', 'true'); |
| 291 | + }); |
| 292 | + } |
| 293 | + |
| 294 | + if (document.readyState === 'loading') { |
| 295 | + document.addEventListener('DOMContentLoaded', initHighlighting); |
| 296 | + } else { |
| 297 | + initHighlighting(); |
| 298 | + } |
| 299 | +})(); |
0 commit comments