primer commit
This commit is contained in:
@@ -0,0 +1,94 @@
|
||||
# scripts/build-manual.js
|
||||
|
||||
Genera el PDF del manual a partir de `templates/manual.html` usando Playwright (Chromium headless).
|
||||
|
||||
## Requisitos
|
||||
|
||||
- Node.js 18 o superior.
|
||||
- Playwright instalado:
|
||||
|
||||
```bash
|
||||
cd scripts
|
||||
npm install
|
||||
npx playwright install chromium
|
||||
```
|
||||
|
||||
Si ya tienes la skill `playwright-skill` de lackeyjb, no necesitas instalar Chromium dos veces: la skill lo trae.
|
||||
|
||||
## Uso básico
|
||||
|
||||
Desde la raíz de la skill:
|
||||
|
||||
```bash
|
||||
node scripts/build-manual.js
|
||||
```
|
||||
|
||||
Esto:
|
||||
|
||||
1. Lee `templates/manual.html`.
|
||||
2. Carga `templates/manual.css` y `templates/manual.js`.
|
||||
3. Copia `artifacts/` a un directorio temporal (si existe).
|
||||
4. Renderiza el HTML con Chromium y exporta a `dist/manual-{aplicacion}-v{version}.pdf`.
|
||||
|
||||
## Opciones
|
||||
|
||||
```text
|
||||
--html <ruta> HTML fuente (por defecto templates/manual.html).
|
||||
--out <archivo> Ruta exacta del PDF de salida.
|
||||
--out-dir <carpeta> Carpeta de salida (por defecto dist/).
|
||||
--no-background No imprime colores de fondo.
|
||||
--landscape Orientación horizontal.
|
||||
--margin-{top,bottom,left,right} <valor>
|
||||
Margen CSS cuando --no-css-page-size está activo.
|
||||
--no-css-page-size Ignora @page y usa los márgenes dados.
|
||||
--show-header-footer Habilita la cabecera/pie nativa de Chromium.
|
||||
-h, --help Ayuda.
|
||||
```
|
||||
|
||||
### Ejemplos
|
||||
|
||||
Generar el PDF por defecto:
|
||||
|
||||
```bash
|
||||
node scripts/build-manual.js
|
||||
```
|
||||
|
||||
Generar otro manual (por ejemplo, el manual de administración):
|
||||
|
||||
```bash
|
||||
node scripts/build-manual.js --html templates/admin.html --out dist/manual-admin-v1.0.0.pdf
|
||||
```
|
||||
|
||||
Sobrescribir el archivo de salida en una ruta absoluta:
|
||||
|
||||
```bash
|
||||
node scripts/build-manual.js --out /tmp/borrador.pdf
|
||||
```
|
||||
|
||||
## Estructura esperada del proyecto
|
||||
|
||||
```
|
||||
manuales-factorpesca/
|
||||
├── SKILL.md
|
||||
├── references/
|
||||
├── templates/
|
||||
│ ├── manual.html # fuente
|
||||
│ ├── manual.css # estilos pantalla + impresión
|
||||
│ └── manual.js # lógica de presentación
|
||||
├── scripts/
|
||||
│ ├── build-manual.js # este script
|
||||
│ └── package.json
|
||||
├── artifacts/ # capturas de pantalla (opcional)
|
||||
│ ├── 01-login.png
|
||||
│ └── 02-panel.png
|
||||
└── dist/ # PDFs generados (se crea al ejecutar)
|
||||
```
|
||||
|
||||
## Solución de problemas
|
||||
|
||||
- **"No se encuentra 'playwright'"**: ejecuta `npm install` en `scripts/`.
|
||||
- **"Chromium no encontrado"**: ejecuta `npx playwright install chromium`.
|
||||
- **El PDF sale sin color**: revisa `references/estilo.md` y asegúrate de que la regla `-webkit-print-color-adjust: exact` sigue presente en `manual.css`.
|
||||
- **El índice está vacío**: el HTML no contiene elementos con `data-chapter` o los `h1` están fuera de `[data-chapters]`.
|
||||
- **Las anotaciones aparecen en otra posición**: comprueba que `data-annotations` use porcentajes (`x`, `y` de 0 a 100) y que la imagen ocupe todo el ancho de `.screenshot-frame`.
|
||||
- **La cabecera del PDF no muestra el capítulo**: Chromium necesita que el texto se asigne vía `string(chapter-title)` Paged Media. Si no se ve, asegúrate de que la versión de Chromium es reciente (Playwright 1.40+).
|
||||
@@ -0,0 +1,218 @@
|
||||
#!/usr/bin/env node
|
||||
/* =========================================================
|
||||
manuales-factorpesca / scripts/build-manual.js
|
||||
Convierte templates/manual.html en un PDF Letter profesional.
|
||||
|
||||
Uso:
|
||||
node scripts/build-manual.js
|
||||
node scripts/build-manual.js --html ruta/al/manual.html
|
||||
node scripts/build-manual.js --out dist/manual-v1.0.0.pdf
|
||||
node scripts/build-manual.js --slug capturas-app
|
||||
|
||||
Por defecto busca el HTML en templates/manual.html y escribe
|
||||
el PDF en dist/manual.pdf.
|
||||
========================================================= */
|
||||
|
||||
'use strict';
|
||||
|
||||
const path = require('node:path');
|
||||
const fs = require('node:fs');
|
||||
const os = require('node:os');
|
||||
|
||||
const SKILL_DIR = path.resolve(__dirname, '..');
|
||||
const DEFAULT_HTML = path.join(SKILL_DIR, 'templates', 'manual.html');
|
||||
const DEFAULT_OUT_DIR = path.join(SKILL_DIR, 'dist');
|
||||
const ARTIFACTS_DIR = path.join(SKILL_DIR, 'artifacts');
|
||||
|
||||
function parseArgs(argv) {
|
||||
const out = {
|
||||
html: DEFAULT_HTML,
|
||||
outDir: DEFAULT_OUT_DIR,
|
||||
outFile: null,
|
||||
printBackground: true,
|
||||
landscape: false,
|
||||
marginTop: '0',
|
||||
marginBottom: '0',
|
||||
marginLeft: '0',
|
||||
marginRight: '0',
|
||||
preferCSSPageSize: true,
|
||||
displayHeaderFooter: false,
|
||||
};
|
||||
|
||||
for (let i = 2; i < argv.length; i++) {
|
||||
const a = argv[i];
|
||||
switch (a) {
|
||||
case '--html': out.html = path.resolve(argv[++i]); break;
|
||||
case '--out': out.outFile = path.resolve(argv[++i]); break;
|
||||
case '--out-dir': out.outDir = path.resolve(argv[++i]); break;
|
||||
case '--no-background': out.printBackground = false; break;
|
||||
case '--landscape': out.landscape = true; break;
|
||||
case '--margin-top': out.marginTop = argv[++i]; break;
|
||||
case '--margin-bottom': out.marginBottom = argv[++i]; break;
|
||||
case '--margin-left': out.marginLeft = argv[++i]; break;
|
||||
case '--margin-right': out.marginRight = argv[++i]; break;
|
||||
case '--no-css-page-size': out.preferCSSPageSize = false; break;
|
||||
case '--show-header-footer': out.displayHeaderFooter = true; break;
|
||||
case '--help':
|
||||
case '-h':
|
||||
printHelp();
|
||||
process.exit(0);
|
||||
break;
|
||||
default:
|
||||
console.warn(`[build-manual] argumento desconocido ignorado: ${a}`);
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function printHelp() {
|
||||
console.log(`Uso: node build-manual.js [opciones]
|
||||
|
||||
Opciones:
|
||||
--html <ruta> Ruta al HTML fuente. Por defecto templates/manual.html
|
||||
--out <archivo> Ruta completa del PDF de salida.
|
||||
--out-dir <carpeta> Carpeta de salida. Por defecto dist/.
|
||||
--no-background No imprimir colores de fondo.
|
||||
--landscape Orientación horizontal.
|
||||
--margin-top <valor> Margen superior CSS (p. ej. 22mm).
|
||||
--margin-bottom <valor> Margen inferior CSS.
|
||||
--margin-left <valor> Margen izquierdo CSS.
|
||||
--margin-right <valor> Margen derecho CSS.
|
||||
--no-css-page-size Ignorar @page del CSS y usar los márgenes dados.
|
||||
--show-header-footer Habilita la cabecera/pie de Chromium (no usado).
|
||||
-h, --help Muestra esta ayuda.
|
||||
`);
|
||||
}
|
||||
|
||||
function ensurePlaywright() {
|
||||
try {
|
||||
return require('playwright');
|
||||
} catch (err) {
|
||||
console.error(`[build-manual] No se encontró 'playwright' en el PATH.
|
||||
Instálalo ejecutando:
|
||||
npm install playwright
|
||||
o usa la skill playwright-skill de lackeyjb para preparar el entorno:
|
||||
npx skills add lackeyjb/playwright-skill --skill playwright-skill --yes
|
||||
`);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
function buildPdfUrl(htmlPath) {
|
||||
const abs = path.resolve(htmlPath);
|
||||
if (!fs.existsSync(abs)) {
|
||||
console.error(`[build-manual] No se encuentra el HTML: ${abs}`);
|
||||
process.exit(1);
|
||||
}
|
||||
return 'file://' + abs;
|
||||
}
|
||||
|
||||
function copyArtifactsIntoTemp(htmlPath) {
|
||||
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'manual-build-'));
|
||||
const htmlDst = path.join(tmp, path.basename(htmlPath));
|
||||
fs.copyFileSync(htmlPath, htmlDst);
|
||||
const cssSrc = path.join(path.dirname(htmlPath), 'manual.css');
|
||||
const jsSrc = path.join(path.dirname(htmlPath), 'manual.js');
|
||||
if (fs.existsSync(cssSrc)) fs.copyFileSync(cssSrc, path.join(tmp, 'manual.css'));
|
||||
if (fs.existsSync(jsSrc)) fs.copyFileSync(jsSrc, path.join(tmp, 'manual.js'));
|
||||
if (fs.existsSync(ARTIFACTS_DIR)) {
|
||||
copyDir(ARTIFACTS_DIR, path.join(tmp, 'artifacts'));
|
||||
console.log(`[build-manual] Capturas copiadas a ${path.join(tmp, 'artifacts')}`);
|
||||
}
|
||||
return tmp;
|
||||
}
|
||||
|
||||
function copyDir(src, dst) {
|
||||
if (!fs.existsSync(src)) return;
|
||||
fs.mkdirSync(dst, { recursive: true });
|
||||
for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
|
||||
const s = path.join(src, entry.name);
|
||||
const d = path.join(dst, entry.name);
|
||||
if (entry.isDirectory()) copyDir(s, d);
|
||||
else fs.copyFileSync(s, d);
|
||||
}
|
||||
}
|
||||
|
||||
function defaultOutFile(htmlPath) {
|
||||
const html = fs.readFileSync(htmlPath, 'utf8');
|
||||
const base = path.basename(htmlPath, path.extname(htmlPath));
|
||||
const versionMatch = html.match(/data-meta-version[^>]*>([^<]+)/);
|
||||
const version = (versionMatch ? versionMatch[1].trim() : '0.0.0').replace(/^v/i, '').replace(/[^a-zA-Z0-9._-]/g, '') || '0.0.0';
|
||||
const slug = base.replace(/^manual[-_]?/, '').replace(/[^a-zA-Z0-9._-]/g, '') || 'app';
|
||||
return `manual-${slug}-v${version}.pdf`;
|
||||
}
|
||||
|
||||
async function waitForFonts(page) {
|
||||
try {
|
||||
await page.evaluate(() => document.fonts && document.fonts.ready);
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const args = parseArgs(process.argv);
|
||||
const playwright = ensurePlaywright();
|
||||
const { chromium } = playwright;
|
||||
|
||||
if (!fs.existsSync(args.html)) {
|
||||
console.error(`[build-manual] HTML no encontrado: ${args.html}`);
|
||||
process.exit(1);
|
||||
}
|
||||
fs.mkdirSync(args.outDir, { recursive: true });
|
||||
|
||||
const outFile = args.outFile || path.join(args.outDir, defaultOutFile(args.html));
|
||||
const assetsTmp = copyArtifactsIntoTemp(args.html);
|
||||
const url = 'file://' + path.join(assetsTmp, path.basename(args.html));
|
||||
|
||||
console.log(`[build-manual] Renderizando: ${args.html}`);
|
||||
console.log(`[build-manual] Salida: ${outFile}`);
|
||||
|
||||
const browser = await chromium.launch({
|
||||
headless: true,
|
||||
args: ['--no-sandbox', '--disable-dev-shm-usage'],
|
||||
});
|
||||
try {
|
||||
const context = await browser.newContext({
|
||||
viewport: { width: 1100, height: 1400 },
|
||||
deviceScaleFactor: 2,
|
||||
});
|
||||
const page = await context.newPage();
|
||||
page.on('pageerror', (e) => console.error('[page error]', e));
|
||||
page.on('console', (msg) => {
|
||||
if (msg.type() === 'error') console.error('[page console]', msg.text());
|
||||
});
|
||||
|
||||
await page.goto(url, { waitUntil: 'networkidle', timeout: 60_000 });
|
||||
await waitForFonts(page);
|
||||
await page.waitForTimeout(400);
|
||||
|
||||
const pdfOptions = {
|
||||
path: outFile,
|
||||
printBackground: args.printBackground,
|
||||
preferCSSPageSize: args.preferCSSPageSize,
|
||||
displayHeaderFooter: args.displayHeaderFooter,
|
||||
};
|
||||
if (!args.preferCSSPageSize) {
|
||||
pdfOptions.format = args.landscape ? { width: '11in', height: '8.5in' } : { width: '8.5in', height: '11in' };
|
||||
pdfOptions.margin = {
|
||||
top: args.marginTop,
|
||||
bottom: args.marginBottom,
|
||||
left: args.marginLeft,
|
||||
right: args.marginRight,
|
||||
};
|
||||
}
|
||||
|
||||
await page.pdf(pdfOptions);
|
||||
const stat = fs.statSync(outFile);
|
||||
console.log(`[build-manual] PDF generado: ${outFile} (${(stat.size / 1024).toFixed(1)} KB)`);
|
||||
} finally {
|
||||
await browser.close();
|
||||
if (assetsTmp) {
|
||||
try { fs.rmSync(assetsTmp, { recursive: true, force: true }); } catch (_) {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
console.error('[build-manual] Error inesperado:', err);
|
||||
process.exit(1);
|
||||
});
|
||||
Generated
+46
@@ -0,0 +1,46 @@
|
||||
{
|
||||
"name": "manuales-factorpesca-build",
|
||||
"version": "1.0.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "manuales-factorpesca-build",
|
||||
"version": "1.0.0",
|
||||
"license": "UNLICENSED",
|
||||
"dependencies": {
|
||||
"playwright": "^1.62.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/playwright": {
|
||||
"version": "1.64.0",
|
||||
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.64.0.tgz",
|
||||
"integrity": "sha512-kVzTnYFYEyQ9RGzBjNCd2+yEqWmVbxl3AHPynv3qQ6JTjv4a+kwRcnqQwIpuP3NnrnIrE/Q6wWAIAvMfIE2/Yg==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"playwright-core": "1.64.0"
|
||||
},
|
||||
"bin": {
|
||||
"playwright": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
}
|
||||
},
|
||||
"node_modules/playwright-core": {
|
||||
"version": "1.64.0",
|
||||
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.64.0.tgz",
|
||||
"integrity": "sha512-T9r+MZkTECl2+oUcZ26YgZzTrYidTsIhYYkJix+6iDOymOb1FB2RLLSOxRGnZGv5DStt+aduIVbt+E8WygXYpg==",
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"playwright-core": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"name": "manuales-factorpesca-build",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"description": "Generador de PDF para manuales FactorPesca. Usa Playwright (Chromium) para renderizar templates/manual.html con su CSS de impresión.",
|
||||
"main": "build-manual.js",
|
||||
"scripts": {
|
||||
"setup": "npm install",
|
||||
"build": "node build-manual.js",
|
||||
"build:html": "node build-manual.js --html",
|
||||
"build:out": "node build-manual.js --out"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"dependencies": {
|
||||
"playwright": "^1.62.0"
|
||||
},
|
||||
"keywords": [
|
||||
"factorpesca",
|
||||
"manual",
|
||||
"pdf",
|
||||
"playwright",
|
||||
"documentation"
|
||||
],
|
||||
"license": "UNLICENSED"
|
||||
}
|
||||
Reference in New Issue
Block a user