congthuc / index.html
CVNSS's picture
Update index.html
0038977 verified
<!DOCTYPE html>
<html lang="vi">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>CVNSS 4.0 — Bộ Chuyển Đổi Tốc Ký Chữ Việt</title>
<style>
/* ── OFFLINE SYSTEM FONTS (no internet needed) ── */
@font-face {
font-family: 'SysMono';
src: local('Courier New'), local('Courier'), local('Liberation Mono'), local('DejaVu Sans Mono');
}
@font-face {
font-family: 'SysSans';
src: local('Segoe UI'), local('Ubuntu'), local('Helvetica Neue'), local('Arial');
}
:root {
--bg:#f3f5f9;--surface:#fff;--s2:#edf0f6;--s3:#e4e9f2;
--bd:#c8d4e4;--bd2:#aabcd0;
--tx:#14202e;--mu:#5e7288;--fa:#8fa5bc;
--l1:#0058c0;--l1bg:#e3edfb;--l1bd:#7daae8;
--l2:#b03600;--l2bg:#fdeae0;--l2bd:#e8a07a;
--l3:#14683a;--l3bg:#e2f4ea;--l3bd:#7ac898;
--l4:#6418b0;--l4bg:#f1e4fc;--l4bd:#ba88e8;
--ok:#008840;--acc:#004ab0;
/* OUTPUT NODE: red bg gold text */
--out-bg:#cc1111;--out-txt:#ffd700;
}
*{margin:0;padding:0;box-sizing:border-box}
html{scroll-behavior:smooth}
body{
background:var(--bg);color:var(--tx);
font-family:'SysSans','Segoe UI',Ubuntu,'Helvetica Neue',Arial,sans-serif;
font-size:14px;min-height:100vh;
background-image:linear-gradient(rgba(140,165,195,.11) 1px,transparent 1px),
linear-gradient(90deg,rgba(140,165,195,.11) 1px,transparent 1px);
background-size:22px 22px;
}
code,pre,.mono{font-family:'SysMono','Courier New',Courier,monospace}
/* ══ HEADER ══ */
.hdr{background:var(--surface);border-bottom:2px solid var(--bd);padding:13px 26px;
display:flex;align-items:center;justify-content:space-between;position:relative}
.hdr::before{content:'';position:absolute;top:0;left:0;right:0;height:3px;
background:linear-gradient(90deg,var(--l1),var(--l2),var(--l3),var(--l4))}
.hdr-l h1{font-size:18px;font-weight:800;letter-spacing:-.3px}
.hdr-l h1 em{color:var(--acc);font-style:normal}
.hdr-l p{font-size:11px;color:var(--mu);margin-top:2px;font-family:'SysMono',monospace}
.chips{display:flex;gap:4px;flex-wrap:wrap}
.chip{font-family:'SysMono',monospace;font-size:9px;font-weight:700;
padding:3px 8px;border-radius:3px;letter-spacing:1px;text-transform:uppercase;border:1px solid}
.c1{background:var(--l1bg);color:var(--l1);border-color:var(--l1bd)}
.c2{background:var(--l2bg);color:var(--l2);border-color:var(--l2bd)}
.c3{background:var(--l3bg);color:var(--l3);border-color:var(--l3bd)}
.c4{background:var(--l4bg);color:var(--l4);border-color:var(--l4bd)}
/* ══ WRAP ══ */
.wrap{max-width:1260px;margin:0 auto;padding:18px 18px 0}
/* ══ CONVERTER CARD ══ */
.cvt-card{background:var(--surface);border:1.5px solid var(--bd2);border-radius:8px;
padding:16px 20px 14px;margin-bottom:20px;box-shadow:0 2px 8px rgba(0,30,90,.06);position:relative}
.cvt-card::after{content:'IC-CONVERTER';position:absolute;top:-10px;right:16px;
font-family:'SysMono',monospace;font-size:9px;color:var(--mu);background:var(--surface);
padding:0 8px;letter-spacing:1.5px;border:1px solid var(--bd);border-radius:3px}
.mode-bar{display:flex;gap:3px;margin-bottom:12px;background:var(--s2);
border:1px solid var(--bd);padding:3px;border-radius:6px;width:fit-content}
.mtab{padding:5px 14px;border:none;border-radius:4px;font-family:'SysMono',monospace;
font-size:11px;font-weight:700;cursor:pointer;transition:all .15s;background:none;
color:var(--mu);letter-spacing:.5px}
.mtab.on{background:var(--surface);color:var(--acc);
box-shadow:0 1px 4px rgba(0,30,90,.12);border:1px solid var(--bd)}
.cvt-row{display:grid;grid-template-columns:1fr 110px 1fr;gap:12px;align-items:end}
.fld label{display:block;font-size:9px;font-weight:700;letter-spacing:1.5px;
text-transform:uppercase;color:var(--mu);margin-bottom:5px;font-family:'SysMono',monospace}
.fld input{width:100%;padding:9px 12px;border:1.5px solid var(--bd);border-radius:5px;
font-family:'SysMono',monospace;font-size:17px;font-weight:600;color:var(--tx);
background:var(--surface);outline:none;transition:border-color .2s,box-shadow .2s}
.fld input:focus{border-color:var(--acc);box-shadow:0 0 0 3px rgba(0,74,176,.1)}
.cvt-mid{display:flex;flex-direction:column;align-items:center;gap:6px;padding-bottom:2px}
.btn{background:var(--acc);color:#fff;border:none;padding:10px 16px;
font-family:'SysMono',monospace;font-weight:700;font-size:11px;letter-spacing:1px;
border-radius:5px;cursor:pointer;transition:background .2s,transform .1s;
text-transform:uppercase;white-space:nowrap}
.btn:hover{background:#003090}.btn:active{transform:scale(.97)}
/* ── OUTPUT BOX: red bg, gold text ── */
.out-box{
background:var(--out-bg);border:2px solid #990a0a;border-radius:5px;
padding:9px 13px;font-family:'SysMono',monospace;font-size:22px;font-weight:800;
color:var(--out-txt);min-height:46px;display:flex;align-items:center;
letter-spacing:3px;position:relative;overflow:hidden;transition:all .3s;
box-shadow:0 2px 10px rgba(180,0,0,.2);text-shadow:0 1px 3px rgba(0,0,0,.4)
}
.out-box::before{content:'';position:absolute;left:0;top:0;width:4px;height:100%;background:#ffd700}
.ph{color:rgba(255,215,0,.45);font-size:13px;font-weight:400;letter-spacing:0;text-shadow:none}
/* ══ SIGNAL FLOW ══ */
.ftop{display:flex;flex-direction:column;align-items:center}
.node-in{background:var(--surface);border:2px solid var(--bd2);border-radius:5px;
padding:8px 34px;font-weight:700;font-size:12px;letter-spacing:2px;text-transform:uppercase;
font-family:'SysMono',monospace;box-shadow:3px 3px 0 var(--bd);transition:all .3s;text-align:center}
.node-in.on{border-color:var(--acc);box-shadow:3px 3px 0 var(--l1bd)}
.node-in small{display:block;font-size:9px;letter-spacing:1.5px;color:var(--mu);font-weight:400;margin-bottom:2px}
.node-val{font-size:20px;color:var(--acc);letter-spacing:3px;min-height:24px}
/* BUS */
.bus-wrap{height:34px;position:relative;overflow:visible;margin:0}
.bus-h{position:absolute;top:3px;left:12.5%;width:75%;height:1.5px;
background:linear-gradient(90deg,var(--l1),var(--l2),var(--l3),var(--l4));opacity:.4;transition:opacity .3s}
.bus-h.on{opacity:1}
.vias{position:absolute;top:-3px;left:12.5%;width:75%;display:flex;justify-content:space-around}
.via{width:8px;height:8px;border-radius:50%;border:2px solid;background:var(--surface);transition:all .3s}
.v1{border-color:var(--l1bd)}.v2{border-color:var(--l2bd)}
.v3{border-color:var(--l3bd)}.v4{border-color:var(--l4bd)}
.v1.on{background:var(--l1);border-color:var(--l1)}
.v2.on{background:var(--l2);border-color:var(--l2)}
.v3.on{background:var(--l3);border-color:var(--l3)}
.v4.on{background:var(--l4);border-color:var(--l4)}
.vert-wires{position:absolute;top:7px;left:12.5%;width:75%;display:flex;justify-content:space-around;height:28px}
.vw{width:2px;height:100%}
.vw1{background:linear-gradient(180deg,var(--l1),rgba(0,88,192,.15))}
.vw2{background:linear-gradient(180deg,var(--l2),rgba(176,54,0,.15))}
.vw3{background:linear-gradient(180deg,var(--l3),rgba(20,104,58,.15))}
.vw4{background:linear-gradient(180deg,var(--l4),rgba(100,24,176,.15))}
/* ══ LANES ══ */
.lanes{display:grid;grid-template-columns:repeat(4,1fr);gap:14px}
.lc{border-radius:6px;border:1.5px solid;background:var(--surface);
box-shadow:0 2px 6px rgba(0,0,0,.04);overflow:hidden;transition:box-shadow .2s}
.lc:hover{box-shadow:0 4px 14px rgba(0,0,0,.08)}
.l1c{border-color:var(--l1bd)}.l2c{border-color:var(--l2bd)}
.l3c{border-color:var(--l3bd)}.l4c{border-color:var(--l4bd)}
.lhead{padding:8px 12px;font-family:'SysMono',monospace;font-size:10px;font-weight:700;
letter-spacing:1.5px;text-transform:uppercase;border-bottom:1px solid;
display:flex;align-items:center;justify-content:space-between}
.l1c .lhead{background:var(--l1bg);color:var(--l1);border-color:var(--l1bd)}
.l2c .lhead{background:var(--l2bg);color:var(--l2);border-color:var(--l2bd)}
.l3c .lhead{background:var(--l3bg);color:var(--l3);border-color:var(--l3bd)}
.l4c .lhead{background:var(--l4bg);color:var(--l4);border-color:var(--l4bd)}
.rbadge{font-family:'SysMono',monospace;font-size:12px;font-weight:700;
padding:2px 8px;border-radius:3px;min-width:50px;text-align:center;
border:1px solid;background:var(--surface);letter-spacing:1px;transition:all .3s}
.l1c .rbadge{border-color:var(--l1bd);color:var(--l1)}
.l2c .rbadge{border-color:var(--l2bd);color:var(--l2)}
.l3c .rbadge{border-color:var(--l3bd);color:var(--l3)}
.l4c .rbadge{border-color:var(--l4bd);color:var(--l4)}
.rbadge.lit{color:#fff!important;border-color:transparent!important}
.l1c .rbadge.lit{background:var(--l1)}.l2c .rbadge.lit{background:var(--l2)}
.l3c .rbadge.lit{background:var(--l3)}.l4c .rbadge.lit{background:var(--l4)}
.lproc{margin:7px 11px 0;padding:6px 10px;background:var(--s2);border:1px solid var(--bd);
border-radius:4px;font-family:'SysMono',monospace;font-size:11px;
display:none;align-items:center;gap:7px}
.lproc.show{display:flex}
.lp-in{color:var(--mu)}.lp-arr{color:var(--fa);font-size:10px}
.lp-out{font-weight:700;font-size:13px}
.l1c .lp-out{color:var(--l1)}.l2c .lp-out{color:var(--l2)}
.l3c .lp-out{color:var(--l3)}.l4c .lp-out{color:var(--l4)}
.lbody{padding:11px}
.rsect{margin-bottom:9px}.rsect:last-child{margin-bottom:0}
.rtitle{font-family:'SysMono',monospace;font-size:9px;font-weight:700;
letter-spacing:1.5px;text-transform:uppercase;color:var(--fa);
margin-bottom:4px;padding-bottom:3px;border-bottom:1px dashed var(--bd)}
.mtags{display:flex;flex-wrap:wrap;gap:3px;margin-top:3px}
.mt{font-family:'SysMono',monospace;font-size:11px;padding:2px 6px;
border:1px solid var(--bd);border-radius:3px;background:var(--s2);
white-space:nowrap;transition:all .22s;cursor:default}
.mt .f{color:var(--mu)}.mt .a{color:var(--fa);margin:0 2px}.mt .t{font-weight:700}
.l1c .mt .t{color:var(--l1)}.l2c .mt .t{color:var(--l2)}
.l3c .mt .t{color:var(--l3)}.l4c .mt .t{color:var(--l4)}
.mt.hi{border-color:transparent!important;color:#fff!important}
.l1c .mt.hi{background:var(--l1)}.l2c .mt.hi{background:var(--l2)}
.l3c .mt.hi{background:var(--l3)}.l4c .mt.hi{background:var(--l4)}
.mt.hi .f,.mt.hi .a{color:rgba(255,255,255,.6)!important}
.mt.hi .t{color:#fff!important}
.rlist{list-style:none}
.rlist li{display:flex;gap:5px;align-items:flex-start;padding:2px 0;font-size:12px}
.rlist li .dot{width:5px;height:5px;border-radius:50%;margin-top:5px;flex-shrink:0}
.l1c .dot{background:var(--l1)}.l2c .dot{background:var(--l2)}
.l3c .dot{background:var(--l3)}.l4c .dot{background:var(--l4)}
/* ══ OUTPUT NODE (red/gold) ══ */
.merge-area{margin-top:0}
.mwires{display:grid;grid-template-columns:repeat(4,1fr);gap:14px;height:28px}
.mw{width:2px;height:100%;margin:0 auto;position:relative}
.mw::after{content:'';position:absolute;bottom:-4px;left:50%;transform:translateX(-50%);
width:8px;height:8px;border-radius:50%;border:2px solid var(--bd2);background:var(--surface)}
.mw1{background:linear-gradient(180deg,var(--l1),#999)}.mw1::after{border-color:var(--l1)}
.mw2{background:linear-gradient(180deg,var(--l2),#999)}.mw2::after{border-color:var(--l2)}
.mw3{background:linear-gradient(180deg,var(--l3),#999)}.mw3::after{border-color:var(--l3)}
.mw4{background:linear-gradient(180deg,var(--l4),#999)}.mw4::after{border-color:var(--l4)}
.mnode{
background:var(--out-bg);border:2px solid #880808;border-radius:8px;
padding:14px 22px;text-align:center;
box-shadow:0 4px 20px rgba(150,0,0,.25),inset 0 1px 0 rgba(255,215,0,.15);
position:relative
}
.mnode::before{content:'OUTPUT NODE';position:absolute;top:-10px;left:50%;transform:translateX(-50%);
font-family:'SysMono',monospace;font-size:9px;color:var(--out-txt);background:var(--out-bg);
padding:0 10px;letter-spacing:2px;border:1px solid #cc6600;border-radius:3px}
.mformula{font-family:'SysMono',monospace;font-size:11px;color:rgba(255,215,0,.7);
margin-bottom:7px;letter-spacing:.5px;line-height:1.5}
.mformula b.s1{color:#7be0ff}.mformula b.s2{color:#ffaa77}
.mformula b.s3{color:#88ee99}.mformula b.s4{color:#cc99ff}
.mout{font-family:'SysMono',monospace;font-size:30px;font-weight:800;
color:rgba(255,215,0,.4);letter-spacing:5px;min-height:36px;transition:all .3s;
text-shadow:0 0 0 transparent}
.mout.res{color:var(--out-txt);text-shadow:0 2px 8px rgba(255,200,0,.5)}
.msub{font-size:11px;color:rgba(255,215,0,.55);margin-top:5px;
font-family:'SysMono',monospace;letter-spacing:.5px}
/* ══ BOTTOM REFERENCE SECTION ══ */
.bot{display:grid;grid-template-columns:1fr 1fr 1fr;gap:14px;margin-top:20px}
/* Scrollable reference card */
.ref-card{background:var(--surface);border:1.5px solid var(--bd);
border-radius:6px;overflow:hidden;display:flex;flex-direction:column}
.ref-head{padding:9px 14px;font-family:'SysMono',monospace;font-size:9px;font-weight:700;
letter-spacing:2px;text-transform:uppercase;background:var(--s2);
border-bottom:1px solid var(--bd);color:var(--mu);
display:flex;align-items:center;justify-content:space-between;gap:8px;flex-shrink:0}
.ref-head .dot{width:6px;height:6px;border-radius:50%;flex-shrink:0}
.ref-head-left{display:flex;align-items:center;gap:8px}
.ref-scroll{overflow-y:auto;max-height:300px;padding:12px 14px;flex:1}
.ref-scroll::-webkit-scrollbar{width:5px}
.ref-scroll::-webkit-scrollbar-track{background:var(--s2)}
.ref-scroll::-webkit-scrollbar-thumb{background:var(--bd2);border-radius:3px}
/* KHD table */
.khd-g{display:grid;grid-template-columns:repeat(3,1fr);gap:7px}
.khd-grp{padding:7px;border-radius:4px;border:1px solid var(--bd)}
.khd-gt{font-family:'SysMono',monospace;font-size:8px;font-weight:700;
letter-spacing:1px;text-transform:uppercase;color:var(--mu);margin-bottom:4px}
.khd-row{display:flex;justify-content:space-between;align-items:center;
padding:2px 0;font-family:'SysMono',monospace;font-size:11px}
.khd-tn{color:var(--mu)}.khd-sym{font-weight:700;font-size:13px;
padding:1px 5px;border-radius:2px;border:1px solid var(--bd);background:var(--s2)}
.grp-non .khd-sym{color:var(--l1);background:var(--l1bg);border-color:var(--l1bd)}
.grp-trang .khd-sym{color:var(--l3);background:var(--l3bg);border-color:var(--l3bd)}
.grp-plain .khd-sym{color:var(--l4);background:var(--l4bg);border-color:var(--l4bd)}
.grp-ngang .khd-sym{color:var(--l2);background:var(--l2bg);border-color:var(--l2bd)}
/* 52 Rules list */
.rule-group{margin-bottom:12px}
.rule-group:last-child{margin-bottom:0}
.rg-title{font-family:'SysMono',monospace;font-size:9px;font-weight:700;
letter-spacing:1px;text-transform:uppercase;color:var(--acc);
margin-bottom:6px;padding:3px 6px;background:var(--l1bg);
border-radius:3px;border-left:3px solid var(--l1);display:flex;align-items:center;justify-content:space-between}
.rg-title .cnt{font-size:8px;background:var(--l1);color:#fff;padding:1px 5px;border-radius:10px}
.rule-row{display:flex;align-items:center;gap:5px;padding:3px 4px;
border-bottom:1px solid var(--s3);font-family:'SysMono',monospace;font-size:11px;
cursor:pointer;border-radius:3px;transition:background .15s}
.rule-row:last-child{border-bottom:none}
.rule-row:hover{background:var(--s2)}
.rule-row .rnum{color:var(--fa);font-size:9px;min-width:18px}
.rule-row .rfrom{color:var(--mu)}
.rule-row .rarr{color:var(--fa);margin:0 3px;font-size:10px}
.rule-row .rto{font-weight:700;color:var(--acc)}
.rule-row .rinfo{margin-left:auto;font-size:9px;color:var(--fa)}
.rule-row .popup-btn{
margin-left:auto;font-size:8px;padding:1px 5px;background:var(--s2);
border:1px solid var(--bd);border-radius:2px;cursor:pointer;color:var(--mu);
transition:all .15s;white-space:nowrap;font-family:'SysMono',monospace}
.rule-row:hover .popup-btn{background:var(--acc);color:#fff;border-color:var(--acc)}
/* Examples grid */
.ex-g{display:grid;grid-template-columns:repeat(2,1fr);gap:4px}
.ex-i{display:flex;align-items:center;gap:6px;padding:6px 9px;border:1px solid var(--bd);
border-radius:4px;background:var(--s2);font-family:'SysMono',monospace;
cursor:pointer;transition:all .15s}
.ex-i:hover{border-color:var(--acc);background:var(--l1bg)}
.ex-f{font-size:13px;color:var(--tx)}.ex-a{color:var(--bd2);font-size:10px}
.ex-t{font-size:13px;font-weight:700;color:var(--acc)}
/* ══ POPUP MODAL ══ */
.popup-overlay{position:fixed;inset:0;background:rgba(0,20,60,.45);
display:none;align-items:center;justify-content:center;z-index:1000;
backdrop-filter:blur(3px);padding:20px}
.popup-overlay.open{display:flex}
.popup-box{background:var(--surface);border:1.5px solid var(--bd2);border-radius:10px;
max-width:540px;width:100%;max-height:80vh;overflow:hidden;
box-shadow:0 8px 40px rgba(0,20,80,.2);display:flex;flex-direction:column;
animation:popIn .2s ease}
@keyframes popIn{from{opacity:0;transform:scale(.93) translateY(8px)}to{opacity:1;transform:none}}
.popup-head{padding:14px 18px;border-bottom:1px solid var(--bd);display:flex;
align-items:center;justify-content:space-between;background:var(--s2);flex-shrink:0}
.popup-head h3{font-size:14px;font-weight:700;font-family:'SysMono',monospace;
color:var(--acc);letter-spacing:1px}
.popup-close{background:none;border:1.5px solid var(--bd);border-radius:4px;
width:28px;height:28px;cursor:pointer;font-size:16px;color:var(--mu);
display:flex;align-items:center;justify-content:center;transition:all .15s;
font-family:'SysMono',monospace}
.popup-close:hover{background:var(--l2bg);border-color:var(--l2);color:var(--l2)}
.popup-body{padding:18px;overflow-y:auto;flex:1}
.popup-rule-title{font-size:18px;font-weight:800;font-family:'SysMono',monospace;
color:var(--tx);margin-bottom:6px;letter-spacing:2px}
.popup-rule-title span{color:var(--acc)}
.popup-desc{font-size:13px;color:var(--mu);margin-bottom:14px;line-height:1.6}
.popup-examples{display:flex;flex-direction:column;gap:5px}
.popup-ex-row{display:flex;align-items:center;gap:10px;padding:8px 12px;
background:var(--s2);border-radius:5px;font-family:'SysMono',monospace;font-size:13px;
border:1px solid var(--bd)}
.popup-ex-row .pe-cqn{color:var(--tx);min-width:80px}
.popup-ex-row .pe-arr{color:var(--fa)}
.popup-ex-row .pe-cvn{color:var(--l3);font-weight:700;min-width:80px}
.popup-ex-row .pe-arr2{color:var(--fa)}
.popup-ex-row .pe-cvss{color:var(--acc);font-weight:800;font-size:15px}
.popup-tag{display:inline-block;padding:2px 8px;border-radius:3px;font-size:10px;
font-family:'SysMono',monospace;font-weight:700;letter-spacing:1px;text-transform:uppercase;margin-top:8px}
.pt-l1{background:var(--l1bg);color:var(--l1);border:1px solid var(--l1bd)}
.pt-l2{background:var(--l2bg);color:var(--l2);border:1px solid var(--l2bd)}
.pt-l3{background:var(--l3bg);color:var(--l3);border:1px solid var(--l3bd)}
.pt-l4{background:var(--l4bg);color:var(--l4);border:1px solid var(--l4bd)}
/* ══ FOOTER ══ */
.footer{text-align:center;padding:16px 20px;font-size:12px;color:var(--mu);
border-top:1px solid var(--bd);margin-top:24px;font-family:'SysMono',monospace;
background:var(--surface);letter-spacing:.5px}
.footer strong{color:var(--acc)}
/* ══ ANIM ══ */
@keyframes slideIn{from{opacity:0;transform:translateY(-3px)}to{opacity:1;transform:none}}
.anim{animation:slideIn .3s ease}
/* ══ RESPONSIVE ══ */
@media(max-width:980px){.bot{grid-template-columns:1fr 1fr}}
@media(max-width:920px){.lanes{grid-template-columns:repeat(2,1fr)}}
@media(max-width:640px){
.bot{grid-template-columns:1fr}.cvt-row{grid-template-columns:1fr}
.khd-g{grid-template-columns:1fr 1fr}.hdr{flex-direction:column;gap:8px}
.lanes{grid-template-columns:1fr}
}
</style>
</head>
<body>
<!-- ══ HEADER ══ -->
<div class="hdr">
<div class="hdr-l">
<h1>CHỮ <em>VN SONG SONG</em> 4.0</h1>
<p class="mono">Bộ chuyển đổi tốc ký · CQN ⟷ CVN ⟷ CVNSS4.0 · BQ 1850/2020/QTG · Offline Ready</p>
</div>
<div class="chips">
<div class="chip c1">LÀN 1: VẦN</div>
<div class="chip c2">LÀN 2: DẤU</div>
<div class="chip c3">LÀN 3: CHỮ</div>
<div class="chip c4">LÀN 4: P</div>
</div>
</div>
<div class="wrap">
<!-- ══ CONVERTER ══ -->
<div class="cvt-card">
<div class="mode-bar">
<button class="mtab on" data-mode="cqn" onclick="setMode(this)">CQN → CVNSS</button>
<button class="mtab" data-mode="cvss" onclick="setMode(this)">CVNSS → CQN</button>
<button class="mtab" data-mode="cvn" onclick="setMode(this)">CVN → CVNSS</button>
</div>
<div class="cvt-row">
<div class="fld">
<label id="lbl-in">Nhập CQN (Chữ Quốc Ngữ)</label>
<input id="inp" type="text" placeholder="VD: nghiêng, tuyết, đường..." autocomplete="off">
</div>
<div class="cvt-mid">
<button class="btn" id="btnConv">▶ CHUYỂN</button>
</div>
<div class="fld">
<label id="lbl-out">Kết quả CVNSS 4.0</label>
<div class="out-box" id="outBox"><span class="ph">Kết quả hiển thị ở đây...</span></div>
</div>
</div>
</div>
<!-- ══ SIGNAL TOP NODE ══ -->
<div class="ftop">
<div class="node-in" id="nodeIn">
<small class="mono">INPUT SIGNAL</small>
<div class="node-val mono" id="nodeVal"></div>
</div>
</div>
<!-- BUS LINE -->
<div class="bus-wrap">
<div class="bus-h" id="busH"></div>
<div class="vias">
<div class="via v1" id="v1"></div><div class="via v2" id="v2"></div>
<div class="via v3" id="v3"></div><div class="via v4" id="v4"></div>
</div>
<div class="vert-wires">
<div class="vw vw1"></div><div class="vw vw2"></div>
<div class="vw vw3"></div><div class="vw vw4"></div>
</div>
</div>
<!-- ══ 4 LANES ══ -->
<div class="lanes">
<!-- L1 -->
<div class="lc l1c">
<div class="lhead"><span>LÀN 1 · VẦN</span><span class="rbadge" id="r1"></span></div>
<div class="lproc" id="pr1"><span class="lp-in mono" id="p1i"></span><span class="lp-arr"></span><span class="lp-out mono" id="p1o"></span></div>
<div class="lbody">
<div class="rsect">
<div class="rtitle">Rút NAG còn 1 nguyên âm</div>
<div class="mtags">
<div class="mt"><span class="f">uyê</span><span class="a"></span><span class="t">Y</span></div>
<div class="mt"><span class="f">iê/yê</span><span class="a"></span><span class="t">I</span></div>
<div class="mt"><span class="f"></span><span class="a"></span><span class="t">U</span></div>
<div class="mt"><span class="f">ươ</span><span class="a"></span><span class="t">Ư</span></div>
<div class="mt"><span class="f"></span><span class="a"></span><span class="t">Â</span></div>
<div class="mt"><span class="f"></span><span class="a"></span><span class="t">Ơ</span></div>
<div class="mt"><span class="f"></span><span class="a"></span><span class="t">Ă</span></div>
<div class="mt"><span class="f">oe</span><span class="a"></span><span class="t">E</span></div>
<div class="mt"><span class="f">oa</span><span class="a"></span><span class="t">O/A</span></div>
</div>
</div>
<div class="rsect">
<div class="rtitle">Đổi âm cuối (vần ghép)</div>
<div class="mtags">
<div class="mt"><span class="f">t</span><span class="a"></span><span class="t">d</span></div>
<div class="mt"><span class="f">p</span><span class="a"></span><span class="t">f</span></div>
<div class="mt"><span class="f">c</span><span class="a"></span><span class="t">s</span></div>
<div class="mt"><span class="f">n</span><span class="a"></span><span class="t">l</span></div>
<div class="mt"><span class="f">m</span><span class="a"></span><span class="t">v</span></div>
<div class="mt"><span class="f">ng</span><span class="a"></span><span class="t">z</span></div>
<div class="mt"><span class="f">o/u</span><span class="a"></span><span class="t">w</span></div>
<div class="mt"><span class="f">i/y</span><span class="a"></span><span class="t">j</span></div>
</div>
</div>
</div>
</div>
<!-- L2 -->
<div class="lc l2c">
<div class="lhead"><span>LÀN 2 · DẤU THANH</span><span class="rbadge" id="r2"></span></div>
<div class="lproc" id="pr2"><span class="lp-in mono" id="p2i"></span><span class="lp-arr"></span><span class="lp-out mono" id="p2o"></span></div>
<div class="lbody">
<div class="rsect">
<div class="rtitle">Nón ^ (â ê ô)</div>
<div class="mtags">
<div class="mt" id="k-B"><span class="f">sắc+^</span><span class="a">=</span><span class="t">B</span></div>
<div class="mt" id="k-D"><span class="f">huyền+^</span><span class="a">=</span><span class="t">D</span></div>
<div class="mt" id="k-Q"><span class="f">hỏi+^</span><span class="a">=</span><span class="t">Q</span></div>
<div class="mt" id="k-G"><span class="f">ngã+^</span><span class="a">=</span><span class="t">G</span></div>
<div class="mt" id="k-F"><span class="f">nặng+^</span><span class="a">=</span><span class="t">F</span></div>
<div class="mt" id="k-Y"><span class="f">ngang+^</span><span class="a">=</span><span class="t">Y</span></div>
</div>
</div>
<div class="rsect">
<div class="rtitle">Trăng/Móc (ă ơ ư)</div>
<div class="mtags">
<div class="mt" id="k-X"><span class="f">sắc+ˀ</span><span class="a">=</span><span class="t">X</span></div>
<div class="mt" id="k-K"><span class="f">huyền+ˀ</span><span class="a">=</span><span class="t">K</span></div>
<div class="mt" id="k-V"><span class="f">hỏi+ˀ</span><span class="a">=</span><span class="t">V</span></div>
<div class="mt" id="k-W"><span class="f">ngã+ˀ</span><span class="a">=</span><span class="t">W</span></div>
<div class="mt" id="k-H"><span class="f">nặng+ˀ</span><span class="a">=</span><span class="t">H</span></div>
<div class="mt" id="k-O"><span class="f">ngang+ˀ</span><span class="a">=</span><span class="t">O</span></div>
</div>
</div>
<div class="rsect">
<div class="rtitle">Không dấu phụ</div>
<div class="mtags">
<div class="mt" id="k-J"><span class="f">sắc</span><span class="a">=</span><span class="t">J</span></div>
<div class="mt" id="k-L"><span class="f">huyền</span><span class="a">=</span><span class="t">L</span></div>
<div class="mt" id="k-Z"><span class="f">hỏi</span><span class="a">=</span><span class="t">Z</span></div>
<div class="mt" id="k-S"><span class="f">ngã</span><span class="a">=</span><span class="t">S</span></div>
<div class="mt" id="k-R"><span class="f">nặng</span><span class="a">=</span><span class="t">R</span></div>
</div>
</div>
</div>
</div>
<!-- L3 -->
<div class="lc l3c">
<div class="lhead"><span>LÀN 3 · PHỤ ÂM</span><span class="rbadge" id="r3"></span></div>
<div class="lproc" id="pr3"><span class="lp-in mono" id="p3i"></span><span class="lp-arr"></span><span class="lp-out mono" id="p3o"></span></div>
<div class="lbody">
<div class="rsect">
<div class="rtitle">Đổi phụ âm đầu</div>
<div class="mtags" id="tags-dau">
<div class="mt" data-cqn="ph" data-cvn="f"><span class="f">ph</span><span class="a"></span><span class="t">F</span></div>
<div class="mt" data-cqn="qu" data-cvn="q"><span class="f">qu</span><span class="a"></span><span class="t">Q</span></div>
<div class="mt" data-cqn="k" data-cvn="c"><span class="f">k</span><span class="a"></span><span class="t">C</span></div>
<div class="mt" data-cqn="kh" data-cvn="k"><span class="f">kh</span><span class="a"></span><span class="t">K</span></div>
<div class="mt" data-cqn="dz" data-cvn="z"><span class="f">d</span><span class="a"></span><span class="t">Z</span></div>
<div class="mt" data-cqn="đ" data-cvn="d"><span class="f">đ</span><span class="a"></span><span class="t">D</span></div>
<div class="mt" data-cqn="gi" data-cvn="j"><span class="f">gi</span><span class="a"></span><span class="t">J</span></div>
<div class="mt" data-cqn="gh" data-cvn="g"><span class="f">gh</span><span class="a"></span><span class="t">G</span></div>
<div class="mt" data-cqn="ngh" data-cvn="w"><span class="f">ng/ngh</span><span class="a"></span><span class="t">W</span></div>
</div>
</div>
<div class="rsect">
<div class="rtitle">Cuối + Y/UY + bỏ sắc c/p/t</div>
<div class="mtags" id="tags-cuoi3">
<div class="mt" data-cqnc="ng" data-cvnc="g"><span class="f">ng</span><span class="a"></span><span class="t">G</span></div>
<div class="mt" data-cqnc="nh" data-cvnc="h"><span class="f">nh</span><span class="a"></span><span class="t">H</span></div>
<div class="mt" data-cqnc="ch" data-cvnc="k"><span class="f">ch</span><span class="a"></span><span class="t">K</span></div>
<div class="mt"><span class="f">y</span><span class="a"></span><span class="t">i</span></div>
<div class="mt"><span class="f">uy</span><span class="a"></span><span class="t">y</span></div>
<div class="mt"><span class="f" style="font-size:10px">c/p/t: -sắc</span></div>
</div>
</div>
</div>
</div>
<!-- L4 -->
<div class="lc l4c">
<div class="lhead"><span>LÀN 4 · TRƯỜNG HỢP P</span><span class="rbadge" id="r4"></span></div>
<div class="lproc" id="pr4"><span class="lp-in mono" id="p4i"></span><span class="lp-arr"></span><span class="lp-out mono" id="p4o"></span></div>
<div class="lbody">
<div class="rsect">
<div class="rtitle">Khi nào thêm P cuối?</div>
<ul class="rlist">
<li><span class="dot"></span>Vần KHÔNG dấu phụ + KHÔNG dấu thanh</li>
<li><span class="dot"></span>Rơi vào 1 trong 18 vần đặc biệt → +P</li>
</ul>
</div>
<div class="rsect">
<div class="rtitle">18 vần cần thêm P</div>
<div class="mtags" id="tags-p">
<div class="mt" data-p="ag"><span class="t">ag</span></div>
<div class="mt" data-p="ah"><span class="t">ah</span></div>
<div class="mt" data-p="aj"><span class="t">aj</span></div>
<div class="mt" data-p="eg"><span class="t">eg</span></div>
<div class="mt" data-p="el"><span class="t">el</span></div>
<div class="mt" data-p="ev"><span class="t">ev</span></div>
<div class="mt" data-p="ew"><span class="t">ew</span></div>
<div class="mt" data-p="ez"><span class="t">ez</span></div>
<div class="mt" data-p="ih"><span class="t">ih</span></div>
<div class="mt" data-p="oah"><span class="t">oah</span></div>
<div class="mt" data-p="og"><span class="t">og</span></div>
<div class="mt" data-p="oj"><span class="t">oj</span></div>
<div class="mt" data-p="ol"><span class="t">ol</span></div>
<div class="mt" data-p="ov"><span class="t">ov</span></div>
<div class="mt" data-p="ow"><span class="t">ow</span></div>
<div class="mt" data-p="oz"><span class="t">oz</span></div>
<div class="mt" data-p="ug"><span class="t">ug</span></div>
<div class="mt" data-p="yh"><span class="t">yh</span></div>
</div>
</div>
</div>
</div>
</div><!-- end .lanes -->
<!-- ══ MERGE / OUTPUT NODE ══ -->
<div class="merge-area">
<div class="mwires">
<div class="mw mw1"></div><div class="mw mw2"></div>
<div class="mw mw3"></div><div class="mw mw4"></div>
</div>
<div class="mnode">
<div class="mformula mono">
<b class="s1">CVN(vần)</b> + <b class="s2">KHD(dấu)</b> + <b class="s3">Phụ âm đổi</b> + <b class="s4">P(nếu cần)</b>
</div>
<div class="mout mono" id="mout"></div>
<div class="msub mono" id="msub">Nhập từ và nhấn ▶ CHUYỂN để thấy 4 làn hoạt động</div>
</div>
</div>
<!-- ══ BOTTOM 3-COLUMN REFERENCE ══ -->
<div class="bot">
<!-- Col 1: KHD Table -->
<div class="ref-card">
<div class="ref-head">
<div class="ref-head-left"><div class="dot" style="background:var(--l2)"></div>BẢNG KHD — 18 KÝ HIỆU DẤU</div>
<span style="font-size:9px;color:var(--fa)">5+5+5+3 = 18</span>
</div>
<div class="ref-scroll">
<div class="khd-g">
<div class="khd-grp grp-non">
<div class="khd-gt">Nón ^ (â ê ô)</div>
<div class="khd-row"><span class="khd-tn">sắc</span><span class="khd-sym">B</span></div>
<div class="khd-row"><span class="khd-tn">huyền</span><span class="khd-sym">D</span></div>
<div class="khd-row"><span class="khd-tn">hỏi</span><span class="khd-sym">Q</span></div>
<div class="khd-row"><span class="khd-tn">ngã</span><span class="khd-sym">G</span></div>
<div class="khd-row"><span class="khd-tn">nặng</span><span class="khd-sym">F</span></div>
</div>
<div class="khd-grp grp-trang">
<div class="khd-gt">Trăng/Móc (ă ơ ư)</div>
<div class="khd-row"><span class="khd-tn">sắc</span><span class="khd-sym">X</span></div>
<div class="khd-row"><span class="khd-tn">huyền</span><span class="khd-sym">K</span></div>
<div class="khd-row"><span class="khd-tn">hỏi</span><span class="khd-sym">V</span></div>
<div class="khd-row"><span class="khd-tn">ngã</span><span class="khd-sym">W</span></div>
<div class="khd-row"><span class="khd-tn">nặng</span><span class="khd-sym">H</span></div>
</div>
<div class="khd-grp grp-plain">
<div class="khd-gt">Không dấu phụ</div>
<div class="khd-row"><span class="khd-tn">sắc</span><span class="khd-sym">J</span></div>
<div class="khd-row"><span class="khd-tn">huyền</span><span class="khd-sym">L</span></div>
<div class="khd-row"><span class="khd-tn">hỏi</span><span class="khd-sym">Z</span></div>
<div class="khd-row"><span class="khd-tn">ngã</span><span class="khd-sym">S</span></div>
<div class="khd-row"><span class="khd-tn">nặng</span><span class="khd-sym">R</span></div>
</div>
</div>
<div style="margin-top:10px">
<div class="khd-grp grp-ngang">
<div class="khd-gt">Thanh Ngang (không dấu)</div>
<div class="khd-row"><span class="khd-tn">Nón ^</span><span class="khd-sym">Y</span></div>
<div class="khd-row"><span class="khd-tn">Trăng/Móc</span><span class="khd-sym">O</span></div>
<div class="khd-row"><span class="khd-tn">Tránh nhầm</span><span class="khd-sym">P</span></div>
</div>
</div>
<div style="margin-top:10px;padding:8px;background:var(--s2);border-radius:4px;border:1px solid var(--bd);font-size:11px;font-family:'SysMono',monospace;line-height:1.7;color:var(--mu)">
<b style="color:var(--acc)">Lưu ý:</b> Chữ kết thúc bằng C, P, T <b>không thêm J</b><br>
VD: khác=kac · áp=ap · phút=fut
</div>
</div>
</div>
<!-- Col 2: 52 Vần rút gọn -->
<div class="ref-card">
<div class="ref-head">
<div class="ref-head-left"><div class="dot" style="background:var(--l1)"></div>52 VẦN RÚT GỌN (LÀN 1)</div>
<button onclick="openPopup('popup-rules')" style="font-size:8px;padding:2px 7px;background:var(--l1bg);border:1px solid var(--l1bd);border-radius:3px;color:var(--l1);cursor:pointer;font-family:'SysMono',monospace;font-weight:700;letter-spacing:.5px">? CHÚ GIẢI</button>
</div>
<div class="ref-scroll" id="rules-scroll"><!-- built by JS --></div>
</div>
<!-- Col 3: Examples -->
<div class="ref-card">
<div class="ref-head">
<div class="ref-head-left"><div class="dot" style="background:var(--acc)"></div>VÍ DỤ · NHẤN ĐỂ THỬ</div>
</div>
<div class="ref-scroll">
<div class="ex-g" id="exG"></div>
</div>
</div>
</div><!-- end .bot -->
<!-- ══ FOOTER ══ -->
<div class="footer">
Nguồn: <strong>CVNSS4.0</strong> · Kiều Trường Lâm &amp; Trần Tư Bình · Bản quyền số 1850/2020/QTG · Cục Bản quyền tác giả, Bộ VHTTDL · <strong>2026</strong>
</div>
</div><!-- end .wrap -->
<!-- ══ POPUP: 52 Rules Explanation ══ -->
<div class="popup-overlay" id="popup-rules" onclick="closePopupIfOverlay(event,this)">
<div class="popup-box">
<div class="popup-head">
<h3>52 VẦN RÚT GỌN — CHÚ GIẢI CHI TIẾT</h3>
<button class="popup-close" onclick="closePopup('popup-rules')"></button>
</div>
<div class="popup-body">
<p style="font-size:13px;color:var(--mu);line-height:1.7;margin-bottom:14px">
CQN có <b>52 vần dài</b> (3–4 chữ cái). CVNSS4.0 rút gọn còn <b>2 chữ cái</b> theo 2 bước <em>cùng lúc</em>:
</p>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:10px;margin-bottom:14px">
<div style="padding:10px;background:var(--l1bg);border:1px solid var(--l1bd);border-radius:5px">
<div style="font-size:10px;font-weight:700;letter-spacing:1px;color:var(--l1);margin-bottom:6px;font-family:'SysMono',monospace">BƯỚC 1: Rút NAG → 1 nguyên âm</div>
<div style="font-family:'SysMono',monospace;font-size:11px;line-height:1.8;color:var(--tx)">
UYÊ → Y &nbsp;|&nbsp; IÊ/YÊ → I<br>
UÔ → U &nbsp;|&nbsp; ƯƠ → Ư<br>
UÂ → Â &nbsp;|&nbsp; UƠ → Ơ<br>
OĂ → Ă &nbsp;|&nbsp; OE → E<br>
OA → O &nbsp;|&nbsp; OA → A (oay)
</div>
</div>
<div style="padding:10px;background:var(--l3bg);border:1px solid var(--l3bd);border-radius:5px">
<div style="font-size:10px;font-weight:700;letter-spacing:1px;color:var(--l3);margin-bottom:6px;font-family:'SysMono',monospace">BƯỚC 2: Đổi âm cuối (cùng lúc)</div>
<div style="font-family:'SysMono',monospace;font-size:11px;line-height:1.8;color:var(--tx)">
T → D &nbsp;|&nbsp; P → F<br>
C → S &nbsp;|&nbsp; N → L<br>
M → V &nbsp;|&nbsp; NG → Z<br>
O/U → W &nbsp;|&nbsp; I/Y → J
</div>
</div>
</div>
<div style="padding:10px;background:var(--s2);border:1px solid var(--bd);border-radius:5px;font-family:'SysMono',monospace;font-size:12px;line-height:2;color:var(--tx)">
<b style="color:var(--acc)">Ví dụ minh họa:</b><br>
tuyết → <b style="color:var(--l1)">tyd</b> &nbsp;|&nbsp; hiệp → <b style="color:var(--l1)">hịf</b> &nbsp;|&nbsp; thuốc → <b style="color:var(--l1)">thus</b><br>
lượn → <b style="color:var(--l1)">lựl</b> &nbsp;|&nbsp; suất → <b style="color:var(--l1)">sâd</b> &nbsp;|&nbsp; thường → <b style="color:var(--l1)">thừz</b><br>
rượu → <b style="color:var(--l1)">rựw</b> &nbsp;|&nbsp; người → <b style="color:var(--l1)">wừj</b> &nbsp;|&nbsp; xoăn → <b style="color:var(--l1)">xăl</b>
</div>
</div>
</div>
</div>
<!-- ══ POPUP: Single Rule Detail ══ -->
<div class="popup-overlay" id="popup-rule-detail" onclick="closePopupIfOverlay(event,this)">
<div class="popup-box">
<div class="popup-head">
<h3 id="prd-title">Quy tắc</h3>
<button class="popup-close" onclick="closePopup('popup-rule-detail')"></button>
</div>
<div class="popup-body" id="prd-body"></div>
</div>
</div>
<script>
const CVNSSConverter = (function () {
// Bảng ánh xạ ký tự đặc biệt
const specialChars = [
"`", "“", "”", "<", ">", "@", "-", ";", "=", "…", " ", ",", ".", "?", "!",
'"', "'", "(", ")", "[", "]", "{", "}", "%", "#", "$", "&", "_", "\\", "/",
"*", ":", "+", "~", "^", "|", "\r\n", "\r", "\n"
];
// Bảng ánh xạ phụ âm
const consonants = {
cqn: ["ngh", "ng", "ch", "gh", "kh", "nh", "ph", "th", "tr", "gi", "qu", "b", "k", "d", "đ", "g", "h", "c", "l", "m", "n", "r", "s", "t", "v", "x"],
cvn: ["w", "w", "ch", "g", "k", "nh", "f", "th", "tr", "j", "q", "b", "c", "z", "d", "g", "h", "c", "l", "m", "n", "r", "s", "t", "v", "x"]
};
// Bảng ánh xạ nguyên âm
const vowels = {
cqn: [
"a", "à", "ả", "ã", "á", "ạ", "oa", "òa", "ỏa", "õa", "óa", "ọa", "oà", "oả", "oã", "oá", "oạ", "oác", "oạc", "oách", "oạch",
"oai", "oài", "oải", "oãi", "oái", "oại", "oao", "oào", "oảo", "oão", "oáo", "oạo", "oáp", "oạp", "oát", "oạt", "oắt", "oặt", "oắc", "oặc",
"oăn", "oằn", "oẳn", "oẵn", "oắn", "oặn", "oăm", "oằm", "oẳm", "oẵm", "oắm", "oặm", "oăng", "oằng", "oẳng", "oẵng", "oắng", "oặng", "oay", "oày",
"oảy", "oãy", "oáy", "oạy", "ác", "ạc", "ách", "ạch", "ai", "ài", "ải", "ãi", "ái", "ại", "am", "àm", "ảm", "ãm", "ám", "ạm", "an", "àn",
"ản", "ãn", "án", "ạn", "oan", "oàn", "oản", "oãn", "oán", "oạn", "oanh", "oành", "oảnh", "oãnh", "oánh", "oạnh", "ang", "àng", "ảng", "ãng",
"áng", "ạng", "oang", "oàng", "oảng", "oãng", "oáng", "oạng", "anh", "ành", "ảnh", "ãnh", "ánh", "ạnh", "ao", "ào", "ảo", "ão", "áo", "ạo",
"áp", "ạp", "át", "ạt", "au", "àu", "ảu", "áu", "ạu", "ay", "ày", "ảy", "ãy", "áy", "ạy", "ắc", "ặc", "ăm", "ằm", "ẳm", "ẵm", "ắm", "ặm",
"ăn", "ằn", "ẳn", "ẵn", "ắn", "ặn", "ăng", "ằng", "ẳng", "ẵng", "ắng", "ặng", "ắp", "ặp", "ắt", "ặt", "ấc", "ậc", "âm", "ầm", "ẩm", "ẫm",
"ấm", "ậm", "ân", "ần", "ẩn", "ẫn", "ấn", "ận", "âng", "ầng", "ẩng", "ẫng", "ấng", "ậng", "uân", "uần", "uẩn", "uẫn", "uấn", "uận", "uâng",
"uầng", "uẩng", "uẫng", "uấng", "uậng", "ấp", "ập", "ất", "ật", "uất", "uật", "âu", "ầu", "ẩu", "ẫu", "ấu", "ậu", "ây", "ầy", "ẩy", "ẫy",
"ấy", "ậy", "uây", "uầy", "uẩy", "uẫy", "uấy", "uậy", "e", "è", "ẻ", "ẽ", "é", "ẹ", "oe", "òe", "ỏe", "õe", "óe", "ọe", "éc", "ẹc", "em",
"èm", "ẻm", "ẽm", "ém", "ẹm", "en", "èn", "ẻn", "ẽn", "én", "ẹn", "oen", "oèn", "oẻn", "oẽn", "oén", "oẹn", "eng", "èng", "ẻng", "ẽng",
"éng", "ẹng", "eo", "èo", "ẻo", "ẽo", "éo", "ẹo", "oeo", "oèo", "oẻo", "oẽo", "oéo", "oẹo", "ép", "ẹp", "ét", "ẹt", "oét", "oẹt", "ê",
"ề", "ể", "ễ", "ế", "ệ", "uê", "uề", "uể", "uễ", "uế", "uệ", "ếch", "ệch", "uếch", "uệch", "êm", "ềm", "ểm", "ễm", "ếm", "ệm", "ên", "ền",
"ển", "ễn", "ến", "ện", "ênh", "ềnh", "ểnh", "ễnh", "ếnh", "ệnh", "uênh", "uềnh", "uểnh", "uễnh", "uếnh", "uệnh", "ếp", "ệp", "ết", "ệt",
"êu", "ều", "ểu", "ễu", "ếu", "ệu", "i", "ì", "ỉ", "ĩ", "í", "ị", "uy", "ùy", "ủy", "ũy", "úy", "ụy", "uỳ", "uỷ", "uỹ", "uý", "uỵ", "ia",
"ìa", "ỉa", "ĩa", "ía", "ịa", "uya", "íc", "ích", "ịch", "uých", "uỵch", "iếc", "iệc", "iêm", "iềm", "iểm", "iễm", "iếm", "iệm", "iên",
"iền", "iển", "iễn", "iến", "iện", "uyên", "uyền", "uyển", "uyễn", "uyến", "uyện", "iêng", "iềng", "iểng", "iễng", "iếng", "iệng", "iếp",
"iệp", "iết", "iệt", "uyết", "uyệt", "iêu", "iều", "iểu", "iễu", "iếu", "iệu", "yêt", "yệt", "yên", "yền", "yển", "yễn", "yến", "yện",
"yêm", "yềm", "yểm", "yễm", "yếm", "yệm", "yêng", "yềng", "yểng", "yễng", "yếng", "yệnh", "yêu", "yều", "yểu", "yễu", "yếu", "yệu", "im",
"ìm", "ỉm", "ĩm", "ím", "ịm", "in", "ìn", "ỉn", "ĩn", "ín", "ịn", "inh", "ình", "ỉnh", "ĩnh", "ính", "ịnh", "uynh", "uỳnh", "uỷnh", "uỹnh",
"uýnh", "uỵnh", "íp", "ịp", "uýp", "uỵp", "ít", "ịt", "uýt", "uỵt", "iu", "ìu", "ỉu", "ĩu", "íu", "ịu", "uyu", "uỳu", "uỷu", "uỹu", "uýu",
"uỵu", "uỳn", "uỷn", "uỹn", "uýn", "uỵn", "o", "ò", "ỏ", "õ", "ó", "ọ", "óc", "ọc", "oi", "òi", "ỏi", "õi", "ói", "ọi", "om", "òm", "ỏm",
"õm", "óm", "ọm", "on", "òn", "ỏn", "õn", "ón", "ọn", "ong", "òng", "ỏng", "õng", "óng", "ọng", "oóc", "oong", "oòng", "oỏng", "oõng",
"oóng", "oòng", "oọng", "óp", "ọp", "ót", "ọt", "ô", "ồ", "ổ", "ỗ", "ố", "ộ", "ốc", "ộc", "ôi", "ồi", "ổi", "ỗi", "ối", "ội", "ôm", "ồm",
"ổm", "ỗm", "ốm", "ộm", "ôn", "ồn", "ổn", "ỗn", "ốn", "ộn", "ông", "ồng", "ổng", "ỗng", "ống", "ộng", "ốp", "ộp", "ốt", "ột", "ơ", "ờ",
"ở", "ỡ", "ớ", "ợ", "ơi", "ời", "ởi", "ỡi", "ới", "ợi", "ơm", "ờm", "ởm", "ỡm", "ớm", "ợm", "ơn", "ờn", "ởn", "ỡn", "ớn", "ợn", "ơng",
"ờng", "ởng", "ỡng", "ớng", "ợng", "ớp", "ợp", "ớt", "ợt", "u", "ù", "ủ", "ũ", "ú", "ụ", "ua", "ùa", "ủa", "ũa", "úa", "ụa", "úc", "ục",
"ui", "ùi", "ủi", "ũi", "úi", "ụi", "um", "ùm", "ủm", "ũm", "úm", "ụm", "un", "ùn", "ủn", "ũn", "ún", "ụn", "ung", "ùng", "ủng", "ũng",
"úng", "ụng", "uơ", "uờ", "uở", "uỡ", "uớ", "uợ", "uơn", "uờn", "uởn", "uỡn", "uớn", "uợn", "uớt", "uợt", "uốc", "uộc", "uôi", "uồi", "uổi",
"uỗi", "uối", "uội", "uôm", "uồm", "uổm", "uỗm", "uốm", "uộm", "uôn", "uồn", "uổn", "uỗn", "uốn", "uộn", "uông", "uồng", "uổng", "uỗng",
"uống", "uộng", "uốt", "uột", "uốp", "uộp", "úp", "ụp", "út", "ụt", "ư", "ừ", "ử", "ữ", "ứ", "ự", "ưa", "ừa", "ửa", "ữa", "ứa", "ựa",
"ức", "ực", "ưi", "ừi", "ửi", "ữi", "ứi", "ựi", "ưm", "ừm", "ửm", "ữm", "ứm", "ựm", "ưn", "ừn", "ửn", "ữn", "ứn", "ựn", "ưng", "ừng",
"ửng", "ững", "ứng", "ựng", "ước", "ược", "ươi", "ười", "ưởi", "ưỡi", "ưới", "ượi", "ươm", "ườm", "ưởm", "ưỡm", "ướm", "ượm", "ươn",
"ườn", "ưởn", "ưỡn", "ướn", "ượn", "ương", "ường", "ưởng", "ưỡng", "ướng", "ượng", "ướp", "ượp", "ướt", "ượt", "ươu", "ườu", "ưởu",
"ưỡu", "ướu", "ượu", "ứt", "ựt", "ưu", "ừu", "ửu", "ữu", "ứu", "ựu", "y", "ỳ", "ỷ", "ỹ", "ý", "ỵ", "ỳa", "ỷa", "ỹa", "ýa", "ỵa"
],
cvn: [
"a", "al", "az", "as", "aj", "ar", "oa", "oal", "oaz", "oas", "oaj", "oar", "oal", "oaz", "oas", "oaj", "oar", "osj", "osr", "oakj", "oakr",
"ojp", "ojl", "ojz", "ojs", "ojj", "ojr", "owp", "owl", "owz", "ows", "owj", "owr", "ofj", "ofr", "odj", "odr", "adx", "adh", "asx", "ash",
"alo", "alk", "alv", "alw", "alx", "alh", "avo", "avk", "avv", "avw", "avx", "avh", "azo", "azk", "azv", "azw", "azx", "azh", "ajp", "ajl",
"ajz", "ajs", "ajj", "ajr", "ac", "acr", "akj", "akr", "ai", "ail", "aiz", "ais", "aij", "air", "am", "aml", "amz", "ams", "amj", "amr",
"an", "anl", "anz", "ans", "anj", "anr", "olp", "oll", "olz", "ols", "olj", "olr", "oahp", "oahl", "oahz", "oahs", "oahj", "oahr", "agp",
"agl", "agz", "ags", "agj", "agr", "ozp", "ozl", "ozz", "ozs", "ozj", "ozr", "ahp", "ahl", "ahz", "ahs", "ahj", "ahr", "ao", "aol", "aoz",
"aos", "aoj", "aor", "ap", "apr", "at", "atr", "au", "aul", "auz", "auj", "aur", "ay", "ayl", "ayz", "ays", "ayj", "ayr", "acx", "ach",
"amo", "amk", "amv", "amw", "amx", "amh", "ano", "ank", "anv", "anw", "anx", "anh", "ago", "agk", "agv", "agw", "agx", "agh", "apx", "aph",
"atx", "ath", "acb", "acf", "amy", "amd", "amq", "amg", "amb", "amf", "any", "and", "anq", "ang", "anb", "anf", "agy", "agd", "agq", "agg",
"agb", "agf", "aly", "ald", "alq", "alg", "alb", "alf", "azy", "azd", "azq", "azg", "azb", "azf", "apb", "apf", "atb", "atf", "adb", "adf",
"auy", "aud", "auq", "aug", "aub", "auf", "ayy", "ayd", "ayq", "ayg", "ayb", "ayf", "ajy", "ajd", "ajq", "ajg", "ajb", "ajf", "e", "el",
"ez", "es", "ej", "er", "oe", "oel", "oez", "oes", "oej", "oer", "ec", "ecr", "em", "eml", "emz", "ems", "emj", "emr", "en", "enl", "enz",
"ens", "enj", "enr", "elp", "ell", "elz", "els", "elj", "elr", "egp", "egl", "egz", "egs", "egj", "egr", "eo", "eol", "eoz", "eos", "eoj",
"eor", "ewp", "ewl", "ewz", "ews", "ewj", "ewr", "ep", "epr", "et", "etr", "edj", "edr", "ey", "ed", "eq", "eg", "eb", "ef", "uey", "ued",
"ueq", "ueg", "ueb", "uef", "ekb", "ekf", "uekb", "uekf", "emy", "emd", "emq", "emg", "emb", "emf", "eny", "end", "enq", "eng", "enb", "enf",
"ehy", "ehd", "ehq", "ehg", "ehb", "ehf", "uehy", "uehd", "uehq", "uehg", "uehb", "uehf", "epb", "epf", "etb", "etf", "euy", "eud", "euq",
"eug", "eub", "euf", "i", "il", "iz", "is", "ij", "ir", "y", "yl", "yz", "ys", "yj", "yr", "yl", "yz", "ys", "yj", "yr", "ia", "ial", "iaz",
"ias", "iaj", "iar", "ya", "ic", "ikj", "ikr", "ykj", "ykr", "isb", "isf", "ivy", "ivd", "ivq", "ivg", "ivb", "ivf", "ily", "ild", "ilq",
"ilg", "ilb", "ilf", "yly", "yld", "ylq", "ylg", "ylb", "ylf", "izy", "izd", "izq", "izg", "izb", "izf", "ifb", "iff", "idb", "idf", "ydb",
"ydf", "iwy", "iwd", "iwq", "iwg", "iwb", "iwf", "idb", "idf", "ily", "ild", "ilq", "ilg", "ilb", "ilf", "ivy", "ivd", "ivq", "ivg", "ivb",
"ivf", "izy", "izd", "izq", "izg", "izb", "izf", "iwy", "iwd", "iwq", "iwg", "iwb", "iwf", "im", "iml", "imz", "ims", "imj", "imr", "in",
"inl", "inz", "ins", "inj", "inr", "ihp", "ihl", "ihz", "ihs", "ihj", "ihr", "yhp", "yhl", "yhz", "yhs", "yhj", "yhr", "ip", "ipr", "yp",
"ypr", "it", "itr", "yt", "ytr", "iu", "iul", "iuz", "ius", "iuj", "iur", "yu", "yul", "yuz", "yus", "yuj", "yur", "ynl", "ynz", "yns",
"ynj", "ynr", "o", "ol", "oz", "os", "oj", "or", "oc", "ocr", "oi", "oil", "oiz", "ois", "oij", "oir", "om", "oml", "omz", "oms", "omj",
"omr", "on", "onl", "onz", "ons", "onj", "onr", "ogp", "ogl", "ogz", "ogs", "ogj", "ogr", "ooc", "oog", "oogl", "oogz", "oogs", "oogj",
"oogl", "oogr", "op", "opr", "ot", "otr", "oy", "od", "oq", "og", "ob", "of", "ocb", "ocf", "oiy", "oid", "oiq", "oig", "oib", "oif",
"omy", "omd", "omq", "omg", "omb", "omf", "ony", "ond", "onq", "ong", "onb", "onf", "ogy", "ogd", "ogq", "ogg", "ogb", "ogf", "opb",
"opf", "otb", "otf", "oo", "ok", "ov", "ow", "ox", "oh", "oio", "oik", "oiv", "oiw", "oix", "oih", "omo", "omk", "omv", "omw", "omx",
"omh", "ono", "onk", "onv", "onw", "onx", "onh", "ogo", "ogk", "ogv", "ogw", "ogx", "ogh", "opx", "oph", "otx", "oth", "u", "ul", "uz",
"us", "uj", "ur", "ua", "ual", "uaz", "uas", "uaj", "uar", "uc", "ucr", "ui", "uil", "uiz", "uis", "uij", "uir", "um", "uml", "umz", "ums",
"umj", "umr", "un", "unl", "unz", "uns", "unj", "unr", "ugp", "ugl", "ugz", "ugs", "ugj", "ugr", "uoo", "uok", "uov", "uow", "uox", "uoh",
"olo", "olk", "olv", "olw", "olx", "olh", "odx", "odh", "usb", "usf", "ujy", "ujd", "ujq", "ujg", "ujb", "ujf", "uvy", "uvd", "uvq", "uvg",
"uvb", "uvf", "uly", "uld", "ulq", "ulg", "ulb", "ulf", "uzy", "uzd", "uzq", "uzg", "uzb", "uzf", "udb", "udf", "ufb", "uff", "up", "upr",
"ut", "utr", "uo", "uk", "uv", "uw", "ux", "uh", "uao", "uak", "uav", "uaw", "uax", "uah", "ucx", "uch", "uio", "uik", "uiv", "uiw", "uix",
"uih", "umo", "umk", "umv", "umw", "umx", "umh", "uno", "unk", "unv", "unw", "unx", "unh", "ugo", "ugk", "ugv", "ugw", "ugx", "ugh", "usx",
"ush", "ujo", "ujk", "ujv", "ujw", "ujx", "ujh", "uvo", "uvk", "uvv", "uvw", "uvx", "uvh", "ulo", "ulk", "ulv", "ulw", "ulx", "ulh", "uzo",
"uzk", "uzv", "uzw", "uzx", "uzh", "ufx", "ufh", "udx", "udh", "uwo", "uwk", "uwv", "uww", "uwx", "uwh", "utx", "uth", "uuo", "uuk", "uuv",
"uuw", "uux", "uuh", "i", "il", "iz", "is", "ij", "ir", "ial", "iaz", "ias", "iaj", "iar"
],
cvss: [
"a", "al", "az", "as", "aj", "ar", "oa", "oal", "oaz", "oas", "oaj", "oar", "oal", "oaz", "oas", "oaj", "oar", "osj", "osr", "oakj", "oakr",
"ojp", "ojl", "ojz", "ojs", "ojj", "ojr", "owp", "owl", "owz", "ows", "owj", "owr", "ofj", "ofr", "odj", "odr", "adx", "adh", "asx", "ash",
"alo", "alk", "alv", "alw", "alx", "alh", "avo", "avk", "avv", "avw", "avx", "avh", "azo", "azk", "azv", "azw", "azx", "azh", "ajp", "ajl",
"ajz", "ajs", "ajj", "ajr", "ac", "acr", "akj", "akr", "ai", "ail", "aiz", "ais", "aij", "air", "am", "aml", "amz", "ams", "amj", "amr",
"an", "anl", "anz", "ans", "anj", "anr", "olp", "oll", "olz", "ols", "olj", "olr", "oahp", "oahl", "oahz", "oahs", "oahj", "oahr", "agp",
"agl", "agz", "ags", "agj", "agr", "ozp", "ozl", "ozz", "ozs", "ozj", "ozr", "ahp", "ahl", "ahz", "ahs", "ahj", "ahr", "ao", "aol", "aoz",
"aos", "aoj", "aor", "ap", "apr", "at", "atr", "au", "aul", "auz", "auj", "aur", "ay", "ayl", "ayz", "ays", "ayj", "ayr", "acx", "ach",
"amo", "amk", "amv", "amw", "amx", "amh", "ano", "ank", "anv", "anw", "anx", "anh", "ago", "agk", "agv", "agw", "agx", "agh", "apx", "aph",
"atx", "ath", "acb", "acf", "amy", "amd", "amq", "amg", "amb", "amf", "any", "and", "anq", "ang", "anb", "anf", "agy", "agd", "agq", "agg",
"agb", "agf", "aly", "ald", "alq", "alg", "alb", "alf", "azy", "azd", "azq", "azg", "azb", "azf", "apb", "apf", "atb", "atf", "adb", "adf",
"auy", "aud", "auq", "aug", "aub", "auf", "ayy", "ayd", "ayq", "ayg", "ayb", "ayf", "ajy", "ajd", "ajq", "ajg", "ajb", "ajf", "e", "el",
"ez", "es", "ej", "er", "oe", "oel", "oez", "oes", "oej", "oer", "ec", "ecr", "em", "eml", "emz", "ems", "emj", "emr", "en", "enl", "enz",
"ens", "enj", "enr", "elp", "ell", "elz", "els", "elj", "elr", "egp", "egl", "egz", "egs", "egj", "egr", "eo", "eol", "eoz", "eos", "eoj",
"eor", "ewp", "ewl", "ewz", "ews", "ewj", "ewr", "ep", "epr", "et", "etr", "edj", "edr", "ey", "ed", "eq", "eg", "eb", "ef", "uey", "ued",
"ueq", "ueg", "ueb", "uef", "ekb", "ekf", "uekb", "uekf", "emy", "emd", "emq", "emg", "emb", "emf", "eny", "end", "enq", "eng", "enb", "enf",
"ehy", "ehd", "ehq", "ehg", "ehb", "ehf", "uehy", "uehd", "uehq", "uehg", "uehb", "uehf", "epb", "epf", "etb", "etf", "euy", "eud", "euq",
"eug", "eub", "euf", "i", "il", "iz", "is", "ij", "ir", "y", "yl", "yz", "ys", "yj", "yr", "yl", "yz", "ys", "yj", "yr", "ia", "ial", "iaz",
"ias", "iaj", "iar", "ya", "ic", "ikj", "ikr", "ykj", "ykr", "isb", "isf", "ivy", "ivd", "ivq", "ivg", "ivb", "ivf", "ily", "ild", "ilq",
"ilg", "ilb", "ilf", "yly", "yld", "ylq", "ylg", "ylb", "ylf", "izy", "izd", "izq", "izg", "izb", "izf", "ifb", "iff", "idb", "idf", "ydb",
"ydf", "iwy", "iwd", "iwq", "iwg", "iwb", "iwf", "idb", "idf", "ily", "ild", "ilq", "ilg", "ilb", "ilf", "ivy", "ivd", "ivq", "ivg", "ivb",
"ivf", "izy", "izd", "izq", "izg", "izb", "izf", "iwy", "iwd", "iwq", "iwg", "iwb", "iwf", "im", "iml", "imz", "ims", "imj", "imr", "in",
"inl", "inz", "ins", "inj", "inr", "ihp", "ihl", "ihz", "ihs", "ihj", "ihr", "yhp", "yhl", "yhz", "yhs", "yhj", "yhr", "ip", "ipr", "yp",
"ypr", "it", "itr", "yt", "ytr", "iu", "iul", "iuz", "ius", "iuj", "iur", "yu", "yul", "yuz", "yus", "yuj", "yur", "ynl", "ynz", "yns",
"ynj", "ynr", "o", "ol", "oz", "os", "oj", "or", "oc", "ocr", "oi", "oil", "oiz", "ois", "oij", "oir", "om", "oml", "omz", "oms", "omj",
"omr", "on", "onl", "onz", "ons", "onj", "onr", "ogp", "ogl", "ogz", "ogs", "ogj", "ogr", "ooc", "oog", "oogl", "oogz", "oogs", "oogj",
"oogl", "oogr", "op", "opr", "ot", "otr", "oy", "od", "oq", "og", "ob", "of", "ocb", "ocf", "oiy", "oid", "oiq", "oig", "oib", "oif",
"omy", "omd", "omq", "omg", "omb", "omf", "ony", "ond", "onq", "ong", "onb", "onf", "ogy", "ogd", "ogq", "ogg", "ogb", "ogf", "opb",
"opf", "otb", "otf", "oo", "ok", "ov", "ow", "ox", "oh", "oio", "oik", "oiv", "oiw", "oix", "oih", "omo", "omk", "omv", "omw", "omx",
"omh", "ono", "onk", "onv", "onw", "onx", "onh", "ogo", "ogk", "ogv", "ogw", "ogx", "ogh", "opx", "oph", "otx", "oth", "u", "ul", "uz",
"us", "uj", "ur", "ua", "ual", "uaz", "uas", "uaj", "uar", "uc", "ucr", "ui", "uil", "uiz", "uis", "uij", "uir", "um", "uml", "umz", "ums",
"umj", "umr", "un", "unl", "unz", "uns", "unj", "unr", "ugp", "ugl", "ugz", "ugs", "ugj", "ugr", "uoo", "uok", "uov", "uow", "uox", "uoh",
"olo", "olk", "olv", "olw", "olx", "olh", "odx", "odh", "usb", "usf", "ujy", "ujd", "ujq", "ujg", "ujb", "ujf", "uvy", "uvd", "uvq", "uvg",
"uvb", "uvf", "uly", "uld", "ulq", "ulg", "ulb", "ulf", "uzy", "uzd", "uzq", "uzg", "uzb", "uzf", "udb", "udf", "ufb", "uff", "up", "upr",
"ut", "utr", "uo", "uk", "uv", "uw", "ux", "uh", "uao", "uak", "uav", "uaw", "uax", "uah", "ucx", "uch", "uio", "uik", "uiv", "uiw", "uix",
"uih", "umo", "umk", "umv", "umw", "umx", "umh", "uno", "unk", "unv", "unw", "unx", "unh", "ugo", "ugk", "ugv", "ugw", "ugx", "ugh", "usx",
"ush", "ujo", "ujk", "ujv", "ujw", "ujx", "ujh", "uvo", "uvk", "uvv", "uvw", "uvx", "uvh", "ulo", "ulk", "ulv", "ulw", "ulx", "ulh", "uzo",
"uzk", "uzv", "uzw", "uzx", "uzh", "ufx", "ufh", "udx", "udh", "uwo", "uwk", "uwv", "uww", "uwx", "uwh", "utx", "uth", "uuo", "uuk", "uuv",
"uuw", "uux", "uuh", "i", "il", "iz", "is", "ij", "ir", "ial", "iaz", "ias", "iaj", "iar"
]
};
// Bảng ánh xạ nguyên âm cơ bản
const baseVowels = [
"aàảãáạ", "ăằẳẵắặ", "âầẩẫấậ", "eèẻẽéẹ", "êềểễếệ", "iìỉĩíị",
"oòỏõóọ", "ôồổỗốộ", "ơờởỡớợ", "uùủũúụ", "ưừửữứự", "yỳỷỹýỵ"
];
// Bảng ánh xạ thay thế đặc biệt
const specialReplacements = {
y: "yỳỷỹýỵ",
i: "iìỉĩíị"
};
// Quy tắc điều chỉnh phụ âm
const consonantAdjustments = {
phu_am: ["ngh", "gh", "k"],
phu_am_chuyen_doi: ["ng", "g", "c"],
nguyen_am: "ieê"
};
// Hàm kiểm tra chữ in hoa
function isUpperCase(str) {
return /^[A-ZÀÁẢÃẠĂẰẮẲẴẶÂẦẤẨẪẬĐÈÉẺẼẸÊỀẾỂỄỆÌÍỈĨỊÒÓỎÕỌÔỒỐỔỖỘƠỜỚỞỠỢÙÚỦŨỤƯỪỨỬỮỰỲÝỶỸỴ]+$/.test(str);
}
// Hàm lấy nguyên âm cơ bản
function getBaseVowel(char) {
for (const vowelGroup of baseVowels) {
if (vowelGroup.includes(char)) return vowelGroup[0];
}
return char;
}
// Hàm tách chuỗi thành token
function splitString(str) {
str = str.normalize("NFC");
const tokens = str.split(/([\s|,|;|`|@|<|>|“|”|.|=|…|?|!|\\|'|"|(|)|[|\]|{|}|%|#|$|&|\-|_|/|*|:|+|~|^|||\r\n|\n|\r])/gm);
return tokens.filter(token => token !== "");
}
// Hàm điều chỉnh phụ âm và nguyên âm
function adjustConsonantVowel(cqnPad, cqnVan) {
const firstChar = cqnVan[0] || "";
if (cqnPad === "qu" && getBaseVowel(firstChar) === "u") {
cqnPad = "q";
}
if (!cqnPad && getBaseVowel(firstChar) === "i") {
cqnVan = cqnVan.replace(firstChar, specialReplacements.y[specialReplacements.i.indexOf(firstChar)]);
}
if (cqnPad === "gi" && getBaseVowel(firstChar) === "i") {
cqnPad = "g";
}
if (consonantAdjustments.phu_am.includes(cqnPad) && !consonantAdjustments.nguyen_am.includes(getBaseVowel(firstChar))) {
cqnPad = consonantAdjustments.phu_am_chuyen_doi[consonantAdjustments.phu_am.indexOf(cqnPad)];
}
return { cqnPad, cqnVan };
}
// Hàm chuyển từ CQN sang CVN và CVSS
function cqnToCvnAndCvss(word) {
const lowerWord = word.toLowerCase();
let consonant = "", vowelPart = lowerWord, cvnConsonant = "", cqnResult = "", cvnResult = "", cvssResult = "";
// Tìm phụ âm
for (const c of consonants.cqn) {
if (lowerWord.startsWith(c)) {
consonant = c;
vowelPart = lowerWord.replace(c, "");
break;
}
}
// Điều chỉnh đặc biệt
if (consonant === "gi" && vowelPart !== "a" && vowels.cqn.includes("i" + vowelPart)) {
vowelPart = "i" + vowelPart;
}
if (consonant === "qu" && getBaseVowel(vowelPart[0]) === "y") {
vowelPart = "u" + vowelPart;
}
if (consonant === "g" && getBaseVowel(vowelPart[0]) === "i") {
consonant = "gi";
}
// Ánh xạ phụ âm
cvnConsonant = consonants.cqn.includes(consonant) ? consonants.cvn[consonants.cqn.indexOf(consonant)] : consonant;
// Ánh xạ nguyên âm
const vowelIndex = vowels.cqn.indexOf(vowelPart);
if (vowelIndex !== -1) {
cqnResult = vowelPart;
cvnResult = vowels.cvn[vowelIndex];
cvssResult = vowels.cvss[vowelIndex];
} else {
cqnResult = vowelPart;
cvnResult = vowelPart;
cvssResult = vowelPart;
}
// Kết hợp kết quả
let cvnOutput = cvnConsonant + cvnResult;
let cvssOutput = cvnConsonant + cvssResult;
// Xử lý chữ hoa
if (word[0] !== word[0].toLowerCase()) {
if (cqnResult.length) cqnResult = cqnResult[0].toUpperCase() + cqnResult.slice(1);
if (cvnOutput.length) cvnOutput = cvnOutput[0].toUpperCase() + cvnOutput.slice(1);
if (cvssOutput.length) cvssOutput = cvssOutput[0].toUpperCase() + cvssOutput.slice(1);
}
if (isUpperCase(word)) {
cqnResult = cqnResult.toUpperCase();
cvnOutput = cvnOutput.toUpperCase();
cvssOutput = cvssOutput.toUpperCase();
}
return { cqn: cqnResult, cvn: cvnOutput, cvss: cvssOutput };
}
// Hàm chuyển từ CVN sang CQN và CVSS
function cvnToCqnAndCvss(word) {
const lowerWord = word.toLowerCase();
let consonant = "", vowelPart = lowerWord, cqnConsonant = "", cqnResult = "", cvnResult = "", cvssResult = "";
// Tìm phụ âm
for (const c of consonants.cvn) {
if (lowerWord.startsWith(c)) {
consonant = c;
cqnConsonant = consonants.cqn[consonants.cvn.indexOf(c)];
vowelPart = lowerWord.replace(c, "");
break;
}
}
// Ánh xạ nguyên âm
const vowelIndex = vowels.cvn.indexOf(vowelPart);
if (vowelIndex !== -1) {
cqnResult = vowels.cqn[vowelIndex];
cvnResult = vowelPart;
cvssResult = vowels.cvss[vowelIndex];
} else {
cqnResult = vowelPart;
cvnResult = vowelPart;
cvssResult = vowelPart;
}
// Điều chỉnh đặc biệt
if (consonant === "j" && vowelPart === "ịa") {
cqnConsonant = "gi";
cqnResult = "ỵa";
}
// Điều chỉnh phụ âm và nguyên âm
const adjusted = adjustConsonantVowel(cqnConsonant, cqnResult);
cqnConsonant = adjusted.cqnPad;
cqnResult = adjusted.cqnVan;
// Kết hợp kết quả
let cqnOutput = cqnConsonant + cqnResult;
let cvssOutput = consonant + cvssResult;
// Xử lý chữ hoa
if (word[0] !== word[0].toLowerCase()) {
if (cqnOutput.length) cqnOutput = cqnOutput[0].toUpperCase() + cqnOutput.slice(1);
if (cvnResult.length) cvnResult = cvnResult[0].toUpperCase() + cvnResult.slice(1);
if (cvssOutput.length) cvssOutput = cvssOutput[0].toUpperCase() + cvssOutput.slice(1);
}
if (isUpperCase(word)) {
cqnOutput = cqnOutput.toUpperCase();
cvnResult = cvnResult.toUpperCase();
cvssOutput = cvssOutput.toUpperCase();
}
return { cqn: cqnOutput, cvn: cvnResult, cvss: cvssOutput };
}
// Hàm chuyển từ CVSS sang CQN và CVN
function cvssToCqnAndCvn(word) {
const lowerWord = word.toLowerCase();
let consonant = "", vowelPart = lowerWord, cqnConsonant = "", cqnResult = "", cvnResult = "", cvssResult = "";
// Tìm phụ âm
for (const c of consonants.cvn) {
if (lowerWord.startsWith(c)) {
consonant = c;
cqnConsonant = consonants.cqn[consonants.cvn.indexOf(c)];
vowelPart = lowerWord.replace(c, "");
break;
}
}
// Ánh xạ nguyên âm
const vowelIndex = vowels.cvss.indexOf(vowelPart);
if (vowelIndex !== -1) {
cqnResult = vowels.cqn[vowelIndex];
cvnResult = vowels.cvn[vowelIndex];
cvssResult = vowelPart;
} else {
cqnResult = vowelPart;
cvnResult = vowelPart;
cvssResult = vowelPart;
}
// Điều chỉnh đặc biệt
if (consonant === "j" && vowelPart === "iar") {
cqnConsonant = "gi";
cqnResult = "ỵa";
}
if (lowerWord === "it") {
cqnResult = "ít";
}
if (lowerWord === "ikj") {
cqnResult = "ích";
}
// Điều chỉnh phụ âm và nguyên âm
const adjusted = adjustConsonantVowel(cqnConsonant, cqnResult);
cqnConsonant = adjusted.cqnPad;
cqnResult = adjusted.cqnVan;
// Kết hợp kết quả
let cqnOutput = cqnConsonant + cqnResult;
let cvnOutput = consonant + cvnResult;
// Xử lý chữ hoa
if (word[0] !== word[0].toLowerCase()) {
if (cqnOutput.length) cqnOutput = cqnOutput[0].toUpperCase() + cqnOutput.slice(1);
if (cvnOutput.length) cvnOutput = cvnOutput[0].toUpperCase() + cvnOutput.slice(1);
if (cvssResult.length) cvssResult = cvssResult[0].toUpperCase() + cvssResult.slice(1);
}
if (isUpperCase(word)) {
cqnOutput = cqnOutput.toUpperCase();
cvnOutput = cvnOutput.toUpperCase();
cvssResult = cvssResult.toUpperCase();
}
return { cqn: cqnOutput, cvn: cvnOutput, cvss: cvssResult };
}
// Hàm chuyển đổi toàn bộ văn bản
function convertText(input, mode) {
const tokens = splitString(input);
const result = { cqn: [], cvn: [], cvss: [] };
tokens.forEach(token => {
if (specialChars.includes(token)) {
result.cqn.push(token);
result.cvn.push(token);
result.cvss.push(token);
} else {
let converted;
if (mode === "cqn") {
converted = cqnToCvnAndCvss(token);
} else if (mode === "cvn") {
converted = cvnToCqnAndCvss(token);
} else if (mode === "cvss") {
converted = cvssToCqnAndCvn(token);
}
result.cqn.push(converted.cqn);
result.cvn.push(converted.cvn);
result.cvss.push(converted.cvss);
}
});
return {
cqn: result.cqn.join(""),
cvn: result.cvn.join(""),
cvss: result.cvss.join("")
};
}
// Xuất module
return {
convert: convertText,
specialChars
};
})();
// Xuất module cho môi trường Node.js hoặc trình duyệt
if (typeof module !== "undefined" && typeof module.exports !== "undefined") {
module.exports = CVNSSConverter;
} else {
window.CVNSSConverter = CVNSSConverter;
}
// ══════════════════════════════════════════════════════
// 52 VẦN DATA
// ══════════════════════════════════════════════════════
const RULE_GROUPS = [
{ title:"UYÊ → Y", color:"l1", rules:[
{n:1,from:"uyêt",to:"yd",ex:[["tuyết","tyd"],["khuyết","kyd"]]},
{n:2,from:"uyên",to:"yl",ex:[["nguyên","wyly"],["huyền","hyld"],["nguyễn","wylg"]]}
]},
{ title:"IÊ/YÊ → I", color:"l1", rules:[
{n:3,from:"iêt",to:"id",ex:[["viết","vid"],["thiết","tid"]]},
{n:4,from:"iêp",to:"if",ex:[["hiệp","hịf"],["nghiệp","wịf"]]},
{n:5,from:"iêc",to:"is",ex:[["việc","vịs"],["chiếc","cis"]]},
{n:6,from:"iên",to:"il",ex:[["tiền","tild"],["biên","bily"]]},
{n:7,from:"iêm",to:"iv",ex:[["hiểm","hỉv"],["tiệm","tịv"]]},
{n:8,from:"iêng",to:"iz",ex:[["nghiêng","wizy"],["tiếng","tíz"]]},
{n:9,from:"iêu",to:"iw",ex:[["liệu","lịw"],["điều","diwd"]]},
{n:10,from:"yêt",to:"id",ex:[["yết","íd"],["tuyết","tyd"]]},
{n:11,from:"yên",to:"il",ex:[["yên","ily"],["chuyên","cyl"]]},
{n:12,from:"yêm",to:"iv",ex:[["yểm","ỉv"]]},
{n:13,from:"yêng",to:"iz",ex:[["yêng","izy"]]},
{n:14,from:"yêu",to:"iw",ex:[["yếu","íw"],["chiều","ciwd"]]}
]},
{ title:"UÔ → U", color:"l3", rules:[
{n:15,from:"uôt",to:"ud",ex:[["nuốt","nud"],["cuốt","cud"]]},
{n:16,from:"uôc",to:"us",ex:[["cuộc","cụs"],["thuốc","thus"]]},
{n:17,from:"uôn",to:"ul",ex:[["luôn","luly"],["buồn","bùl"]]},
{n:18,from:"uôm",to:"uv",ex:[["nhuộm","nhụv"]]},
{n:19,from:"uông",to:"uz",ex:[["uổng","ủzy"],["chuông","cuzy"]]},
{n:20,from:"uôi",to:"uj",ex:[["ruồi","rùj"],["tuổi","tủj"]]}
]},
{ title:"ƯƠ → Ư", color:"l3", rules:[
{n:21,from:"ươt",to:"ưd",ex:[["lượt","lựd"],["cướt","cứd"]]},
{n:22,from:"ươp",to:"ưf",ex:[["cướp","cứf"]]},
{n:23,from:"ươc",to:"ưs",ex:[["bước","bứs"],["được","dựs"]]},
{n:24,from:"ươn",to:"ưl",ex:[["mướn","mứl"],["lượn","lựl"]]},
{n:25,from:"ươm",to:"ưv",ex:[["cườm","cừv"]]},
{n:26,from:"ương",to:"ưz",ex:[["tưởng","tửv"],["đường","duzk"],["thường","thừzy"]]},
{n:27,from:"ươu",to:"ưw",ex:[["rượu","rựw"]]},
{n:28,from:"ươi",to:"ưj",ex:[["người","wừj"],["lưỡi","lữj"]]}
]},
{ title:"UÂ → Â", color:"l2", rules:[
{n:29,from:"uât",to:"âd",ex:[["luật","lậd"],["suất","sâd"]]},
{n:30,from:"uân",to:"âl",ex:[["khuẩn","kẩly"],["quân","qâly"]]},
{n:31,from:"uâng",to:"âz",ex:[["khuâng","kâzy"]]},
{n:32,from:"uây",to:"âj",ex:[["khuây","kâj"]]}
]},
{ title:"UƠ → Ơ", color:"l2", rules:[
{n:33,from:"uơt",to:"ơd",ex:[["hướt","hơd"]]},
{n:34,from:"uơn",to:"ơl",ex:[["hướn","hơl"]]}
]},
{ title:"OĂ → Ă", color:"l4", rules:[
{n:35,from:"oăt",to:"ăd",ex:[["ngoắt","wắd"]]},
{n:36,from:"oăc",to:"ăs",ex:[["hoặc","hặs"]]},
{n:37,from:"oăn",to:"ăl",ex:[["xoắn","xăl"],["xoăn","xăly"]]},
{n:38,from:"oăm",to:"ăv",ex:[["oăm","ăvy"]]},
{n:39,from:"oăng",to:"ăz",ex:[["hoẵng","hẵz"]]}
]},
{ title:"OE → E", color:"l4", rules:[
{n:40,from:"oet",to:"ed",ex:[["toét","ted"]]},
{n:41,from:"oen",to:"el",ex:[["khoen","kely"]]},
{n:42,from:"oem",to:"ev",ex:[["ngoém","wévy"]]},
{n:43,from:"oeo",to:"ew",ex:[["ngoẻo","wẻwy"]]}
]},
{ title:"OA → O / A", color:"l3", rules:[
{n:44,from:"oat",to:"od",ex:[["loạt","lọd"]]},
{n:45,from:"oap",to:"of",ex:[["ngoáp","wóf"]]},
{n:46,from:"oac",to:"os",ex:[["khoác","kos"]]},
{n:47,from:"oan",to:"ol",ex:[["toán","tójl"],["loan","lolp"]]},
{n:48,from:"oam",to:"ov",ex:[["ngoạm","wọv"]]},
{n:49,from:"oang",to:"oz",ex:[["khoảng","kỏzy"],["hoang","hozy"]]},
{n:50,from:"oao",to:"ow",ex:[["ngoáo","wów"]]},
{n:51,from:"oai",to:"oj",ex:[["xoài","xòj"],["ngoại","wọj"]]},
{n:52,from:"oay",to:"aj",ex:[["xoay","xajp"],["loay hoay","lajp hajp"]]}
]}
];
// Build 52 rules in scroll area
function buildRules() {
const container = document.getElementById('rules-scroll');
let html = '';
let num = 0;
RULE_GROUPS.forEach(g => {
html += `<div class="rule-group">
<div class="rg-title" style="background:var(--${g.color}bg);color:var(--${g.color});border-left-color:var(--${g.color})">
<span>${g.title}</span><span class="cnt" style="background:var(--${g.color})">${g.rules.length}</span>
</div>`;
g.rules.forEach(r => {
num++;
html += `<div class="rule-row" onclick="showRuleDetail(${r.n})">
<span class="rnum mono">${r.n}.</span>
<span class="rfrom mono">${r.from}</span>
<span class="rarr">→</span>
<span class="rto mono">${r.to}</span>
<button class="popup-btn" onclick="event.stopPropagation();showRuleDetail(${r.n})">? xem</button>
</div>`;
});
html += `</div>`;
});
container.innerHTML = html;
}
// Show detail popup for a rule
function showRuleDetail(num) {
let rule = null, groupTitle = '';
for(const g of RULE_GROUPS) {
const r = g.rules.find(r => r.n === num);
if(r){ rule = r; groupTitle = g.title; break; }
}
if(!rule) return;
document.getElementById('prd-title').textContent = `Quy tắc ${rule.n}: ${rule.from.toUpperCase()}${rule.to.toUpperCase()}`;
let body = `
<div class="popup-rule-title"><span>${rule.from}</span> → <span>${rule.to}</span></div>
<div class="popup-desc">Nhóm <b>${groupTitle}</b> — Vần CQN <b>${rule.from}</b> rút gọn thành <b>${rule.to}</b> (2 ký tự)</div>
<div class="popup-examples">`;
rule.ex.forEach(([cqn]) => {
const r = CVNSSConverter.convert(cqn, 'cqn');
body += `<div class="popup-ex-row">
<span class="pe-cqn">${cqn}</span>
<span class="pe-arr">→</span>
<span class="pe-cvn">${r.cvn}</span>
<span class="pe-arr2">→</span>
<span class="pe-cvss">${r.cvss}</span>
</div>`;
});
body += `</div>`;
document.getElementById('prd-body').innerHTML = body;
openPopup('popup-rule-detail');
}
function openPopup(id){ document.getElementById(id).classList.add('open'); }
function closePopup(id){ document.getElementById(id).classList.remove('open'); }
function closePopupIfOverlay(e, el){ if(e.target === el) el.classList.remove('open'); }
document.addEventListener('keydown', e => { if(e.key==='Escape') document.querySelectorAll('.popup-overlay.open').forEach(el=>el.classList.remove('open')); });
// ══════════════════════════════════════════════════════
// EXAMPLES
// ══════════════════════════════════════════════════════
const EXAMPLES = [
["nghiêng","wizy"],["tuyết","tyd"],["đường","duzk"],["nguyễn","wylg"],
["phượng","fuzh"],["tiền","tild"],["khoảng","kỏzy"],["viết","vid"],
["tưởng","tửvy"],["luật","lậd"],["không","kogb"],["được","dựsy"],
["xuân","xâly"],["giữ","juw"],["xoài","xòj"],["long","logp"],
["xoay","xajp"],["reng","regp"],["phở","fov"],["huyền","hyld"],
];
function buildExamples() {
const eg = document.getElementById("exG");
EXAMPLES.forEach(([f,t]) => {
const d = document.createElement("div");
d.className = "ex-i";
d.innerHTML = `<span class="ex-f mono">${f}</span><span class="ex-a">→</span><span class="ex-t mono">${t}</span>`;
d.onclick = () => {
document.querySelectorAll(".mtab").forEach(b=>b.classList.remove("on"));
document.querySelector('[data-mode="cqn"]').classList.add("on");
mode = "cqn"; updateLabels();
document.getElementById("inp").value = f;
doConvert();
window.scrollTo({top:0,behavior:'smooth'});
};
eg.appendChild(d);
});
}
// ══════════════════════════════════════════════════════
// UI CONTROLLER
// ══════════════════════════════════════════════════════
let mode = "cqn";
function setMode(btn) {
document.querySelectorAll(".mtab").forEach(b=>b.classList.remove("on"));
btn.classList.add("on");
mode = btn.dataset.mode;
updateLabels(); resetAll();
}
function updateLabels() {
const m = {
cqn: ["Nhập CQN (Chữ Quốc Ngữ)", "Kết quả CVNSS 4.0", "VD: nghiêng, tuyết, đường..."],
cvss: ["Nhập CVNSS 4.0", "Kết quả CQN (Chữ Quốc Ngữ)", "VD: wizy, tyd, duzk, kogb..."],
cvn: ["Nhập CVN (còn dấu, ngắn hơn CQN)","Kết quả CVNSS 4.0", "VD: wĩ, lựd, cứf..."],
};
document.getElementById("lbl-in").textContent = m[mode][0];
document.getElementById("lbl-out").textContent = m[mode][1];
document.getElementById("inp").placeholder = m[mode][2];
}
function doConvert() {
const input = document.getElementById("inp").value.trim();
if(!input){ resetAll(); return; }
const words = input.split(/\s+/);
const fw = words[0];
const full = CVNSSConverter.convert(input, mode);
const single = CVNSSConverter.convert(fw, mode);
const target = mode==="cvss" ? full.cqn : full.cvss;
const tSingle = mode==="cvss" ? single.cqn : single.cvss;
// Output box
const ob = document.getElementById("outBox");
ob.innerHTML = `<span style="font-weight:800;letter-spacing:3px">${target||"—"}</span>`;
ob.classList.add("anim");
setTimeout(()=>ob.classList.remove("anim"),400);
// Node
document.getElementById("nodeVal").textContent = fw;
document.getElementById("nodeIn").classList.add("on");
document.getElementById("busH").classList.add("on");
["v1","v2","v3","v4"].forEach(id=>document.getElementById(id).classList.add("on"));
// Lanes
animateLanes(fw, single);
// Merge node
const mo = document.getElementById("mout");
mo.textContent = target||"—";
mo.classList.toggle("res", !!target && target!=="—");
const sub = mode==="cvss"
? `CVSS: ${full.cvss} · CVN: ${full.cvn}`
: `CQN: ${full.cqn} · CVN: ${full.cvn} · CVSS: ${full.cvss}`;
document.getElementById("msub").textContent = sub;
highlightKHD(tSingle);
}
function highlightKHD(cvss) {
document.querySelectorAll('[id^="k-"]').forEach(el=>el.classList.remove("hi"));
if(!cvss||cvss==="—") return;
const s = cvss.toUpperCase();
for(const k of ["B","D","Q","G","F","Y","X","K","V","W","H","O","J","L","Z","S","R"]) {
if(s.endsWith(k)){ const el=document.getElementById("k-"+k); if(el) el.classList.add("hi"); break; }
}
}
function highlightP(cvss) {
document.querySelectorAll("#tags-p .mt").forEach(el=>el.classList.remove("hi"));
if(!cvss) return;
const s = cvss.toLowerCase().replace(/p$/,"");
document.querySelectorAll("#tags-p .mt").forEach(el=>{ if(el.dataset.p===s) el.classList.add("hi"); });
}
function animateLanes(word, result) {
const {cqn,cvn,cvss} = result;
setProc(1, word, cvss||"—"); setResult("r1", cvss);
setProc(2, word, cvss||"—"); setResult("r2", cvss);
setProc(3, word, cvn||"—"); setResult("r3", cvn);
const pVans = ["ag","ah","aj","eg","el","ev","ew","ez","ih","oah","og","oj","ol","ov","ow","oz","ug","yh"];
const base = cvss ? cvss.toLowerCase().replace(/p$/,"") : "";
const needsP = cvss && pVans.includes(base) && cvss.endsWith("p");
setProc(4, cvss||"—", needsP ? "→ thêm P ✓" : "không cần P");
setResult("r4", needsP ? "P ✓" : "OK");
highlightP(cvss);
highlightConsonant(word, cvn);
}
function highlightConsonant(cqnW, cvnW) {
document.querySelectorAll("#tags-dau .mt, #tags-cuoi3 .mt").forEach(el=>el.classList.remove("hi"));
if(!cqnW||!cvnW) return;
const cl=cqnW.toLowerCase(), cvl=cvnW.toLowerCase();
document.querySelectorAll("#tags-dau .mt[data-cqn]").forEach(el => {
const cc=el.dataset.cqn, cv=el.dataset.cvn;
if((cc==="ngh" && (cl.startsWith("ng")||cl.startsWith("ngh")) && cvl.startsWith("w")) ||
(cl.startsWith(cc) && cvl.startsWith(cv))) el.classList.add("hi");
});
}
function setProc(n, inV, outV) {
document.getElementById("p"+n+"i").textContent = inV;
document.getElementById("p"+n+"o").textContent = outV;
document.getElementById("pr"+n).classList.add("show");
}
function setResult(id, val) {
const el=document.getElementById(id);
el.textContent = val||"—";
el.classList.toggle("lit", !!val && val!=="—");
}
function resetAll() {
document.getElementById("nodeVal").textContent="—";
document.getElementById("nodeIn").classList.remove("on");
document.getElementById("busH").classList.remove("on");
["v1","v2","v3","v4"].forEach(id=>document.getElementById(id).classList.remove("on"));
[1,2,3,4].forEach(n=>{ document.getElementById("pr"+n).classList.remove("show"); setResult("r"+n,""); });
document.getElementById("mout").textContent="—";
document.getElementById("mout").classList.remove("res");
document.getElementById("msub").textContent="Nhập từ và nhấn ▶ CHUYỂN để thấy 4 làn hoạt động";
document.getElementById("outBox").innerHTML='<span class="ph">Kết quả hiển thị ở đây...</span>';
document.querySelectorAll('[id^="k-"],.mt').forEach(el=>el.classList.remove("hi"));
}
document.getElementById("btnConv").onclick = doConvert;
document.getElementById("inp").addEventListener("keydown", e=>{ if(e.key==="Enter") doConvert(); });
// Init
buildRules();
buildExamples();
</script>
</body>
</html>