|
|
<!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> |
|
|
|