A Pen by mode-mercury on CodePen.
Created
October 27, 2025 04:16
-
-
Save mode-mercury/a0b35352980ae5aed4557ef4ad60cd4e to your computer and use it in GitHub Desktop.
Untitled
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <html lang="en"> | |
| <head> | |
| <meta charset="utf-8" /> | |
| <meta name="viewport" content="width=device-width,initial-scale=1" /> | |
| <title>Whitespace & Stego Inspector</title> | |
| <style> | |
| :root{--bg:#0f1724;--card:#0b1220;--muted:#9aa4b2;--accent:#7dd3fc;--danger:#ff6b6b;--glass: rgba(255,255,255,0.03)} | |
| html,body{height:100%;margin:0;font-family:Inter,ui-sans-serif,system-ui,-apple-system,"Segoe UI",Roboto,'Helvetica Neue',Arial} | |
| body{background:linear-gradient(180deg,#071026 0%,#071824 60%);color:#e6eef6;padding:28px} | |
| .container{max-width:1200px;margin:0 auto} | |
| .header{display:flex;gap:16px;align-items:center;margin-bottom:18px} | |
| .logo{width:56px;height:56px;border-radius:10px;background:linear-gradient(135deg,var(--accent),#60a5fa);display:flex;align-items:center;justify-content:center;color:#04263b;font-weight:700;box-shadow:0 6px 18px rgba(13,40,60,0.4)} | |
| .h1{font-size:20px;font-weight:600} | |
| .lead{color:var(--muted);font-size:13px} | |
| .grid{display:grid;grid-template-columns:1fr 420px;gap:18px} | |
| .card{background:var(--card);padding:14px;border-radius:12px;box-shadow:0 6px 24px rgba(2,6,23,0.6);border:1px solid rgba(255,255,255,0.03)} | |
| textarea{width:100%;min-height:260px;padding:12px;border-radius:8px;background:transparent;color:inherit;border:1px dashed rgba(255,255,255,0.04);resize:vertical;font-family:ui-monospace, SFMono-Regular, Menlo, Monaco, monospace} | |
| .controls{display:flex;gap:8px;flex-wrap:wrap;margin-top:10px} | |
| .btn{background:var(--glass);border:1px solid rgba(255,255,255,0.02);padding:8px 10px;border-radius:8px;color:var(--accent);cursor:pointer;font-weight:600} | |
| .btn.ghost{background:transparent;color:var(--muted);border:1px solid rgba(255,255,255,0.02)} | |
| .small{font-size:12px;padding:6px 8px} | |
| .analysis{margin-top:12px;display:flex;flex-direction:column;gap:10px} | |
| .row{display:flex;gap:12px;align-items:flex-start} | |
| .panel{flex:1;background:linear-gradient(180deg, rgba(255,255,255,0.01), rgba(255,255,255,0.01));padding:10px;border-radius:8px;border:1px solid rgba(255,255,255,0.02)} | |
| .code{font-family:ui-monospace, SFMono-Regular, Menlo, Monaco, monospace;font-size:13px;background:rgba(0,0,0,0.12);padding:10px;border-radius:6px;overflow:auto;max-height:220px} | |
| .hint{color:var(--muted);font-size:12px} | |
| .badge{display:inline-block;padding:6px 8px;background:rgba(255,255,255,0.02);border-radius:8px;font-weight:600} | |
| .highlight{background:linear-gradient(90deg, rgba(125,211,252,0.12), rgba(96,165,250,0.06));border-radius:6px;padding:6px} | |
| .table{display:grid;grid-template-columns:1fr 200px;gap:8px} | |
| .kv{display:flex;justify-content:space-between;gap:8px} | |
| .legend{font-size:12px;color:var(--muted)} | |
| .visual{white-space:pre-wrap;word-break:break-word;font-family:ui-monospace,monospace;background:rgba(0,0,0,0.06);padding:8px;border-radius:6px;max-height:360px;overflow:auto} | |
| .result{font-size:13px;color:var(--muted)} | |
| .notice{font-size:13px;color:var(--muted);margin-top:8px} | |
| .footer{margin-top:18px;color:var(--muted);font-size:12px} | |
| .token{display:inline-block;background:#062231;padding:6px 8px;border-radius:6px;color:#8ddafc;font-weight:700} | |
| .line-num{opacity:0.28;margin-right:8px} | |
| .invis{display:inline-block;padding:1px 6px;border-radius:4px;margin:0 1px;font-size:11px;background:rgba(255,255,255,0.02);color:var(--muted);border:1px solid rgba(255,255,255,0.02)} | |
| .highlight-warning{border-left:4px solid var(--danger);padding-left:10px} | |
| .dropzone{border:2px dashed rgba(255,255,255,0.03);padding:12px;border-radius:8px;text-align:center;color:var(--muted)} | |
| .form-row{display:flex;gap:8px;align-items:center} | |
| .opt-label{font-size:12px;color:var(--muted)} | |
| .input{background:transparent;border:1px solid rgba(255,255,255,0.03);padding:6px 8px;border-radius:6px;color:inherit} | |
| </style> | |
| </head> | |
| <body> | |
| <div class="container"> | |
| <div class="header"> | |
| <div class="logo">WS</div> | |
| <div> | |
| <div class="h1">Whitespace & Stego Inspector — Extended</div> | |
| <div class="lead">Now supports configurable whitespace mappings, expanded confusable detection, additional text‑based steg methods (zero‑width binary, whitespace‑Morse), and custom rules.</div> | |
| </div> | |
| </div> <div class="grid"> | |
| <div class="card"> | |
| <div style="display:flex;gap:12px;align-items:flex-start;"> | |
| <div style="flex:1"> | |
| <label class="hint">Input (paste, type, or drop a .txt)</label> | |
| <textarea id="input" placeholder="Paste text here or drop a text file..."></textarea> | |
| </div> | |
| <div style="width:220px;display:flex;flex-direction:column;gap:8px;"> | |
| <button id="analyze" class="btn">Analyze</button> | |
| <button id="visualize" class="btn ghost small">Visualize Invisibles</button> | |
| <button id="clear" class="btn ghost small">Clear</button> <div style="margin-top:8px;"> | |
| <div class="opt-label">Whitespace mapping</div> | |
| <select id="whitespaceMap" class="input" style="width:100%;margin-top:6px;"> | |
| <option value="space0tab1">space=0 / tab=1 (SNOW default)</option> | |
| <option value="space1tab0">space=1 / tab=0 (reverse)</option> | |
| <option value="space0tab1-nbsp">space=0 / tab=1 + NBSP=1</option> | |
| <option value="custom">Custom mapping (see box)</option> | |
| </select> | |
| </div> | |
| <div style="margin-top:6px;"> | |
| <div class="opt-label">Byte order</div> | |
| <select id="byteOrder" class="input" style="width:100%;margin-top:6px;"> | |
| <option value="lsb">LSB-first (common SNOW)</option> | |
| <option value="msb">MSB-first (straight binary)</option> | |
| </select> | |
| </div> | |
| <div style="margin-top:6px;"> | |
| <div class="opt-label">Bits per byte</div> | |
| <input id="bitsPerByte" class="input" style="width:100%;margin-top:6px;" value="8" /> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="controls"> | |
| <label class="badge">Zero-width binary</label> | |
| <label class="badge">Whitespace‑Morse</label> | |
| <label class="badge">Punctuation bits</label> | |
| <label class="badge">Expanded confusables</label> | |
| </div> | |
| <div class="analysis"> | |
| <div id="summary" class="panel"></div> | |
| <div class="row"> | |
| <div class="panel"> | |
| <div style="font-weight:700;margin-bottom:8px">Invisible / Zero-width characters</div> | |
| <div id="zerowidth" class="code">—</div> | |
| <div class="hint">Shows zero-width and control characters and their code points. Also offers zero-width → binary mapping options in the inspector panel.</div> | |
| </div> | |
| <div class="panel"> | |
| <div style="font-weight:700;margin-bottom:8px">Unicode confusables</div> | |
| <div id="confusables" class="code">—</div> | |
| <div class="hint">Flags characters that look like ASCII but come from other scripts (Cyrillic, Greek, Latin‑extended). Editable list below for custom checks.</div> | |
| </div> | |
| </div> | |
| <div class="row"> | |
| <div class="panel"> | |
| <div style="font-weight:700;margin-bottom:8px">SNOW / Whitespace extraction</div> | |
| <div id="snow" class="code">—</div> | |
| <div class="hint">Extracts bits from trailing/leading whitespace. Choose mapping and byte order at right. Supports custom mapping via a JSON-like string (e.g. {" ":"0"," ":"1"," ":"1"}).</div> | |
| </div> | |
| <div class="panel"> | |
| <div style="font-weight:700;margin-bottom:8px">Punctuation → Morse / Bits</div> | |
| <div id="punctmorse" class="code">—</div> | |
| <div class="hint">Detects dot/dash sequences, sentence‑end punctuation mapping to bits (.=0, !=1 by default). Toggle rules in inspector.</div> | |
| </div> | |
| </div> | |
| <div class="panel"> | |
| <div style="font-weight:700;margin-bottom:8px">Visualization (invisibles replaced with markers)</div> | |
| <div id="visual" class="visual">—</div> | |
| <div class="hint">Hover markers to see codepoint info. Useful to visually inspect hidden content.</div> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="card"> | |
| <div style="font-weight:700;margin-bottom:8px">Inspector tools & options</div> | |
| <div class="panel" style="margin-bottom:8px;"> | |
| <div class="kv"><div class="legend">Characters</div><div id="charcount" class="result">0</div></div> | |
| <div class="kv"><div class="legend">Lines</div><div id="linecount" class="result">0</div></div> | |
| <div class="kv"><div class="legend">Zero-width found</div><div id="zwcount" class="result">0</div></div> | |
| <div class="kv"><div class="legend">Confusable glyphs</div><div id="cfcount" class="result">0</div></div> | |
| </div> | |
| <div class="panel highlight-warning"> | |
| <div style="font-weight:700;margin-bottom:8px">Quick decode</div> | |
| <div style="display:flex;gap:8px;align-items:center;margin-bottom:8px;"><button id="decodeSnow" class="btn small">Decode Whitespace</button><button id="decodePunct" class="btn small ghost">Decode Punctuation</button></div> | |
| <div id="quickout" class="code">—</div> | |
| </div> | |
| <div class="panel" style="margin-top:8px;"> | |
| <div style="font-weight:700;margin-bottom:8px">Zero-width → Binary options</div> | |
| <div class="form-row"><label class="opt-label">Map:</label> | |
| <select id="zwMap" class="input" style="width:170px;"><option value="zw-space">ZW-space=0 ZW-joiner=1</option><option value="zw-presence">presence-of-zw=1</option><option value="zw-index">index-mod2</option></select> | |
| </div> | |
| <div class="hint" style="margin-top:8px">Choose how zero-width characters are converted to bits when decoding. These heuristics help when different creators used different schemes.</div> | |
| </div> | |
| <div class="panel" style="margin-top:8px;"> | |
| <div style="font-weight:700;margin-bottom:8px">Custom confusables (one per line: char TAB meaning)</div> | |
| <textarea id="customConf" style="min-height:120px;">А Cyrillic A | |
| е Cyrillic e | |
| О Cyrillic O | |
| Ι Greek Iota | |
| Ϲ Greek/Greek-like S</textarea> | |
| <div class="hint">Edit or paste your own confusable list. Entries are parsed as: character [tab] description.</div> | |
| </div> | |
| <div style="margin-top:12px;display:flex;gap:8px;align-items:center;"> | |
| <button id="copyVis" class="btn small">Copy Visual</button> | |
| <button id="copyRaw" class="btn small ghost">Copy Raw</button> | |
| <button id="download" class="btn small">Download .txt</button> | |
| </div> | |
| <div class="dropzone" id="dropzone">Drop a .txt file here to load text</div> | |
| <div class="footer"> | |
| <div>Built to inspect hidden whitespace, zero-width, confusables, punctuation Morse, and more. Runs locally in your browser.</div> | |
| <div class="notice">Tip: Try toggling byte order (LSB/MSB) if decoded text looks jumbled.</div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> <script> | |
| // ------------------ Utilities & Config ------------------ | |
| const DEFAULT_ZERO_WIDTH = { | |
| '': 'ZERO WIDTH SPACE', | |
| '': 'ZERO WIDTH NON-JOINER', | |
| '': 'ZERO WIDTH JOINER', | |
| '': 'ZERO WIDTH NO-BREAK SPACE (BOM)', | |
| '': 'WORD JOINER', | |
| '': 'MONGOLIAN VOWEL SEPARATOR' | |
| }; | |
| // Expanded confusable set (examples) - can be overridden by custom confusables textarea | |
| let CONFUSABLE_PATTERNS = [ | |
| {char:'А',looksLike:'A',desc:'Cyrillic CAPITAL A U+0410'}, | |
| {char:'В',looksLike:'B',desc:'Cyrillic CAPITAL VE U+0412'}, | |
| {char:'С',looksLike:'C',desc:'Cyrillic CAPITAL ES U+0421'}, | |
| {char:'Е',looksLike:'E',desc:'Cyrillic CAPITAL IE U+0415'}, | |
| {char:'О',looksLike:'O',desc:'Cyrillic CAPITAL O U+041E'}, | |
| {char:'а',looksLike:'a',desc:'Cyrillic SMALL A U+0430'}, | |
| {char:'е',looksLike:'e',desc:'Cyrillic SMALL IE U+0435'}, | |
| {char:'о',looksLike:'o',desc:'Cyrillic SMALL O U+043E'}, | |
| {char:'Ι',looksLike:'I',desc:'Greek CAPITAL IOTA U+0399'}, | |
| {char:'Μ',looksLike:'M',desc:'Greek CAPITAL MU U+039C'}, | |
| {char:'Ϲ',looksLike:'C',desc:'Greek Lunate Sigma-like U+03F9'}, | |
| {char:'₩',looksLike:'W',desc:'Won sign used like W U+20A9'}, | |
| {char:'۱',looksLike:'1',desc:'Arabic-Indic digit one U+06F1'}, | |
| {char:'٠',looksLike:'0',desc:'Arabic-Indic digit zero U+0660'}, | |
| {char:'ⅼ',looksLike:'l',desc:'Roman Numeral Fifty? small L U+217C'} | |
| ]; | |
| const MORSE = { 'A':'.-','B':'-...','C':'-.-.','D':'-..','E':'.','F':'..-.','G':'--.','H':'....','I':'..','J':'.---','K':'-.-','L':'.-..','M':'--','N':'-.','O':'---','P':'.--.','Q':'--.-','R':'.-.','S':'...','T':'-','U':'..-','V':'...-','W':'.--','X':'-..-','Y':'-.--','Z':'--..','0':'-----','1':'.----','2':'..---','3':'...--','4':'....-','5':'.....','6':'-....','7':'--...','8':'---..','9':'----.' }; | |
| const MORSE_DECODE = Object.fromEntries(Object.entries(MORSE).map(([k,v])=>[v,k])); | |
| function codePointHex(ch){return 'U+'+ch.codePointAt(0).toString(16).toUpperCase().padStart(4,'0')} | |
| function escapeHtml(s){return s.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>')} | |
| // ------------------ Visualizer ------------------ | |
| function revealInvisibles(text){ | |
| let out = ''; | |
| for (let i=0;i<text.length;i++){ | |
| const ch = text[i]; | |
| if (ch===' ') out += '<span class="invis" title="SPACE U+0020">␣</span>'; | |
| else if (ch===' ') out += '<span class="invis" title="TAB U+0009">→ </span>'; | |
| else if (ch===' | |
| ') out += '<span class="invis" title="LF U+000A">↵ | |
| </span>'; | |
| else if (ch===' | |
| ') out += '<span class="invis" title="CR U+000D">␍</span>'; | |
| else if (ch===' ') out += '<span class="invis" title="NO-BREAK SPACE U+00A0">⍽</span>'; | |
| else if (DEFAULT_ZERO_WIDTH[ch]){ | |
| out += `<span class="invis" title="${DEFAULT_ZERO_WIDTH[ch]} ${codePointHex(ch)}">${DEFAULT_ZERO_WIDTH[ch].split(' ')[0]}</span>`; | |
| } else { | |
| out += escapeHtml(ch); | |
| } | |
| } | |
| return out; | |
| } | |
| // ------------------ Finders ------------------ | |
| function findZeroWidth(text){ | |
| const found = []; | |
| for (const [k,v] of Object.entries(DEFAULT_ZERO_WIDTH)){ | |
| const count = text.split(k).length-1; | |
| if (count>0) found.push({cp:codePointHex(k),char:k,name:v,count}); | |
| } | |
| // C0 controls excluding , | |
| , | |
| for (let i=0;i<text.length;i++){ | |
| const code = text.charCodeAt(i); | |
| if (code>=0 && code<=31 && code!==9 && code!==10 && code!==13){ | |
| found.push({cp:'U+'+code.toString(16).padStart(4,'0'),char:text[i],name:'CONTROL',count:1}); | |
| } | |
| } | |
| // NBSP and thin spaces | |
| const nbspCount = text.split(' ').length-1; if (nbspCount>0) found.push({cp:'U+00A0',name:'NO-BREAK SPACE',count:nbspCount}); | |
| const thin = text.split(' ').length-1; if (thin>0) found.push({cp:'U+2009',name:'THIN SPACE',count:thin}); | |
| return found; | |
| } | |
| function findConfusables(text,custom){ | |
| const list = [...CONFUSABLE_PATTERNS]; | |
| // parse custom | |
| if (custom && custom.trim()){ | |
| const lines = custom.split(/ | |
| /); | |
| for (const ln of lines){ | |
| const parts = ln.split(/ +/); | |
| if (parts[0]) list.push({char:parts[0],looksLike:'?',desc:parts[1]||'custom'}); | |
| } | |
| } | |
| const found = []; | |
| for (const pat of list){ | |
| if (text.includes(pat.char)){ | |
| const count = text.split(pat.char).length-1; | |
| found.push({char:pat.char,looksLike:pat.looksLike,desc:pat.desc,count}); | |
| } | |
| } | |
| return found; | |
| } | |
| // ------------------ Whitespace extraction (configurable) ------------------ | |
| function parseMapping(selector){ | |
| if (selector==='space0tab1') return {' ': '0',' ':'1'}; | |
| if (selector==='space1tab0') return {' ': '1',' ':'0'}; | |
| if (selector==='space0tab1-nbsp') return {' ': '0',' ':'1',' ':'1'}; | |
| return null; // custom handled elsewhere | |
| } | |
| function extractWhitespaceBits(text,{mode='trailing',mapping,byteOrder='lsb',bitsPerByte=8} = {}){ | |
| // mapping is an object mapping literal characters to '0'/'1' | |
| const lines = text.split(/ | |
| /); | |
| let bits = ''; | |
| for (const ln of lines){ | |
| let seq = ''; | |
| if (mode==='trailing'){ | |
| const m = ln.match(/([ ]+)$/); | |
| if (m) seq = m[1]; | |
| } else { | |
| const m = ln.match(/^([ ]+)/); | |
| if (m) seq = m[1]; | |
| } | |
| for (const ch of seq){ | |
| const key = (ch===' ')? ' ' : ch; | |
| if (mapping[key]!==undefined) bits += mapping[key]; | |
| else if (mapping[ch]!==undefined) bits += mapping[ch]; | |
| } | |
| } | |
| // chunk bits into bytes according to bitsPerByte and byteOrder | |
| const bytes = []; | |
| for (let i=0;i+bitsPerByte<=bits.length;i+=bitsPerByte){ | |
| let chunk = bits.slice(i,i+bitsPerByte); | |
| if (byteOrder==='lsb') chunk = chunk.split('').reverse().join(''); | |
| const val = parseInt(chunk,2); | |
| bytes.push(val); | |
| } | |
| const textDecoded = bytes.map(c=>isPrintable(c)?String.fromCharCode(c):'?').join(''); | |
| return {bits,bytes,textDecoded}; | |
| } | |
| function isPrintable(code){return code>=32 && code<=126} | |
| // ------------------ Zero-width binary heuristics ------------------ | |
| function extractZeroWidthAsBits(text,method='zw-space',bitsPerByte=8,byteOrder='lsb'){ | |
| // method options: 'zw-space' map certain zw chars to 0/1; 'zw-presence' presence=1; 'zw-index' index-based | |
| const zwChars = Object.keys(DEFAULT_ZERO_WIDTH); | |
| let bits = ''; | |
| if (method==='zw-space'){ | |
| // example mapping: ZW-SPACE=0, ZW-JOINER=1 | |
| for (let i=0;i<text.length;i++){ | |
| const ch = text[i]; | |
| if (ch==='') bits += '0'; | |
| else if (ch==='') bits += '1'; | |
| } | |
| } else if (method==='zw-presence'){ | |
| // group per character position: if any zw at pos -> 1 else 0 (naive) | |
| for (let i=0;i<text.length;i++) if (zwChars.includes(text[i])) bits += '1'; | |
| } else if (method==='zw-index'){ | |
| // take index of zw occurrence: odd ->1 even->0 | |
| let idx=0; for (let i=0;i<text.length;i++){ if (zwChars.includes(text[i])){ bits += (idx%2? '1':'0'); idx++; }} | |
| } | |
| const bytes = []; | |
| for (let i=0;i+bitsPerByte<=bits.length;i+=bitsPerByte){ let chunk = bits.slice(i,i+bitsPerByte); if (byteOrder==='lsb') chunk = chunk.split('').reverse().join(''); bytes.push(parseInt(chunk,2)); } | |
| const textDecoded = bytes.map(c=>isPrintable(c)?String.fromCharCode(c):'?').join(''); | |
| return {bits,bytes,textDecoded}; | |
| } | |
| // ------------------ Punctuation / Morse extraction ------------------ | |
| function extractPunctuationMorse(text){ | |
| const dotChars = ['.', '·','•','·','•']; | |
| const dashChars = ['-','–','—','_']; | |
| let sequences = []; | |
| let run=''; let runRaw=''; | |
| for (let i=0;i<text.length;i++){ | |
| const ch = text[i]; | |
| if (dotChars.includes(ch) || dashChars.includes(ch)){ | |
| run += dotChars.includes(ch)?'.':'-'; runRaw += ch; | |
| } else { if (run.length>0){ sequences.push({morse:run,raw:runRaw}); run=''; runRaw=''; } } | |
| } | |
| if (run.length>0) sequences.push({morse:run,raw:runRaw}); | |
| const decoded = sequences.map(s=>({morse:s.morse,decoded:MORSE_DECODE[s.morse] ?? '[?]'})); | |
| // sentence-ends mapping | |
| const ends = text.match(/[.!?]+/g) || []; | |
| let bitstr = ''; | |
| for (let i=0;i<ends.length;i++){ | |
| const e = ends[i]; if (e.includes('!')||e.includes('?')) bitstr += '1'; else bitstr += '0'; | |
| } | |
| const bytes = []; | |
| for (let i=0;i+8<=bitstr.length;i+=8) bytes.push(parseInt(bitstr.slice(i,i+8),2)); | |
| const bytext = bytes.map(b=>isPrintable(b)?String.fromCharCode(b):'?').join(''); | |
| return {sequences,decoded,bitstr,bytext}; | |
| } | |
| // ------------------ UI glue ------------------ | |
| function analyze(){ | |
| const txt = document.getElementById('input').value; | |
| document.getElementById('charcount').textContent = txt.length; | |
| document.getElementById('linecount').textContent = txt.split(/ | |
| /).length; | |
| const zw = findZeroWidth(txt); | |
| document.getElementById('zerowidth').textContent = zw.length?zw.map(z=>`${z.name} ${z.cp || ''} ×${z.count}`).join(' | |
| '):'—'; | |
| document.getElementById('zwcount').textContent = zw.length; | |
| const custom = document.getElementById('customConf').value; | |
| const cf = findConfusables(txt,custom); | |
| document.getElementById('confusables').textContent = cf.length?cf.map(c=>`${c.char} looksLike ${c.looksLike} (${c.desc}) ×${c.count}`).join(' | |
| '):'—'; | |
| document.getElementById('cfcount').textContent = cf.length; | |
| // mapping | |
| const mapSel = document.getElementById('whitespaceMap').value; | |
| let mapping = parseMapping(mapSel); | |
| if (!mapping){ // try parse custom JSON-like | |
| const txtmap = prompt('Enter mapping as JSON object, e.g. {" ":"0","\t":"1","\u00A0":"1"}'); | |
| try{ mapping = JSON.parse(txtmap); } | |
| catch(e){ mapping = {' ': '0',' ': '1'}; } | |
| } | |
| const byteOrder = document.getElementById('byteOrder').value; | |
| const bpb = parseInt(document.getElementById('bitsPerByte').value) || 8; | |
| const snowT = extractWhitespaceBits(txt,{mode:'trailing',mapping,byteOrder,bitsPerByte:bpb}); | |
| const snowL = extractWhitespaceBits(txt,{mode:'leading',mapping,byteOrder,bitsPerByte:bpb}); | |
| const snowSummary = `Trailing bits: ${snowT.bits.length} bits → ${snowT.bytes.length} bytes | |
| Decoded (trailing): ${snowT.textDecoded} | |
| Leading bits: ${snowL.bits.length} bits → ${snowL.bytes.length} bytes | |
| Decoded (leading): ${snowL.textDecoded}`; | |
| document.getElementById('snow').textContent = snowSummary; | |
| const pm = extractPunctuationMorse(txt); | |
| const pmSummary = `Found ${pm.sequences.length} dot/dash runs | |
| Runs: ${pm.sequences.map(s=>s.morse).slice(0,8).join(' ')} | |
| Decoded text from sentence-ends bits: ${pm.bytext || '[none]'}`; | |
| document.getElementById('punctmorse').textContent = pmSummary; | |
| const zwMethod = document.getElementById('zwMap').value; | |
| const zwDecoded = extractZeroWidthAsBits(txt,zwMethod,bpb,byteOrder); | |
| const vis = revealInvisibles(txt); | |
| document.getElementById('visual').innerHTML = vis; | |
| const summ = []; | |
| if (zw.length) summ.push(`Zero-width chars: ${zw.length}`); | |
| if (cf.length) summ.push(`Confusable glyphs: ${cf.length}`); | |
| if (snowT.bytes.length || snowL.bytes.length) summ.push('Potential whitespace stego data found'); | |
| if (pm.sequences.length || pm.bytext.length) summ.push('Punctuation patterns detected'); | |
| if (zwDecoded.bytes.length) summ.push('Zero-width binary data found'); | |
| document.getElementById('summary').innerHTML = summ.length?('<div class="highlight">'+summ.join('<br>')+'</div>'):'No obvious hidden markers detected (but custom schemes may exist)'; | |
| document.getElementById('quickout').textContent = `Whitespace (trailing) → ${snowT.textDecoded} | |
| Zero-width → ${zwDecoded.textDecoded || '[none]'} | |
| Punctuation bits → ${pm.bytext || '[none]'}`; | |
| } | |
| // ------------------ UI events ------------------ | |
| document.getElementById('analyze').onclick = analyze; | |
| document.getElementById('visualize').onclick = ()=>{ const txt=document.getElementById('input').value; document.getElementById('visual').innerHTML = revealInvisibles(txt); } | |
| document.getElementById('clear').onclick = ()=>{ document.getElementById('input').value=''; analyze(); } | |
| // quick decoders | |
| nd=document.getElementById; | |
| document.getElementById('decodeSnow').onclick = ()=>{ | |
| const txt=document.getElementById('input').value; | |
| const mapSel = document.getElementById('whitespaceMap').value; let mapping = parseMapping(mapSel); | |
| if (!mapping){ try{ mapping = JSON.parse(prompt('Enter mapping as JSON')) } catch(e){ mapping = {' ':'0',' ':'1'} } } | |
| const bo = document.getElementById('byteOrder').value; const bpb = parseInt(document.getElementById('bitsPerByte').value)||8; | |
| const r = extractWhitespaceBits(txt,{mode:'trailing',mapping,byteOrder:bo,bitsPerByte:bpb}); | |
| alert('Whitespace (trailing) decoded: | |
| '+r.textDecoded+' | |
| Bits:'+r.bits.slice(0,256)); | |
| } | |
| document.getElementById('decodePunct').onclick = ()=>{ const txt=document.getElementById('input').value; const r=extractPunctuationMorse(txt); alert('Punctuation bits decoded text: | |
| '+(r.bytext||'[none]')+' | |
| Runs: '+r.sequences.map(s=>s.morse).slice(0,20).join(' ')); } | |
| // copy / download | |
| document.getElementById('copyVis').onclick = ()=>{ navigator.clipboard.writeText(document.getElementById('visual').innerText||'').catch(()=>{}); } | |
| document.getElementById('copyRaw').onclick = ()=>{ navigator.clipboard.writeText(document.getElementById('input').value||'').catch(()=>{}); } | |
| document.getElementById('download').onclick = ()=>{ const blob = new Blob([document.getElementById('input').value],{type:'text/plain;charset=utf-8'}); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href=url; a.download='inspected.txt'; document.body.appendChild(a); a.click(); a.remove(); URL.revokeObjectURL(url); } | |
| // drag & drop | |
| const dz = document.getElementById('dropzone'); ['dragenter','dragover'].forEach(e=>dz.addEventListener(e,ev=>{ev.preventDefault();dz.style.borderColor='#2b6cb0'})); ['dragleave','drop'].forEach(e=>dz.addEventListener(e,ev=>{ev.preventDefault();dz.style.borderColor='rgba(255,255,255,0.03)'})); dz.addEventListener('drop',async ev=>{ const f = ev.dataTransfer.files[0]; if (!f) return; const txt = await f.text(); document.getElementById('input').value = txt; analyze(); }); | |
| // initial sample | |
| H</script> </body> | |
| </html> | |
| i |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment