|
135 | 135 | word-break: break-word; |
136 | 136 | } |
137 | 137 |
|
| 138 | + /* Subtle styling for visualization glyphs (interpunct, NUL, etc.) */ |
| 139 | + .viz-char { |
| 140 | + opacity: 0.3; |
| 141 | + } |
| 142 | + |
138 | 143 | .label { |
139 | 144 | display: inline-flex; |
140 | 145 | align-items: center; |
@@ -454,7 +459,8 @@ <h2 id="summary-header">Summary</h2> |
454 | 459 | const decoder = new TextDecoder("utf-8", { fatal: false }); |
455 | 460 | const MAX_PREVIEW = 512; |
456 | 461 | const MAX_HEX_BYTES = 256; |
457 | | - const INTERPUNCT = "\u00B7"; |
| 462 | + const INTERPUNCT = "\u00B7"; // · (space/whitespace visualizer) |
| 463 | + const NULL_SYMBOL = "\u2300"; // ⌀ (lighter null marker) |
458 | 464 |
|
459 | 465 | const elements = { |
460 | 466 | fileInput: document.getElementById("corpus-input"), |
@@ -482,24 +488,42 @@ <h2 id="summary-header">Summary</h2> |
482 | 488 | } |
483 | 489 |
|
484 | 490 | function sanitizeString(bytes) { |
| 491 | + // Return safe HTML with visualization glyphs wrapped in a span for reduced opacity. |
485 | 492 | if (!bytes || !bytes.length) { |
486 | 493 | return ""; |
487 | 494 | } |
488 | 495 | try { |
489 | 496 | const truncated = bytes.length > MAX_PREVIEW ? bytes.slice(0, MAX_PREVIEW) : bytes; |
490 | 497 | const decoded = decoder.decode(truncated); |
491 | | - return visualizeWhitespace(decoded); |
| 498 | + return renderVisibleHTML(decoded); |
492 | 499 | } catch (err) { |
493 | 500 | console.debug("Failed to decode as UTF-8", err); |
494 | 501 | return ""; |
495 | 502 | } |
496 | 503 | } |
497 | 504 |
|
498 | | - function visualizeWhitespace(text) { |
499 | | - if (!text) { |
500 | | - return ""; |
| 505 | + function escapeHTML(s) { |
| 506 | + return s.replace(/[&<>"']/g, (ch) => ( |
| 507 | + ch === '&' ? '&' : ch === '<' ? '<' : ch === '>' ? '>' : ch === '"' ? '"' : ''' |
| 508 | + )); |
| 509 | + } |
| 510 | + |
| 511 | + function renderVisibleHTML(text) { |
| 512 | + if (!text) return ""; |
| 513 | + // Build escaped HTML, substituting visualization spans for NULs and whitespace. |
| 514 | + let out = ""; |
| 515 | + for (let i = 0; i < text.length; i += 1) { |
| 516 | + const ch = text[i]; |
| 517 | + const code = text.charCodeAt(i); |
| 518 | + if (code === 0x0000) { |
| 519 | + out += `<span class="viz-char" title="NUL (0x00)">${NULL_SYMBOL}</span>`; |
| 520 | + } else if (ch === ' ' || ch === '\t' || ch === '\v' || ch === '\f' || ch === '\r' || ch === '\u00A0') { |
| 521 | + out += `<span class="viz-char" title="Whitespace">${INTERPUNCT}</span>`; |
| 522 | + } else { |
| 523 | + out += escapeHTML(ch); |
| 524 | + } |
501 | 525 | } |
502 | | - return text.replace(/[\u00A0 \t\v\f\r]/g, INTERPUNCT); |
| 526 | + return out; |
503 | 527 | } |
504 | 528 |
|
505 | 529 | function toHex(bytes) { |
@@ -573,7 +597,7 @@ <h2 id="summary-header">Summary</h2> |
573 | 597 | const previewCell = document.createElement("td"); |
574 | 598 | previewCell.className = "mono"; |
575 | 599 | previewCell.dataset.label = "Preview"; |
576 | | - previewCell.textContent = row.preview || "\u00a0"; |
| 600 | + previewCell.innerHTML = row.preview || "\u00a0"; |
577 | 601 |
|
578 | 602 | const hexCell = document.createElement("td"); |
579 | 603 | hexCell.className = "mono"; |
|
0 commit comments