{"id":32746,"date":"2025-10-28T22:49:08","date_gmt":"2025-10-28T15:49:08","guid":{"rendered":"https:\/\/dps.media\/?p=32746"},"modified":"2025-10-28T22:49:08","modified_gmt":"2025-10-28T15:49:08","slug":"32746-2","status":"publish","type":"post","link":"https:\/\/dps.media\/en\/32746-2\/","title":{"rendered":""},"content":{"rendered":"<?xml encoding=\"utf-8\" ?><!-- CSV\/XLSX \u2192 Cloudflare Zone Converter - DPS.MEDIA branded, CSS isolated --><div id=\"csvToZoneWidget\"><\/div><script>\r\n(function(){\r\n  const ROOT_ID = 'csvToZoneWidget';\r\n  const root = document.getElementById(ROOT_ID);\r\n  if (!root) return;\r\n\r\n  \/\/ Styles (isolated with #csvToZoneWidget prefix)\r\n  const styles = `\r\n  #${ROOT_ID} {\r\n    all: initial !important;\r\n    font-family: system-ui, -apple-system, \"Segoe UI\", Roboto, Arial, sans-serif !important;\r\n  }\r\n  #${ROOT_ID} *, #${ROOT_ID} *::before, #${ROOT_ID} *::after {\r\n    box-sizing: border-box !important;\r\n    margin: 0 !important;\r\n    padding: 0 !important;\r\n    border: none !important;\r\n    background: none !important;\r\n    outline: none !important;\r\n    text-decoration: none !important;\r\n    list-style: none !important;\r\n    font: inherit !important;\r\n    color: inherit !important;\r\n    vertical-align: baseline !important;\r\n    line-height: normal !important;\r\n  }\r\n\r\n  \/* Container *\/\r\n  #${ROOT_ID} .cz-container {\r\n    max-width: 1000px !important;\r\n    margin: 24px auto !important;\r\n    border: 1px solid #e5e7eb !important;\r\n    border-radius: 16px !important;\r\n    overflow: hidden !important;\r\n    background: #fff !important;\r\n    box-shadow: 0 10px 25px -5px rgba(21,21,119,0.1), 0 4px 6px -2px rgba(21,21,119,0.05) !important;\r\n    font-family: system-ui, -apple-system, \"Segoe UI\", Roboto, Arial, sans-serif !important;\r\n  }\r\n\r\n  \/* Header *\/\r\n  #${ROOT_ID} .cz-header {\r\n    background: linear-gradient(135deg, #151577 0%, #1e1e7a 100%) !important;\r\n    padding: 20px !important;\r\n    color: #fff !important;\r\n    position: relative !important;\r\n    overflow: hidden !important;\r\n  }\r\n  #${ROOT_ID} .cz-header-bg1 {\r\n    position: absolute !important; top: -20px !important; right: -20px !important;\r\n    width: 100px !important; height: 100px !important; background: rgba(255,255,255,0.05) !important; border-radius: 50% !important;\r\n  }\r\n  #${ROOT_ID} .cz-header-bg2 {\r\n    position: absolute !important; bottom: -30px !important; left: -30px !important;\r\n    width: 80px !important; height: 80px !important; background: rgba(50,181,97,0.15) !important; border-radius: 50% !important;\r\n  }\r\n  #${ROOT_ID} .cz-header-content { position: relative !important; z-index: 1 !important; }\r\n  #${ROOT_ID} .cz-header-top { display: flex !important; align-items: center !important; justify-content: space-between !important; margin-bottom: 8px !important; }\r\n  #${ROOT_ID} .cz-header-left { display: flex !important; align-items: center !important; gap: 12px !important; }\r\n  #${ROOT_ID} .cz-icon {\r\n    width: 40px !important; height: 40px !important; border-radius: 12px !important; display: flex !important; align-items: center !important; justify-content: center !important;\r\n    background: linear-gradient(135deg, #32b561, #28a555) !important; color: #fff !important; font-weight: 700 !important; font-size: 20px !important;\r\n    box-shadow: 0 4px 8px rgba(50,181,97,0.3) !important;\r\n  }\r\n  #${ROOT_ID} .cz-title { font-size: 20px !important; font-weight: 700 !important; letter-spacing: -0.5px !important; }\r\n  #${ROOT_ID} .cz-subtitle { font-size: 13px !important; opacity: 0.85 !important; }\r\n  #${ROOT_ID} .cz-brand { text-align: right !important; font-size: 12px !important; opacity: 0.75 !important; }\r\n  #${ROOT_ID} .cz-brand-name { color: #32b561 !important; font-weight: 700 !important; }\r\n  #${ROOT_ID} .cz-desc { font-size: 13px !important; opacity: 0.95 !important; }\r\n\r\n  \/* Main *\/\r\n  #${ROOT_ID} .cz-main { padding: 20px !important; }\r\n  #${ROOT_ID} .cz-actions { display: flex !important; flex-wrap: wrap !important; gap: 12px !important; align-items: end !important; margin-bottom: 12px !important; }\r\n  #${ROOT_ID} .cz-file {\r\n    padding: 12px !important; border: 2px dashed #cbd5e1 !important; border-radius: 12px !important; background: #f8fafc !important; color: #374151 !important;\r\n    display: flex !important; align-items: center !important; gap: 10px !important; flex: 1 !important; min-width: 240px !important;\r\n  }\r\n  #${ROOT_ID} .cz-file input[type=\"file\"] { display: none !important; }\r\n  #${ROOT_ID} .cz-btn { padding: 12px 18px !important; border-radius: 10px !important; cursor: pointer !important; font-weight: 700 !important; font-size: 13px !important; transition: all 0.25s !important; }\r\n  #${ROOT_ID} .cz-btn-primary { background: linear-gradient(135deg, #151577, #1e1e7a) !important; color: #fff !important; box-shadow: 0 4px 12px rgba(21,21,119,0.3) !important; }\r\n  #${ROOT_ID} .cz-btn-primary:hover { transform: translateY(-1px) !important; box-shadow: 0 6px 18px rgba(21,21,119,0.4) !important; }\r\n  #${ROOT_ID} .cz-btn-secondary { background: #fff !important; border: 2px solid #cbd5e1 !important; color: #374151 !important; }\r\n  #${ROOT_ID} .cz-btn-danger { background: #fff !important; border: 2px solid #dc2626 !important; color: #dc2626 !important; }\r\n  #${ROOT_ID} .cz-btn:disabled { background: #6b7280 !important; color: #fff !important; cursor: not-allowed !important; box-shadow: none !important; transform: none !important; }\r\n\r\n  #${ROOT_ID} .cz-statusbar { display: flex !important; gap: 10px !important; flex-wrap: wrap !important; align-items: center !important; padding: 12px !important; background: linear-gradient(90deg, #f8fafc, #f1f5f9) !important; border-radius: 12px !important; border: 1px solid #e2e8f0 !important; margin-bottom: 12px !important; }\r\n  #${ROOT_ID} .cz-status { font-size: 12px !important; font-weight: 600 !important; color: #151577 !important; margin-left: auto !important; }\r\n  #${ROOT_ID} .cz-badge { font-size: 12px !important; padding: 6px 10px !important; border-radius: 8px !important; background: #eef2ff !important; color: #151577 !important; border: 1px solid #c7d2fe !important; }\r\n  #${ROOT_ID} .cz-error { display: none !important; background: linear-gradient(135deg, #fef2f2, #fee2e2) !important; border: 2px solid #fca5a5 !important; color: #991b1b !important; padding: 12px !important; border-radius: 12px !important; margin-bottom: 12px !important; align-items: center !important; gap: 10px !important; }\r\n\r\n  \/* Output *\/\r\n  #${ROOT_ID} .cz-output { border: 2px solid #e5e7eb !important; border-radius: 12px !important; background: #fafafa !important; min-height: 160px !important; padding: 12px !important; overflow: auto !important; font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, \"Liberation Mono\", \"Courier New\", monospace !important; font-size: 12px !important; white-space: pre !important; }\r\n  #${ROOT_ID} .cz-empty { text-align: center !important; color: #64748b !important; font-style: italic !important; }\r\n\r\n  \/* Toggle groups *\/\r\n  #${ROOT_ID} .cz-row { display: flex !important; flex-wrap: wrap !important; gap: 12px !important; align-items: center !important; margin-bottom: 12px !important; }\r\n  #${ROOT_ID} .cz-toggle { display: inline-flex !important; gap: 6px !important; background: #f1f5f9 !important; border: 1px solid #e2e8f0 !important; padding: 4px !important; border-radius: 10px !important; }\r\n  #${ROOT_ID} .cz-toggle-btn { padding: 8px 12px !important; border-radius: 8px !important; font-size: 12px !important; cursor: pointer !important; background: #fff !important; color: #374151 !important; border: 1px solid transparent !important; }\r\n  #${ROOT_ID} .cz-toggle-btn.active { background: linear-gradient(135deg, #151577, #1e1e7a) !important; color: #fff !important; border-color: #151577 !important; box-shadow: 0 4px 12px rgba(21,21,119,0.25) !important; }\r\n\r\n  \/* Footer *\/\r\n  #${ROOT_ID} .cz-footer { padding: 12px 20px !important; border-top: 1px solid #e5e7eb !important; background: #f9fafb !important; }\r\n  #${ROOT_ID} .cz-footer-content { display: flex !important; gap: 8px !important; align-items: center !important; justify-content: center !important; font-size: 12px !important; color: #6b7280 !important; }\r\n  #${ROOT_ID} .cz-footer-brand { color: #32b561 !important; font-weight: 700 !important; }\r\n  #${ROOT_ID} .cz-footer-link { color: #151577 !important; text-decoration: underline !important; }\r\n  `;\r\n\r\n  \/\/ Inject CSS into <head> (safer across WP themes)\r\n  const styleEl = document.createElement('style');\r\n  styleEl.textContent = styles;\r\n  document.head.appendChild(styleEl);\r\n\r\n  \/\/ HTML structure\r\n  root.innerHTML = `\r\n    <div class=\"cz-container\">\r\n      <div class=\"cz-header\">\r\n        <div class=\"cz-header-bg1\">\r\n        <div class=\"cz-header-bg2\">\r\n        <div class=\"cz-header-content\">\r\n          <div class=\"cz-header-top\">\r\n            <div class=\"cz-header-left\">\r\n              <div class=\"cz-icon\">\ud83e\udde9\r\n              <div>\r\n                <div class=\"cz-title\">CSV\/XLSX \u2192 Cloudflare Zone\r\n                <div class=\"cz-subtitle\">Chuy\u1ec3n t\u1eeb b\u1ea3ng DNS sang file BIND (Cloudflare)\r\n              \r\n            \r\n            <div class=\"cz-brand\">\r\n              <div class=\"cz-brand-name\">DPS.MEDIA\r\n              <div>Digital Tools\r\n            \r\n          \r\n          <div class=\"cz-desc\">Nh\u1eadp file CSV\/XLSX c\u00f3 c\u1ed9t: <strong>Host, <strong>Type, <strong>Data, <strong>TTL. K\u1ebft qu\u1ea3 gi\u1ed1ng v\u00ed d\u1ee5 trong <em>zone-sample.\r\n        \r\n      \r\n      <div class=\"cz-main\">\r\n        <div class=\"cz-actions\">\r\n          <label class=\"cz-file\">\r\n            <div>\ud83d\udcc1 Ch\u1ecdn file (.csv, .xlsx, .txt)\r\n            <input id=\"cz-file-input\" type=\"file\" accept=\".csv,.xlsx,.xls,.txt,.zone\" \/>\r\n          \r\n          <button id=\"cz-convert\" class=\"cz-btn cz-btn-primary\" disabled>\ud83d\ude80 Chuy\u1ec3n \u0110\u1ed5i\r\n          <button id=\"cz-clear\" class=\"cz-btn cz-btn-danger\">\ud83d\uddd1\ufe0f X\u00f3a\r\n        \r\n        \r\n        <div class=\"cz-row\">\r\n          <div class=\"cz-toggle\" id=\"cz-input-mode\">\r\n            <button class=\"cz-toggle-btn active\" data-mode=\"file\">T\u1ec7p\r\n            <button class=\"cz-toggle-btn\" data-mode=\"paste\">D\u00e1n\r\n          \r\n          <div class=\"cz-toggle\" id=\"cz-paste-type\" style=\"display:none\">\r\n            <button class=\"cz-toggle-btn active\" data-ptype=\"zone\">Zone (.txt)\r\n            <button class=\"cz-toggle-btn\" data-ptype=\"csv\">CSV\r\n          \r\n        \r\n\r\n        <div class=\"cz-row\" id=\"cz-paste-wrap\" style=\"display:none\">\r\n          <textarea id=\"cz-paste\" rows=\"8\" style=\"width:100%; padding:12px; border:2px solid #e5e7eb; border-radius:12px; background:#fafbfc; color:#374151; font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace; font-size:12px; resize:vertical;\">\r\n        \r\n        <div class=\"cz-statusbar\">\r\n          <div id=\"cz-fileinfo\" class=\"cz-badge\">Ch\u01b0a ch\u1ecdn file\r\n          <div class=\"cz-toggle\" id=\"cz-view-mode\">\r\n            <button class=\"cz-toggle-btn active\" data-view=\"zone\">Hi\u1ec3n th\u1ecb Zone\r\n            <button class=\"cz-toggle-btn\" data-view=\"csv\">Hi\u1ec3n th\u1ecb CSV\r\n          \r\n          <button id=\"cz-copy\" class=\"cz-btn cz-btn-secondary\" disabled>\ud83d\udccb Sao ch\u00e9p\r\n          <button id=\"cz-download-zone\" class=\"cz-btn cz-btn-secondary\" disabled>\ud83d\udcbe T\u1ea3i .txt\r\n          <button id=\"cz-download-csv\" class=\"cz-btn cz-btn-secondary\" disabled>\ud83d\udcbe T\u1ea3i .csv\r\n          <button id=\"cz-download-xlsx\" class=\"cz-btn cz-btn-secondary\" disabled>\ud83d\udcbe T\u1ea3i .xlsx\r\n          <div id=\"cz-status\" class=\"cz-status\">\r\n        \r\n        <div id=\"cz-error\" class=\"cz-error\"><div>\u26a0\ufe0f<div id=\"cz-error-text\">\r\n        <div id=\"cz-output\" class=\"cz-output\">\r\n          <div class=\"cz-empty\">S\u1eb5n s\u00e0ng chuy\u1ec3n \u0111\u1ed5i... T\u1ea3i t\u1ec7p ho\u1eb7c d\u00e1n n\u1ed9i dung (CSV\/XLSX\/Zone) \u0111\u1ec3 b\u1eaft \u0111\u1ea7u.\r\n        \r\n      \r\n      <div class=\"cz-footer\">\r\n        <div class=\"cz-footer-content\">\r\n          <span>Ph\u00e1t tri\u1ec3n b\u1edfi\r\n          <strong class=\"cz-footer-brand\">DPS.MEDIA\r\n          <span>\u2022\r\n          <span>C\u00f4ng c\u1ee5 s\u1ed1 chuy\u00ean nghi\u1ec7p\r\n          <span>\u2022\r\n          <a class=\"cz-footer-link\" href=\"https:\/\/dps.media\" target=\"_blank\">dps.media\r\n        \r\n      \r\n    \r\n  `;\r\n\r\n  \/\/ Elements\r\n  const fileInput = root.querySelector('#cz-file-input');\r\n  const btnConvert = root.querySelector('#cz-convert');\r\n  const btnClear = root.querySelector('#cz-clear');\r\n  const btnCopy = root.querySelector('#cz-copy');\r\n  const btnDownloadZone = root.querySelector('#cz-download-zone');\r\n  const btnDownloadCSV = root.querySelector('#cz-download-csv');\r\n  const btnDownloadXLSX = root.querySelector('#cz-download-xlsx');\r\n  const outEl = root.querySelector('#cz-output');\r\n  const statusEl = root.querySelector('#cz-status');\r\n  const fileInfoEl = root.querySelector('#cz-fileinfo');\r\n  const errorEl = root.querySelector('#cz-error');\r\n  const errorTextEl = root.querySelector('#cz-error-text');\r\n  const inputModeWrap = root.querySelector('#cz-input-mode');\r\n  const pasteTypeWrap = root.querySelector('#cz-paste-type');\r\n  const pasteWrap = root.querySelector('#cz-paste-wrap');\r\n  const pasteTextarea = root.querySelector('#cz-paste');\r\n  const viewModeWrap = root.querySelector('#cz-view-mode');\r\n\r\n  let lastZoneText = '';\r\n  let lastCSVText = '';\r\n  let currentFile = null;\r\n  let inputMode = 'file'; \/\/ 'file' | 'paste'\r\n  let pasteType = 'zone'; \/\/ 'zone' | 'csv'\r\n  let viewMode = 'zone'; \/\/ 'zone' | 'csv'\r\n\r\n  function setStatus(msg){ statusEl.textContent = msg || ''; }\r\n  function showError(msg){ errorEl.style.display = 'flex'; errorTextEl.textContent = msg; }\r\n  function clearError(){ errorEl.style.display = 'none'; errorTextEl.textContent = ''; }\r\n  function setOutputText(text){ outEl.textContent = text || ''; if(!text){ outEl.innerHTML = '<div class=\"cz-empty\">Kh\u00f4ng c\u00f3 d\u1eef li\u1ec7u...'; } }\r\n  function refreshOutputView(){\r\n    if (viewMode === 'zone') {\r\n      setOutputText(lastZoneText);\r\n    } else {\r\n      setOutputText(lastCSVText);\r\n    }\r\n    btnCopy.disabled = !(viewMode === 'zone' ? lastZoneText : lastCSVText);\r\n    btnDownloadZone.disabled = !lastZoneText;\r\n    btnDownloadCSV.disabled = !lastCSVText;\r\n    btnDownloadXLSX.disabled = !lastCSVText;\r\n  }\r\n\r\n  \/\/ Lightweight CSV parser (handles quotes, commas, newlines)\r\n  function parseCSV(text){\r\n    const rows = [];\r\n    let i = 0, field = '', row = [], inQuotes = false; \r\n    while (i < text.length){\r\n      const c = text[i];\r\n      if (inQuotes){\r\n        if (c === '\"'){\r\n          if (i + 1 < text.length && text[i+1] === '\"'){ field += '\"'; i += 2; continue; }\r\n          inQuotes = false; i++; continue;\r\n        } else { field += c; i++; continue; }\r\n      } else {\r\n        if (c === '\"'){ inQuotes = true; i++; continue; }\r\n        if (c === ','){ row.push(field); field = ''; i++; continue; }\r\n        if (c === '\\n'){ row.push(field); rows.push(row); field=''; row=[]; i++; continue; }\r\n        if (c === '\\r'){ \/\/ handle CRLF\r\n          if (i + 1 < text.length && text[i+1] === '\\n'){ i += 2; row.push(field); rows.push(row); field=''; row=[]; continue; }\r\n          i++; continue;\r\n        }\r\n        field += c; i++; continue;\r\n      }\r\n    }\r\n    \/\/ last field\r\n    if (field.length || row.length){ row.push(field); rows.push(row); }\r\n    return rows;\r\n  }\r\n\r\n  \/\/ Utilities\r\n  function toHeadersMap(arr){\r\n    const map = {};\r\n    arr.forEach((h, idx) => { if (!h) return; map[String(h).trim().toLowerCase()] = idx; });\r\n    return map;\r\n  }\r\n  function safeGet(row, idx){ return (idx != null && idx >= 0 && idx < row.length) ? String(row[idx]).trim() : ''; }\r\n  function normalizeTXT(val){\r\n    if (val == null) return '\"\"';\r\n    let s = String(val).trim();\r\n    if (!s.length) return '\"\"';\r\n    \/\/ If already wrapped in quotes, return as-is (normalize to double quotes)\r\n    const starts = s.startsWith('\"') || s.startsWith(\"'\");\r\n    const ends = s.endsWith('\"') || s.endsWith(\"'\");\r\n    if (starts && ends && s.length >= 2){\r\n      \/\/ normalize outer to double quotes\r\n      s = '\"' + s.slice(1, -1) + '\"';\r\n      return s;\r\n    }\r\n    \/\/ Escape embedded quotes by doubling\r\n    s = s.replace(\/\"\/g, '\"\"');\r\n    return '\"' + s + '\"';\r\n  }\r\n  function normalizeHost(h){\r\n    return String(h || '').trim();\r\n  }\r\n  function normalizeType(t){ return String(t || '').trim().toUpperCase(); }\r\n  function toInt(v, def){ const n = parseInt(String(v||'').trim(), 10); return Number.isFinite(n) ? n : def; }\r\n  function isNumberToken(tok){ return \/^\\d+$\/.test(String(tok||'').trim()); }\r\n  function extractMX(data){\r\n    const parts = String(data||'').trim().split(\/\\s+\/);\r\n    if (parts.length >= 2 && isNumberToken(parts[0])){\r\n      return { Priority: parts[0], Data: parts.slice(1).join(' ') };\r\n    }\r\n    return { Priority: '', Data: String(data||'').trim() };\r\n  }\r\n\r\n  function splitMultiDataIfNeeded(type, data){\r\n    if (type === 'NS'){\r\n      \/\/ Support comma-separated NS targets\r\n      return String(data||'').split(',').map(s => s.trim()).filter(Boolean);\r\n    }\r\n    return [String(data||'').trim()];\r\n  }\r\n\r\n  \/\/ Build zone lines from structured rows\r\n  function buildZoneLines(rows){\r\n    const lines = [];\r\n    for (const r of rows){\r\n      const host = normalizeHost(r.Host ?? r.host ?? r.HOST ?? r[\"Host\"]);\r\n      const type = normalizeType(r.Type ?? r.type ?? r.TYPE ?? r[\"Type\"]);\r\n      const ttl = toInt(r.TTL ?? r.ttl ?? r.Ttl ?? r[\"TTL\"], 3600);\r\n      let data = r.Data ?? r.data ?? r.DATA ?? r[\"Data\"];\r\n\r\n      \/\/ MX may have Priority column\r\n      if (type === 'MX' && (r.Priority != null || r.priority != null)){\r\n        const prio = String(r.Priority ?? r.priority).trim();\r\n        if (prio) data = prio + ' ' + String(data ?? '').trim();\r\n      }\r\n\r\n      if (!host || !type){ continue; }\r\n\r\n      if (type === 'TXT'){\r\n        const txtVal = normalizeTXT(String(data ?? ''));\r\n        lines.push(`${host}\\t${ttl}\\tIN\\tTXT\\t${txtVal}`);\r\n        continue;\r\n      }\r\n\r\n      const parts = splitMultiDataIfNeeded(type, data);\r\n      for (const p of parts){\r\n        if (!p) continue;\r\n        lines.push(`${host}\\t${ttl}\\tIN\\t${type}\\t${p}`);\r\n      }\r\n    }\r\n    return lines;\r\n  }\r\n\r\n  \/\/ Parse Cloudflare zone txt (line-based) \u2192 records\r\n  function parseZoneToRecords(text){\r\n    const lines = String(text||'').split(\/\\r?\\n\/);\r\n    const out = [];\r\n    for (let raw of lines){\r\n      let line = String(raw||'').trim();\r\n      if (!line || line.startsWith(';') || line.startsWith('#')) continue;\r\n      const parts = line.split(\/\\s+\/);\r\n      if (parts.length < 3) continue;\r\n      let i = 0;\r\n      const host = parts[i++];\r\n      let ttl = 3600;\r\n      if (parts[i] && isNumberToken(parts[i])){ ttl = parseInt(parts[i++], 10); }\r\n      if (parts[i] && \/^IN$\/i.test(parts[i])){ i++; }\r\n      const type = (parts[i++]||'').toUpperCase();\r\n      if (!type) continue;\r\n      const data = parts.slice(i).join(' ');\r\n      const rec = { Host: host, Type: type, Data: data, TTL: ttl };\r\n      if (type === 'MX'){\r\n        const mx = extractMX(data);\r\n        rec.Data = mx.Data; rec.Priority = mx.Priority;\r\n      }\r\n      out.push(rec);\r\n    }\r\n    return out;\r\n  }\r\n\r\n  function recordsToCSVText(records){\r\n    const normalized = records.map(r => ({\r\n      Host: normalizeHost(r.Host ?? r.host),\r\n      Type: normalizeType(r.Type ?? r.type),\r\n      Data: String(r.Data ?? r.data ?? ''),\r\n      TTL: toInt(r.TTL ?? r.ttl, 3600),\r\n      Priority: r.Priority ?? r.priority ?? ''\r\n    }));\r\n    const hasPriority = normalized.some(r => String(r.Priority||'').trim());\r\n    const header = hasPriority ? ['Host','Type','Data','TTL','Priority'] : ['Host','Type','Data','TTL'];\r\n    const escapeCSV = (v)=>{\r\n      const s = String(v ?? '');\r\n      if (\/[\",\\n]\/.test(s)) return '\"' + s.replace(\/\"\/g,'\"\"') + '\"';\r\n      return s;\r\n    };\r\n    const lines = [header.join(',')];\r\n    for (const r of normalized){\r\n      const arr = hasPriority ? [r.Host, r.Type, r.Data, r.TTL, r.Priority] : [r.Host, r.Type, r.Data, r.TTL];\r\n      lines.push(arr.map(escapeCSV).join(','));\r\n    }\r\n    return lines.join('\\n');\r\n  }\r\n\r\n  function replaceTabsOutsideQuotes(s){\r\n    let out = '';\r\n    let inQuotes = false;\r\n    for (let i=0;i<s.length;i++){\r\n      const c = s[i];\r\n      if (c === '\"'){\r\n        \/\/ double quotes inside quotes\r\n        if (inQuotes && i+1 < s.length && s[i+1] === '\"'){ out += '\"\"'; i++; continue; }\r\n        inQuotes = !inQuotes; out += '\"'; continue;\r\n      }\r\n      if (!inQuotes && c === '\\t'){ out += ','; continue; }\r\n      out += c;\r\n    }\r\n    return out;\r\n  }\r\n\r\n  \/\/ Parse CSV\/TSV text to array of objects using header row\r\n  function csvTextToObjects(text){\r\n    let body = String(text||'');\r\n    \/\/ If looks like TSV, normalize to CSV\r\n    const tabCount = (body.match(\/\\t\/g)||[]).length;\r\n    const commaCount = (body.match(\/,\/g)||[]).length;\r\n    if (tabCount > commaCount) body = replaceTabsOutsideQuotes(body);\r\n    const table = parseCSV(body);\r\n    if (!table.length) return [];\r\n    const header = table[0];\r\n    const hmap = toHeadersMap(header);\r\n    const out = [];\r\n    for (let i=1; i<table.length; i++){\r\n      const row = table[i];\r\n      if (!row || row.length === 0) continue;\r\n      const obj = {};\r\n      for (const [key, idx] of Object.entries(hmap)){\r\n        obj[key] = safeGet(row, idx);\r\n      }\r\n      \/\/ Normalize keys to canonical casing\r\n      const record = {\r\n        Host: obj['host'] ?? '',\r\n        Type: obj['type'] ?? '',\r\n        Data: obj['data'] ?? '',\r\n        TTL: obj['ttl'] ?? obj['time to live'] ?? ''\r\n      };\r\n      \/\/ Optional Priority\r\n      if (obj['priority'] != null) record.Priority = obj['priority'];\r\n      out.push(record);\r\n    }\r\n    return out;\r\n  }\r\n\r\n  \/\/ XLSX loader (from CDN) \u2014 only if needed\r\n  let xlsxReady = false;\r\n  async function ensureXLSX(){\r\n    if (xlsxReady || (typeof window !== 'undefined' && window.XLSX)) { xlsxReady = true; return; }\r\n    setStatus('\u0110ang t\u1ea3i th\u01b0 vi\u1ec7n XLSX...');\r\n    await new Promise((resolve, reject) => {\r\n      const s = document.createElement('script');\r\n      s.src = 'https:\/\/cdn.jsdelivr.net\/npm\/xlsx@0.18.5\/dist\/xlsx.full.min.js';\r\n      s.onload = () => { xlsxReady = true; resolve(); };\r\n      s.onerror = () => reject(new Error('Kh\u00f4ng t\u1ea3i \u0111\u01b0\u1ee3c th\u01b0 vi\u1ec7n XLSX. Vui l\u00f2ng d\u00f9ng CSV.'));\r\n      document.head.appendChild(s);\r\n    });\r\n  }\r\n\r\n  async function fileToRows(file){\r\n    const name = (file?.name || '').toLowerCase();\r\n    if (name.endsWith('.csv')){\r\n      const text = await file.text();\r\n      return csvTextToObjects(text);\r\n    }\r\n    if (name.endsWith('.xlsx') || name.endsWith('.xls')){\r\n      await ensureXLSX();\r\n      const ab = await file.arrayBuffer();\r\n      const wb = window.XLSX.read(ab, { type: 'array' });\r\n      const sheetName = wb.SheetNames[0];\r\n      const ws = wb.Sheets[sheetName];\r\n      \/\/ to JSON with header row detection\r\n      const arr = window.XLSX.utils.sheet_to_json(ws, { defval: '' });\r\n      \/\/ Attempt to map any casing\r\n      return arr.map(r => ({\r\n        Host: r.Host ?? r.host ?? r.HOST ?? r[\"Host\"] ?? r[\"HOST\"] ?? '',\r\n        Type: r.Type ?? r.type ?? r.TYPE ?? r[\"Type\"] ?? r[\"TYPE\"] ?? '',\r\n        Data: r.Data ?? r.data ?? r.DATA ?? r[\"Data\"] ?? r[\"DATA\"] ?? '',\r\n        TTL: r.TTL ?? r.ttl ?? r.Ttl ?? r[\"TTL\"] ?? r[\"Ttl\"] ?? '',\r\n        Priority: r.Priority ?? r.priority ?? r.PRIORITY\r\n      }));\r\n    }\r\n    if (name.endsWith('.txt') || name.endsWith('.zone')){\r\n      const text = await file.text();\r\n      return parseZoneToRecords(text);\r\n    }\r\n    throw new Error('\u0110\u1ecbnh d\u1ea1ng kh\u00f4ng h\u1ed7 tr\u1ee3. H\u00e3y ch\u1ecdn .csv ho\u1eb7c .xlsx');\r\n  }\r\n\r\n  function deriveDownloadName(file){\r\n    if (!file || !file.name) return 'cloudflare-zone.txt';\r\n    const base = file.name.replace(\/\\.[^.]+$\/,'');\r\n    const pre = base.includes('-') ? base.split('-')[0] : base;\r\n    return pre + '-cloudflare-bind-fixed.txt';\r\n  }\r\n\r\n  async function convertCurrent(){\r\n    clearError(); setStatus('\u0110ang x\u1eed l\u00fd...');\r\n    try {\r\n      let records = [];\r\n      if (inputMode === 'file'){\r\n        if (!currentFile){ showError('Vui l\u00f2ng ch\u1ecdn t\u1ec7p CSV\/XLSX\/TXT.'); setStatus(''); return; }\r\n        records = await fileToRows(currentFile);\r\n      } else {\r\n        const text = pasteTextarea.value || '';\r\n        if (!text.trim()){ showError('Vui l\u00f2ng d\u00e1n n\u1ed9i dung \u0111\u1ec3 chuy\u1ec3n \u0111\u1ed5i.'); setStatus(''); return; }\r\n        if (pasteType === 'csv') records = csvTextToObjects(text);\r\n        else records = parseZoneToRecords(text);\r\n      }\r\n\r\n      if (!records.length){ lastZoneText=''; lastCSVText=''; refreshOutputView(); setStatus('Kh\u00f4ng t\u00ecm th\u1ea5y d\u1eef li\u1ec7u.'); return; }\r\n\r\n      const zoneLines = buildZoneLines(records);\r\n      lastZoneText = zoneLines.join('\\n');\r\n      lastCSVText = recordsToCSVText(records);\r\n      refreshOutputView();\r\n      setStatus(`\u2705 Ho\u00e0n th\u00e0nh: ${records.length} b\u1ea3n ghi`);\r\n    } catch(e){\r\n      showError(e.message || String(e)); setStatus('\u274c L\u1ed7i');\r\n    }\r\n  }\r\n\r\n  \/\/ Events\r\n  fileInput.addEventListener('change', () => {\r\n    clearError();\r\n    const f = fileInput.files && fileInput.files[0];\r\n    currentFile = f || null;\r\n    if (currentFile){\r\n      fileInfoEl.textContent = `\u0110\u00e3 ch\u1ecdn: ${currentFile.name}`;\r\n      btnConvert.disabled = false;\r\n    } else {\r\n      fileInfoEl.textContent = 'Ch\u01b0a ch\u1ecdn file';\r\n      btnConvert.disabled = true;\r\n      btnCopy.disabled = true; btnDownloadZone.disabled = true; btnDownloadCSV.disabled = true; btnDownloadXLSX.disabled = true;\r\n      lastZoneText = ''; lastCSVText=''; setOutputText('');\r\n    }\r\n    setStatus('');\r\n  });\r\n\r\n  btnConvert.addEventListener('click', convertCurrent);\r\n\r\n  btnClear.addEventListener('click', () => {\r\n    fileInput.value = '';\r\n    currentFile = null;\r\n    lastZoneText = '';\r\n    lastCSVText = '';\r\n    btnConvert.disabled = true;\r\n    btnCopy.disabled = true;\r\n    btnDownloadZone.disabled = true;\r\n    btnDownloadCSV.disabled = true;\r\n    btnDownloadXLSX.disabled = true;\r\n    fileInfoEl.textContent = 'Ch\u01b0a ch\u1ecdn file';\r\n    setOutputText('');\r\n    clearError();\r\n    setStatus('\u0110\u00e3 x\u00f3a.');\r\n  });\r\n\r\n  async function copyToClipboard(text){\r\n    if (!text) return;\r\n    if (navigator.clipboard && navigator.clipboard.writeText){ await navigator.clipboard.writeText(text); return; }\r\n    const ta = document.createElement('textarea');\r\n    ta.value = text; document.body.appendChild(ta); ta.select(); document.execCommand('copy'); document.body.removeChild(ta);\r\n  }\r\n\r\n  btnCopy.addEventListener('click', async () => {\r\n    const payload = (viewMode === 'zone') ? lastZoneText : lastCSVText;\r\n    if (!payload){ setStatus('\u274c Kh\u00f4ng c\u00f3 d\u1eef li\u1ec7u \u0111\u1ec3 sao ch\u00e9p'); return; }\r\n    try { await copyToClipboard(payload); setStatus('\u2705 \u0110\u00e3 sao ch\u00e9p v\u00e0o clipboard'); } catch(e){ setStatus('\u274c Sao ch\u00e9p th\u1ea5t b\u1ea1i'); }\r\n  });\r\n\r\n  btnDownloadZone.addEventListener('click', () => {\r\n    if (!lastZoneText){ setStatus('\u274c Kh\u00f4ng c\u00f3 d\u1eef li\u1ec7u \u0111\u1ec3 t\u1ea3i'); return; }\r\n    const blob = new Blob([lastZoneText], { type: 'text\/plain;charset=utf-8' });\r\n    const url = URL.createObjectURL(blob);\r\n    const a = document.createElement('a');\r\n    a.href = url; a.download = deriveDownloadName(currentFile);\r\n    document.body.appendChild(a); a.click(); document.body.removeChild(a);\r\n    URL.revokeObjectURL(url);\r\n    setStatus('\ud83d\udcbe \u0110\u00e3 t\u1ea3i zone.txt');\r\n  });\r\n\r\n  btnDownloadCSV.addEventListener('click', () => {\r\n    if (!lastCSVText){ setStatus('\u274c Kh\u00f4ng c\u00f3 d\u1eef li\u1ec7u \u0111\u1ec3 t\u1ea3i'); return; }\r\n    const name = (currentFile?.name || 'cloudflare-zone').replace(\/\\.[^.]+$\/,'') + '.csv';\r\n    const blob = new Blob([lastCSVText], { type: 'text\/csv;charset=utf-8' });\r\n    const url = URL.createObjectURL(blob);\r\n    const a = document.createElement('a');\r\n    a.href = url; a.download = name;\r\n    document.body.appendChild(a); a.click(); document.body.removeChild(a);\r\n    URL.revokeObjectURL(url);\r\n    setStatus('\ud83d\udcbe \u0110\u00e3 t\u1ea3i CSV');\r\n  });\r\n\r\n  btnDownloadXLSX.addEventListener('click', async () => {\r\n    try{\r\n      await ensureXLSX();\r\n      const records = csvTextToObjects(lastCSVText);\r\n      const ws = window.XLSX.utils.json_to_sheet(records);\r\n      const wb = window.XLSX.utils.book_new();\r\n      window.XLSX.utils.book_append_sheet(wb, ws, 'DNS');\r\n      const name = (currentFile?.name || 'cloudflare-zone').replace(\/\\.[^.]+$\/,'') + '.xlsx';\r\n      window.XLSX.writeFile(wb, name);\r\n      setStatus('\ud83d\udcbe \u0110\u00e3 t\u1ea3i XLSX');\r\n    }catch(e){ setStatus('\u274c L\u1ed7i xu\u1ea5t XLSX'); showError(e.message || String(e)); }\r\n  });\r\n\r\n  \/\/ Input mode toggles\r\n  function updateInputMode(){\r\n    const fileActive = inputMode === 'file';\r\n    pasteWrap.style.display = fileActive ? 'none' : 'flex';\r\n    pasteTypeWrap.style.display = fileActive ? 'none' : 'inline-flex';\r\n    fileInfoEl.textContent = fileActive ? (currentFile ? `\u0110\u00e3 ch\u1ecdn: ${currentFile.name}` : 'Ch\u01b0a ch\u1ecdn file') : (pasteType === 'zone' ? 'D\u00e1n: Zone' : 'D\u00e1n: CSV');\r\n    if (fileActive){\r\n      btnConvert.disabled = !currentFile;\r\n    } else {\r\n      btnConvert.disabled = !(pasteTextarea.value && pasteTextarea.value.trim());\r\n    }\r\n  }\r\n  inputModeWrap.querySelectorAll('.cz-toggle-btn').forEach(btn => {\r\n    btn.addEventListener('click', ()=>{\r\n      inputModeWrap.querySelectorAll('.cz-toggle-btn').forEach(b=>b.classList.remove('active'));\r\n      btn.classList.add('active');\r\n      inputMode = btn.getAttribute('data-mode');\r\n      updateInputMode();\r\n    });\r\n  });\r\n  pasteTypeWrap.querySelectorAll('.cz-toggle-btn').forEach(btn => {\r\n    btn.addEventListener('click', ()=>{\r\n      pasteTypeWrap.querySelectorAll('.cz-toggle-btn').forEach(b=>b.classList.remove('active'));\r\n      btn.classList.add('active');\r\n      pasteType = btn.getAttribute('data-ptype');\r\n      updateInputMode();\r\n    });\r\n  });\r\n\r\n  pasteTextarea.addEventListener('input', ()=>{\r\n    if (inputMode === 'paste'){\r\n      btnConvert.disabled = !(pasteTextarea.value && pasteTextarea.value.trim());\r\n    }\r\n  });\r\n\r\n  \/\/ View mode toggles\r\n  function updateViewMode(){\r\n    viewModeWrap.querySelectorAll('.cz-toggle-btn').forEach(b=>b.classList.remove('active'));\r\n    const activeBtn = viewModeWrap.querySelector(`.cz-toggle-btn[data-view=\"${viewMode}\"]`);\r\n    if (activeBtn) activeBtn.classList.add('active');\r\n    refreshOutputView();\r\n  }\r\n  viewModeWrap.querySelectorAll('.cz-toggle-btn').forEach(btn => {\r\n    btn.addEventListener('click', ()=>{\r\n      viewMode = btn.getAttribute('data-view');\r\n      updateViewMode();\r\n    });\r\n  });\r\n\r\n  updateInputMode();\r\n  updateViewMode();\r\n})();\r\n<\/script><!-- End CSV\/XLSX \u2192 Cloudflare Zone Converter -->\n<style>\r\n.lwrp.link-whisper-related-posts{\r\n            \r\n            margin-top: 40px;\nmargin-bottom: 30px;\r\n        }\r\n        .lwrp .lwrp-title{\r\n            \r\n            \r\n        }.lwrp .lwrp-description{\r\n            \r\n            \r\n\r\n        }\r\n        .lwrp .lwrp-list-container{\r\n        }\r\n        .lwrp .lwrp-list-multi-container{\r\n            display: flex;\r\n        }\r\n        .lwrp .lwrp-list-double{\r\n            width: 48%;\r\n        }\r\n        .lwrp .lwrp-list-triple{\r\n            width: 32%;\r\n        }\r\n        .lwrp .lwrp-list-row-container{\r\n            display: flex;\r\n            justify-content: space-between;\r\n        }\r\n        .lwrp .lwrp-list-row-container .lwrp-list-item{\r\n            width: calc(33% - 20px);\r\n        }\r\n        .lwrp .lwrp-list-item:not(.lwrp-no-posts-message-item){\r\n            \r\n            max-width: 150px;\r\n        }\r\n        .lwrp .lwrp-list-item img{\r\n            max-width: 100%;\r\n            height: auto;\r\n            object-fit: cover;\r\n            aspect-ratio: 1 \/ 1;\r\n        }\r\n        .lwrp .lwrp-list-item.lwrp-empty-list-item{\r\n            background: initial !important;\r\n        }\r\n        .lwrp .lwrp-list-item .lwrp-list-link .lwrp-list-link-title-text,\r\n        .lwrp .lwrp-list-item .lwrp-list-no-posts-message{\r\n            \r\n            \r\n            \r\n            \r\n        }@media screen and (max-width: 480px) {\r\n            .lwrp.link-whisper-related-posts{\r\n                \r\n                \r\n            }\r\n            .lwrp .lwrp-title{\r\n                \r\n                \r\n            }.lwrp .lwrp-description{\r\n                \r\n                \r\n            }\r\n            .lwrp .lwrp-list-multi-container{\r\n                flex-direction: column;\r\n            }\r\n            .lwrp .lwrp-list-multi-container ul.lwrp-list{\r\n                margin-top: 0px;\r\n                margin-bottom: 0px;\r\n                padding-top: 0px;\r\n                padding-bottom: 0px;\r\n            }\r\n            .lwrp .lwrp-list-double,\r\n            .lwrp .lwrp-list-triple{\r\n                width: 100%;\r\n            }\r\n            .lwrp .lwrp-list-row-container{\r\n                justify-content: initial;\r\n                flex-direction: column;\r\n            }\r\n            .lwrp .lwrp-list-row-container .lwrp-list-item{\r\n                width: 100%;\r\n            }\r\n            .lwrp .lwrp-list-item:not(.lwrp-no-posts-message-item){\r\n                \r\n                max-width: initial;\r\n            }\r\n            .lwrp .lwrp-list-item .lwrp-list-link .lwrp-list-link-title-text,\r\n            .lwrp .lwrp-list-item .lwrp-list-no-posts-message{\r\n                \r\n                \r\n                \r\n                \r\n            };\r\n        }<\/style>\r\n<div id=\"link-whisper-related-posts-widget\" class=\"link-whisper-related-posts lwrp\">\r\n            <div class=\"lwrp-title\">Related Posts<\/div>    \r\n        <div class=\"lwrp-list-container\">\r\n                                <div class=\"lwrp-list lwrp-list-row-container lwrp-list-double-row\">\r\n                <div class=\"lwrp-list-item\"><a href=\"https:\/\/dps.media\/en\/how-to-optimize-tiktok-pixel-to-increase-conversions\/\" class=\"lwrp-list-link\"><span class=\"lwrp-list-link-title-text\">How to Optimize TikTok Pixel to Increase Conversions<\/span><\/a><\/div><div class=\"lwrp-list-item\"><a href=\"https:\/\/dps.media\/en\/livestream-seeding-service-also-increases-real-interaction-dps-media\/\" class=\"lwrp-list-link\"><span class=\"lwrp-list-link-title-text\">Con C\u01b0ng livestream seeding service \u2013 increase real interaction \u2013 DPS.MEDIA<\/span><\/a><\/div><div class=\"lwrp-list-item\"><a href=\"https:\/\/dps.media\/en\/buy-facebook-page-run-ads-fanpage-many-likes-resistant-to-ads\/\" class=\"lwrp-list-link\"><span class=\"lwrp-list-link-title-text\">Buy Facebook Pages Running Ads \u2013 Fanpages with Many Likes, Already Ad-Resistant<\/span><\/a><\/div>                <\/div>\r\n                            <div class=\"lwrp-list lwrp-list-row-container lwrp-list-double-row\">\r\n                <div class=\"lwrp-list-item\"><a href=\"https:\/\/dps.media\/en\/automation-cool-or-automatically-send-morning-greeting-quote-with-n8n\/\" class=\"lwrp-list-link\"><span class=\"lwrp-list-link-title-text\">\u00a0Awesome automation: Automatically send morning quotes with n8n<\/span><\/a><\/div><div class=\"lwrp-list-item\"><a href=\"https:\/\/dps.media\/en\/techcombank-qr-pay-how-to-withdraw-money-by-qr-bidv-vietcombank-vietinbank\/\" class=\"lwrp-list-link\"><span class=\"lwrp-list-link-title-text\">Techcombank QR Pay &amp; how to withdraw money using QR BIDV, Vietcombank, Vietinbank<\/span><\/a><\/div><div class=\"lwrp-list-item\"><a href=\"https:\/\/dps.media\/en\/grand-opening-restaurant-idea-without-a-big-budget\/\" class=\"lwrp-list-link\"><span class=\"lwrp-list-link-title-text\">Grand opening ideas for restaurants without a big budget<\/span><\/a><\/div>                <\/div>\r\n                <\/div>\r\n<\/div>","protected":false},"excerpt":{"rendered":"<p>B\u00e0i Vi\u1ebft C\u00f9ng Ch\u1ee7 \u0110\u1ec1 C\u00e1ch t\u1ed1i \u01b0u h\u00f3a TikTok Pixel \u0111\u1ec3 t\u0103ng chuy\u1ec3n \u0111\u1ed5iD\u1ecbch v\u1ee5 seeding livestream Con C\u01b0ng &#8211; t\u0103ng t\u01b0\u01a1ng t\u00e1c th\u1eadt &#8211; DPS.MEDIAMua Page Facebook Ch\u1ea1y Qu\u1ea3ng C\u00e1o \u2013 Fanpage Nhi\u1ec1u Like, \u0110\u00e3 Kh\u00e1ng Ads \u00a0Automation c\u1ef1c hay: T\u1ef1 \u0111\u1ed9ng g\u1eedi quote ch\u00e0o bu\u1ed5i s\u00e1ng v\u1edbi n8nTechcombank QR Pay &#038; c\u00e1ch [&hellip;]<\/p>","protected":false},"author":1,"featured_media":32749,"comment_status":"closed","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"_acf_changed":false,"footnotes":""},"categories":[1],"tags":[],"class_list":["post-32746","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-uncategorized"],"acf":[],"rankmath_keywords":{"primary":"","secondary":[""]},"yoast_keywords":{"primary":"","secondary":[]},"yoast_focuskw":"","rankmath_focuskw":"","seo_keywords":{"primary":"","secondary":[""]},"_links":{"self":[{"href":"https:\/\/dps.media\/en\/wp-json\/wp\/v2\/posts\/32746","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/dps.media\/en\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/dps.media\/en\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/dps.media\/en\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/dps.media\/en\/wp-json\/wp\/v2\/comments?post=32746"}],"version-history":[{"count":1,"href":"https:\/\/dps.media\/en\/wp-json\/wp\/v2\/posts\/32746\/revisions"}],"predecessor-version":[{"id":32747,"href":"https:\/\/dps.media\/en\/wp-json\/wp\/v2\/posts\/32746\/revisions\/32747"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/dps.media\/en\/wp-json\/wp\/v2\/media\/32749"}],"wp:attachment":[{"href":"https:\/\/dps.media\/en\/wp-json\/wp\/v2\/media?parent=32746"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/dps.media\/en\/wp-json\/wp\/v2\/categories?post=32746"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/dps.media\/en\/wp-json\/wp\/v2\/tags?post=32746"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}