docuguru 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
docuguru/__init__.py ADDED
@@ -0,0 +1,4 @@
1
+ from docuguru.cli import app
2
+
3
+ if __name__ == "__main__":
4
+ app()
docuguru/cli.py ADDED
@@ -0,0 +1,280 @@
1
+ import os
2
+ from pathlib import Path
3
+ from typing import Annotated, Optional
4
+
5
+ from typer import Argument, Option, Typer
6
+
7
+ from docuguru.default import DEFAULT_CSS
8
+ from docuguru.html_to_pdf import HTMLToPDF
9
+ from docuguru.markdon_to_html import MarkdownToHTML
10
+
11
+ app = Typer(help="Herramienta para convertir documentos Markdown a PDF.")
12
+
13
+
14
+ @app.command(name="convert")
15
+ def convert(
16
+ markdown_file: str = Argument(help="Ruta al archivo Markdown a convertir."),
17
+ output: Optional[str] = Option(
18
+ None,
19
+ "--output",
20
+ "-o",
21
+ help="Ruta del archivo PDF de salida. Si no se especifica, se usa el nombre del archivo Markdown con extensión .pdf",
22
+ ),
23
+ title: Optional[str] = Option(
24
+ None,
25
+ "--title",
26
+ "-t",
27
+ help="Título del documento. Si no se especifica, se extrae del primer h1 o se usa el nombre del archivo.",
28
+ ),
29
+ no_cover: bool = Option(
30
+ False,
31
+ "--no-cover",
32
+ help="No incluir página de portada en el PDF.",
33
+ ),
34
+ badge: Optional[str] = Option(
35
+ None,
36
+ "--badge",
37
+ "-b",
38
+ help="Texto del badge en la portada. Por defecto: 'Propuesta técnica'.",
39
+ ),
40
+ date: Optional[str] = Option(
41
+ None,
42
+ "--date",
43
+ "-d",
44
+ help="Fecha para la portada (ej: 'Diciembre 2025'). Si no se especifica, se usa la fecha actual.",
45
+ ),
46
+ ):
47
+ """
48
+ Convierte un archivo Markdown a PDF.
49
+
50
+ Soporta títulos (h1-h6), tablas y listas ordenadas/no ordenadas.
51
+ Genera un PDF con estilos predefinidos e incluye una página de portada por defecto.
52
+ """
53
+ markdown_to_html = MarkdownToHTML()
54
+ body_html = markdown_to_html.convert_file(markdown_file)
55
+
56
+ if title is None:
57
+ title = _extract_title_from_html(body_html) or Path(markdown_file).stem
58
+
59
+ if output is None:
60
+ output = str(Path(markdown_file).with_suffix(".pdf"))
61
+
62
+ html_to_pdf = HTMLToPDF()
63
+ html_to_pdf.generate_pdf(
64
+ body_html,
65
+ title,
66
+ DEFAULT_CSS,
67
+ output,
68
+ include_cover=not no_cover,
69
+ cover_badge=badge if badge else "Propuesta técnica",
70
+ cover_date=date,
71
+ )
72
+ print(f"PDF generado exitosamente: {output}")
73
+
74
+
75
+ @app.command(name="blocks")
76
+ def list_blocks(
77
+ output_file: Optional[str] = Option(
78
+ None,
79
+ "--output",
80
+ "-o",
81
+ help="Archivo donde guardar la documentación de bloques. Si no se especifica, se imprime en stdout.",
82
+ ),
83
+ ):
84
+ """
85
+ Muestra la documentación de todos los bloques HTML personalizados disponibles.
86
+
87
+ Lista todos los componentes custom con ejemplos de uso y explicación.
88
+ """
89
+ blocks_doc = _generate_blocks_documentation()
90
+
91
+ if output_file:
92
+ with open(output_file, "w", encoding="utf-8") as f:
93
+ f.write(blocks_doc)
94
+ print(f"Documentación guardada en: {output_file}")
95
+ else:
96
+ print(blocks_doc)
97
+
98
+
99
+ def _generate_blocks_documentation() -> str:
100
+ return """# Bloques HTML Personalizados Disponibles
101
+
102
+ ## 1. Architecture Diagram
103
+
104
+ Muestra un diagrama de arquitectura centrado.
105
+
106
+ **Uso:**
107
+ ```html
108
+ <div class="architecture-diagram">
109
+ <img src="ruta/imagen.png" alt="Descripción" />
110
+ </div>
111
+ ```
112
+
113
+ ---
114
+
115
+ ## 2. Info Card
116
+
117
+ Tarjeta informativa con bordes y sombras, ideal para fases o información destacada.
118
+
119
+ **Uso:**
120
+ ```html
121
+ <div class="info-card">
122
+ <h3>Título</h3>
123
+ <p><strong>Subtítulo en negrita</strong></p>
124
+ <p>Información adicional</p>
125
+ <p>Más detalles</p>
126
+ </div>
127
+ ```
128
+
129
+ ---
130
+
131
+ ## 3. Timeline
132
+
133
+ Línea de tiempo vertical con items conectados.
134
+
135
+ **Uso:**
136
+ ```html
137
+ <div class="timeline">
138
+ <div class="timeline-item">
139
+ <strong>Título del Hito</strong>
140
+ <p>Descripción del hito o fase</p>
141
+ </div>
142
+ <div class="timeline-item">
143
+ <strong>Siguiente Hito</strong>
144
+ <p>Otra descripción</p>
145
+ </div>
146
+ </div>
147
+ ```
148
+
149
+ ---
150
+
151
+ ## 4. Summary Cards
152
+
153
+ Tarjetas de resumen horizontales con valores destacados.
154
+
155
+ **Uso:**
156
+ ```html
157
+ <div class="summary-cards">
158
+ <div class="summary-card">
159
+ <h3>Categoría</h3>
160
+ <div class="value">$1.000.000</div>
161
+ <div class="label">periodo</div>
162
+ </div>
163
+ <div class="summary-card">
164
+ <h3>Otra Categoría</h3>
165
+ <div class="value">500</div>
166
+ <div class="label">unidades</div>
167
+ </div>
168
+ </div>
169
+ ```
170
+
171
+ ---
172
+
173
+ ## 5. Support Packages
174
+
175
+ Tarjetas de paquetes de servicios lado a lado.
176
+
177
+ **Uso:**
178
+ ```html
179
+ <div class="support-packages">
180
+ <div class="package-card">
181
+ <div class="package-header">
182
+ <h3 class="package-name">Básico</h3>
183
+ <div class="package-price">$500.000</div>
184
+ <div class="package-price-label">mensual</div>
185
+ </div>
186
+ <ul class="package-features">
187
+ <li class="no-style">Feature 1</li>
188
+ <li class="no-style">Feature 2</li>
189
+ </ul>
190
+ </div>
191
+
192
+ <div class="package-card featured">
193
+ <div class="package-header">
194
+ <span class="package-badge">Recomendado</span>
195
+ <h3 class="package-name">Premium</h3>
196
+ <div class="package-price">$1.200.000</div>
197
+ <div class="package-price-label">mensual</div>
198
+ </div>
199
+ <ul class="package-features">
200
+ <li class="no-style">Feature premium 1</li>
201
+ <li class="no-style">Feature premium 2</li>
202
+ </ul>
203
+ </div>
204
+ </div>
205
+ ```
206
+
207
+ **Nota:** Usa `class="featured"` para destacar un paquete.
208
+
209
+ ---
210
+
211
+ ## 6. Styled List
212
+
213
+ Lista con checkmarks personalizados (✔).
214
+
215
+ **Uso:**
216
+ ```html
217
+ <ul class="styled-list">
218
+ <li>Item 1 con checkmark</li>
219
+ <li>Item 2 con checkmark</li>
220
+ <li>Item 3 con checkmark</li>
221
+ </ul>
222
+ ```
223
+
224
+ ---
225
+
226
+ ## 7. Warranty Notice
227
+
228
+ Bloque de aviso o garantía destacado con fondo azul claro.
229
+
230
+ **Uso:**
231
+ ```html
232
+ <div class="warranty-notice">
233
+ <p><strong>Título del Aviso:</strong> Texto del aviso o garantía aquí.</p>
234
+ </div>
235
+ ```
236
+
237
+ ---
238
+
239
+ ## Características Adicionales
240
+
241
+ ### Listas Ordenadas Estilizadas
242
+
243
+ Las listas ordenadas (`<ol>`) se muestran con números en círculos azules automáticamente.
244
+
245
+ **Para desactivar el estilo:**
246
+ ```html
247
+ <ol class="no-style">
248
+ <li>Item con numeración estándar</li>
249
+ <li>Otro item sin estilización</li>
250
+ </ol>
251
+ ```
252
+
253
+ ### Listas dentro de Package Features
254
+
255
+ Usa `class="no-style"` en cada `<li>` dentro de `package-features` para evitar los checkmarks:
256
+
257
+ ```html
258
+ <ul class="package-features">
259
+ <li class="no-style">Sin checkmark</li>
260
+ </ul>
261
+ ```
262
+
263
+ ---
264
+
265
+ ## Notas Importantes
266
+
267
+ 1. Todos los bloques respetan `page-break-inside: avoid` para evitar cortes en medio del contenido.
268
+ 2. Los colores utilizan variables CSS definidas en el tema (--primary, --accent, --accent-dark, etc.).
269
+ 3. Los gradientes usan la paleta azul del tema (#7dd3fc, #3b82f6).
270
+ """
271
+
272
+
273
+ def _extract_title_from_html(html: str) -> str:
274
+ import re
275
+
276
+ match = re.search(r"<h1[^>]*>(.*?)</h1>", html, re.IGNORECASE | re.DOTALL)
277
+ if match:
278
+ title_text = re.sub(r"<[^>]+>", "", match.group(1))
279
+ return title_text.strip()
280
+ return None
docuguru/default.py ADDED
@@ -0,0 +1,650 @@
1
+ DEFAULT_CSS = """@page {
2
+ size: A4;
3
+ margin: 2.5cm 2cm;
4
+
5
+ @bottom-center {
6
+ content: element(footer);
7
+ }
8
+ }
9
+
10
+ @font-face {
11
+ font-family: 'Inter';
12
+ src: url('fonts/Inter-Regular.ttf');
13
+ font-weight: 400;
14
+ }
15
+
16
+ @font-face {
17
+ font-family: 'Inter';
18
+ src: url('fonts/Inter-Medium.ttf');
19
+ font-weight: 500;
20
+ }
21
+
22
+ @font-face {
23
+ font-family: 'Inter';
24
+ src: url('fonts/Inter-SemiBold.ttf');
25
+ font-weight: 600;
26
+ }
27
+
28
+ @font-face {
29
+ font-family: 'Inter';
30
+ src: url('fonts/Inter-Bold.ttf');
31
+ font-weight: 700;
32
+ }
33
+
34
+ :root {
35
+ --primary: #1e293b;
36
+ --secondary: #64748b;
37
+ --accent: #7dd3fc;
38
+ --accent-dark: #3b82f6;
39
+ --surface: #f8fafc;
40
+ --border: #e2e8f0;
41
+ --muted: #94a3b8;
42
+ }
43
+
44
+ * {
45
+ box-sizing: border-box;
46
+ }
47
+
48
+ body {
49
+ margin: 0;
50
+ font-family: 'Inter', sans-serif;
51
+ font-size: 14px;
52
+ line-height: 1.7;
53
+ color: var(--primary);
54
+ }
55
+
56
+ .header {
57
+ margin: -2.5cm -2cm 0 -2cm;
58
+ padding: 2cm;
59
+ background: linear-gradient(135deg, #0a0f1a, #0f172a, #1e293b);
60
+ color: #ffffff;
61
+ position: relative;
62
+ overflow: hidden;
63
+ }
64
+
65
+ .header-content {
66
+ position: relative;
67
+ z-index: 1;
68
+ display: flex;
69
+ gap: 32px;
70
+ }
71
+
72
+ .header-logo {
73
+ width: 110px;
74
+ height: 110px;
75
+ background: #fff;
76
+ border-radius: 20px;
77
+ padding: 12px;
78
+ box-shadow: 0 8px 24px rgba(0, 0, 0, .3);
79
+ }
80
+
81
+ .header-logo img {
82
+ width: 100%;
83
+ height: 100%;
84
+ object-fit: contain;
85
+ }
86
+
87
+ h1 {
88
+ font-size: 32px;
89
+ margin: 0;
90
+ font-weight: 700;
91
+ }
92
+
93
+ .content h1 {
94
+ margin: 48px 0 20px;
95
+ position: relative;
96
+ padding-bottom: 12px;
97
+ }
98
+
99
+ .content h1::after {
100
+ content: '';
101
+ position: absolute;
102
+ bottom: 0;
103
+ left: 0;
104
+ right: 0;
105
+ width: 100%;
106
+ height: 4px;
107
+ background: linear-gradient(90deg, #7dd3fc, #3b82f6);
108
+ border-radius: 2px;
109
+ }
110
+
111
+ .badge {
112
+ display: inline-block;
113
+ margin-top: 12px;
114
+ padding: 6px 14px;
115
+ font-size: 11px;
116
+ font-weight: 600;
117
+ text-transform: uppercase;
118
+ color: #7dd3fc;
119
+ background: rgba(59, 130, 246, .2);
120
+ border-radius: 6px;
121
+ border: 1px solid rgba(125, 211, 252, .3);
122
+ }
123
+
124
+ .header-date {
125
+ margin-top: 20px;
126
+ font-size: 13px;
127
+ color: #94a3b8;
128
+ }
129
+
130
+ .content {
131
+ margin-top: 30px;
132
+ }
133
+
134
+ .toc {
135
+ margin: 28px 0 36px;
136
+ padding: 24px;
137
+ background: white;
138
+ border: 1px solid var(--border);
139
+ border-radius: 12px;
140
+ box-shadow: 0 4px 16px rgba(59, 130, 246, .12), 0 0 0 1px rgba(125, 211, 252, .18);
141
+ page-break-inside: avoid;
142
+ break-inside: avoid;
143
+ }
144
+
145
+ .toc-heading {
146
+ margin: 0 0 12px;
147
+ }
148
+
149
+ .toc-list {
150
+ margin: 0;
151
+ padding-left: 0;
152
+ list-style: none;
153
+ }
154
+
155
+ .toc-list li {
156
+ margin: 10px 0;
157
+ padding-left: 0 !important;
158
+ }
159
+
160
+ .toc-list a {
161
+ display: block;
162
+ color: var(--secondary);
163
+ text-decoration: none;
164
+ font-weight: 600;
165
+ }
166
+
167
+ .toc-list a::after {
168
+ content: leader('.') target-counter(attr(href), page);
169
+ color: var(--muted);
170
+ font-weight: 500;
171
+ }
172
+
173
+ .architecture-summary {
174
+ page-break-inside: avoid;
175
+ break-inside: avoid;
176
+ }
177
+
178
+ .architecture-diagram {
179
+ margin: 24px 0;
180
+ page-break-inside: avoid;
181
+ break-inside: avoid;
182
+ text-align: center;
183
+ }
184
+
185
+ .architecture-diagram img {
186
+ width: 100%;
187
+ max-width: 100%;
188
+ height: auto;
189
+ display: block;
190
+ margin: 0 auto;
191
+ }
192
+
193
+ h2 {
194
+ font-size: 24px;
195
+ margin: 48px 0 20px;
196
+ font-weight: 700;
197
+ position: relative;
198
+ padding-bottom: 12px;
199
+ }
200
+
201
+ h2::after {
202
+ content: '';
203
+ position: absolute;
204
+ bottom: 0;
205
+ left: 0;
206
+ width: 60px;
207
+ height: 4px;
208
+ background: linear-gradient(90deg, #7dd3fc, #3b82f6);
209
+ border-radius: 2px;
210
+ }
211
+
212
+ h3 {
213
+ font-size: 18px;
214
+ margin: 32px 0 16px;
215
+ font-weight: 700;
216
+ color: var(--primary);
217
+ page-break-after: avoid;
218
+ break-after: avoid;
219
+ }
220
+
221
+ h4 {
222
+ font-size: 16px;
223
+ margin: 24px 0 12px;
224
+ font-weight: 600;
225
+ color: var(--primary);
226
+ }
227
+
228
+ p {
229
+ color: var(--secondary);
230
+ }
231
+
232
+ table {
233
+ width: 100%;
234
+ margin-top: 24px;
235
+ border-collapse: separate;
236
+ border-spacing: 0;
237
+ border: 1px solid var(--border);
238
+ border-radius: 12px;
239
+ overflow: hidden;
240
+ page-break-inside: avoid;
241
+ break-inside: avoid;
242
+ }
243
+
244
+ thead {
245
+ display: table-header-group;
246
+ }
247
+
248
+ tbody {
249
+ display: table-row-group;
250
+ }
251
+
252
+ tr {
253
+ page-break-inside: avoid;
254
+ break-inside: avoid;
255
+ }
256
+
257
+ th,
258
+ td {
259
+ padding: 14px 16px;
260
+ font-size: 13px;
261
+ }
262
+
263
+ thead {
264
+ background: linear-gradient(135deg, #0f172a, #1e293b);
265
+ }
266
+
267
+ thead th {
268
+ color: white;
269
+ font-size: 11px;
270
+ text-transform: uppercase;
271
+ }
272
+
273
+ tbody tr:nth-child(even) {
274
+ background: var(--surface);
275
+ }
276
+
277
+ .timeline {
278
+ margin-top: 28px;
279
+ padding-left: 30px;
280
+ border-left: 3px solid #e0f2fe;
281
+ page-break-inside: avoid;
282
+ break-inside: avoid;
283
+ }
284
+
285
+ .timeline-item {
286
+ margin-bottom: 26px;
287
+ position: relative;
288
+ }
289
+
290
+ .timeline-item::before {
291
+ content: '';
292
+ position: absolute;
293
+ left: -38px;
294
+ top: 4px;
295
+ width: 14px;
296
+ height: 14px;
297
+ background: #3b82f6;
298
+ border-radius: 50%;
299
+ border: 3px solid white;
300
+ }
301
+
302
+ .timeline-item strong {
303
+ display: block;
304
+ margin-bottom: 4px;
305
+ color: var(--primary);
306
+ }
307
+
308
+ .timeline-item p {
309
+ margin: 0;
310
+ color: var(--secondary);
311
+ }
312
+
313
+ ul {
314
+ margin-top: 16px;
315
+ padding-left: 0;
316
+ list-style: none;
317
+ }
318
+
319
+ ul li {
320
+ padding-left: 28px;
321
+ margin-bottom: 10px;
322
+ position: relative;
323
+ color: var(--secondary);
324
+ }
325
+
326
+ ul li::before {
327
+ content: '✔';
328
+ position: absolute;
329
+ left: 0;
330
+ color: var(--accent-dark);
331
+ font-weight: 700;
332
+ }
333
+
334
+ ul li.no-style::before,
335
+ .package-features li::before,
336
+ .toc-list li::before {
337
+ content: none;
338
+ display: none;
339
+ }
340
+
341
+ ul li.no-style,
342
+ .package-features li,
343
+ .toc-list li {
344
+ padding-left: 0;
345
+ }
346
+
347
+ ol {
348
+ margin-top: 16px;
349
+ padding-left: 0;
350
+ list-style: none;
351
+ counter-reset: ol-counter;
352
+ }
353
+
354
+ ol li {
355
+ padding-left: 40px;
356
+ margin-bottom: 10px;
357
+ position: relative;
358
+ color: var(--secondary);
359
+ counter-increment: ol-counter;
360
+ }
361
+
362
+ ol li::before {
363
+ content: counter(ol-counter);
364
+ position: absolute;
365
+ left: 0;
366
+ top: 0;
367
+ width: 28px;
368
+ height: 28px;
369
+ display: flex;
370
+ align-items: center;
371
+ justify-content: center;
372
+ background: linear-gradient(135deg, #7dd3fc, #3b82f6);
373
+ color: white;
374
+ font-weight: 700;
375
+ font-size: 14px;
376
+ border-radius: 50%;
377
+ line-height: 1;
378
+ }
379
+
380
+ ol.no-style {
381
+ margin-top: 16px;
382
+ padding-left: 20px;
383
+ list-style: decimal;
384
+ counter-reset: none;
385
+ }
386
+
387
+ ol.no-style li {
388
+ padding-left: 0;
389
+ margin-bottom: 10px;
390
+ position: static;
391
+ color: var(--secondary);
392
+ counter-increment: none;
393
+ }
394
+
395
+ ol.no-style li::before {
396
+ content: none;
397
+ display: none;
398
+ }
399
+
400
+ .info-cards {
401
+ display: flex;
402
+ justify-content: start;
403
+ gap: 20px;
404
+ margin-top: 24px;
405
+ flex-wrap: wrap;
406
+ page-break-inside: avoid;
407
+ break-inside: avoid;
408
+ }
409
+
410
+ .info-card {
411
+ flex: 0 1 calc(33.333% - 14px);
412
+ min-width: 180px;
413
+ max-width: 100%;
414
+ background: white;
415
+ border: 1px solid var(--border);
416
+ border-radius: 12px;
417
+ padding: 24px;
418
+ box-shadow: 0 4px 16px rgba(59, 130, 246, .15), 0 0 0 1px rgba(125, 211, 252, .2);
419
+ position: relative;
420
+ overflow: hidden;
421
+ page-break-inside: avoid;
422
+ break-inside: avoid;
423
+ }
424
+
425
+ @media (max-width: 600px) {
426
+ .info-card {
427
+ flex: 0 1 100%;
428
+ }
429
+ }
430
+
431
+ .info-card::before {
432
+ content: '';
433
+ position: absolute;
434
+ top: 0;
435
+ left: 0;
436
+ right: 0;
437
+ height: 4px;
438
+ background: linear-gradient(90deg, #7dd3fc, #3b82f6);
439
+ }
440
+
441
+ .info-card h3 {
442
+ margin: 0 0 8px;
443
+ font-size: 16px;
444
+ font-weight: 700;
445
+ color: var(--primary);
446
+ }
447
+
448
+ .info-card p {
449
+ margin: 0;
450
+ font-size: 14px;
451
+ color: var(--secondary);
452
+ }
453
+
454
+ .info-card .value {
455
+ font-size: 24px;
456
+ font-weight: 700;
457
+ color: var(--accent-dark);
458
+ margin: 8px 0 4px;
459
+ }
460
+
461
+ .summary-cards {
462
+ display: flex;
463
+ gap: 24px;
464
+ margin-top: 24px;
465
+ flex-wrap: wrap;
466
+ page-break-inside: avoid;
467
+ break-inside: avoid;
468
+ }
469
+
470
+ .summary-card {
471
+ flex: 1;
472
+ min-width: 220px;
473
+ background: linear-gradient(135deg, #f8fafc, #ffffff);
474
+ border: 2px solid var(--accent);
475
+ border-radius: 16px;
476
+ padding: 32px;
477
+ text-align: center;
478
+ box-shadow: 0 4px 12px rgba(0, 0, 0, .08);
479
+ position: relative;
480
+ overflow: hidden;
481
+ page-break-inside: avoid;
482
+ break-inside: avoid;
483
+ }
484
+
485
+ .summary-card::before {
486
+ content: '';
487
+ position: absolute;
488
+ top: 0;
489
+ left: 0;
490
+ right: 0;
491
+ height: 4px;
492
+ background: linear-gradient(90deg, #7dd3fc, #3b82f6);
493
+ }
494
+
495
+ .summary-card h3 {
496
+ margin: 0 0 16px;
497
+ font-size: 13px;
498
+ font-weight: 600;
499
+ color: var(--secondary);
500
+ text-transform: uppercase;
501
+ letter-spacing: .5px;
502
+ }
503
+
504
+ .summary-card .value {
505
+ font-size: 36px;
506
+ font-weight: 700;
507
+ color: var(--primary);
508
+ margin: 0;
509
+ line-height: 1.2;
510
+ }
511
+
512
+ .summary-card .label {
513
+ font-size: 12px;
514
+ color: var(--muted);
515
+ margin-top: 8px;
516
+ }
517
+
518
+ .support-packages {
519
+ display: flex;
520
+ gap: 12px;
521
+ margin-top: 24px;
522
+ flex-wrap: nowrap;
523
+ page-break-inside: avoid;
524
+ break-inside: avoid;
525
+ }
526
+
527
+ .package-card {
528
+ flex: 1 1 0;
529
+ min-width: 0;
530
+ max-width: 100%;
531
+ background: white;
532
+ border: 2px solid var(--border);
533
+ border-radius: 16px;
534
+ padding: 20px;
535
+ position: relative;
536
+ overflow: hidden;
537
+ transition: transform 0.2s, box-shadow 0.2s;
538
+ page-break-inside: avoid;
539
+ break-inside: avoid;
540
+ }
541
+
542
+ .package-card::before {
543
+ content: '';
544
+ position: absolute;
545
+ top: 0;
546
+ left: 0;
547
+ right: 0;
548
+ height: 4px;
549
+ background: linear-gradient(90deg, #7dd3fc, #3b82f6);
550
+ }
551
+
552
+ .package-card.featured {
553
+ border-color: var(--accent-dark);
554
+ box-shadow: 0 8px 24px rgba(59, 130, 246, .2);
555
+ }
556
+
557
+ .package-card.featured::before {
558
+ height: 6px;
559
+ }
560
+
561
+ .package-header {
562
+ text-align: center;
563
+ margin-bottom: 16px;
564
+ }
565
+
566
+ .package-name {
567
+ font-size: 18px;
568
+ font-weight: 700;
569
+ color: var(--primary);
570
+ margin: 0 0 6px;
571
+ }
572
+
573
+ .package-price {
574
+ font-size: 28px;
575
+ font-weight: 700;
576
+ color: var(--accent-dark);
577
+ margin: 0;
578
+ }
579
+
580
+ .package-price-label {
581
+ font-size: 11px;
582
+ color: var(--muted);
583
+ margin-top: 4px;
584
+ }
585
+
586
+ .package-features {
587
+ margin: 16px 0;
588
+ padding: 0;
589
+ list-style: none;
590
+ }
591
+
592
+ .package-features li {
593
+ padding: 8px 0 !important;
594
+ border-bottom: 1px solid var(--surface);
595
+ color: var(--secondary);
596
+ font-size: 12px;
597
+ line-height: 1.5;
598
+ }
599
+
600
+ .package-features li:last-child {
601
+ border-bottom: none;
602
+ }
603
+
604
+ .package-features li strong {
605
+ color: var(--primary);
606
+ font-weight: 600;
607
+ }
608
+
609
+ .package-badge {
610
+ display: inline-block;
611
+ margin-bottom: 12px;
612
+ padding: 4px 12px;
613
+ font-size: 10px;
614
+ font-weight: 600;
615
+ text-transform: uppercase;
616
+ color: #fff;
617
+ background: var(--accent-dark);
618
+ border-radius: 12px;
619
+ }
620
+
621
+ .warranty-notice {
622
+ background: linear-gradient(135deg, #f0f9ff, #e0f2fe);
623
+ border-left: 4px solid var(--accent-dark);
624
+ padding: 20px;
625
+ margin-top: 24px;
626
+ border-radius: 8px;
627
+ page-break-inside: avoid;
628
+ break-inside: avoid;
629
+ }
630
+
631
+ .warranty-notice p {
632
+ margin: 0;
633
+ color: var(--primary);
634
+ font-size: 13px;
635
+ }
636
+
637
+ .warranty-notice strong {
638
+ color: var(--accent-dark);
639
+ }
640
+
641
+ footer {
642
+ position: running(footer);
643
+ border-top: 2px solid var(--border);
644
+ padding-top: 12px;
645
+ font-size: 11px;
646
+ color: var(--muted);
647
+ display: flex;
648
+ justify-content: space-between;
649
+ }
650
+ """
@@ -0,0 +1,198 @@
1
+ import re
2
+ from typing import Dict, List, Tuple, Union
3
+ from weasyprint import HTML, CSS
4
+
5
+
6
+ class HTMLToPDF:
7
+ def __init__(self):
8
+ pass
9
+
10
+ def _process_css(self, css: Union[str, Dict[str, Dict[str, str]]]) -> str:
11
+ if isinstance(css, str):
12
+ return css
13
+ css_rules = []
14
+ for selector, properties in css.items():
15
+ css_rule = f"{selector} {{\n"
16
+ for property_name, property_value in properties.items():
17
+ css_rule += f" {property_name}: {property_value};\n"
18
+ css_rule += "}"
19
+ css_rules.append(css_rule)
20
+ return "\n\n".join(css_rules)
21
+
22
+ def _generate_html(
23
+ self,
24
+ body_content: str,
25
+ title: str,
26
+ css: Union[str, Dict[str, Dict[str, str]]],
27
+ ) -> str:
28
+ css_content = self._process_css(css)
29
+
30
+ html = f"""<!DOCTYPE html>
31
+ <html lang="es">
32
+ <head>
33
+ <meta charset="utf-8">
34
+ <title>{title}</title>
35
+ <style>
36
+ {css_content}
37
+ </style>
38
+ </head>
39
+ <body>
40
+ {body_content}
41
+ </body>
42
+ </html>"""
43
+ return html
44
+
45
+ def generate_html(
46
+ self,
47
+ body_content: str,
48
+ title: str,
49
+ css: Union[str, Dict[str, Dict[str, str]]],
50
+ ) -> str:
51
+ return self._generate_html(body_content, title, css)
52
+
53
+ def _extract_headings(self, html: str) -> List[Tuple[int, str, str]]:
54
+ headings = []
55
+ pattern = r"<h([1-6])[^>]*>(.*?)</h[1-6]>"
56
+
57
+ for match in re.finditer(pattern, html, re.IGNORECASE | re.DOTALL):
58
+ level = int(match.group(1))
59
+ content = match.group(2)
60
+ text = re.sub(r"<[^>]+>", "", content).strip()
61
+ if text:
62
+ headings.append((level, text, match.group(0)))
63
+
64
+ return headings
65
+
66
+ def _generate_heading_id(self, text: str) -> str:
67
+ text = text.lower()
68
+ text = re.sub(r"[^\w\s-]", "", text)
69
+ text = re.sub(r"[-\s]+", "-", text)
70
+ text = text.strip("-")
71
+ return text
72
+
73
+ def _add_ids_to_headings(self, html: str) -> str:
74
+ headings = self._extract_headings(html)
75
+ used_ids = set()
76
+
77
+ for level, text, original_tag in headings:
78
+ base_id = self._generate_heading_id(text)
79
+ heading_id = base_id
80
+ counter = 1
81
+
82
+ while heading_id in used_ids:
83
+ heading_id = f"{base_id}-{counter}"
84
+ counter += 1
85
+
86
+ used_ids.add(heading_id)
87
+
88
+ if "id=" not in original_tag.lower():
89
+ new_tag = re.sub(
90
+ r"(<h[1-6])", rf'\1 id="{heading_id}"', original_tag, count=1
91
+ )
92
+ html = html.replace(original_tag, new_tag, 1)
93
+
94
+ return html
95
+
96
+ def generate_table_of_contents(self, html: str) -> str:
97
+ pattern = r"<h([1-6])[^>]*(?:id=\"([^\"]+)\")?[^>]*>(.*?)</h[1-6]>"
98
+ headings = []
99
+
100
+ for match in re.finditer(pattern, html, re.IGNORECASE | re.DOTALL):
101
+ level = int(match.group(1))
102
+ if level > 2:
103
+ continue
104
+ heading_id = match.group(2) or self._generate_heading_id(
105
+ re.sub(r"<[^>]+>", "", match.group(3)).strip()
106
+ )
107
+ text = re.sub(r"<[^>]+>", "", match.group(3)).strip()
108
+ if text:
109
+ headings.append((level, text, heading_id))
110
+
111
+ if not headings:
112
+ return ""
113
+
114
+ toc_items = []
115
+ for level, text, heading_id in headings:
116
+ indent_class = f"toc-level-{level}"
117
+ toc_items.append(
118
+ f'<li class="{indent_class} no-style">'
119
+ f'<a href="#{heading_id}">{text}</a>'
120
+ f"</li>"
121
+ )
122
+
123
+ toc_html = f"""<div class="toc">
124
+ <h2 class="toc-heading">Tabla de Contenidos</h2>
125
+ <ul class="toc-list">
126
+ {chr(10).join(toc_items)}
127
+ </ul>
128
+ </div>"""
129
+
130
+ return toc_html
131
+
132
+ def generate_cover_page(
133
+ self,
134
+ title: str,
135
+ badge: str = "Propuesta técnica",
136
+ date: str = None,
137
+ ) -> str:
138
+ if date is None:
139
+ from datetime import datetime
140
+
141
+ months = [
142
+ "Enero",
143
+ "Febrero",
144
+ "Marzo",
145
+ "Abril",
146
+ "Mayo",
147
+ "Junio",
148
+ "Julio",
149
+ "Agosto",
150
+ "Septiembre",
151
+ "Octubre",
152
+ "Noviembre",
153
+ "Diciembre",
154
+ ]
155
+ now = datetime.now()
156
+ date = f"{months[now.month - 1]} {now.year}"
157
+
158
+ cover_html = f"""<div class="header">
159
+ <div class="header-content">
160
+ <div>
161
+ <h1>{title}</h1>
162
+ <span class="badge">{badge}</span>
163
+ <div class="header-date">{date}</div>
164
+ </div>
165
+ </div>
166
+ </div>"""
167
+ return cover_html
168
+
169
+ def generate_pdf(
170
+ self,
171
+ body_content: str,
172
+ title: str,
173
+ css: Union[str, Dict[str, Dict[str, str]]],
174
+ output_path: str,
175
+ include_cover: bool = True,
176
+ cover_badge: str = "Propuesta técnica",
177
+ cover_date: str = None,
178
+ include_toc: bool = True,
179
+ ) -> None:
180
+ body_content = self._add_ids_to_headings(body_content)
181
+
182
+ if include_cover:
183
+ cover_page = self.generate_cover_page(title, cover_badge, cover_date)
184
+ body_content = cover_page + '\n<div class="content">\n' + body_content
185
+ else:
186
+ body_content = '<div class="content">\n' + body_content
187
+
188
+ if include_toc:
189
+ toc = self.generate_table_of_contents(body_content)
190
+ if toc:
191
+ body_content = body_content.replace(
192
+ '<div class="content">\n', '<div class="content">\n' + toc + "\n", 1
193
+ )
194
+
195
+ body_content = body_content + "\n</div>"
196
+
197
+ html_content = self._generate_html(body_content, title, css)
198
+ HTML(string=html_content).write_pdf(output_path)
@@ -0,0 +1,19 @@
1
+ import markdown
2
+ from markdown.extensions.tables import TableExtension
3
+
4
+
5
+ class MarkdownToHTML:
6
+ def __init__(self):
7
+ self.md = markdown.Markdown(
8
+ extensions=["tables", "fenced_code", "nl2br", "toc"]
9
+ )
10
+
11
+ def convert(self, markdown_text: str) -> str:
12
+ html = self.md.convert(markdown_text)
13
+ self.md.reset()
14
+ return html
15
+
16
+ def convert_file(self, file_path: str) -> str:
17
+ with open(file_path, "r", encoding="utf-8") as f:
18
+ markdown_content = f.read()
19
+ return self.convert(markdown_content)
@@ -0,0 +1,194 @@
1
+ Metadata-Version: 2.4
2
+ Name: docuguru
3
+ Version: 0.1.0
4
+ Summary:
5
+ Author: Cristian Cubillos
6
+ Author-email: ccubillosreyes1@gmail.com
7
+ Requires-Python: >=3.12
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: Programming Language :: Python :: 3.12
10
+ Classifier: Programming Language :: Python :: 3.13
11
+ Classifier: Programming Language :: Python :: 3.14
12
+ Requires-Dist: jinja2 (>=3.1.6,<4.0.0)
13
+ Requires-Dist: markdown (>=3.10,<4.0)
14
+ Requires-Dist: pygments (>=2.19.2,<3.0.0)
15
+ Requires-Dist: typer (>=0.21.0,<0.22.0)
16
+ Requires-Dist: weasyprint (>=67.0,<68.0)
17
+ Description-Content-Type: text/markdown
18
+
19
+ # DocuGuru
20
+
21
+ Herramienta profesional para convertir documentos Markdown a PDF con estilos predefinidos y componentes personalizados.
22
+
23
+ ## Características
24
+
25
+ - ✅ Conversión de Markdown a PDF de alta calidad
26
+ - ✅ Página de portada automática con título, badge y fecha
27
+ - ✅ Tabla de contenidos generada automáticamente (H1, H2, H3)
28
+ - ✅ Soporte completo para títulos (h1-h6), tablas y listas
29
+ - ✅ Listas ordenadas con números estilizados en círculos
30
+ - ✅ Listas no ordenadas con checkmarks personalizados
31
+ - ✅ Componentes HTML personalizados (info-cards, timelines, summary-cards, etc.)
32
+ - ✅ Estilos profesionales predefinidos
33
+ - ✅ Optimizado para impresión en formato A4
34
+
35
+ ## Instalación
36
+
37
+ ### Requisitos
38
+
39
+ - Python >= 3.12
40
+ - Poetry (para gestión de dependencias)
41
+
42
+ ### Instalación con Poetry
43
+
44
+ ```bash
45
+ # Clonar el repositorio
46
+ git clone <repository-url>
47
+ cd docuguru
48
+
49
+ # Instalar dependencias
50
+ poetry install
51
+
52
+ # Activar el entorno virtual
53
+ poetry shell
54
+ ```
55
+
56
+ ### Instalación del paquete
57
+
58
+ ```bash
59
+ # Construir el paquete
60
+ poetry build
61
+
62
+ # Instalar desde el wheel generado
63
+ pip install dist/docuguru-0.1.0-py3-none-any.whl
64
+ ```
65
+
66
+ ## Uso
67
+
68
+ ### Comando Básico
69
+
70
+ ```bash
71
+ docuguru convert documento.md
72
+ ```
73
+
74
+ Esto generará un archivo `documento.pdf` en el mismo directorio.
75
+
76
+ ### Opciones Disponibles
77
+
78
+ ```bash
79
+ docuguru convert documento.md [OPCIONES]
80
+ ```
81
+
82
+ **Opciones:**
83
+
84
+ - `-o, --output <archivo>`: Especifica la ruta del archivo PDF de salida
85
+ - `-t, --title <título>`: Define el título del documento (por defecto se extrae del primer H1)
86
+ - `--no-cover`: No incluir página de portada
87
+ - `-b, --badge <texto>`: Texto del badge en la portada (por defecto: "Propuesta técnica")
88
+ - `-d, --date <fecha>`: Fecha para la portada (ej: "Diciembre 2025", por defecto: fecha actual)
89
+
90
+ ### Ejemplos
91
+
92
+ ```bash
93
+ # Conversión básica
94
+ docuguru convert propuesta.md
95
+
96
+ # Con título personalizado y fecha
97
+ docuguru convert propuesta.md -t "Propuesta Técnica" -d "Enero 2025"
98
+
99
+ # Sin portada
100
+ docuguru convert documento.md --no-cover
101
+
102
+ # Especificar archivo de salida
103
+ docuguru convert documento.md -o salida/propuesta.pdf
104
+
105
+ # Con badge personalizado
106
+ docuguru convert documento.md -b "Informe Técnico"
107
+ ```
108
+
109
+ ## Documentación de Bloques Personalizados
110
+
111
+ Para ver todos los bloques HTML personalizados disponibles y cómo usarlos:
112
+
113
+ ```bash
114
+ # Mostrar en consola
115
+ docuguru blocks
116
+
117
+ # Guardar en archivo
118
+ docuguru blocks -o bloques-documentacion.md
119
+ ```
120
+
121
+ ## Componentes Personalizados Disponibles
122
+
123
+ ### 1. Architecture Diagram
124
+ Diagrama de arquitectura centrado.
125
+
126
+ ### 2. Info Card
127
+ Tarjeta informativa para fases o información destacada.
128
+
129
+ ### 3. Timeline
130
+ Línea de tiempo vertical con items conectados.
131
+
132
+ ### 4. Summary Cards
133
+ Tarjetas de resumen horizontales con valores destacados.
134
+
135
+ ### 5. Support Packages
136
+ Tarjetas de paquetes de servicios lado a lado.
137
+
138
+ ### 6. Styled List
139
+ Listas con checkmarks personalizados (✔).
140
+
141
+ ### 7. Warranty Notice
142
+ Bloque de aviso o garantía destacado.
143
+
144
+ Para más detalles y ejemplos de uso, ejecuta `docuguru blocks`.
145
+
146
+ ## Características de Markdown Soportadas
147
+
148
+ - **Títulos**: Todos los niveles (h1-h6)
149
+ - **Tablas**: Formato estándar de Markdown
150
+ - **Listas ordenadas**: Con numeración estilizada automática
151
+ - **Listas no ordenadas**: Con checkmarks personalizados
152
+ - **Código**: Bloques de código con syntax highlighting
153
+ - **HTML personalizado**: Componentes custom embebidos
154
+
155
+ ## Estilos Predefinidos
156
+
157
+ El proyecto incluye un conjunto completo de estilos CSS predefinidos que incluyen:
158
+
159
+ - Paleta de colores profesional (azules y grises)
160
+ - Tipografía Inter (con múltiples pesos)
161
+ - Gradientes y sombras modernas
162
+ - Optimización para impresión (page-breaks, márgenes)
163
+ - Diseño responsive
164
+
165
+ ## Estructura del Proyecto
166
+
167
+ ```
168
+ docuguru/
169
+ ├── src/
170
+ │ └── docuguru/
171
+ │ ├── __init__.py
172
+ │ ├── cli.py # Interfaz de línea de comandos
173
+ │ ├── markdon_to_html.py # Conversor Markdown a HTML
174
+ │ ├── html_to_pdf.py # Generador de PDF
175
+ │ └── default.py # Estilos CSS predefinidos
176
+ ├── tests/ # Tests unitarios
177
+ ├── playground/ # Archivos de ejemplo
178
+ ├── pyproject.toml # Configuración del proyecto
179
+ └── README.md # Este archivo
180
+ ```
181
+
182
+ ## Desarrollo
183
+
184
+ ### Ejecutar Tests
185
+
186
+ ```bash
187
+ poetry run pytest
188
+ ```
189
+
190
+ ### Construir el Proyecto
191
+
192
+ ```bash
193
+ poetry build
194
+ ```
@@ -0,0 +1,9 @@
1
+ docuguru/__init__.py,sha256=XjXVNbXJZfebYlsm3bBEYnNnzgKDD8o3_GVpqwD1Pmw,67
2
+ docuguru/cli.py,sha256=FWBrCghMWziDKew55IXJivw-HID2B2sINexdog3GvNw,6960
3
+ docuguru/default.py,sha256=A-RZoa2iOl52CRXG9aANrL-yokvcdenFkCRRowkwXjI,11315
4
+ docuguru/html_to_pdf.py,sha256=ZTXLQ2PqjMoSS7Ny0jynCMph0YnY_2_9h0kaF3xKppA,5944
5
+ docuguru/markdon_to_html.py,sha256=UOTzTcd1IPxr8BA6xvC2BAULCTLtV8DkgU65ox40tfc,567
6
+ docuguru-0.1.0.dist-info/METADATA,sha256=jjPL1nEgJk2JeystIlVEf7EBWJb1XKGRQBwVWO4ZT0w,5018
7
+ docuguru-0.1.0.dist-info/WHEEL,sha256=zp0Cn7JsFoX2ATtOhtaFYIiE2rmFAD4OcMhtUki8W3U,88
8
+ docuguru-0.1.0.dist-info/entry_points.txt,sha256=Styiun1iDiCp5UgGyZBBu7Si18dfwJ-0M0eSf-pUsCI,45
9
+ docuguru-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: poetry-core 2.2.1
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,3 @@
1
+ [console_scripts]
2
+ docuguru=docuguru.cli:app
3
+