(line: string, lang: string)
| 172 | * reorders characters. |
| 173 | */ |
| 174 | export function tokenizeCodeLine(line: string, lang: string): CodeToken[] { |
| 175 | const fam = langFamily(lang); |
| 176 | const tokens: CodeToken[] = []; |
| 177 | const kw = KEYWORDS[fam]; |
| 178 | const cmt = commentMarker(fam); |
| 179 | let plain = ''; |
| 180 | const flush = () => { if (plain) { tokens.push({ text: plain }); plain = ''; } }; |
| 181 | let i = 0; |
| 182 | while (i < line.length) { |
| 183 | const rest = line.slice(i); |
| 184 | // Line comment to end of line. |
| 185 | if (cmt && rest.startsWith(cmt)) { flush(); tokens.push({ text: rest, dim: true }); break; } |
| 186 | // Same-line block comment (or to EOL if unterminated). |
| 187 | if ((fam === 'js' || fam === 'css' || fam === 'go' || fam === 'rust' || fam === 'php') && rest.startsWith('/*')) { |
| 188 | const end = rest.indexOf('*/'); |
| 189 | const seg = end >= 0 ? rest.slice(0, end + 2) : rest; |
| 190 | flush(); tokens.push({ text: seg, dim: true }); i += seg.length; continue; |
| 191 | } |
| 192 | const ch = line[i]!; |
| 193 | // Strings (double, single, backtick). Unterminated → consume to EOL (still verbatim). |
| 194 | if (ch === '"' || ch === "'" || ch === '`') { |
| 195 | let j = i + 1; |
| 196 | while (j < line.length) { |
| 197 | if (line[j] === '\\') { j += 2; continue; } |
| 198 | if (line[j] === ch) { j++; break; } |
| 199 | j++; |
| 200 | } |
| 201 | flush(); tokens.push({ text: line.slice(i, j), color: 'green' }); i = j; continue; |
| 202 | } |
| 203 | // Numbers. |
| 204 | if (ch >= '0' && ch <= '9') { |
| 205 | const nm = rest.match(/^\d[\d_]*\.?\d*([eExX][+-]?[0-9a-fA-F]+)?/); |
| 206 | if (nm) { flush(); tokens.push({ text: nm[0], color: 'yellow' }); i += nm[0].length; continue; } |
| 207 | } |
| 208 | // Identifiers / keywords. |
| 209 | if (/[A-Za-z_$]/.test(ch)) { |
| 210 | const idm = rest.match(/^[A-Za-z_$][A-Za-z0-9_$]*/)!; |
| 211 | const word = idm[0]; |
| 212 | if (kw && kw.has(word)) { flush(); tokens.push({ text: word, color: 'magenta' }); } |
| 213 | else { plain += word; } |
| 214 | i += word.length; continue; |
| 215 | } |
| 216 | plain += ch; i++; |
| 217 | } |
| 218 | flush(); |
| 219 | return tokens.length ? tokens : [{ text: line }]; |
| 220 | } |
| 221 | |
| 222 | function CodeBlock({ lang, body, incomplete }: { lang: string; body: string; incomplete?: boolean }): React.ReactElement { |
| 223 | const lines = body.split('\n'); |
no test coverage detected