Teencode / index.html
CVNSS's picture
Update index.html
d232838 verified
<!doctype html>
<html lang="vi">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>Teencode Tool – Faithful Clone (Offline • 2-way)</title>
<meta name="description" content="Công cụ giúp bạn tạo Teencode tự động tới 12 loại khác nhau. Chạy offline. Có chuyển đổi 2 chiều." />
<style>
:root{
--bg:#f3f4f6; --card:#fff; --text:#111827; --muted:#6b7280;
--line:#e5e7eb; --primary:#2563eb; --primary2:#1d4ed8;
--warn:#b45309; --ok:#16a34a; --shadow:0 10px 30px rgba(0,0,0,.08);
--radius:14px;
--mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono","Courier New", monospace;
--sans: ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, "Apple Color Emoji","Segoe UI Emoji";
}
*{box-sizing:border-box}
body{margin:0; font-family:var(--sans); background:var(--bg); color:var(--text);}
.wrap{max-width:1200px; margin:0 auto; padding:22px;}
.top{
display:flex; justify-content:space-between; gap:16px; align-items:flex-start; margin-bottom:14px;
}
.title h1{margin:0; font-size:22px; line-height:1.25}
.title p{margin:8px 0 0; color:var(--muted); line-height:1.5}
.badge{
font-size:12px; padding:6px 10px; border-radius:999px;
border:1px solid var(--line); background:#fff; color:var(--muted);
height:fit-content;
}
.grid{display:grid; grid-template-columns:1.15fr .85fr; gap:14px;}
@media (max-width: 980px){ .grid{grid-template-columns:1fr;} }
.card{
background:var(--card);
border:1px solid var(--line);
border-radius:var(--radius);
box-shadow:var(--shadow);
}
.panel{padding:16px;}
label.small{display:block; font-size:13px; color:var(--muted); margin-bottom:8px;}
textarea{
width:100%; min-height:140px; resize:vertical;
padding:12px; border-radius:12px; border:1px solid var(--line);
outline:none; font-size:15px; line-height:1.45; background:#fff;
}
textarea:focus{border-color:#93c5fd; box-shadow:0 0 0 4px rgba(59,130,246,.15)}
.row{display:flex; gap:10px; align-items:center; justify-content:space-between; flex-wrap:wrap; margin-top:10px;}
.btn{
border:0; cursor:pointer; padding:10px 14px; border-radius:12px;
font-weight:700; font-size:14px;
display:inline-flex; align-items:center; gap:8px;
transition: transform .05s ease, background .15s ease, opacity .15s ease;
}
.btn:active{transform: translateY(1px)}
.btn.primary{background:var(--primary); color:#fff;}
.btn.primary:hover{background:var(--primary2)}
.btn.ghost{background:#f9fafb; border:1px solid var(--line); color:#111827;}
.btn.ghost:hover{background:#f3f4f6}
.btn.warn{background:#fff7ed; border:1px solid #fed7aa; color:var(--warn);}
.btn.warn:hover{background:#ffedd5}
.btn.small{padding:8px 10px; font-size:13px; border-radius:10px;}
.hint{color:var(--muted); font-size:13px;}
.modes h2{margin:0 0 10px; font-size:14px;}
.mode-list{display:grid; gap:8px; max-height:540px; overflow:auto; padding-right:4px;}
.mode{
display:flex; gap:10px; align-items:flex-start;
border:1px solid var(--line); border-radius:12px;
padding:10px; background:#fff;
}
.mode:hover{border-color:#cbd5e1}
.mode input{margin-top:3px}
.mode .ex{font-family:var(--mono); font-size:13px;}
.mode .tag{font-size:12px; color:var(--muted); margin-top:2px;}
.toolbar{
display:flex; gap:8px; align-items:center; flex-wrap:wrap;
padding:10px 12px; border:1px solid var(--line); border-radius:12px;
background:#fff; margin-bottom:10px;
}
.seg{
display:flex; border:1px solid var(--line); border-radius:12px; overflow:hidden;
background:#fff;
}
.seg button{
padding:8px 10px; border:0; cursor:pointer; background:#fff; color:#111827;
font-weight:700; font-size:13px;
}
.seg button.active{background:#111827; color:#fff;}
.kv{
display:flex; gap:8px; align-items:center; flex-wrap:wrap;
font-size:12px; color:var(--muted);
}
.kv strong{color:#111827}
.toast{
position:fixed; right:16px; bottom:16px;
background:#0b1220; color:#fff; padding:10px 12px;
border-radius:12px; box-shadow:0 16px 50px rgba(0,0,0,.25);
opacity:0; transform: translateY(8px); pointer-events:none;
transition:opacity .18s ease, transform .18s ease;
font-size:13px;
}
.toast.show{opacity:1; transform: translateY(0)}
.note{
margin-top:10px; padding:10px 12px; border-radius:12px;
border:1px dashed var(--line); background:#fff; color:var(--muted);
font-size:12px; line-height:1.5;
}
.spin{
width:14px; height:14px; border-radius:999px;
border:2px solid rgba(255,255,255,.35);
border-top-color:#fff;
animation:spin .7s linear infinite;
}
@keyframes spin{to{transform:rotate(360deg)}}
.loading{
display:none; align-items:center; gap:8px;
padding:8px 10px; border-radius:12px;
background:var(--primary); color:#fff; font-weight:800;
font-size:12px;
}
.loading.show{display:inline-flex}
</style>
</head>
<body>
<div class="wrap">
<div class="top">
<div class="title">
<h1>Công cụ giúp bạn tạo Teencode tự động online</h1>
<p>
Công cụ giúp bạn tạo Teencode tự động tới <b>12 loại</b> khác nhau...<br/>
Hướng dẫn tạo: Bạn nhập nội dung cần viết vào ô bên dưới, rồi chọn kiểu chữ, xem qua nội dung được tạo,
bấm nút <b>Copy All</b> để sao chép nội dung và dán vào nơi bạn cần.
</p>
</div>
<div class="badge">Offline • Faithful Clone • 2-way</div>
</div>
<div class="grid">
<!-- LEFT: Input/Output -->
<div class="card panel">
<div class="toolbar">
<div class="seg" aria-label="direction">
<button id="dirForward" type="button" class="active">Normal → Teen</button>
<button id="dirReverse" type="button">Teen → Normal</button>
</div>
<button id="btnSwap" class="btn ghost small" type="button" title="Đổi chỗ Input/Output">⇄ Swap</button>
<div class="loading" id="loadingBox">
<span class="spin"></span> Converting...
</div>
<div class="kv" style="margin-left:auto">
<span>Mode: <strong id="modeBadge">1</strong></span>
<span></span>
<span>Auto-update: <strong>ON</strong></span>
</div>
</div>
<label class="small" for="inputText">Nhập chữ vào đây nhé</label>
<textarea id="inputText" placeholder="Nhập chữ vào đây nhé"></textarea>
<div class="row">
<div class="hint">Chọn 1 kiểu teen code bên phải → kết quả cập nhật ngay.</div>
<div style="display:flex; gap:8px; flex-wrap:wrap;">
<button id="btnClear" class="btn ghost" type="button">Xoá</button>
<button id="btnCopy" class="btn primary" type="button">Copy All</button>
</div>
</div>
<hr style="border:0;border-top:1px solid var(--line); margin:14px 0;">
<label class="small" for="outputText">Kết quả</label>
<textarea id="outputText" readonly placeholder="Kết quả sẽ hiện ở đây..."></textarea>
<div class="note">
<b>Ghi chú 2 chiều:</b> Một số mode (#1/#2/#3/#4/#6/#7/#8/#12) <i>mất thông tin</i> (mã hoá/biến đổi),
nên <b>Teen → Normal</b> chỉ khôi phục <b>xấp xỉ</b> (best-effort) — đúng bản chất “teencode”.
</div>
</div>
<!-- RIGHT: Modes -->
<div class="card panel modes">
<h2>Chọn kiểu chữ teen code</h2>
<div class="mode-list" id="modeList"></div>
</div>
</div>
</div>
<div class="toast" id="toast">Đã copy!</div>
<script>
/************************************************************
* Faithful Clone Core (bám sát logic gốc teencode1.js)
* + Offline UI + 2-way convert (reverse = best-effort)
************************************************************/
// ===== helpers UI =====
const $ = (sel) => document.querySelector(sel);
const inputText = $("#inputText");
const outputText = $("#outputText");
const modeList = $("#modeList");
const modeBadge = $("#modeBadge");
const toast = $("#toast");
const btnCopy = $("#btnCopy");
const btnClear = $("#btnClear");
const btnSwap = $("#btnSwap");
const loadingBox = $("#loadingBox");
const dirForward = $("#dirForward");
const dirReverse = $("#dirReverse");
let currentMode = 1;
let direction = "forward"; // forward | reverse
function showToast(msg){
toast.textContent = msg;
toast.classList.add("show");
setTimeout(()=>toast.classList.remove("show"), 900);
}
function showLoading(ms=350){
loadingBox.classList.add("show");
setTimeout(()=>loadingBox.classList.remove("show"), ms);
}
// ===== faithful-ish normalization (replaces toNormalText in a robust UTF-8 way) =====
function toNormalText(text){
// faithful intent: remove Vietnamese diacritics + đ/Đ
return String(text)
.normalize("NFD")
.replace(/[\u0300-\u036f]/g, "")
.replace(/đ/g,"d")
.replace(/Đ/g,"D");
}
// =======================
// FORWARD: 12 modes (match teencode1.js behavior)
// =======================
function toTeenCode1(text){
text = toNormalText(text);
text = text.toLowerCase();
const textArr = text.split(' '); // faithful: split by single space
for (let i in textArr){
let word = textArr[i];
if(word.indexOf('y')>=0){
if(word.indexOf('ay')>=0){
// keep
}else if(word.indexOf('uy')>=0){
word = word.replace('uy','y');
}else{
word = word.replace('y','i');
}
}
word = word.replace(/ch$/g,"x");
word = word.replace(/^ph/g,"f");
word = word.replace(/^kh/g,"k");
word = word.replace(/^gi/g,"j");
word = word.replace(/^gh/g,"g");
word = word.replace(/^ngh/g,"ng");
word = word.replace(/^qu/g,"q");
word = word.replace(/ng$/g,"g");
word = word.replace(/nh$/g,"h");
word = word.replace('i','j'); // only first 'i'
textArr[i] = word;
}
return textArr.join(' ');
}
function toTeenCode2(text){
text = toNormalText(text);
text = text.toUpperCase();
const textArr = text.split(' ');
for (let i in textArr){
let word = textArr[i];
word = word.replace(/A/g,"4");
word = word.replace(/B/g,"13");
word = word.replace(/C/g,"6");
word = word.replace(/D/g,"10");
word = word.replace(/E/g,"3");
word = word.replace(/G/g,"9");
word = word.replace(/H/g,"76");
word = word.replace(/I/g,"1");
word = word.replace(/K/g,"14");
word = word.replace(/L/g,"2");
word = word.replace(/M/g,"111");
word = word.replace(/N/g,"11");
word = word.replace(/O/g,"0");
word = word.replace(/P/g,"19");
word = word.replace(/Q/g,"62");
word = word.replace(/R/g,"12");
word = word.replace(/S/g,"5");
word = word.replace(/T/g,"7");
word = word.replace(/U/g,"21");
word = word.replace(/Ư/g,"22"); // faithful intent of /Ư/
word = word.replace(/V/g,"17");
word = word.replace(/X/g,"96");
word = word.replace(/Y/g,"29");
word = word.replace(/X/g,"96");
textArr[i] = word;
}
return textArr.join(' ');
}
function toTeenCode3(text){
text = toNormalText(text);
text = text.toLowerCase();
const flipTable = {
a:'\u0250', b:'q', c:'\u0254', d:'p', e:'\u01DD', f:'\u025F', g:'\u0183',
h:'\u0265', i:'\u0131', j:'\u027E', k:'\u029E', l:'\u0283', m:'\u026F',
n:'u', r:'\u0279', t:'\u0287', v:'\u028C', w:'\u028D', y:'\u028E',
'.':'\u02D9','[':']','(':')','{':'}','?':'\u00BF','!':'\u00A1',
"'":',','<':'>','_':'\u203E',';':'\u061B','\u203F':'\u2040',
'\u2045':'\u2046','\u2234':'\u2235','\r':'\n'
};
const last = text.length - 1;
const result = new Array(text.length);
for (let i = last; i >= 0; --i){
const c = text.charAt(i);
const r = flipTable[c];
result[last - i] = (r !== undefined) ? r : c;
}
return result.join('');
}
function toTeenCode4(text){
text = toNormalText(text);
text = text.toUpperCase();
const textArr = text.split(' ');
for (let i in textArr){
let word = textArr[i];
word = word.replace(/A/g,"4");
word = word.replace(/E/g,"3");
word = word.replace(/G/g,"q");
// faithful: if(word[0]=="H"){word[0]='h';} else {...}
// JS strings are immutable, but intention: keep initial H untouched.
if(word[0] === "H"){
// do nothing (faithful effect)
}else{
word = word.replace(/KH/g,"kh");
word = word.replace(/H/g,"k");
}
word = word.replace(/I/g,"j");
word = word.replace(/O/g,"0");
textArr[i] = word;
}
return textArr.join(' ').toLowerCase();
}
function toTeenCode5(text){
text = text.replace(/l/g,'n');
text = text.replace(/L/g,'N');
return text;
}
function toTeenCode6(text){
text = text.toLowerCase();
const textArr = text.split(' ');
// faithful intent: if starts with vowel => add "ngh"
const vowelStart = /^(a|ă|â|e|ê|i|o|ô|ơ|u|ư|y|á|à|ả|ã|ạ|ắ|ằ|ẳ|ẵ|ặ|ấ|ầ|ẩ|ẫ|ậ|é|è|ẻ|ẽ|ẹ|ế|ề|ể|ễ|ệ|í|ì|ỉ|ĩ|ị|ó|ò|ỏ|õ|ọ|ố|ồ|ổ|ỗ|ộ|ớ|ờ|ở|ỡ|ợ|ú|ù|ủ|ũ|ụ|ứ|ừ|ử|ữ|ự|ý|ỳ|ỷ|ỹ|ỵ)/i;
for (let i in textArr){
let word = textArr[i];
if(vowelStart.test(word)){
word = "ngh" + word;
}
// faithful spirit: replace initial cluster with "nh"
word = word.replace(/^(ngh|ch|gh|gi|kh|ng|nh|ph|qu|th|tr|[bcdđghklmnpqrstvx])/i, "nh");
textArr[i] = word;
}
return textArr.join(' ').toLowerCase();
}
function toTeenCode7(text){
text = text.toLowerCase();
const textArr = text.split(' ');
for (let i in textArr){
let word = textArr[i];
word = word.replace(/b/g,'|3');
word = word.replace(/đ/g,'+)');
word = word.replace(/d/g,'])');
word = word.replace(/l/g,'|_');
// a-family
word = word.replace(/[áắấ]/g,"4'");
word = word.replace(/[àằầ]/g,"4`");
word = word.replace(/[aăâ]/g,"4");
word = word.replace(/[ảẳẩ]/g,"4");
word = word.replace(/[ạặậ]/g,"4");
word = word.replace(/[ãẵẫ]/g,"4");
// e-family
word = word.replace(/[éế]/g,"3'");
word = word.replace(/[ẻể]/g,"3?");
word = word.replace(/[èề]/g,"3`");
word = word.replace(/[eê]/g,"3");
word = word.replace(/[ẽễ]/g,"3");
word = word.replace(/[ẹệ]/g,"3");
// i-family
word = word.replace(/i/g,"j");
word = word.replace(/ì/g,"j`");
word = word.replace(/í/g,"j'");
word = word.replace(/[ỉĩị]/g,"j");
// o-family
word = word.replace(/[ỏổở]/g,"0");
word = word.replace(/[oôơ]/g,"0");
word = word.replace(/[òồờ]/g,"0`");
word = word.replace(/[óốớ]/g,"0'");
word = word.replace(/[õỗỡ]/g,"0");
word = word.replace(/[ọộợ]/g,"0.");
// u-family
word = word.replace(/[úứ]/g,"u'");
word = word.replace(/[ùừ]/g,"u`");
word = word.replace(/[ủử]/g,"u");
word = word.replace(/[ũữ]/g,"u");
word = word.replace(/[ụự]/g,"u.");
word = word.replace(/[uư]/g,"u");
// y
word = word.replace(/y/g,"ij");
textArr[i] = word;
}
return textArr.join(' ').toLowerCase();
}
function toTeenCode8(text){
text = text.toLowerCase();
const textArr = text.split(' ');
for (let i in textArr){
let word = textArr[i];
// vowels first
word = word.replace(/[áắấ]/g,"Cl'");
word = word.replace(/[àằầ]/g,"Cl`");
word = word.replace(/[aăâ]/g,"Cl");
word = word.replace(/[ảẳẩ]/g,"Cl");
word = word.replace(/[ạặậ]/g,"Cl");
word = word.replace(/[ãẵẫ]/g,"Cl");
word = word.replace(/[éế]/g,"F_'");
word = word.replace(/[ẻể]/g,"F_");
word = word.replace(/[èề]/g,"F_`");
word = word.replace(/[eê]/g,"F_");
word = word.replace(/[ẽễ]/g,"F_");
word = word.replace(/[ẹệ]/g,"F_");
word = word.replace(/[ỏổở]/g,"º");
word = word.replace(/[oôơ]/g,"º");
word = word.replace(/[òồờ]/g,"º`");
word = word.replace(/[óốớ]/g,"º'");
word = word.replace(/[õỗỡ]/g,"º");
word = word.replace(/[ọộợ]/g,"º");
word = word.replace(/i/g,"]");
word = word.replace(/ì/g,"]`");
word = word.replace(/í/g,"]'");
word = word.replace(/[ỉĩị]/g,"]");
word = word.replace(/[úứ]/g,"µ'");
word = word.replace(/[ùừ]/g,"µ`");
word = word.replace(/[ủử]/g,"µ");
word = word.replace(/[ũữ]/g,"µ");
word = word.replace(/[ụự]/g,"µ");
word = word.replace(/[uư]/g,"µ");
// consonants
word = word.replace(/d/g, "])");
word = word.replace(/đ/g, "+)");
word = word.replace(/n/g, "]\\[");
word = word.replace(/h/g, "¨|");
word = word.replace(/g/g, "(¬");
word = word.replace(/k/g, "]<");
word = word.replace(/t/g, "¨ ");
word = word.replace(/v/g, "\\/");
word = word.replace(/m/g, "/v\\");
word = word.replace(/^qu/g,"v\\/");
word = word.replace(/r/g, "Pv");
word = word.replace(/w/g, "v\\/");
word = word.replace(/y/g, "¥");
word = word.replace(/s/g, "§");
word = word.replace(/x/g, "><");
word = word.replace(/p/g, "]º");
word = word.replace(/b/g, "]3");
word = word.replace(/c/g, "(");
textArr[i] = word;
}
return textArr.join(' ');
}
function toTeenCode9(text, s){
if(s === undefined) s = "'s";
const textArr = text.split(' ');
for (let i in textArr){
let word = textArr[i];
const symbol = "~`!@#$%^&*()_-+={}[]:>;'.,</?*-+";
if(word.match(/[^A-Za-z0-9]/g) == null){
word = word + s;
}else{
const spArr = word.match(/[^A-Za-z0-9]/g);
let pos = word.length;
for (let x in spArr){
if(symbol.indexOf(spArr[x]) !== -1){
pos = word.indexOf(spArr[x]);
break;
}
}
word = [word.slice(0,pos), s, word.slice(pos)].join('');
}
textArr[i] = word;
}
return textArr.join(' ');
}
function toTeenCode10(text){ return toTeenCode9(text,"'ss"); }
function toTeenCode11(text){ return toTeenCode9(text,"'sss"); }
function toTeenCode12(text){
text = text.toLowerCase();
const textArr = text.split(' ');
for (let i in textArr){
let word = textArr[i].split('');
const rand = Math.floor((Math.random()*10)+1);
let j = (rand<=5) ? 0 : 1;
for(; j<word.length; j+=2){
word[j] = String(word[j]).toUpperCase();
}
textArr[i] = word.join('');
}
return textArr.join(' ');
}
// =======================
// REVERSE: best-effort decode
// =======================
function decodeMode1(text){
// best-effort: restore some patterns (lossy)
let t = String(text).toLowerCase();
const arr = t.split(' ');
for (let i in arr){
let w = arr[i];
// endings
w = w.replace(/x$/g, "ch");
w = w.replace(/g$/g, "ng"); // might be original g
w = w.replace(/h$/g, "nh"); // might be original h
// starts
w = w.replace(/^f/g, "ph");
w = w.replace(/^q/g, "qu");
w = w.replace(/^j/g, "gi");
w = w.replace(/^k/g, "kh"); // might be original k
// restore j -> i elsewhere
w = w.replace(/j/g, "i");
arr[i] = w;
}
return arr.join(' ');
}
function decodeMode2(text){
// reverse mapping: greedy longest match
const codeToChar = {
"111":"M","96":"X","76":"H","62":"Q","29":"Y","22":"Ư","21":"U",
"19":"P","17":"V","14":"K","13":"B","12":"R","11":"N","10":"D",
"9":"G","7":"T","6":"C","5":"S","4":"A","3":"E","2":"L","1":"I","0":"O"
};
const codes = Object.keys(codeToChar).sort((a,b)=>b.length-a.length);
const words = String(text).trim().split(/\s+/);
const out = words.map(w=>{
let s = w;
let res = "";
while(s.length){
let matched = false;
for(const c of codes){
if(s.startsWith(c)){
res += codeToChar[c];
s = s.slice(c.length);
matched = true;
break;
}
}
if(!matched){
// keep unknown char and move on
res += s[0];
s = s.slice(1);
}
}
return res.toLowerCase();
});
return out.join(" ");
}
function decodeMode3(text){
// build inverse flip table, then reverse string and map back
const flipTable = {
a:'\u0250', b:'q', c:'\u0254', d:'p', e:'\u01DD', f:'\u025F', g:'\u0183',
h:'\u0265', i:'\u0131', j:'\u027E', k:'\u029E', l:'\u0283', m:'\u026F',
n:'u', r:'\u0279', t:'\u0287', v:'\u028C', w:'\u028D', y:'\u028E',
'.':'\u02D9','[':']','(':')','{':'}','?':'\u00BF','!':'\u00A1',
"'":',','<':'>','_':'\u203E',';':'\u061B','\u203F':'\u2040',
'\u2045':'\u2046','\u2234':'\u2235','\r':'\n'
};
const inv = {};
for(const k in flipTable){
inv[flipTable[k]] = k;
}
const s = String(text);
let out = "";
for(let i=s.length-1; i>=0; i--){
const ch = s[i];
out += (inv[ch] !== undefined) ? inv[ch] : ch;
}
return out;
}
function decodeMode4(text){
// best-effort: reverse leet symbols
let t = toNormalText(String(text)).toLowerCase();
t = t.replace(/4/g,"a").replace(/3/g,"e").replace(/0/g,"o").replace(/q/g,"g").replace(/j/g,"i");
// H/K ambiguity can't be restored
return t;
}
function decodeMode5(text){
// lossy reverse (n -> l)
return String(text).replace(/n/g,"l").replace(/N/g,"L");
}
function decodeMode6(text){
// cannot restore original clusters reliably
return String(text);
}
function decodeWithTokens(text, tokenMap){
// tokenMap: array [token, char], ordered longest-first
const s = String(text);
let i = 0;
let out = "";
while(i < s.length){
let matched = false;
for(const [tok, val] of tokenMap){
if(s.startsWith(tok, i)){
out += val;
i += tok.length;
matched = true;
break;
}
}
if(!matched){
out += s[i];
i++;
}
}
return out;
}
function decodeMode7(text){
// IMPORTANT: longest-first
const tokens = [
["0.", "o"], ["0`","o"], ["0'","o"],
["u.","u"], ["u`","u"], ["u'","u"],
["j`","i"], ["j'","i"], ["ij","y"],
["|_","l"], ["|3","b"], ["+)", "đ"], ["])", "d"], ["])","d"], // safety
["4`","a"], ["4'","a"], ["4","a"],
["3`","e"], ["3'","e"], ["3?","e"], ["3","e"],
["0","o"], ["u","u"], ["j","i"]
];
// unify "])" exact
const cleaned = String(text).replace(/\]\)/g, "])");
return decodeWithTokens(cleaned, tokens).toLowerCase();
}
function decodeMode8(text){
const tokens = [
["/v\\","m"],
["v\\/","qu"], // qu + w ambiguity
["\\/", "v"],
["Pv","r"],
["]\\[","n"],
["(¬","g"],
["]<","k"],
["]º","p"],
["]3","b"],
["><","x"],
["+)", "đ"],
["])", "d"],
["¨|","h"],
["¨ ","t"],
["§","s"],
["¥","y"],
["µ`","u"], ["µ'","u"], ["µ","u"],
["º`","o"], ["º'","o"], ["º","o"],
["F_`","e"], ["F_'","e"], ["F_","e"],
["Cl`","a"], ["Cl'","a"], ["Cl","a"],
["]`","i"], ["]'","i"], ["]","i"],
["(","c"]
];
return decodeWithTokens(String(text), tokens).toLowerCase();
}
function decodeMode9(text){
return String(text).replace(/'s\b/g,"").replace(/'s(?=[^A-Za-z0-9]|$)/g,"");
}
function decodeMode10(text){
return String(text).replace(/'ss\b/g,"").replace(/'ss(?=[^A-Za-z0-9]|$)/g,"");
}
function decodeMode11(text){
return String(text).replace(/'sss\b/g,"").replace(/'sss(?=[^A-Za-z0-9]|$)/g,"");
}
function decodeMode12(text){
return String(text).toLowerCase();
}
// ===== apply =====
function applyForward(mode, text){
switch(mode){
case 1: return toTeenCode1(text);
case 2: return toTeenCode2(text);
case 3: return toTeenCode3(text);
case 4: return toTeenCode4(text);
case 5: return toTeenCode5(text);
case 6: return toTeenCode6(text);
case 7: return toTeenCode7(text);
case 8: return toTeenCode8(text);
case 9: return toTeenCode9(text);
case 10: return toTeenCode10(text);
case 11: return toTeenCode11(text);
case 12: return toTeenCode12(text);
default: return text;
}
}
function applyReverse(mode, text){
switch(mode){
case 1: return decodeMode1(text);
case 2: return decodeMode2(text);
case 3: return decodeMode3(text);
case 4: return decodeMode4(text);
case 5: return decodeMode5(text);
case 6: return decodeMode6(text);
case 7: return decodeMode7(text);
case 8: return decodeMode8(text);
case 9: return decodeMode9(text);
case 10: return decodeMode10(text);
case 11: return decodeMode11(text);
case 12: return decodeMode12(text);
default: return text;
}
}
function convertNow(){
showLoading(450);
modeBadge.textContent = String(currentMode);
const origin = inputText.value ?? "";
let out = "";
if(direction === "forward"){
out = applyForward(currentMode, origin);
}else{
out = applyReverse(currentMode, origin);
}
outputText.value = out;
}
// ===== UI modes list (faithful labels) =====
const MODE_META = [
{id:1, ex:"day la vj du", tag:"Chuẩn hoá + teen hoá (bỏ dấu, i→j...)"},
{id:2, ex:"10429 24 171 1021", tag:"Mã hoá số (mapping chữ cái)"},
{id:3, ex:"up ıʌ ɐʃ ʎɐp", tag:"Lật chữ (upside-down)"},
{id:4, ex:"d4y l4 vj du", tag:"Leet cơ bản (a→4, e→3, o→0...)"},
{id:5, ex:"Đây nà ví dụ", tag:"Cute-speak (l→n)"},
{id:6, ex:"nhây nhà nhí nhụ", tag:"Nh-speak"},
{id:7, ex:"+)4ij |_4` vj' ])u.", tag:"ASCII/leet nâng cao (kiểu 1)"},
{id:8, ex:"+)Cl¥ lCl` \\/]' ])µ", tag:"ASCII/leet nâng cao (kiểu 2)"},
{id:9, ex:"Đây's là's ví's dụ's", tag:"Thêm hậu tố 's"},
{id:10, ex:"Đây'ss là'ss ví'ss dụ'ss", tag:"Thêm hậu tố 'ss"},
{id:11, ex:"Đây'sss là'sss ví'sss dụ'sss", tag:"Thêm hậu tố 'sss"},
{id:12, ex:"ĐâY Là Ví Dụ", tag:"Xen kẽ hoa/thường (ngẫu nhiên)"},
];
function esc(s){
return String(s).replace(/[&<>"']/g, ch => (
{ "&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;","'":"&#39;" }[ch]
));
}
function renderModes(){
modeList.innerHTML = MODE_META.map(m => `
<div class="mode" data-mode="${m.id}">
<input type="checkbox" id="m-${m.id}" ${m.id===currentMode ? "checked":""} />
<label for="m-${m.id}">
<div class="ex">${esc(m.ex)}</div>
<div class="tag">${esc(m.tag)}</div>
</label>
</div>
`).join("");
// faithful behavior: only one checkbox checked
modeList.querySelectorAll(".mode").forEach(box=>{
box.addEventListener("click", (e)=>{
const id = Number(box.getAttribute("data-mode"));
currentMode = id;
// uncheck all
modeList.querySelectorAll("input[type='checkbox']").forEach(cb => cb.checked = false);
// check current
const mine = box.querySelector("input[type='checkbox']");
mine.checked = true;
convertNow();
});
});
}
// ===== direction toggle =====
function setDirection(dir){
direction = dir;
if(dir === "forward"){
dirForward.classList.add("active");
dirReverse.classList.remove("active");
}else{
dirReverse.classList.add("active");
dirForward.classList.remove("active");
}
convertNow();
}
dirForward.addEventListener("click", ()=>setDirection("forward"));
dirReverse.addEventListener("click", ()=>setDirection("reverse"));
// ===== events =====
inputText.addEventListener("input", convertNow);
btnClear.addEventListener("click", ()=>{
inputText.value = "";
outputText.value = "";
inputText.focus();
showToast("Đã xoá");
});
btnSwap.addEventListener("click", ()=>{
const a = inputText.value;
const b = outputText.value;
inputText.value = b;
outputText.value = a;
showToast("Đã swap");
convertNow();
});
btnCopy.addEventListener("click", async ()=>{
const text = outputText.value || "";
if(!text.trim()){ showToast("Chưa có nội dung để copy"); return; }
try{
await navigator.clipboard.writeText(text);
showToast("Đã copy!");
}catch(_){
// fallback
outputText.focus();
outputText.select();
try{
const ok = document.execCommand("copy");
showToast(ok ? "Đã copy!" : "Copy bị chặn");
}catch(e){
showToast("Copy bị chặn");
}
outputText.setSelectionRange(0,0);
}
});
// ===== init =====
renderModes();
convertNow();
</script>
</body>
</html>