From faa1844646337fb09f22ad18762474b5f7dcb278 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=ADctor=20Manuel=20J=C3=A1quez=20Leal?= Date: Sat, 1 Aug 2026 21:14:29 +0200 Subject: [PATCH 01/11] css: arreglar advertencias de biome --- css/cpeum.css | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/css/cpeum.css b/css/cpeum.css index 4bc1cb73..905a0a15 100644 --- a/css/cpeum.css +++ b/css/cpeum.css @@ -153,20 +153,6 @@ aside.sidebar .sidebar-title { color: #222; } -aside.sidebar ul { - margin: 0; - padding: 0; - list-style: none; -} - -aside.sidebar ul > li { - border-bottom: thin solid #e0e0e0; -} - -aside.sidebar ul > li:last-child { - border-bottom: none; -} - aside.sidebar :where(li) { margin-bottom: 0; padding: 6px 0; @@ -431,3 +417,17 @@ main > p, main > section:first-of-type { display: block; } + +aside.sidebar ul { + margin: 0; + padding: 0; + list-style: none; +} + +aside.sidebar ul > li { + border-bottom: thin solid #e0e0e0; +} + +aside.sidebar ul > li:last-child { + border-bottom: none; +} From 91865cddd390724d217e822b08b353c4b7f48c85 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=ADctor=20Manuel=20J=C3=A1quez=20Leal?= Date: Sat, 1 Aug 2026 09:58:42 +0200 Subject: [PATCH 02/11] =?UTF-8?q?A=C3=B1adir=20dependencias=20node=20para?= =?UTF-8?q?=20generar=20diffs=20de=20reformas?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Incluye package.json, package-lock.json y .gitignore para el script de generación de diffs basado en diff2html. --- .gitignore | 12 ++++++ package-lock.json | 94 +++++++++++++++++++++++++++++++++++++++++++++++ package.json | 15 ++++++++ 3 files changed, 121 insertions(+) create mode 100644 .gitignore create mode 100644 package-lock.json create mode 100644 package.json diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..e9beb69c --- /dev/null +++ b/.gitignore @@ -0,0 +1,12 @@ +# Node +node_modules/ +npm-debug.log* + +# Entorno +__pycache__/ +*.pyc +.venv/ +.ruff_cache/ + +# Salida generada +html/ diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 00000000..99b51809 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,94 @@ +{ + "name": "cpeum-diffs", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "cpeum-diffs", + "version": "0.1.0", + "dependencies": { + "diff": "^7.0.0", + "diff2html": "^3.4.56" + }, + "devDependencies": {} + }, + "node_modules/@profoundlogic/hogan": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@profoundlogic/hogan/-/hogan-3.0.4.tgz", + "integrity": "sha512-pmNVGuooS30Mm7YbZd5T7E5zYVO6D5Ct91sn4T39mUvMUc3sCGridcnhAufL1/Bz2QzAtzEn0agNrdk3+5yWzw==", + "license": "Apache-2.0", + "dependencies": { + "nopt": "1.0.10" + }, + "bin": { + "hulk": "bin/hulk" + } + }, + "node_modules/abbrev": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", + "license": "ISC" + }, + "node_modules/diff": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-7.0.0.tgz", + "integrity": "sha512-PJWHUb1RFevKCwaFA9RlG5tCd+FO5iRh9A8HEtkmBH2Li03iJriB6m6JIN4rGz3K3JLawI7/veA1xzRKP6ISBw==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/diff2html": { + "version": "3.4.56", + "resolved": "https://registry.npmjs.org/diff2html/-/diff2html-3.4.56.tgz", + "integrity": "sha512-u9gfn+BlbHcyO7vItCIC4z49LJDUt31tODzOfAuJ5R1E7IdlRL6KjugcB9zOpejD+XiR+dDZbsnHSQ3g6A/u8A==", + "license": "MIT", + "dependencies": { + "@profoundlogic/hogan": "^3.0.4", + "diff": "^8.0.3" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "highlight.js": "11.11.1" + } + }, + "node_modules/diff2html/node_modules/diff": { + "version": "8.0.4", + "resolved": "https://registry.npmjs.org/diff/-/diff-8.0.4.tgz", + "integrity": "sha512-DPi0FmjiSU5EvQV0++GFDOJ9ASQUVFh5kD+OzOnYdi7n3Wpm9hWWGfB/O2blfHcMVTL5WkQXSnRiK9makhrcnw==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/highlight.js": { + "version": "11.11.1", + "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-11.11.1.tgz", + "integrity": "sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w==", + "license": "BSD-3-Clause", + "optional": true, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/nopt": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-1.0.10.tgz", + "integrity": "sha512-NWmpvLSqUrgrAC9HCuxEvb+PSloHpqVu+FqcO4eeF2h5qYRhA7ev6KvelyQAKtegUbC6RypJnlEOhd8vloNKYg==", + "license": "MIT", + "dependencies": { + "abbrev": "1" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": "*" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 00000000..bcddec95 --- /dev/null +++ b/package.json @@ -0,0 +1,15 @@ +{ + "name": "cpeum-diffs", + "version": "0.1.0", + "private": true, + "description": "Genera un diff HTML (estilo diff2html/github) por cada reforma constitucional", + "type": "commonjs", + "scripts": { + "reformas": "node scripts/generar_reformas.js" + }, + "dependencies": { + "diff": "^7.0.0", + "diff2html": "^3.4.56" + }, + "devDependencies": {} +} From b2c316939dc3c46187801c7fec1a73d3613b1577 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=ADctor=20Manuel=20J=C3=A1quez=20Leal?= Date: Sat, 1 Aug 2026 10:00:47 +0200 Subject: [PATCH 03/11] AGENTS.md: documentar el linting de JavaScript y el script de reformas --- AGENTS.md | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/AGENTS.md b/AGENTS.md index 399f5a80..b57c1126 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -39,6 +39,18 @@ partir de los decretos y otros cambios Constitucionales registrados en uv run ../scripts/rst2html5.py toc.rst ../html/index.html ``` +- `scripts/generar_reformas.js`: es un script en JavaScript que, usando + el paquete [`diff2html`](https://www.npmjs.com/package/diff2html), + genera una página HTML con el diff de cada reforma constitucional + (con una apariencia similar a la de GitHub) y un índice de todas las + reformas en `html/reformas/`. + + Para ejecutar el script: + + ```bash + node scripts/generar_reformas.js + ``` + ### Uso de Python Python se usa a través de un entorno virtual controlado con `uv`. El @@ -63,6 +75,26 @@ Usar `biome` para validar y corregir formato de archivos CSS. npx @biomejs/biome lint --write css/ ``` +### JavaScript + +Usar `biome` para validar y corregir formato de los scripts en +JavaScript. + +```bash +npx @biomejs/biome check scripts/*.js +npx @biomejs/biome lint --write scripts/*.js +npx @biomejs/biome format --write scripts/*.js +``` + +Las dependencias de los scripts en JavaScript se gestionan con `npm` y +se describen en `package.json`. Para instalar las dependencias y +generar los diffs de reformas se ejecuta: + +```bash +npm install +node scripts/generar_reformas.js +``` + ## pre-commit El proyecto utiliza `pre-commit` para realizar revisiones automáticas a From 37ae90bab926aa318f76d87e29219ed9fa0d6949 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=ADctor=20Manuel=20J=C3=A1quez=20Leal?= Date: Sat, 1 Aug 2026 10:02:07 +0200 Subject: [PATCH 04/11] =?UTF-8?q?pre-commit:=20a=C3=B1adir=20linting=20de?= =?UTF-8?q?=20biome=20para=20JavaScript?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Añade un hook biome-check-javascript con alcance a archivos .js para verificar el código JavaScript en cada commit. --- .pre-commit-config.yaml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index f412b6ef..8a3e132a 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -35,6 +35,9 @@ repos: rev: v2.5.5 hooks: - id: biome-check + - id: biome-check + name: biome-check-javascript + files: '\.(js)$' - repo: local hooks: - id: diccionario From dd60fe1f36d6977d66f5632ecbbd5b9d07703906 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=ADctor=20Manuel=20J=C3=A1quez=20Leal?= Date: Sat, 1 Aug 2026 09:58:49 +0200 Subject: [PATCH 05/11] scripts: generar_reformas.js: generar diff HTML por reforma MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Script en JavaScript que usa el paquete diff2html para generar una página HTML con el diff de cada reforma constitucional (estilo github), además de un índice con todas las reformas en html/reformas/. --- scripts/generar_reformas.js | 688 ++++++++++++++++++++++++++++++++++++ 1 file changed, 688 insertions(+) create mode 100644 scripts/generar_reformas.js diff --git a/scripts/generar_reformas.js b/scripts/generar_reformas.js new file mode 100644 index 00000000..44e1046b --- /dev/null +++ b/scripts/generar_reformas.js @@ -0,0 +1,688 @@ +#!/usr/bin/env node +/** + * Genera un diff HTML (estilo diff2html/github) por cada reforma + * constitucional representada en un commit, así como un índice de todas + * las reformas. + * + * Para cada commit cuyo asunto comienza con "Artículo" o "Artículos" + * se obtienen los archivos `.rst` modificados y se construye un diff + * unificado usando el paquete `diff`. Ese diff se renderiza con + * `diff2html` en su formato de líneas (outputFormat "line"), el mismo + * aspecto que produce diff2html-cli. + * + * Salida: + * html/decretos/index.html — índice de reformas (una por decreto) + * html/decretos/.html — diff individual de cada decreto + */ + +const { execSync } = require("node:child_process"); +const fs = require("node:fs"); +const path = require("node:path"); +const { createPatch } = require("diff"); +const { html: diff2html } = require("diff2html"); + +function htmlEscape(text) { + return String(text).replace(/[&<>"']/g, (char) => { + switch (char) { + case "&": + return "&"; + case "<": + return "<"; + case ">": + return ">"; + case '"': + return """; + default: + return "'"; + } + }); +} + +/* Convierte en vínculos las URLs del Diario Oficial de la Federación + (dof.gob.mx). Recibe texto ya escapado con htmlEscape. */ +function linkifyDof(text) { + return text.replace( + /(https?:\/\/[^\s<>"']*dof\.gob\.mx[^\s<>"']*)/g, + '$1', + ); +} + +const OUTDIR = path.resolve(__dirname, "..", "html", "decretos"); +const DECRETOS_PATH = path.resolve(__dirname, "..", "html", "decretos.json"); +const GIT_LOG_FORMAT = "%H%n%ci%n%s%n%b%n---END---"; +const TITLE = "Decretos"; + +/* Hoja de estilo oficial de diff2html (la misma que usa diff2html-cli). */ +const DIFF2HTML_CSS_PATH = require.resolve( + "diff2html/bundles/css/diff2html.min.css", +); +const DIFF2HTML_CSS = fs.readFileSync(DIFF2HTML_CSS_PATH, "utf-8"); + +const GLOBAL_CSS = `\ +body { + font-family: Georgia, "Noto Serif", "DejaVu Serif", serif; + max-width: 100ch; + margin: 0 auto; + padding: 40px 1.5em 1.5em; + line-height: 1.8; + font-size: 1.05em; + color: #1a1a1a; + background: #fafafa; +} +h1 { font-size: 1.3em; border-bottom: 2px solid #333; padding-bottom: 0.3em; } +h2 { font-size: 1.1em; margin-top: 1.5em; } +.reforma-nav { + display: flex; + justify-content: space-between; + gap: 1em; + margin: 0.5em 0 1em; + font-size: 0.9em; +} +.reforma-nav a { color: #0066cc; text-decoration: none; } +.reforma-nav a:hover { text-decoration: underline; } +.reforma-nav .nav-disabled { color: #999; } +.reforma-nav .nav-prev { max-width: 45%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } +.reforma-nav .nav-next { text-align: right; } +.reforma-nav .nav-next a { + display: inline-block; + max-width: 45vw; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} +.commit-meta { color: #666; font-size: 0.9em; margin-bottom: 0.5em; } +.reforma-numero { color: #0066cc; font-size: 0.85em; margin: 0.1em 0 0.5em; } +.commit-meta a { color: #0066cc; text-decoration: none; } +.commit-meta a:hover { text-decoration: underline; } +.commit-body { + white-space: pre-wrap; + font-size: 0.9em; + margin: 0.5em 0 1.5em 0; + padding: 0.75em 1em; + background: #f9f9f9; + border-left: 3px solid #ccc; + line-height: 1.5; + font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; +} +.commit-body a, .reforma-resumen a { + color: #0066cc; + text-decoration: none; +} +.commit-body a:hover, .reforma-resumen a:hover { + text-decoration: underline; +} +.reforma { margin-bottom: 1.2em; } +.reforma-titulo { font-size: 1em; margin: 0.1em 0; } +.reforma-titulo a { color: #0066cc; text-decoration: none; } +.reforma-titulo a:hover { text-decoration: underline; } +.reforma-fecha { color: #666; font-size: 0.9em; } +.reforma-resumen { + font-size: 1em; + color: #555; + margin: 0.2em 0 0 0; +} +.reforma-sep { + border: none; + border-top: 1px solid #d0d7de; + margin: 1.2em 0 0 0; +} +/* Banner superior fijo, igual que en las páginas del sitio */ +#top-banner { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 40px; + background: #1a3a5c; + color: #ffffff; + display: flex; + align-items: center; + padding: 0 20px; + box-sizing: border-box; + z-index: 200; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2); +} +#top-banner .banner-link { + color: #ffffff; + text-decoration: none; + font-size: 1.1em; + font-weight: bold; + letter-spacing: 0.05em; +} +#top-banner .banner-links { + margin-left: auto; + display: flex; + align-items: center; + gap: 0.9em; +} +#top-banner .banner-icon { + display: block; + fill: currentColor; +} +#top-banner .banner-link:hover { + text-decoration: underline; +} +#top-banner .banner-link:hover .banner-icon { + opacity: 0.8; +} +.menu-toggle { + display: none; + align-items: center; + justify-content: center; + background: transparent; + border: none; + color: #ffffff; + cursor: pointer; + padding: 4px; + margin-right: 8px; + line-height: 0; +} +.menu-toggle:hover { + opacity: 0.8; +} +@media (max-width: 768px) { + #top-banner { + padding: 0 12px; + gap: 4px; + } + #top-banner .banner-link { + font-size: 1em; + } + /* Sin tabla de contenidos, la hamburguesa no se muestra */ + body:not(:has(nav.contents)) .menu-toggle { + display: none; + } +} +`; + +const HTML_HEADER = `\ + + + + + + +`; + +const FOOTER = `\ + + +`; + +const GITHUB_URL = "https://github.com/ceyusa/cpeum"; + +/* Banner superior, igual que el de index.html y acercade.html. + Rutas relativas a html/decretos/index.html. */ +const BANNER = `\ +
+ + + +
+`; + +// --------------------------------------------------------------------------- +// Git helpers +// --------------------------------------------------------------------------- + +function runGit(args) { + return execSync(["git", ...args].join(" "), { + encoding: "utf-8", + timeout: 30000, + stdio: ["ignore", "pipe", "pipe"], + }); +} + +/** Devuelve los commits de decretos, en orden cronológico (antiguo → nuevo). */ +function getReformaCommits() { + const output = runGit([ + "log", + "--reverse", + "--grep=Artículo", + "--grep=Articulo", + "--grep=Artíclos", + `--format=${GIT_LOG_FORMAT}`, + ]); + const commits = []; + for (const block of output.split("---END---")) { + const trimmed = block.trim(); + if (!trimmed) continue; + const lines = trimmed.split("\n"); + if (lines.length < 3) continue; + const body = lines.slice(3).join("\n").trim(); + const pubDate = getPublicationDate(body); + commits.push({ + hash: lines[0].trim(), + date: lines[1].trim(), + subject: lines[2].trim(), + body, + pubDate, + iso: pubDateToIso(pubDate), + decreto: getDecreeSummary(body), + }); + } + return commits; +} + +/** + * Carga y devuelve los decretos de decretos.json ordenados por número. + * @returns {Array<{numero:number, decreto:string, publicacion:string}>} + */ +function loadDecretos() { + const raw = fs.readFileSync(DECRETOS_PATH, "utf8"); + const decretos = JSON.parse(raw); + decretos.sort((a, b) => a.numero - b.numero); + return decretos; +} + +/* Normaliza un texto para comparar títulos de decretos (ignora mayúsculas, + acentos y espacios/puntuación). */ +function normalizeText(text) { + return String(text || "") + .normalize("NFD") + .replace(/[\u0300-\u036f]/g, "") + .toLowerCase() + .replace(/[^\p{L}\p{N}]+/gu, ""); +} + +/** + * Relaciona los commits de reforma con los decretos de decretos.json y + * devuelve la lista de reformas (una por decreto) en orden cronológico. + * + * El emparejamiento usa primero la fecha de publicación (DOF) y, si la + * fecha no coincide, el título del decreto. Cada commit se asigna a un solo + * decreto; los decretos sin commit quedan con `commits` vacío (faltan en + * git). Se emiten avisos por discrepancias. + * + * @param {Array} decretos + * @param {Array} commits + * @returns {Array<*>} + */ +function buildReformas(decretos, commits) { + const byTitle = new Map(); + for (const d of decretos) { + const k = normalizeText(d.decreto); + if (k && !byTitle.has(k)) byTitle.set(k, d); + } + const byDate = new Map(); + for (const d of decretos) { + const arr = byDate.get(d.publicacion) || []; + arr.push(d); + byDate.set(d.publicacion, arr); + } + + const used = new Set(); + const asignar = (commit, decreto) => { + commit.numero = decreto.numero; + commit.publicacion = decreto.publicacion; + used.add(decreto.numero); + if (decreto.publicacion !== commit.iso) { + console.warn( + `AVISO: el commit ${commit.hash.slice(0, 8)} "${commit.subject}" ` + + `(fecha ${commit.iso}) no coincide con la fecha del decreto ` + + `${decreto.numero} (${decreto.publicacion}).`, + ); + } + }; + + // 1) Emparejamiento principal por fecha de publicación (DOF). + const pendientes = []; + for (const commit of commits) { + const grupo = commit.iso ? byDate.get(commit.iso) : null; + if (grupo) { + const noUsados = grupo.filter((d) => !used.has(d.numero)); + const titulo = normalizeText(commit.decreto); + let d = noUsados.find((x) => normalizeText(x.decreto) === titulo); + if (!d) d = noUsados[0]; + if (d) { + asignar(commit, d); + continue; + } + } + pendientes.push(commit); + } + + // 2) Los commits sin fecha coincidente se vinculan por el título del decreto. + for (const commit of pendientes) { + const d = byTitle.get(normalizeText(commit.decreto)); + if (d && !used.has(d.numero)) { + asignar(commit, d); + } else { + console.warn( + `AVISO: el commit ${commit.hash.slice(0, 8)} "${commit.subject}" ` + + "no corresponde a ningún decreto de decretos.json.", + ); + } + } + + const reformas = decretos.map((d) => ({ + numero: d.numero, + decreto: d.decreto, + publicacion: d.publicacion, + commits: commits.filter((c) => c.numero === d.numero), + })); + + for (const r of reformas) { + if (r.commits.length > 1) { + console.warn( + `AVISO: el decreto ${r.numero} tiene ${r.commits.length} commits asociados.`, + ); + } + } + + return reformas; +} + +/* Fecha de publicación en el DOF, extraída del cuerpo del commit. + El año puede aparecer en la línea siguiente (el renglón de la fecha + se parte en dos), por lo que se unen varias líneas antes de buscar. */ +function getPublicationDate(body) { + const lines = String(body).split("\n"); + const idx = lines.findIndex((line) => + /Publicado\s+en\s+el\s+Diario\s+Oficial/i.test(line), + ); + if (idx < 0) return null; + + const joined = lines.slice(idx, idx + 3).join(" "); + const datePattern = + /(\d{1,2}(?:ro|º)?\.?\s+(?:de\s+)?[A-Za-záéíóúñü]+\s+(?:(?:de|del)\s+)?(?:año\s+)?\d{4})/i; + const match = joined.match(datePattern); + return match ? match[1].trim() : null; +} + +/* Meses en español, igual que rst2html5.py. */ +const SPANISH_MONTHS = { + enero: 1, + febrero: 2, + marzo: 3, + abril: 4, + mayo: 5, + junio: 6, + julio: 7, + agosto: 8, + septiembre: 9, + setiembre: 9, + octubre: 10, + noviembre: 11, + diciembre: 12, +}; + +/* Convierte la fecha de publicación en español a ISO 8601 (YYYY-MM-DD), + imitando pub_date_to_iso de rst2html5.py. Devuelve null si no se puede. */ +function pubDateToIso(pubDate) { + if (!pubDate || pubDate === "Sin fecha") return null; + + const match = String(pubDate) + .trim() + .match( + /^(\d{1,2})(?:[roº]+)?\.?(?:\s+de)?\s+([A-Za-záéíóúñü]+)\s+(?:(?:de|del)\s+)?(?:año\s+)?(\d{3,4})$/i, + ); + if (!match) return null; + + const day = Number(match[1]); + const month = SPANISH_MONTHS[match[2].toLowerCase()]; + const year = Number(match[3]); + + if (!month || day < 1 || day > 31 || year < 1900) return null; + + return `${String(year).padStart(4, "0")}-${String(month).padStart(2, "0")}-${String(day).padStart(2, "0")}`; +} + +/* Resumen del decreto: el párrafo que comienza con DECRETO, REFORMA, + REFORMAS, DECLARATORIA o LEY (el resumen puede ocupar varias líneas). */ +function getDecreeSummary(body) { + const paragraphs = String(body).split(/\n\s*\n/); + for (const paragraph of paragraphs) { + if ( + /^(?:DECRETO|REFORMA|REFORMAS|DECLARATORIA|LEY)\b/i.test(paragraph.trim()) + ) { + return paragraph.trim(); + } + } + return null; +} + +/** Archivos .rst modificados por el commit dado. */ +function getRstFiles(commitHash) { + const output = runGit([ + "diff-tree", + "--no-commit-id", + "-r", + "--name-only", + commitHash, + ]); + return output + .trim() + .split("\n") + .filter((file) => file.endsWith(".rst") && file); +} + +/** Contenido de un archivo en un commit dado (o "" si no existe). */ +function getFileContent(commitHash, filepath) { + try { + return runGit(["show", `${commitHash}:${filepath}`]); + } catch { + return ""; + } +} + +// --------------------------------------------------------------------------- +// Diff generation +// --------------------------------------------------------------------------- + +function buildDiff(parentHash, commitHash, rstFile) { + const oldContent = getFileContent(parentHash, rstFile); + const newContent = getFileContent(commitHash, rstFile); + if (oldContent === newContent) return ""; + + const oldName = `${parentHash.slice(0, 8)}/${rstFile}`; + const newName = `${commitHash.slice(0, 8)}/${rstFile}`; + const patch = createPatch(rstFile, oldContent, newContent, oldName, newName); + + const htmlOutput = diff2html(patch, { + drawFileList: false, + matching: "none", + outputFormat: "line", + diffStyle: "word", + fileContentToggle: false, + }); + + // diff2html envuelve el resultado en un documento; extraemos solo el cuerpo. + const bodyMatch = htmlOutput.match(/([\s\S]*?)<\/body>/); + return bodyMatch ? bodyMatch[1] : htmlOutput; +} + +// --------------------------------------------------------------------------- +// HTML generation +// --------------------------------------------------------------------------- + +function writeIndex(reformas) { + const parts = [ + '\n\n\n', + '\n', + '\n', + `CPEUM — Índice de ${TITLE}\n`, + '\n', + '\n', + '\n', + '\n', + '\n', + '\n', + "\n\n\n", + BANNER, + `

Índice de ${TITLE}

\n`, + ]; + + for (const reforma of [...reformas].reverse()) { + const first = reforma.commits[0]; + const fuente = first ? { subject: first.subject } : null; + + parts.push('
\n'); + + if (fuente) { + parts.push( + '

', + ``, + `${reforma.numero} 🔗. ${htmlEscape(fuente.subject)}`, + ); + const iso = reforma.publicacion; + const datetimeAttr = iso ? ` datetime="${iso}"` : ""; + parts.push( + ` 📅 ${htmlEscape(iso || "")}

\n`, + ); + } else { + // Decretos sin commit: solo el número y el campo "decreto", sin vínculo. + parts.push(`

${reforma.numero}.

\n`); + } + + // En los decretos sin commit solo se muestra el contenido del campo + // "decreto" (sin vínculo). + parts.push( + `

${linkifyDof(htmlEscape(reforma.decreto))}

\n`, + ); + parts.push('
\n'); + parts.push("
\n"); + } + + parts.push("\n\n"); + const filepath = path.join(OUTDIR, "index.html"); + fs.writeFileSync(filepath, parts.join(""), "utf-8"); + console.log(` ${path.relative(process.cwd(), filepath)}`); +} + +function writeReformaFile(reforma, prev, next) { + const filename = `${reforma.numero}.html`; + const parts = []; + const first = reforma.commits[0]; + const title = `CPEUM — ${first.subject}`; + const commitUrl = `${GITHUB_URL}/commit/${first.hash}`; + + parts.push(HTML_HEADER); + parts.push(`${htmlEscape(title)}\n`); + parts.push( + `\n`, + ); + parts.push( + '\n', + ); + parts.push('\n'); + parts.push( + `\n`, + ); + parts.push( + '\n', + ); + parts.push( + '\n\n\n', + ); + parts.push(BANNER); + + // Navegación cronológica: anterior / siguiente (solo decretos con commit). + parts.push('\n"); + + parts.push(`

${htmlEscape(first.subject)}

\n`); + parts.push(`

Decreto ${reforma.numero}

\n`); + parts.push( + '

', + ``, + "Ver commit en GitHub", + " — ", + htmlEscape(first.date), + "

\n", + ); + if (first.body) { + parts.push( + `
${linkifyDof(htmlEscape(first.body))}
\n`, + ); + } + + for (const commit of reforma.commits) { + const rstFiles = getRstFiles(commit.hash); + const parent = `${commit.hash}~1`; + + for (const rstFile of rstFiles) { + const diffHtml = buildDiff(parent, commit.hash, rstFile); + if (diffHtml) { + parts.push(diffHtml); + parts.push("\n"); + } else { + parts.push(`

Sin cambios en ${htmlEscape(rstFile)}.

\n`); + } + } + } + + parts.push(FOOTER); + const filepath = path.join(OUTDIR, filename); + fs.writeFileSync(filepath, parts.join(""), "utf-8"); + console.log(` ${path.relative(process.cwd(), filepath)}`); +} + +// --------------------------------------------------------------------------- +// Main +// --------------------------------------------------------------------------- + +function generate() { + fs.mkdirSync(OUTDIR, { recursive: true }); + const decretos = loadDecretos(); + const commits = getReformaCommits(); + const reformas = buildReformas(decretos, commits); + const conCommit = reformas.filter((r) => r.commits.length > 0); + const sinCommit = reformas.filter((r) => r.commits.length === 0); + + console.log( + `${reformas.length} decretos (${conCommit.length} con commit, ${sinCommit.length} sin).`, + ); + writeIndex(reformas); + for (let i = 0; i < conCommit.length; i++) { + writeReformaFile(conCommit[i], conCommit[i - 1], conCommit[i + 1]); + } + console.log(`\nGenerado en: ${OUTDIR}`); +} + +try { + generate(); +} catch (error) { + console.error("Error:", error.message); + process.exit(1); +} From 1cd87762600ff24f2baa6f130bf05d2cd3fab798 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=ADctor=20Manuel=20J=C3=A1quez=20Leal?= Date: Sat, 1 Aug 2026 09:58:56 +0200 Subject: [PATCH 06/11] =?UTF-8?q?scripts:=20rst2html5.py:=20enlazar=20el?= =?UTF-8?q?=20=C3=ADndice=20de=20reformas=20en=20el=20banner?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- scripts/rst2html5.py | 1 + 1 file changed, 1 insertion(+) diff --git a/scripts/rst2html5.py b/scripts/rst2html5.py index f75e2ab5..de1b3644 100755 --- a/scripts/rst2html5.py +++ b/scripts/rst2html5.py @@ -340,6 +340,7 @@ def visit_document(self, node) -> None: "\n" '\n' '