Skip to content

Instantly share code, notes, and snippets.

@mode-mercury
Created October 27, 2025 04:16
Show Gist options
  • Select an option

  • Save mode-mercury/a0b35352980ae5aed4557ef4ad60cd4e to your computer and use it in GitHub Desktop.

Select an option

Save mode-mercury/a0b35352980ae5aed4557ef4ad60cd4e to your computer and use it in GitHub Desktop.
Untitled
<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,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;')}
// ------------------ 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