|
| 1 | +/** |
| 2 | + * cpp-highlight.js - Lightweight C++ syntax highlighter |
| 3 | + * |
| 4 | + * Categories: |
| 5 | + * - keyword : Language keywords (const, while, if, etc.) |
| 6 | + * - string : String and character literals |
| 7 | + * - preprocessor: Preprocessor directives (#include, #define, etc.) |
| 8 | + * - comment : C and C++ style comments |
| 9 | + */ |
| 10 | + |
| 11 | +const CppHighlight = (function () { |
| 12 | + 'use strict' |
| 13 | + |
| 14 | + const KEYWORDS = new Set([ |
| 15 | + // Storage class |
| 16 | + 'auto', 'register', 'static', 'extern', 'mutable', 'thread_local', |
| 17 | + // Type qualifiers |
| 18 | + 'const', 'volatile', 'constexpr', 'consteval', 'constinit', |
| 19 | + // Type specifiers |
| 20 | + 'void', 'bool', 'char', 'short', 'int', 'long', 'float', 'double', |
| 21 | + 'signed', 'unsigned', 'wchar_t', 'char8_t', 'char16_t', 'char32_t', |
| 22 | + // Complex types |
| 23 | + 'class', 'struct', 'union', 'enum', 'typename', 'typedef', |
| 24 | + // Control flow |
| 25 | + 'if', 'else', 'switch', 'case', 'default', 'for', 'while', 'do', |
| 26 | + 'break', 'continue', 'return', 'goto', |
| 27 | + // Exception handling |
| 28 | + 'try', 'catch', 'throw', 'noexcept', |
| 29 | + // OOP |
| 30 | + 'public', 'private', 'protected', 'virtual', 'override', 'final', |
| 31 | + 'friend', 'this', 'operator', 'new', 'delete', |
| 32 | + // Templates |
| 33 | + 'template', 'concept', 'requires', |
| 34 | + // Namespace |
| 35 | + 'namespace', 'using', |
| 36 | + // Other |
| 37 | + 'sizeof', 'alignof', 'alignas', 'decltype', 'typeid', |
| 38 | + 'static_cast', 'dynamic_cast', 'const_cast', 'reinterpret_cast', |
| 39 | + 'static_assert', 'inline', 'explicit', 'export', 'module', 'import', |
| 40 | + 'co_await', 'co_yield', 'co_return', |
| 41 | + // Literals |
| 42 | + 'true', 'false', 'nullptr', 'NULL', |
| 43 | + ]) |
| 44 | + |
| 45 | + function escapeHtml (text) { |
| 46 | + return text |
| 47 | + .replace(/&/g, '&') |
| 48 | + .replace(/</g, '<') |
| 49 | + .replace(/>/g, '>') |
| 50 | + } |
| 51 | + |
| 52 | + function span (cls, content) { |
| 53 | + return `<span class="cpp-${cls}">${escapeHtml(content)}</span>` |
| 54 | + } |
| 55 | + |
| 56 | + function highlight (code) { |
| 57 | + const result = [] |
| 58 | + let i = 0 |
| 59 | + const n = code.length |
| 60 | + |
| 61 | + while (i < n) { |
| 62 | + // Line comment |
| 63 | + if (code[i] === '/' && code[i + 1] === '/') { |
| 64 | + let end = i + 2 |
| 65 | + while (end < n && code[end] !== '\n') end++ |
| 66 | + result.push(span('comment', code.slice(i, end))) |
| 67 | + i = end |
| 68 | + continue |
| 69 | + } |
| 70 | + |
| 71 | + // Block comment |
| 72 | + if (code[i] === '/' && code[i + 1] === '*') { |
| 73 | + let end = i + 2 |
| 74 | + while (end < n - 1 && !(code[end] === '*' && code[end + 1] === '/')) end++ |
| 75 | + end += 2 |
| 76 | + result.push(span('comment', code.slice(i, end))) |
| 77 | + i = end |
| 78 | + continue |
| 79 | + } |
| 80 | + |
| 81 | + // Preprocessor directive (at start of line or after whitespace) |
| 82 | + if (code[i] === '#') { |
| 83 | + // Check if # is at line start (allow leading whitespace) |
| 84 | + let checkPos = i - 1 |
| 85 | + while (checkPos >= 0 && (code[checkPos] === ' ' || code[checkPos] === '\t')) checkPos-- |
| 86 | + if (checkPos < 0 || code[checkPos] === '\n') { |
| 87 | + let end = i + 1 |
| 88 | + // Handle line continuation with backslash |
| 89 | + while (end < n) { |
| 90 | + if (code[end] === '\n') { |
| 91 | + if (code[end - 1] === '\\') { |
| 92 | + end++ |
| 93 | + continue |
| 94 | + } |
| 95 | + break |
| 96 | + } |
| 97 | + end++ |
| 98 | + } |
| 99 | + result.push(span('preprocessor', code.slice(i, end))) |
| 100 | + i = end |
| 101 | + continue |
| 102 | + } |
| 103 | + } |
| 104 | + |
| 105 | + // Raw string literal R"delimiter(...)delimiter" |
| 106 | + if (code[i] === 'R' && code[i + 1] === '"') { |
| 107 | + let delimEnd = i + 2 |
| 108 | + while (delimEnd < n && code[delimEnd] !== '(') delimEnd++ |
| 109 | + const delimiter = code.slice(i + 2, delimEnd) |
| 110 | + const endMarker = ')' + delimiter + '"' |
| 111 | + let end = delimEnd + 1 |
| 112 | + while (end < n) { |
| 113 | + if (code.slice(end, end + endMarker.length) === endMarker) { |
| 114 | + end += endMarker.length |
| 115 | + break |
| 116 | + } |
| 117 | + end++ |
| 118 | + } |
| 119 | + result.push(span('string', code.slice(i, end))) |
| 120 | + i = end |
| 121 | + continue |
| 122 | + } |
| 123 | + |
| 124 | + // String literal (with optional prefix) |
| 125 | + if (code[i] === '"' || |
| 126 | + ((code[i] === 'L' || code[i] === 'u' || code[i] === 'U') && code[i + 1] === '"') || |
| 127 | + (code[i] === 'u' && code[i + 1] === '8' && code[i + 2] === '"')) { |
| 128 | + const start = i |
| 129 | + if (code[i] === 'u' && code[i + 1] === '8') i += 2 |
| 130 | + else if (code[i] !== '"') i++ |
| 131 | + i++ // skip opening quote |
| 132 | + while (i < n && code[i] !== '"') { |
| 133 | + if (code[i] === '\\' && i + 1 < n) i += 2 |
| 134 | + else i++ |
| 135 | + } |
| 136 | + i++ // skip closing quote |
| 137 | + result.push(span('string', code.slice(start, i))) |
| 138 | + continue |
| 139 | + } |
| 140 | + |
| 141 | + // Character literal |
| 142 | + if (code[i] === '\'' || |
| 143 | + ((code[i] === 'L' || code[i] === 'u' || code[i] === 'U') && code[i + 1] === '\'') || |
| 144 | + (code[i] === 'u' && code[i + 1] === '8' && code[i + 2] === '\'')) { |
| 145 | + const start = i |
| 146 | + if (code[i] === 'u' && code[i + 1] === '8') i += 2 |
| 147 | + else if (code[i] !== '\'') i++ |
| 148 | + i++ // skip opening quote |
| 149 | + while (i < n && code[i] !== '\'') { |
| 150 | + if (code[i] === '\\' && i + 1 < n) i += 2 |
| 151 | + else i++ |
| 152 | + } |
| 153 | + i++ // skip closing quote |
| 154 | + result.push(span('string', code.slice(start, i))) |
| 155 | + continue |
| 156 | + } |
| 157 | + |
| 158 | + // Identifier or keyword |
| 159 | + if (/[a-zA-Z_]/.test(code[i])) { |
| 160 | + let end = i + 1 |
| 161 | + while (end < n && /[a-zA-Z0-9_]/.test(code[end])) end++ |
| 162 | + const word = code.slice(i, end) |
| 163 | + if (KEYWORDS.has(word)) { |
| 164 | + result.push(span('keyword', word)) |
| 165 | + } else { |
| 166 | + result.push(escapeHtml(word)) |
| 167 | + } |
| 168 | + i = end |
| 169 | + continue |
| 170 | + } |
| 171 | + |
| 172 | + // Default: single character |
| 173 | + result.push(escapeHtml(code[i])) |
| 174 | + i++ |
| 175 | + } |
| 176 | + |
| 177 | + return result.join('') |
| 178 | + } |
| 179 | + |
| 180 | + function highlightElement (el) { |
| 181 | + el.innerHTML = highlight(el.textContent) |
| 182 | + } |
| 183 | + |
| 184 | + function highlightAll (selector = 'code.cpp, code.c++, pre.cpp, pre.c++') { |
| 185 | + document.querySelectorAll(selector).forEach(highlightElement) |
| 186 | + } |
| 187 | + |
| 188 | + return { |
| 189 | + highlight, |
| 190 | + highlightElement, |
| 191 | + highlightAll, |
| 192 | + KEYWORDS, |
| 193 | + } |
| 194 | +})() |
| 195 | + |
| 196 | +// CommonJS / ES module support |
| 197 | +if (typeof module !== 'undefined' && module.exports) { |
| 198 | + module.exports = CppHighlight |
| 199 | +} |
0 commit comments