| <!DOCTYPE html> |
| <html lang="vi"> |
| <head> |
| <meta charset="UTF-8"> |
| <title>CVNSS4.0 ⇄ Bila Hex/Nhị phân | Chưa hỗ trợ chuỗi dài</title> |
| <style> |
| body { background: #1e2232; font-family: 'Fira Mono', monospace; margin: 0; } |
| .container { max-width: 760px; margin: 44px auto; background: #181f29; border-radius: 16px; |
| box-shadow: 0 3px 18px #000c; padding: 32px 24px 24px 24px;} |
| h2 { color: #e0e7ef; font-weight: 500; margin-bottom: 8px;} |
| label { font-weight: bold; color: #38bdf8; margin-top: 12px; display: block;} |
| textarea { width: 100%; font-size: 1em; background: #1e293b; color: #f1f5f9; |
| border-radius: 7px; padding: 12px; border: 1px solid #334155; |
| margin-bottom: 14px; min-height: 48px; resize: vertical;} |
| button { background: #38bdf8; color: #020617; border: none; border-radius: 8px; |
| padding: 11px 20px; font-size: 1em; font-family: inherit; font-weight: 600; |
| margin: 7px 13px 7px 0; box-shadow: 0 1px 5px #0891b2aa; transition: 0.12s;} |
| button:hover { background: #0ea5e9; color: #fff;} |
| .outblock { background: #222b38; border-radius: 8px; margin-top: 12px; margin-bottom: 10px; } |
| .outlabel { color: #6ee7b7; font-size: 0.99em; padding: 8px 0 2px 2px; } |
| .output { color: #e5e5e5; background: none; padding: 8px 8px 4px 8px; font-size: 1.08em; |
| word-break: break-all; font-family: 'Fira Mono', monospace; min-height: 16px;} |
| .error { color: #f87171; font-weight: bold; padding: 7px 0 0 0;} |
| .note { color: #a5b4fc; font-size: 0.97em; margin-top: 12px;} |
| .byline { text-align: right; margin-top: 26px; color: #64748b; font-size: 0.97em;} |
| @media (max-width: 800px) { .container {padding: 10px; max-width: 99vw;} } |
| ::selection { background: #38bdf888; } |
| </style> |
| </head> |
| <body> |
| <div class="container"> |
| <h2>CVNSS4.0 ⇄ Bila Hex/Nhị phân<br> Hiện chỉ hỗ trợ khoảng 30 ký tự để test</h2> |
|
|
| <label for="ascii">CVNSS4.0:</label> |
| <textarea id="ascii" placeholder="Nhập chữ cái, số, dấu cách... Chuỗi dài cỡ 50 ký tự đều được!"></textarea> |
| <button onclick="asciiToCBOR()">CVNSS4.0 → Bila</button> |
| <button onclick="clearAll()">Xoá</button> |
| <div class="error" id="err_ascii"></div> |
|
|
| <div class="outblock"> |
| <div class="outlabel">Bila Hex:</div> |
| <div class="output" id="cborhex"></div> |
| <div class="outlabel">Bila Nhị phân:</div> |
| <div class="output" id="cborbin"></div> |
| </div> |
|
|
| <label for="cborhex_in">Bila Hex:</label> |
| <textarea id="cborhex_in" placeholder="Nhập mã Hex hợp lệ (header 78xx, 79xxxx, ... không giới hạn độ dài)"></textarea> |
| <button onclick="cborHexToAscii()">Bila Hex → CVNSS4.0</button> |
| <div class="error" id="err_hex"></div> |
|
|
| <label for="cborbin_in">Bila Nhị phân:</label> |
| <textarea id="cborbin_in" placeholder="Nhập mã Nhị phân hợp lệ"></textarea> |
| <button onclick="cborBinToAscii()">Bila Nhị phân → CVNSS4.0</button> |
| <div class="error" id="err_bin"></div> |
|
|
| <div class="outblock"> |
| <div class="outlabel">Kết quả:</div> |
| <div class="output" id="ascii_out"></div> |
| </div> |
|
|
| <div class="note"> |
| • Nhập/dán tối đa 30-40 ký tự.<br> |
| • Tự động loại ký tự không hợp lệ (chỉ giữ A–Z, a–z, 0–9, dấu cách).<br> |
| • Mã Hex dài sẽ tự động dùng header 78xx, 79xxxx theo chuẩn RFC7049.<br> |
| • Kết quả chưa tương thích sẽ báo lỗi, lấy ký tự CVNSS4.0 (<a href="https://chuvnsongsong.com/" target="_blank" style="color:#38bdf8">Chữ VN Song Song 4.0</a>). |
| </div> |
| <div class="byline">© Long Ngo, 2025 – Bản demo đang thử nghiệm</div> |
| </div> |
| <script> |
| |
| document.getElementById('ascii').addEventListener('input', function() { |
| let clean = this.value.replace(/[^A-Za-z0-9 ]+/g, ''); |
| if(this.value !== clean) { |
| this.value = clean; |
| document.getElementById('err_ascii').textContent = '❌ Đã tự động loại bỏ ký tự không hợp lệ!'; |
| } else { |
| document.getElementById('err_ascii').textContent = ''; |
| } |
| }); |
| |
| function cleanASCII(s) { |
| return s.replace(/[\r\n\t\u200B-\u200D\uFEFF]/g, ""); |
| } |
| function checkValidASCII(s) { |
| return /^[A-Za-z0-9 ]+$/.test(s); |
| } |
| |
| |
| function asciiToCBOR() { |
| let t = document.getElementById('ascii').value; |
| document.getElementById('err_ascii').textContent = ""; |
| if (!t) return showCBOR('', ''); |
| let tclean = cleanASCII(t); |
| if (!checkValidASCII(tclean)) { |
| let wrong = tclean.replace(/[A-Za-z0-9 ]/g, ""); |
| document.getElementById('err_ascii').textContent = |
| '❌ Chỉ cho phép A–Z, a–z, 0–9, dấu cách! Ký tự lỗi: [' + wrong.split("").join(" ") + ']'; |
| showCBOR('', ''); |
| return; |
| } |
| let encoder = new TextEncoder(); |
| let bytes = encoder.encode(tclean); |
| let hex = "", b = ""; |
| if (bytes.length < 24) { |
| hex = (0x60 + bytes.length).toString(16); |
| } else if (bytes.length < 256) { |
| hex = "78" + bytes.length.toString(16).padStart(2, "0"); |
| } else if (bytes.length < 65536) { |
| hex = "79" + bytes.length.toString(16).padStart(4, "0"); |
| } else { |
| document.getElementById('err_ascii').textContent = '❌ Chuỗi quá dài (tối đa 65.535 ký tự)!'; |
| showCBOR('', ''); |
| return; |
| } |
| hex += Array.from(bytes).map(x => x.toString(16).padStart(2, "0")).join(''); |
| b = hex.match(/.{1,2}/g).map(x => parseInt(x, 16).toString(2).padStart(8, '0')).join(''); |
| showCBOR(hex, b); |
| } |
| |
| function cborHexToAscii() { |
| let hex = document.getElementById('cborhex_in').value |
| .replace(/[^0-9a-fA-F]/g, '') |
| .toLowerCase(); |
| document.getElementById('err_hex').textContent = ""; |
| if (!hex) return showAscii(''); |
| if (hex.length % 2 !== 0) { |
| document.getElementById('err_hex').textContent = '❌ Hex phải có số ký tự chẵn!'; |
| showAscii(''); |
| return; |
| } |
| if (!/^[0-9a-f]+$/.test(hex)) { |
| document.getElementById('err_hex').textContent = '❌ Hex không hợp lệ!'; |
| showAscii(''); |
| return; |
| } |
| try { |
| let idx=0, l=0, fb=parseInt(hex.substr(idx,2),16); idx+=2; |
| if ((fb&0xe0)===0x60) l=fb&0x1f; |
| else if(fb===0x78) { l=parseInt(hex.substr(idx,2),16); idx+=2; } |
| else if(fb===0x79) { l=parseInt(hex.substr(idx,4),16); idx+=4; } |
| else { document.getElementById('err_hex').textContent = '❌ Không đúng mã string'; showAscii(''); return; } |
| let bh = hex.substr(idx, l*2); |
| if (bh.length<l*2) { |
| document.getElementById('err_hex').textContent = '❌ Thiếu bytes, cần ' + (l*2) + ' ký tự hex, chỉ có ' + bh.length; |
| showAscii(''); |
| return; |
| } |
| let bs=[]; |
| for(let i=0;i<bh.length;i+=2) bs.push(parseInt(bh.substr(i,2),16)); |
| let s; |
| try { |
| s = new TextDecoder().decode(new Uint8Array(bs)); |
| } catch (e) { |
| document.getElementById('err_hex').textContent = '❌ Không thể giải mã UTF-8: ' + e.message; |
| showAscii(''); |
| return; |
| } |
| let clean = cleanASCII(s); |
| if (!checkValidASCII(clean)) { |
| let wrong = clean.replace(/[A-Za-z0-9 ]/g, ""); |
| document.getElementById('err_hex').textContent = |
| '❌ Dữ liệu nhập không hợp lệ (chỉ cho A–Z, a–z, 0–9, dấu cách)! Ký tự lỗi: [' + |
| wrong.split("").join(" ") + ']'; |
| showAscii(''); |
| return; |
| } |
| showAscii(clean); |
| } catch (err) { |
| document.getElementById('err_hex').textContent = '❌ Lỗi giải mã: ' + err.message; |
| showAscii(''); |
| } |
| } |
| |
| function cborBinToAscii() { |
| let bin = document.getElementById('cborbin_in').value.trim().replace(/\s+/g,''); |
| document.getElementById('err_bin').textContent = ""; |
| if (!bin) return showAscii(''); |
| if (!/^[01]+$/.test(bin)) { document.getElementById('err_bin').textContent = '❌ Nhị phân không hợp lệ!'; showAscii(''); return; } |
| if (bin.length%8!==0) { document.getElementById('err_bin').textContent = '❌ Chuỗi phải là bội số 8!'; showAscii(''); return; } |
| try { |
| let idx=0, l=0, fb=parseInt(bin.substr(idx,8),2); idx+=8; |
| if ((fb&0xe0)===0x60) l=fb&0x1f; |
| else if(fb===0x78) { l=parseInt(bin.substr(idx,8),2); idx+=8; } |
| else if(fb===0x79) { l=parseInt(bin.substr(idx,16),2); idx+=16; } |
| else { document.getElementById('err_bin').textContent = '❌ Không đúng CBOR string'; showAscii(''); return; } |
| let bs=[]; |
| for(let i=0;i<l;i++) { |
| if(idx+8>bin.length) { document.getElementById('err_bin').textContent = '❌ Thiếu bytes'; showAscii(''); return; } |
| bs.push(parseInt(bin.substr(idx,8),2)); idx+=8; |
| } |
| let s; |
| try { |
| s = new TextDecoder().decode(new Uint8Array(bs)); |
| } catch (e) { |
| document.getElementById('err_bin').textContent = '❌ Không thể giải mã UTF-8: ' + e.message; |
| showAscii(''); |
| return; |
| } |
| let clean = cleanASCII(s); |
| if (!checkValidASCII(clean)) { |
| let wrong = clean.replace(/[A-Za-z0-9 ]/g, ""); |
| document.getElementById('err_bin').textContent = |
| '❌ Dữ liệu nhập không hợp lệ (chỉ cho A–Z, a–z, 0–9, dấu cách)! Ký tự lỗi: [' + |
| wrong.split("").join(" ") + ']'; |
| showAscii(''); |
| return; |
| } |
| showAscii(clean); |
| } catch (err) { |
| document.getElementById('err_bin').textContent = '❌ Lỗi giải mã: ' + err.message; |
| showAscii(''); |
| } |
| } |
| |
| function showCBOR(hex, bin) { |
| document.getElementById('cborhex').textContent = hex; |
| document.getElementById('cborbin').textContent = bin; |
| document.getElementById('ascii_out').textContent = ""; |
| } |
| function showAscii(ascii) { |
| document.getElementById('ascii_out').textContent = ascii; |
| } |
| function clearAll() { |
| document.getElementById('ascii').value = ''; |
| document.getElementById('cborhex_in').value = ''; |
| document.getElementById('cborbin_in').value = ''; |
| document.getElementById('err_ascii').textContent = ''; |
| document.getElementById('err_hex').textContent = ''; |
| document.getElementById('err_bin').textContent = ''; |
| showCBOR('', ''); |
| showAscii(''); |
| } |
| </script> |
| </body> |
| </html> |
|
|