commit 790e67d5d7666f89dbd5ccb5dd34d4c6a4ddcce1 Author: Josias Castro Date: Thu Jun 18 17:12:16 2026 -0300 Initial commit diff --git a/export-project.js b/export-project.js new file mode 100644 index 0000000..3797b98 --- /dev/null +++ b/export-project.js @@ -0,0 +1,123 @@ +(async () => { + const sleep = ms => new Promise(resolve => setTimeout(resolve, ms)); + + const getAllDocuments = () => { + const docs = [document]; + + const scan = doc => { + [...doc.querySelectorAll("iframe, frame")].forEach(frame => { + try { + if (frame.contentDocument) { + docs.push(frame.contentDocument); + scan(frame.contentDocument); + } + } catch (e) {} + }); + }; + + scan(document); + return docs; + }; + + const waitForElement = async (selector, timeout = 30000) => { + const start = Date.now(); + + while (Date.now() - start < timeout) { + for (const doc of getAllDocuments()) { + const el = doc.querySelector(selector); + if (el) return el; + } + + await sleep(300); + } + + throw new Error(`No se encontró: ${selector}`); + }; + + const waitForEnabledDownload = async (timeout = 10 * 60 * 1000) => { + const start = Date.now(); + + while (Date.now() - start < timeout) { + for (const doc of getAllDocuments()) { + const button = doc.querySelector("#id_pub_button_download"); + + if ( + button && + button.offsetParent !== null && + !button.classList.contains("disabled") && + button.getAttribute("disabled") === null + ) { + return button; + } + } + + await sleep(1000); + } + + throw new Error("El botón Download no se habilitó dentro del tiempo esperado"); + }; + + const click = async el => { + el.scrollIntoView({ block: "center", inline: "center" }); + el.focus(); + await sleep(500); + + const view = el.ownerDocument.defaultView || window; + ["mouseover", "mousedown", "mouseup", "click"].forEach(type => { + el.dispatchEvent( + new view.MouseEvent(type, { + bubbles: true, + cancelable: true, + view + }) + ); + }); + + el.click(); + }; + + const clickAndWait = async (selector, nextSelector) => { + const el = await waitForElement(selector); + await click(el); + await sleep(1200); + + if (nextSelector) { + await waitForElement(nextSelector); + } + }; + + const deploy = await waitForElement("#id_menu_prj_deploy"); + await click(deploy); + + await sleep(1500); + + await clickAndWait( + 'input[name="avancar"][value="Next"][onclick="nm_next()"]', + 'input[name="avancar"][value="Next"][onclick*="esquema"]' + ); + + await clickAndWait( + 'input[name="avancar"][value="Next"][onclick*="esquema"]', + 'input[name="avancar"][value="Next"][onclick*="perfil"]' + ); + + await clickAndWait( + 'input[name="avancar"][value="Next"][onclick*="perfil"]', + 'input[name="avancar"][value="Next"][onclick*="create_dir"]' + ); + + const lastNext = await waitForElement( + 'input[name="avancar"][value="Next"][onclick*="create_dir"]' + ); + + await click(lastNext); + + console.log("Esperando que se habilite Download..."); + + const downloadButton = await waitForEnabledDownload(); + + console.log("Download habilitado:", downloadButton); + await sleep(1500); + await click(await waitForEnabledDownload(30000)); + console.log("Download clickeado."); +})(); diff --git a/generate-source-code.js b/generate-source-code.js new file mode 100644 index 0000000..43ca508 --- /dev/null +++ b/generate-source-code.js @@ -0,0 +1,50 @@ +(async () => { + const sleep = ms => new Promise(resolve => setTimeout(resolve, ms)); + + const getAllDocuments = () => { + const docs = [document]; + + const scan = doc => { + [...doc.querySelectorAll("iframe, frame")].forEach(frame => { + try { + if (frame.contentDocument) { + docs.push(frame.contentDocument); + scan(frame.contentDocument); + } + } catch (e) {} + }); + }; + + scan(document); + return docs; + }; + + const waitForElement = async (selector, timeout = 20000) => { + const start = Date.now(); + + while (Date.now() - start < timeout) { + for (const doc of getAllDocuments()) { + const el = doc.querySelector(selector); + if (el) return el; + } + + await sleep(300); + } + + throw new Error(`No se encontró: ${selector}`); + }; + + const clickElement = async selector => { + const el = await waitForElement(selector); + el.scrollIntoView({ block: "center", inline: "center" }); + await sleep(300); + el.click(); + return el; + }; + + await clickElement("#id_menu_prj_generate"); + + await sleep(1500); + + await clickElement('input[name="avancar"][value="Generate"]'); +})(); \ No newline at end of file diff --git a/increment-version.js b/increment-version.js new file mode 100644 index 0000000..26fb534 --- /dev/null +++ b/increment-version.js @@ -0,0 +1,50 @@ +(async () => { + const sleep = ms => new Promise(resolve => setTimeout(resolve, ms)); + + const getAllDocuments = () => { + const docs = [document]; + + const scan = doc => { + [...doc.querySelectorAll("iframe, frame")].forEach(frame => { + try { + if (frame.contentDocument) { + docs.push(frame.contentDocument); + scan(frame.contentDocument); + } + } catch (e) {} + }); + }; + + scan(document); + return docs; + }; + + const waitForElement = async (selector, timeout = 15000) => { + const start = Date.now(); + + while (Date.now() - start < timeout) { + for (const doc of getAllDocuments()) { + const el = doc.querySelector(selector); + if (el) return el; + } + + await sleep(300); + } + + throw new Error(`No se encontró: ${selector}`); + }; + + const clickElement = async selector => { + const el = await waitForElement(selector); + el.scrollIntoView({ block: "center", inline: "center" }); + await sleep(300); + el.click(); + return el; + }; + + await clickElement("#id_menu_prj_version_increment"); + + await sleep(1000); + + await clickElement('input[name="incrementar"][value="Increment"]'); +})(); \ No newline at end of file diff --git a/lock-version-history.js b/lock-version-history.js new file mode 100644 index 0000000..26fb534 --- /dev/null +++ b/lock-version-history.js @@ -0,0 +1,50 @@ +(async () => { + const sleep = ms => new Promise(resolve => setTimeout(resolve, ms)); + + const getAllDocuments = () => { + const docs = [document]; + + const scan = doc => { + [...doc.querySelectorAll("iframe, frame")].forEach(frame => { + try { + if (frame.contentDocument) { + docs.push(frame.contentDocument); + scan(frame.contentDocument); + } + } catch (e) {} + }); + }; + + scan(document); + return docs; + }; + + const waitForElement = async (selector, timeout = 15000) => { + const start = Date.now(); + + while (Date.now() - start < timeout) { + for (const doc of getAllDocuments()) { + const el = doc.querySelector(selector); + if (el) return el; + } + + await sleep(300); + } + + throw new Error(`No se encontró: ${selector}`); + }; + + const clickElement = async selector => { + const el = await waitForElement(selector); + el.scrollIntoView({ block: "center", inline: "center" }); + await sleep(300); + el.click(); + return el; + }; + + await clickElement("#id_menu_prj_version_increment"); + + await sleep(1000); + + await clickElement('input[name="incrementar"][value="Increment"]'); +})(); \ No newline at end of file diff --git a/manifest.json b/manifest.json new file mode 100644 index 0000000..8b6d738 --- /dev/null +++ b/manifest.json @@ -0,0 +1,11 @@ +{ + "manifest_version": 3, + "name": "Deploy Master", + "version": "1.2", + "description": "Ejecuta los pasos de deploy de Scriptcase desde archivos configurables.", + "permissions": ["activeTab", "scripting", "storage"], + "action": { + "default_popup": "popup.html", + "default_title": "Deploy Master" + } +} diff --git a/popup.css b/popup.css new file mode 100644 index 0000000..099dca5 --- /dev/null +++ b/popup.css @@ -0,0 +1,401 @@ +:root { + color-scheme: dark; + --bg: #111820; + --panel: #20303f; + --panel-2: #192633; + --line: #425569; + --line-soft: rgba(148, 163, 184, 0.24); + --text: #eef5ff; + --muted: #9aaabd; + --green: #36d787; + --blue: #3f9dff; + --blue-dark: #1b72d8; + --error: #ff6b6b; +} + +* { + box-sizing: border-box; +} + +body { + width: 322px; + margin: 0; + font-family: Inter, "Segoe UI", Arial, sans-serif; + background: + radial-gradient(circle at 20% 0%, rgba(73, 130, 190, 0.28), transparent 28%), + linear-gradient(180deg, #182230 0%, #111820 100%); + color: var(--text); +} + +button, +input, +summary { + font: inherit; +} + +.shell { + min-height: 100vh; + padding-bottom: 8px; +} + +.titlebar { + padding: 8px 12px 10px; + background: linear-gradient(180deg, #253244 0%, #172232 100%); + border-bottom: 1px solid rgba(255, 255, 255, 0.08); + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.35); +} + +.brand { + display: flex; + align-items: center; + gap: 8px; +} + +.brand h1 { + display: inline; + margin: 0; + font-size: 21px; + line-height: 1; + font-weight: 750; + letter-spacing: 0; +} + +.brand span { + margin-left: 4px; + color: var(--muted); + font-size: 12px; +} + +.rocket { + width: 27px; + height: 27px; + fill: #a9bdd2; +} + +.panel { + margin: 10px 6px; + padding: 12px 14px 10px; + border: 1px solid rgba(148, 163, 184, 0.24); + border-radius: 8px; + background: + linear-gradient(145deg, rgba(42, 60, 78, 0.92), rgba(25, 38, 51, 0.94)), + var(--panel); + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.04), 0 6px 18px rgba(0, 0, 0, 0.22); +} + +.section-title { + display: flex; + align-items: center; + gap: 8px; + margin-bottom: 12px; + color: #f5f8fc; + font-size: 17px; +} + +.section-title::after { + content: ""; + flex: 1; + height: 1px; + background: linear-gradient(90deg, #7b8da1, transparent); +} + +.switch-row { + display: flex; + align-items: center; + gap: 8px; + margin-bottom: 14px; + font-size: 15px; + cursor: pointer; +} + +.switch-row input, +.option input { + position: absolute; + opacity: 0; + pointer-events: none; +} + +.switch { + position: relative; + width: 39px; + height: 21px; + border-radius: 999px; + background: #526171; + box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.18); + transition: background 160ms ease; +} + +.switch::after { + content: ""; + position: absolute; + top: 3px; + left: 4px; + width: 15px; + height: 15px; + border-radius: 50%; + background: #f8fbff; + box-shadow: 0 1px 4px rgba(0, 0, 0, 0.35); + transition: transform 160ms ease; +} + +.switch-row input:checked + .switch, +.option input:checked + .mini-check { + background: linear-gradient(180deg, #65b8ff, #227cdb); +} + +.switch-row input:checked + .switch::after { + transform: translateX(16px); +} + +.steps { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 12px; + margin: 4px 12px 16px; + position: relative; +} + +.step { + display: grid; + justify-items: center; + gap: 6px; + min-height: 78px; + color: var(--muted); + text-align: center; + font-size: 13px; + line-height: 1.12; + position: relative; +} + +.step::after { + content: ""; + position: absolute; + top: 19px; + left: calc(50% + 22px); + width: calc(100% - 18px); + height: 3px; + background: var(--line); + border-radius: 999px; +} + +.step:nth-child(2)::after { + top: 19px; + left: calc(50% + 22px); + width: calc(100% - 18px); + height: 3px; +} + +.step:nth-child(3)::after { + display: none; +} + +.step:nth-child(3)::before { + display: none; +} + +.step.done::after, +.step.running::after, +.step.done::before, +.step.running::before { + background: var(--green); + box-shadow: 0 0 8px rgba(54, 215, 135, 0.28); +} + +.step-dot { + display: grid; + place-items: center; + width: 38px; + height: 38px; + border-radius: 50%; + background: #5b6470; + color: rgba(255, 255, 255, 0.65); + box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.08); +} + +.step-dot svg { + width: 23px; + height: 23px; + fill: currentColor; +} + +.step.done, +.step.running { + color: #eefaf4; +} + +.step.done .step-dot, +.step.running .step-dot { + background: linear-gradient(180deg, #46d98b, #23b86a); + color: #153927; + box-shadow: 0 0 14px rgba(54, 215, 135, 0.45); +} + +.step.running .step-dot { + animation: pulse 900ms ease-in-out infinite; +} + +.step.error .step-dot { + background: var(--error); + color: #391414; +} + +.step.skipped .step-dot { + background: #4b5563; + color: #a9b1bd; +} + +.primary-button { + width: 100%; + min-height: 43px; + border: 1px solid rgba(180, 220, 255, 0.38); + border-radius: 7px; + color: white; + font-size: 18px; + font-weight: 650; + cursor: pointer; + background: linear-gradient(180deg, #5bb2ff 0%, #267de1 100%); + box-shadow: 0 0 12px rgba(63, 157, 255, 0.45), inset 0 1px 0 rgba(255, 255, 255, 0.26); +} + +.primary-button:disabled, +.secondary-button:disabled { + opacity: 0.58; + cursor: default; +} + +.step-options { + margin-top: 10px; + color: var(--muted); + font-size: 13px; +} + +.step-options summary { + cursor: pointer; + list-style-position: inside; +} + +.options-grid { + display: grid; + gap: 8px; + margin-top: 9px; +} + +.option { + display: flex; + align-items: center; + gap: 8px; + color: #d7e2ed; + cursor: pointer; +} + +.mini-check { + display: grid; + place-items: center; + width: 18px; + height: 18px; + border-radius: 5px; + background: #55616e; +} + +.mini-check::after { + content: ""; + width: 8px; + height: 4px; + border-left: 2px solid white; + border-bottom: 2px solid white; + transform: rotate(-45deg) translateY(-1px); +} + +.tools { + padding-bottom: 11px; +} + +.secondary-button { + display: flex; + align-items: center; + justify-content: center; + gap: 9px; + width: 100%; + min-height: 40px; + margin-top: 10px; + border: 1px solid rgba(148, 163, 184, 0.26); + border-radius: 7px; + color: #edf4fa; + background: rgba(30, 46, 61, 0.78); + cursor: pointer; +} + +.secondary-button svg { + width: 23px; + height: 23px; + fill: #d8e4ef; +} + +.console { + margin: 8px 0 0; + border-top: 1px solid rgba(255, 255, 255, 0.07); + background: #1b1f22; + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.05); +} + +.console-head { + display: flex; + align-items: center; + justify-content: space-between; + height: 28px; + padding: 0 8px 0 12px; + color: #d4d7da; + font-size: 13px; + border-bottom: 1px solid rgba(255, 255, 255, 0.07); +} + +.console-head::before { + content: ""; + width: 0; + height: 0; + border-left: 4px solid transparent; + border-right: 4px solid transparent; + border-top: 5px solid #aab0b5; + margin-right: 7px; +} + +.console-head span { + flex: 1; +} + +.icon-button { + width: 22px; + height: 22px; + border: 0; + color: #b7bdc3; + background: transparent; + cursor: pointer; + font-size: 18px; + line-height: 1; +} + +#logs { + min-height: 76px; + max-height: 130px; + margin: 0; + padding: 8px 12px; + overflow: auto; + color: #d7d7d7; + font: 12px/1.22 Consolas, "Courier New", monospace; + white-space: pre-wrap; +} + +.log-success { + color: #63dd8d; +} + +@keyframes pulse { + 0%, + 100% { + transform: scale(1); + } + + 50% { + transform: scale(1.08); + } +} diff --git a/popup.html b/popup.html new file mode 100644 index 0000000..01a831d --- /dev/null +++ b/popup.html @@ -0,0 +1,73 @@ + + + + + + Deploy Master + + + +
+
+
+ +
+

Deploy Master

+ v1.2 +
+
+
+ +
+
+ Deploy Configuration +
+ + + +
+ + + +
+ Pasos a ejecutar +
+
+
+ +
+
+ Herramientas de Desarrollador +
+ + +
+ +
+
+ Consola de Logs + +
+

+      
+
+ + + + diff --git a/popup.js b/popup.js new file mode 100644 index 0000000..fd03170 --- /dev/null +++ b/popup.js @@ -0,0 +1,312 @@ +const DEPLOY_STEPS = [ + { + id: "export-project", + label: "Exportar proyecto", + file: "export-project.js", + enabled: true + }, + { + id: "increment-version", + label: "Incrementar y bloquear version", + files: ["increment-version.js", "lock-version-history.js"], + enabled: true, + linkedToggle: "toggle-increment-version" + }, + { + id: "generate-source-code", + label: "Generar codigo", + file: "generate-source-code.js", + enabled: true + } +]; + +const REPORTS_DOWNLOAD_URL = "http://172.27.197.229:8092/scriptcase/app/GT_ARIES/blank_PROCESAR_DESCARGAR_REPORTES/"; +const SCRIPT_DELAY_MS = 2500; + +const CHECK_ICON = ` + `; + +const PLAY_ICON = ` + `; + +const SKIP_ICON = ` + `; + +const state = { + running: false, + stepStatus: Object.fromEntries(DEPLOY_STEPS.map(step => [step.id, "pending"])), + logs: [] +}; + +const sleep = ms => new Promise(resolve => setTimeout(resolve, ms)); + +const stepsEl = document.getElementById("steps"); +const optionsEl = document.getElementById("step-options"); +const logsEl = document.getElementById("logs"); +const runButton = document.getElementById("run-deploy"); +const generateIdButton = document.getElementById("generate-test-id"); +const downloadReportsButton = document.getElementById("download-reports-folder"); +const clearLogsButton = document.getElementById("clear-logs"); +const incrementToggle = document.getElementById("toggle-increment-version"); + +document.addEventListener("DOMContentLoaded", init); + +async function init() { + renderOptions(); + renderSteps(); + bindEvents(); + await restoreOptions(); + syncIncrementOption(); + renderSteps(); + log("INFO", "Deploy Master listo."); +} + +function bindEvents() { + runButton.addEventListener("click", runDeploy); + generateIdButton.addEventListener("click", generateTestId); + downloadReportsButton.addEventListener("click", openReportsFolder); + clearLogsButton.addEventListener("click", () => { + state.logs = []; + renderLogs(); + }); + + incrementToggle.addEventListener("change", () => { + syncIncrementOption(); + persistOptions(); + renderSteps(); + }); +} + +function renderOptions() { + optionsEl.innerHTML = DEPLOY_STEPS.map(step => ` + + `).join(""); + + optionsEl.querySelectorAll("[data-step-option]").forEach(input => { + input.addEventListener("change", () => { + const step = DEPLOY_STEPS.find(item => item.id === input.dataset.stepOption); + step.enabled = input.checked; + + if (step.linkedToggle === "toggle-increment-version") { + incrementToggle.checked = input.checked; + } + + persistOptions(); + renderSteps(); + }); + }); +} + +function renderSteps() { + stepsEl.innerHTML = DEPLOY_STEPS.map(step => { + const status = step.enabled ? state.stepStatus[step.id] : "skipped"; + return ` +
+
${getStepIcon(status)}
+
${step.label}
+
+ `; + }).join(""); +} + +function getStepIcon(status) { + if (status === "running") return PLAY_ICON; + if (status === "skipped") return SKIP_ICON; + return CHECK_ICON; +} + +async function runDeploy() { + if (state.running) return; + + const [tab] = await chrome.tabs.query({ active: true, currentWindow: true }); + if (!tab?.id) { + log("ERROR", "No se pudo detectar la pestaña activa."); + return; + } + + const selectedSteps = DEPLOY_STEPS.filter(step => step.enabled); + if (!selectedSteps.length) { + log("WARN", "No hay pasos seleccionados para ejecutar."); + return; + } + + setRunning(true); + resetStepStatus(); + log("INFO", "Deploy iniciado..."); + + try { + for (const step of DEPLOY_STEPS) { + if (!step.enabled) { + setStepStatus(step.id, "skipped"); + log("INFO", `Saltando ${getStepFiles(step).join(", ")}.`); + continue; + } + + setStepStatus(step.id, "running"); + log("INFO", `Ejecutando ${getStepFiles(step).join(", ")}...`); + + const stepFiles = getStepFiles(step); + + for (const [index, file] of stepFiles.entries()) { + await chrome.scripting.executeScript({ + target: { tabId: tab.id, allFrames: false }, + files: [file] + }); + + if (index < stepFiles.length - 1) { + log("INFO", `Esperando ${SCRIPT_DELAY_MS / 1000}s antes del siguiente script...`); + await sleep(SCRIPT_DELAY_MS); + } + } + + setStepStatus(step.id, "done"); + log("INFO", `${step.label} finalizado.`); + } + + log("SUCCESS", "Deploy completado exitosamente!"); + } catch (error) { + const runningStep = DEPLOY_STEPS.find(step => state.stepStatus[step.id] === "running"); + if (runningStep) { + setStepStatus(runningStep.id, "error"); + } + + log("ERROR", error?.message || "Ocurrio un error al ejecutar el deploy."); + } finally { + setRunning(false); + } +} + +async function openReportsFolder() { + await chrome.tabs.create({ url: REPORTS_DOWNLOAD_URL }); + log("INFO", "Abriendo descarga de carpeta de reportes."); +} + +async function generateTestId() { + const [tab] = await chrome.tabs.query({ active: true, currentWindow: true }); + if (!tab?.id) { + log("ERROR", "No se pudo detectar la pestaña activa."); + return; + } + + try { + const [{ result }] = await chrome.scripting.executeScript({ + target: { tabId: tab.id }, + func: generateAndCopyTestId + }); + + log("INFO", `ID generado: ${result}`); + } catch (error) { + log("ERROR", error?.message || "No se pudo generar el ID de prueba."); + } +} + +function generateAndCopyTestId() { + const now = new Date(); + const pad = value => String(value).padStart(2, "0"); + const id = `TEST-${now.getFullYear()}${pad(now.getMonth() + 1)}${pad(now.getDate())}${pad(now.getHours())}${pad(now.getMinutes())}${pad(now.getSeconds())}`; + + const textarea = document.createElement("textarea"); + textarea.value = id; + textarea.style.position = "fixed"; + textarea.style.top = "0"; + textarea.style.left = "0"; + textarea.style.opacity = "0"; + document.body.appendChild(textarea); + textarea.select(); + document.execCommand("copy"); + document.body.removeChild(textarea); + + return id; +} + +function resetStepStatus() { + DEPLOY_STEPS.forEach(step => { + state.stepStatus[step.id] = step.enabled ? "pending" : "skipped"; + }); + renderSteps(); +} + +function setStepStatus(stepId, status) { + state.stepStatus[stepId] = status; + renderSteps(); +} + +function setRunning(isRunning) { + state.running = isRunning; + runButton.disabled = isRunning; + generateIdButton.disabled = isRunning; + downloadReportsButton.disabled = isRunning; + runButton.textContent = isRunning ? "Ejecutando..." : "Ejecutar Deploy"; +} + +function syncIncrementOption() { + const incrementStep = DEPLOY_STEPS.find(step => step.id === "increment-version"); + incrementStep.enabled = incrementToggle.checked; + + const option = optionsEl.querySelector('[data-step-option="increment-version"]'); + if (option) { + option.checked = incrementToggle.checked; + } +} + +async function restoreOptions() { + const { deployStepOptions } = await chrome.storage.local.get("deployStepOptions"); + if (!deployStepOptions) return; + + DEPLOY_STEPS.forEach(step => { + if (typeof deployStepOptions[step.id] === "boolean") { + step.enabled = deployStepOptions[step.id]; + } + }); + + incrementToggle.checked = DEPLOY_STEPS.find(step => step.id === "increment-version").enabled; + + optionsEl.querySelectorAll("[data-step-option]").forEach(input => { + const step = DEPLOY_STEPS.find(item => item.id === input.dataset.stepOption); + input.checked = step.enabled; + }); +} + +async function persistOptions() { + const deployStepOptions = Object.fromEntries(DEPLOY_STEPS.map(step => [step.id, step.enabled])); + await chrome.storage.local.set({ deployStepOptions }); +} + +function getStepFiles(step) { + return step.files || [step.file]; +} + +function log(level, message) { + const line = `[${level}] ${message}`; + state.logs.push({ level, line }); + renderLogs(); +} + +function renderLogs() { + logsEl.innerHTML = state.logs + .map(entry => { + const className = entry.level === "SUCCESS" ? "log-success" : ""; + return `${escapeHtml(entry.line)}`; + }) + .join("\n"); + logsEl.scrollTop = logsEl.scrollHeight; +} + +function escapeHtml(value) { + return value + .replaceAll("&", "&") + .replaceAll("<", "<") + .replaceAll(">", ">") + .replaceAll('"', """) + .replaceAll("'", "'"); +}