Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,16 +39,16 @@ CI: `.github/workflows/tests.yml` runs the full suite headless (Playwright + Chr

## Architecture

Everything is in `index.html` (~2100 lines): CSS in `<style>`, HTML markup in `<body>`, and JavaScript in `<script>`. No modules, no bundler.
Everything is in `index.html` (~2180 lines): CSS in `<style>`, HTML markup in `<body>`, and JavaScript in `<script>`. No modules, no bundler.

### Key JavaScript Functions

| Function | Purpose |
|----------|---------|
| `prettyFormat(xml, indentChars, preserveWhitespace)` | DOM-tree walk to produce indented XML using `DOMParser` |
| `minify(xml, opts)` | Minifies via `DOMParser` + `XMLSerializer` + regex cleanup |
| `highlightXML(xml)` | Regex-based syntax highlighting without libraries |
| `renderOutput(text)` | Renders output in the active view mode (`hl`/`txt`/`tree`) |
| `highlightXML(xml)` / `highlightJSON(json)` | Regex-based syntax highlighting without libraries |
| `renderOutput(text)` | Renders output in the active view mode (`hl`/`txt`/`tree`); auto-detects XML vs JSON |
| `checkFileSize(text)` | Shows confirmation modal for files >10MB, blocks >15MB |
| `updateKPIs(inText, outText)` | Updates the input/output/savings byte counters |
| `collectStats(doc)` | Computes element/attribute/depth stats from a parsed doc |
Expand All @@ -57,7 +57,7 @@ Everything is in `index.html` (~2100 lines): CSS in `<style>`, HTML markup in `<
| `xmlToCsv(xml)` | Tabulates the dominant repeated element (RFC 4180 escaping) |
| `xpathQuery(xml, expr)` | XPath via native `document.evaluate()`; returns string array |
| `diffXML(a, b)` | Semantic diff; returns lines `➕/➖/✏️ /path — detail` (max 1000) |
| `buildTreeHTML(xml)` | Read-only collapsible tree view using native `<details>` |
| `buildTreeHTML(text)` | Read-only collapsible tree view (XML or JSON) using native `<details>` |
| `applySearch()` | Highlights search matches in the highlighted view (max 500) |
| `applyTheme(name)` / `applyURLPreset()` | Visual themes and URL-based config presets |
| `gotoLine()` | Ctrl+G: jump to a specific line in the input editor |
Expand Down
22 changes: 18 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

XMLShield es una herramienta de formateo y minificación de XML que funciona completamente en tu navegador, sin enviar datos a ningún servidor. Diseñada en respuesta a preocupaciones sobre la seguridad de herramientas online, XMLShield te da control total sobre tus archivos XML sensibles.

[![Version](https://img.shields.io/badge/version-2.7-blue.svg)](https://github.com/luisfemojica/xmlshield/releases)
[![Version](https://img.shields.io/badge/version-2.8-blue.svg)](https://github.com/luisfemojica/xmlshield/releases)
[![Tests](https://github.com/luisfemojica/xmlshield/actions/workflows/tests.yml/badge.svg)](https://github.com/luisfemojica/xmlshield/actions/workflows/tests.yml)
[![License](https://img.shields.io/badge/license-Public%20Domain-green.svg)](LICENSE)
[![Live Demo](https://img.shields.io/badge/demo-online-brightgreen.svg)](https://luisfemojica.github.io/xmlshield/)
Expand All @@ -30,8 +30,8 @@ XMLShield es una herramienta de formateo y minificación de XML que funciona com
- 🧭 **XPath queries** con `document.evaluate()` nativo — `//tag[@attr='x']`, `count()`, etc.
- ♻️ **Buscar y reemplazar** en la entrada, integrado con Deshacer
- 🆚 **Diff semántico** entre dos XML — ignora orden de atributos y espacios
- 🎨 **Resaltado de sintaxis** sin dependencias externas
- 👁️ **Vista triple** - Sintaxis resaltada, texto plano o árbol colapsable
- 🎨 **Resaltado de sintaxis XML y JSON** sin dependencias externas
- 👁️ **Vista triple** - Sintaxis resaltada, texto plano o árbol colapsable (XML y JSON)
- 🔍 **Búsqueda con highlight** y navegación entre coincidencias
- 🔢 **Líneas numeradas** en ambos editores, sincronizadas con el scroll
- 📊 **KPIs en tiempo real** - Tamaño, ahorro, elementos, atributos y profundidad
Expand Down Expand Up @@ -277,7 +277,15 @@ Refactorización completa con diseño moderno, panel dividido, gradientes, modo
- ✅ KPIs con unidades legibles (B/KB/MB), favicon, atajos Ctrl+O/F/H y Tab con sangría
- ✅ CI en GitHub Actions: la suite corre headless en cada PR

**🎉 Roadmap v2.x completado de principio a fin** — ~2100 líneas | ~80KB (límite duro: 100KB) | 77 tests de regresión con CI.
### v2.8 - "JSON First-Class" ✅ COMPLETADO
**Fecha:** Junio 2026

- ✅ Resaltado de sintaxis JSON en la vista Resaltada
- ✅ Árbol colapsable para JSON (objetos, arrays, contadores)
- ✅ Estadísticas para JSON en los KPIs
- ✅ Descarga con extensión correcta (.json/.csv/.txt)

**🎉 Roadmap v2.x completado de principio a fin** — ~2177 líneas | ~83KB (límite duro: 100KB) | 85 tests de regresión con CI.

### v3.0+ - "XMLShield Extended" 🔮 2027+
**Proyecto separado** con features avanzadas:
Expand Down Expand Up @@ -391,6 +399,12 @@ Para features más avanzadas, considera XMLShield Extended (v3.0+) o herramienta

## 📝 Changelog

### v2.8 - Junio 2026
- ✅ Resaltado de sintaxis para JSON: claves, strings, números y booleanos con colores (5 temas)
- ✅ Vista de árbol para JSON: objetos y arrays colapsables con contador de claves/elementos
- ✅ Estadísticas también para JSON (valores, claves, profundidad)
- ✅ Descargar usa la extensión y MIME correctos: .json, .csv o .txt según la salida

### v2.7 - Junio 2026
- ✅ Layout móvil real: paneles apilados a ancho completo y página con scroll
- ✅ Botón "📄 Ejemplo" en el panel de entrada + preset `?demo=1`
Expand Down
47 changes: 46 additions & 1 deletion docs/ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

> **Documento vivo** - Se actualiza cada release
> **Última revisión:** Junio 2026
> **Versión actual:** v2.7 — 🎉 Roadmap v2.x COMPLETADO (v2.0 a v2.7)
> **Versión actual:** v2.8 — 🎉 Roadmap v2.x COMPLETADO (v2.0 a v2.8)

---

Expand Down Expand Up @@ -356,6 +356,43 @@ Ctrl+O (abrir), Ctrl+F (buscar en salida), Ctrl+H (reemplazar), y Tab inserta sa

---

### v2.8 - "JSON First-Class" ✅ COMPLETADO
**Fecha de release:** Junio 2026
**Esfuerzo:** 2-3 días
**Prioridad:** Alta (petición directa del mantenedor)

#### Contexto
Desde v2.7 la app formatea, minifica y valida JSON, pero el resaltado y el árbol seguían siendo solo-XML: el JSON salía en texto plano sin colores y la vista Árbol mostraba un aviso de error.

#### Features Implementadas

##### 1. Resaltado de sintaxis JSON ✅ IMPLEMENTADO
**Complejidad:** Media | **Líneas:** ~25

`highlightJSON()` tokeniza el texto crudo (maneja comillas escapadas) y reutiliza las clases de color existentes: claves `.attr`, strings `.value`, números `.tag`, booleanos/null `.comment`. Funciona con los 5 temas sin CSS nuevo. `renderOutput` detecta el tipo de salida por prefijo (`{` o `[`).

##### 2. Vista de árbol para JSON ✅ IMPLEMENTADO
**Complejidad:** Media | **Líneas:** ~20

`jsonTreeHTML()`: objetos `{ }` y arrays `[ ]` colapsables con `<details>` nativo, contador de claves/elementos por nodo, hojas con su valor tipado. `buildTreeHTML` enruta según el tipo de salida.

##### 3. Estadísticas para JSON ✅ IMPLEMENTADO
**Complejidad:** Baja | **Líneas:** ~15

`collectStatsJSON()`: valores totales, claves de objeto y profundidad en los mismos KPIs (antes quedaban en "–" al formatear JSON).

##### 4. Descarga con extensión correcta ✅ IMPLEMENTADO
**Complejidad:** Baja | **Líneas:** ~10

El botón Descargar elegía siempre `.xml`: ahora `.json` para salidas JSON, `.csv` tras convertir a CSV, `.txt` para reportes de diff/XPath, con el MIME correspondiente.

#### Métricas Alcanzadas v2.8
- **Tamaño:** ~83KB (dentro del límite duro de 100KB)
- **Líneas:** ~2177 (bajo el techo de ~2500)
- **Tests:** 85 casos pasando (8 nuevos)

---

## 🔮 Visión Futura (Post v2.6)

### v3.0 - "XMLShield Extended" — ❌ NO-GO (decisión: Junio 2026)
Expand All @@ -382,6 +419,7 @@ Las dos features de v3.0 que sí cabían en la filosofía (XPath y Diff) ya fuer
│ │ v2.5 ✅ │ │ │
│ │ v2.6 ✅ │ │ │
│ │ v2.7 ✅ │ │ │
│ │ v2.8 ✅ │ │ │
└──────────┴──────────┴──────────┴──────────┘
```

Expand Down Expand Up @@ -429,6 +467,12 @@ Las dos features de v3.0 que sí cabían en la filosofía (XPath y Diff) ya fuer
- [x] Cada fix/quick win con su test de regresión
- [x] Archivo final <100KB y <2500 líneas (~80KB, ~2100 líneas)

### v2.8 Success Criteria
- [x] JSON formateado se ve resaltado en la vista Resaltada (5 temas)
- [x] La vista Árbol renderiza JSON colapsable en vez de mostrar un aviso
- [x] KPIs de estadísticas pobladas también para JSON
- [x] Descargar produce .json/.csv/.txt según el tipo real de la salida

---

## 🚫 Features Descartadas
Expand All @@ -454,6 +498,7 @@ Las dos features de v3.0 que sí cabían en la filosofía (XPath y Diff) ya fuer
| v2.5 | 100% | 4/4 | ~1825 | ~66KB | ✅ Released |
| v2.6 | 100% | 1/1 | ~1955 | ~72KB | ✅ Released |
| v2.7 | 100% | 11/11 | ~2100 | ~80KB | ✅ Released |
| v2.8 | 100% | 4/4 | ~2177 | ~83KB | ✅ Released |

---

Expand Down
10 changes: 10 additions & 0 deletions docs/SCOPE.md
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,16 @@ Pulido post-roadmap tras el NO-GO de v3.0:
- ✅ **KPIs legibles, favicon, más atajos** (Ctrl+O/F/H, Tab con sangría)
- ✅ **CI con GitHub Actions** (infraestructura dev, no toca el producto)

#### 🟢 v2.8 (JSON First-Class) ✅ COMPLETADO
**Estimado:** 2-3 días | **Complejidad:** Media | **Líneas:** +75

El soporte JSON de v2.4-v2.7 (validar, formatear, minificar, convertir) se completa en las vistas:

- ✅ **Resaltado de sintaxis JSON** (claves/strings/números/booleanos, reutiliza los colores de los 5 temas)
- ✅ **Árbol colapsable para JSON** con contadores de claves/elementos
- ✅ **Estadísticas para JSON** en los KPIs existentes
- ✅ **Descarga con extensión correcta** (.json/.csv/.txt según la salida)

---

## ❌ Fuera del Alcance (v2.x)
Expand Down
96 changes: 85 additions & 11 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -784,6 +784,7 @@ <h2>🛡️ XMLShield • Your Local XML Fortress</h2>
const VIEW_ORDER = ["hl", "txt", "tree"];
let viewMode = "hl";
let lastOutputText = "";
let lastOutputKind = "auto"; // "auto" (XML/JSON por prefijo) | "csv" — decide extensión al descargar

// --- Utilidades ---
function bytesOf(str){ return new Blob([str]).size; }
Expand Down Expand Up @@ -918,8 +919,11 @@ <h2>🛡️ XMLShield • Your Local XML Fortress</h2>
function renderOutput(text){
if (compareMode) exitCompareMode(); // cualquier operación que escriba la salida cancela el modo comparar
lastOutputText = text || "";
lastOutputKind = "auto"; // los handlers (ej. CSV) pueden sobreescribirlo después
if (viewMode === "hl") {
outHtml.innerHTML = highlightXML(lastOutputText);
outHtml.innerHTML = outputIsJSON(lastOutputText)
? highlightJSON(lastOutputText)
: highlightXML(lastOutputText);
} else if (viewMode === "tree") {
outTree.innerHTML = buildTreeHTML(lastOutputText);
} else {
Expand Down Expand Up @@ -1124,6 +1128,29 @@ <h2>🛡️ XMLShield • Your Local XML Fortress</h2>
return h;
}

// --- Resaltado de sintaxis JSON (v2.8) ---
// Tokeniza sobre el texto crudo y escapa por partes: claves .attr,
// strings .value, números .tag, booleanos/null .comment
function highlightJSON(json) {
const re = /("(?:\\.|[^"\\])*")(\s*:)?|\b(true|false|null)\b|(-?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?)/g;
let out = "", last = 0, m;
while ((m = re.exec(json))) {
out += escapeHTML(json.slice(last, m.index));
if (m[1] !== undefined) {
out += `<span class="${m[2] ? "attr" : "value"}">${escapeHTML(m[1])}</span>${m[2] || ""}`;
} else if (m[3] !== undefined) {
out += `<span class="comment">${m[3]}</span>`;
} else {
out += `<span class="tag">${m[4]}</span>`;
}
last = m.index + m[0].length;
}
return out + escapeHTML(json.slice(last));
}

// La salida es JSON si empieza por { o [ (mismo criterio que la conversión)
function outputIsJSON(text){ return /^[\[{]/.test((text || "").trim()); }

// --- Estadísticas del documento (elementos, atributos, profundidad) ---
function collectStats(doc){
let elems = 0, attrs = 0, depth = 0;
Expand All @@ -1140,6 +1167,21 @@ <h2>🛡️ XMLShield • Your Local XML Fortress</h2>
function resetStats(){
["#statElems", "#statAttrs", "#statDepth"].forEach(s => $(s).textContent = "–");
}
// Equivalente para JSON: valores totales, claves de objeto y profundidad
function collectStatsJSON(data){
let values = 0, keys = 0, depth = 0;
(function walk(v, d){
values++;
if (d > depth) depth = d;
if (v && typeof v === "object") {
if (!Array.isArray(v)) keys += Object.keys(v).length;
for (const x of (Array.isArray(v) ? v : Object.values(v))) walk(x, d + 1);
}
})(data, 1);
$("#statElems").textContent = values;
$("#statAttrs").textContent = keys;
$("#statDepth").textContent = depth;
}

// --- Líneas numeradas (gutter sincronizado con scroll) ---
const gutterLines = new WeakMap();
Expand Down Expand Up @@ -1414,9 +1456,29 @@ <h2>🛡️ XMLShield • Your Local XML Fortress</h2>
const countHTML = ` <span class="tree-count">· ${el.children.length} hijo(s)</span>`;
return `<details open><summary>&lt;${name}${attrs}&gt;${textHTML}${countHTML}</summary>${kids}</details>`;
}
function buildTreeHTML(xml){
if (!xml.trim()) return "";
const doc = new DOMParser().parseFromString(xml, "application/xml");
// Árbol para JSON: objetos {} y arrays [] colapsables, hojas con su valor
function jsonTreeHTML(label, val){
const lbl = label === null ? "" : `<span class="attr">${escapeHTML(label)}</span>: `;
if (val === null || typeof val !== "object") {
const cls = typeof val === "string" ? "txt" : "tag";
return `<div class="tree-leaf">${lbl}<span class="${cls}">${escapeHTML(JSON.stringify(val))}</span></div>`;
}
const isArr = Array.isArray(val);
const entries = isArr ? val.map((v, i) => [i, v]) : Object.entries(val);
const head = `${lbl}<span class="tag">${isArr ? "[ ]" : "{ }"}</span> <span class="tree-count">· ${entries.length} ${isArr ? "elemento(s)" : "clave(s)"}</span>`;
if (!entries.length) return `<div class="tree-leaf">${head}</div>`;
let kids = "";
for (const [k, v] of entries) kids += jsonTreeHTML(String(k), v);
return `<details open><summary>${head}</summary>${kids}</details>`;
}

function buildTreeHTML(text){
if (!text.trim()) return "";
if (outputIsJSON(text)) {
try { return jsonTreeHTML(null, JSON.parse(text)); }
catch { return `<div class="tree-leaf">⚠️ La salida actual no es JSON válido — no se puede mostrar como árbol.</div>`; }
}
const doc = new DOMParser().parseFromString(text, "application/xml");
if (doc.querySelector("parsererror"))
return `<div class="tree-leaf">⚠️ La salida actual no es XML válido — formatea primero para ver el árbol.</div>`;
return treeNodeHTML(doc.documentElement, "/" + doc.documentElement.nodeName);
Expand Down Expand Up @@ -1600,9 +1662,12 @@ <h2>🛡️ XMLShield • Your Local XML Fortress</h2>
let output;
if (/^[\[{]/.test(input.trim())) {
// La entrada es JSON: embellecerlo directamente
try { output = JSON.stringify(JSON.parse(input), null, $("#tabs").checked ? "\t" : (+$("#indent").value || 2)); }
try {
const data = JSON.parse(input);
output = JSON.stringify(data, null, $("#tabs").checked ? "\t" : (+$("#indent").value || 2));
collectStatsJSON(data);
}
catch(e) { throw new Error("❌ JSON inválido: " + e.message); }
resetStats();
} else {
const indent = +$("#indent").value || 0;
const indentChars = $("#tabs").checked ? "\t" : " ".repeat(indent);
Expand Down Expand Up @@ -1635,9 +1700,12 @@ <h2>🛡️ XMLShield • Your Local XML Fortress</h2>
let output;
if (/^[\[{]/.test(input.trim())) {
// La entrada es JSON: compactarlo directamente
try { output = JSON.stringify(JSON.parse(input)); }
try {
const data = JSON.parse(input);
output = JSON.stringify(data);
collectStatsJSON(data);
}
catch(e) { throw new Error("❌ JSON inválido: " + e.message); }
resetStats();
} else {
output = minify(input, {
keepTextWS: $("#keepTextWS").checked,
Expand Down Expand Up @@ -1696,6 +1764,7 @@ <h2>🛡️ XMLShield • Your Local XML Fortress</h2>
setTimeout(() => {
try {
renderOutput(xmlToCsv(input));
lastOutputKind = "csv"; // para descargar como .csv
} catch(e) {
showError(e.message);
} finally {
Expand Down Expand Up @@ -1847,12 +1916,17 @@ <h2>🛡️ XMLShield • Your Local XML Fortress</h2>
showError("No hay nada que descargar. Formatea o minifica primero.");
return;
}
const blob = new Blob([lastOutputText], { type: 'application/xml' });
// Extensión y MIME según el tipo real de la salida
let ext = "xml", mime = "application/xml";
if (lastOutputKind === "csv") { ext = "csv"; mime = "text/csv"; }
else if (outputIsJSON(lastOutputText)) { ext = "json"; mime = "application/json"; }
else if (!lastOutputText.trim().startsWith("<")) { ext = "txt"; mime = "text/plain"; }
const blob = new Blob([lastOutputText], { type: mime });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, -5);
a.href = url;
a.download = `xml_formatted_${timestamp}.xml`;
a.download = `xmlshield_${timestamp}.${ext}`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
Expand Down Expand Up @@ -2096,7 +2170,7 @@ <h2>🛡️ XMLShield • Your Local XML Fortress</h2>
// y disponible en consola para uso avanzado
window.XMLShield = {
prettyFormat, minify, xmlToJson, jsonToXml, xmlToCsv, xpathQuery, diffXML,
highlightXML, asEscapedString, buildTreeHTML, fmtBytes
highlightXML, highlightJSON, asEscapedString, buildTreeHTML, fmtBytes
};
})();
</script>
Expand Down
Loading
Loading