lib.odp_utils

Generador de presentaciones ODP — paquete principal.

Uso:

from lib.odp_utils import SlideTemplates, load_template, save_odp, build_slides_from_yaml

 1"""
 2Generador de presentaciones ODP — paquete principal.
 3
 4Uso:
 5    from lib.odp_utils import SlideTemplates, load_template, save_odp, build_slides_from_yaml
 6"""
 7
 8DEFAULT_VERSION = '1'
 9
10from .xml_helpers import xe, preserve_spaces, build_list, build_link_list, extract_slides, finalize, inject_notes, xe_multiline
11from .styles import inject_code_styles
12from .tokenizers.java import _java_pt
13from .builders.base import SlideTemplates
14from .builders.core import (
15    make_portada, make_objectives, make_index, make_single,
16    make_bullets, make_refs, make_code, make_java,
17)
18from .builders.code import (
19    make_codigo, make_javascript, make_python,
20    make_xml, make_json, make_yaml, make_html, make_jsp, make_css,
21)
22from .builders.content import (
23    make_salida, make_terminal, make_diagrama, make_tabla, make_aviso,
24    make_imagen, make_imagen_texto, make_dos_columnas,
25    make_comparar, make_comparar_python, make_comparar_js,
26    make_comparar_xml, make_comparar_jsp, make_comparar_css, make_comparar_json,
27)
28from .builders.pipeline import build_slides_from_yaml, load_template, save_odp
29
30__all__ = [
31    'DEFAULT_VERSION',
32    'SlideTemplates',
33    'load_template',
34    'save_odp',
35    'preserve_spaces',
36    'xe',
37    'make_portada',
38    'make_objectives',
39    'make_index',
40    'make_single',
41    'make_bullets',
42    'make_refs',
43    'make_code',
44    'make_java',
45    'make_salida',
46    'make_terminal',
47    'make_diagrama',
48    'make_tabla',
49    'make_aviso',
50    'make_dos_columnas',
51    'make_comparar',
52    'make_comparar_python',
53    'make_comparar_js',
54    'make_comparar_xml',
55    'make_comparar_jsp',
56    'make_comparar_css',
57    'make_comparar_json',
58    'make_codigo',
59    'make_javascript',
60    'make_python',
61    'make_yaml',
62    'make_xml',
63    'make_json',
64    'make_html',
65    'make_jsp',
66    'make_css',
67    'make_imagen',
68    'make_imagen_texto',
69    'build_slides_from_yaml',
70    # privadas/helpers usadas en tests
71    '_java_pt',
72    'build_list',
73    'build_link_list',
74    'extract_slides',
75    'finalize',
76    'inject_notes',
77    'xe_multiline',
78]
DEFAULT_VERSION = '1'
class SlideTemplates:
36class SlideTemplates:
37    """Mapeador para encapsular y desacoplar los índices mágicos de las plantillas ODP."""
38    def __init__(self, tpls: list):
39        self.portada    = tpls[0]
40        self.objectives = tpls[1]
41        self.index      = tpls[2]
42        self.single     = tpls[3]
43        self.bullets    = tpls[10]
44        self.conclusion = tpls[19]
45        self.references = tpls[20]
46        self.contact    = tpls[21]
47        self.license    = tpls[22]
48
49    @classmethod
50    def from_config(cls, tpls: list, cfg) -> 'SlideTemplates':
51        obj = object.__new__(cls)
52        obj.portada    = tpls[cfg.idx_portada]
53        obj.objectives = tpls[cfg.idx_objetivos]
54        obj.index      = tpls[cfg.idx_indice]
55        obj.single     = tpls[cfg.idx_intro]
56        obj.bullets    = tpls[cfg.idx_contenido]
57        obj.conclusion = tpls[cfg.idx_conclusion]
58        obj.references = tpls[cfg.idx_referencias]
59        obj.contact    = tpls[cfg.idx_contacto]
60        obj.license    = tpls[cfg.idx_licencia]
61        return obj

Mapeador para encapsular y desacoplar los índices mágicos de las plantillas ODP.

SlideTemplates(tpls: list)
38    def __init__(self, tpls: list):
39        self.portada    = tpls[0]
40        self.objectives = tpls[1]
41        self.index      = tpls[2]
42        self.single     = tpls[3]
43        self.bullets    = tpls[10]
44        self.conclusion = tpls[19]
45        self.references = tpls[20]
46        self.contact    = tpls[21]
47        self.license    = tpls[22]
portada
objectives
index
single
bullets
conclusion
references
contact
license
@classmethod
def from_config(cls, tpls: list, cfg) -> SlideTemplates:
49    @classmethod
50    def from_config(cls, tpls: list, cfg) -> 'SlideTemplates':
51        obj = object.__new__(cls)
52        obj.portada    = tpls[cfg.idx_portada]
53        obj.objectives = tpls[cfg.idx_objetivos]
54        obj.index      = tpls[cfg.idx_indice]
55        obj.single     = tpls[cfg.idx_intro]
56        obj.bullets    = tpls[cfg.idx_contenido]
57        obj.conclusion = tpls[cfg.idx_conclusion]
58        obj.references = tpls[cfg.idx_referencias]
59        obj.contact    = tpls[cfg.idx_contacto]
60        obj.license    = tpls[cfg.idx_licencia]
61        return obj
def load_template(template: pathlib.Path):
282def load_template(template: Path):
283    """Returns (content_xml, all_files, tpls)."""
284    if not template.exists():
285        raise FileNotFoundError(f"La plantilla especificada no existe en: {template.absolute()}")
286    with zipfile.ZipFile(template) as zin:
287        content_xml = zin.read('content.xml').decode('utf-8')
288        all_files = {name: zin.read(name) for name in zin.namelist()}
289    tpls = extract_slides(content_xml)
290    return content_xml, all_files, tpls

Returns (content_xml, all_files, tpls).

def save_odp( output: pathlib.Path, slides: list, content_xml: str, all_files: dict, cfg=None, extra_styles: list = None) -> None:
323def save_odp(output: Path, slides: list, content_xml: str, all_files: dict,
324             cfg=None, extra_styles: list = None) -> None:
325    """Finalize slide numbers, inject styles and write the ODP file."""
326    slides = [finalize(s, i + 1) for i, s in enumerate(slides)]
327    first = content_xml.find('<draw:page ')
328    last  = content_xml.rfind('</draw:page>') + len('</draw:page>')
329    new_xml = inject_code_styles(
330        content_xml[:first] + ''.join(slides) + content_xml[last:],
331        extra_styles,
332    )
333
334    if cfg and cfg.url_contacto:
335        for variant in _url_variants(cfg.url_contacto):
336            if variant != cfg.url_contacto:
337                new_xml = new_xml.replace(variant, cfg.url_contacto)
338    else:
339        new_xml = new_xml.replace('http://www.cursosdedesarrollo.com', 'https://temario.app')
340        new_xml = new_xml.replace('http://cursosdedesarrollo.com', 'https://temario.app')
341        new_xml = new_xml.replace('https://cursosdedesarrollo.com', 'https://temario.app')
342        new_xml = new_xml.replace('http://www.cursosdesarrollo.com', 'https://temario.app')
343        new_xml = new_xml.replace('http://cursosdesarrollo.com', 'https://temario.app')
344
345    all_files = dict(all_files)
346    _update_manifest(all_files)
347
348    buf = BytesIO()
349    with zipfile.ZipFile(buf, 'w', zipfile.ZIP_DEFLATED) as zout:
350        for name, data in all_files.items():
351            zout.writestr(name, new_xml.encode('utf-8') if name == 'content.xml' else data)
352    output.parent.mkdir(parents=True, exist_ok=True)
353    output.write_bytes(buf.getvalue())
354    try:
355        shown = output.relative_to(Path.cwd())
356    except ValueError:
357        shown = output
358    print(_('Generado {} con {} diapositivas').format(shown, len(slides)))

Finalize slide numbers, inject styles and write the ODP file.

def preserve_spaces(s: str) -> str:
56def preserve_spaces(s: str) -> str:
57    """Conserva la indentación de código convirtiendo múltiples espacios consecutivos en elementos <text:s text:c="N"/>."""
58    def repl(match):
59        count = len(match.group(0))
60        return " " + f'<text:s text:c="{count - 1}"/>' if count > 1 else " "
61    return re.sub(r' {2,}', repl, s)

Conserva la indentación de código convirtiendo múltiples espacios consecutivos en elementos .

def xe(s: str) -> str:
6def xe(s: str) -> str:
7    """Escapa de forma segura caracteres especiales para XML usando la biblioteca estándar."""
8    return xml.sax.saxutils.escape(str(s))

Escapa de forma segura caracteres especiales para XML usando la biblioteca estándar.

def make_portada(tpl: str, title: str, unit: str, level: str = 'Básico', cfg=None) -> str:
10def make_portada(tpl: str, title: str, unit: str, level: str = 'Básico', cfg=None) -> str:
11    pt_style = cfg.portada_titulo if cfg else 'T1'
12    pu_style = cfg.portada_unidad if cfg else 'T2'
13    pn_style = cfg.portada_nivel if cfg else 'T4'
14    title_style = _portada_title_style(title, pt_style)
15    s = re.sub(
16        rf'<text:span text:style-name="{re.escape(pt_style)}">[^<]*</text:span>',
17        f'<text:span text:style-name="{title_style}">{xe(title)}</text:span>',
18        tpl,
19    )
20    s = re.sub(rf'(<text:span text:style-name="{re.escape(pu_style)}">)[^<]*(</text:span>)',
21               rf'\g<1>{xe(unit)}\2', s)
22    s = re.sub(rf'(<text:span text:style-name="{re.escape(pn_style)}">)[^<]*(</text:span>)',
23               rf'\g<1>{xe(level)}\2', s)
24    return s
def make_objectives(tpl: str, bullets: list, cfg=None) -> str:
37def make_objectives(tpl: str, bullets: list, cfg=None) -> str:
38    l  = cfg.lista_obj if cfg else 'L3'
39    p  = cfg.parrafo_obj if cfg else 'P16'
40    base = cfg.span_obj if cfg else 'T6'
41    sm_thr = cfg.umbral_obj_sm if cfg else 7
42    xs_thr = getattr(cfg, 'umbral_obj_xs', 8) if cfg else 11
43    span = _list_span(len(bullets), base, 'T_OBJ_SM', 'T_OBJ_XS',
44                      sm_threshold=sm_thr, xs_threshold=xs_thr)
45    new_list = build_list(l, p, span, bullets)
46    return re.sub(rf'<text:list text:style-name="{re.escape(l)}">.*?</text:list>',
47                  new_list, tpl, flags=re.DOTALL)
def make_index(tpl: str, items: list, cfg=None) -> str:
50def make_index(tpl: str, items: list, cfg=None) -> str:
51    l  = cfg.lista_obj if cfg else 'L3'
52    p  = cfg.parrafo_indice if cfg else 'P21'
53    base = cfg.span_indice if cfg else 'T8'
54    span = _list_span(len(items), base, 'T_IDX_SM', 'T_IDX_XS')
55    new_list = build_list(l, p, span, items)
56    return re.sub(rf'<text:list text:style-name="{re.escape(l)}">.*?</text:list>',
57                  new_list, tpl, flags=re.DOTALL)
def make_single(tpl: str, indicator: str, sec_title: str, body: str, cfg=None) -> str:
60def make_single(tpl: str, indicator: str, sec_title: str, body: str, cfg=None) -> str:
61    """Slide con un párrafo de texto (intro de sección o conclusiones)."""
62    ind = cfg.indicador if cfg else 'T7'
63    tit = cfg.titulo if cfg else 'T10'
64    bod = cfg.intro_cuerpo if cfg else 'T9'
65    s = re.sub(rf'(<text:span text:style-name="{re.escape(ind)}">)[^<]*(</text:span>)',
66               rf'\g<1>{xe(indicator)}\2', tpl)
67    s = re.sub(rf'<text:span text:style-name="{re.escape(tit)}">[^<]*</text:span>',
68               f'<text:span text:style-name="T_TITLE">{xe(sec_title)}</text:span>', s)
69    s = _center_title(s)
70    s = re.sub(rf'(<text:span text:style-name="{re.escape(bod)}">).*?(</text:span>)',
71               rf'\g<1>{xe_multiline(body, bod)}\2', s, flags=re.DOTALL)
72    return s

Slide con un párrafo de texto (intro de sección o conclusiones).

def make_bullets( tpl: str, indicator: str, sec_title: str, bullets: list, cfg=None, subtitle: str = '') -> str:
 75def make_bullets(tpl: str, indicator: str, sec_title: str, bullets: list,
 76                 cfg=None, subtitle: str = '') -> str:
 77    """Slide con lista de viñetas. subtitle aparece centrado justo bajo el título."""
 78    ind = cfg.indicador if cfg else 'T7'
 79    tit = cfg.titulo if cfg else 'T10'
 80    l   = cfg.lista_contenido if cfg else 'L4'
 81    p   = cfg.parrafo_contenido if cfg else 'P27'
 82    base = cfg.span_contenido if cfg else 'T6'
 83    sp  = _list_span(len(bullets), base, 'T_BUL_SM', 'T_BUL_XS')
 84    s = re.sub(rf'(<text:span text:style-name="{re.escape(ind)}">)[^<]*(</text:span>)',
 85               rf'\g<1>{xe(indicator)}\2', tpl)
 86    s = re.sub(rf'<text:span text:style-name="{re.escape(tit)}">[^<]*</text:span>',
 87               f'<text:span text:style-name="T_TITLE">{xe(sec_title)}</text:span>', s)
 88    s = _center_title(s)
 89    new_list = build_list(l, p, sp, bullets)
 90    s = re.sub(rf'<text:list text:style-name="{re.escape(l)}">.*?</text:list>',
 91               new_list, s, flags=re.DOTALL)
 92    if subtitle:
 93        sub_frame = (
 94            '<draw:frame draw:style-name="gr4" draw:layer="layout"'
 95            ' svg:width="22.4cm" svg:height="0.8cm"'
 96            ' svg:x="2.6cm" svg:y="1.95cm">'
 97            '<draw:text-box>'
 98            f'<text:p text:style-name="P_SUBTITLE">'
 99            f'<text:span text:style-name="T_SUBTITLE">{xe(subtitle)}</text:span>'
100            f'</text:p>'
101            '</draw:text-box>'
102            '</draw:frame>'
103        )
104        if '<presentation:notes' in s:
105            s = s.replace('<presentation:notes', sub_frame + '<presentation:notes', 1)
106        else:
107            s = s.replace('</draw:page>', sub_frame + '</draw:page>', 1)
108    return s

Slide con lista de viñetas. subtitle aparece centrado justo bajo el título.

def make_refs(tpl: str, refs: list, cfg=None) -> str:
111def make_refs(tpl: str, refs: list, cfg=None) -> str:
112    """Slide de referencias con enlaces clicables."""
113    l = cfg.lista_contenido if cfg else 'L4'
114    p = cfg.parrafo_contenido if cfg else 'P27'
115    sp = cfg.span_contenido if cfg else 'T6'
116    new_list = build_link_list(l, p, sp, refs)
117    return re.sub(rf'<text:list text:style-name="{re.escape(l)}">.*?</text:list>',
118                  new_list, tpl, flags=re.DOTALL)

Slide de referencias con enlaces clicables.

def make_code(tpl: str, indicator: str, sec_title: str, lines: list, cfg=None) -> str:
121def make_code(tpl: str, indicator: str, sec_title: str, lines: list, cfg=None) -> str:
122    """Slide con comandos de shell."""
123    l = cfg.lista_contenido if cfg else 'L4'
124    s = _sec_header(tpl, indicator, sec_title, cfg)
125    parts = []
126    for item in lines:
127        if isinstance(item, tuple) and item[0] == 'cmt':
128            text = '# ' + item[1]
129            span = 'T_COMMENT'
130        elif isinstance(item, tuple) and item[0] == 'ok':
131            text = item[1]
132            span = 'T_CODE_OK'
133        else:
134            text = item
135            span = 'T_CODE'
136        parts.append(
137            f'<text:p text:style-name="P_CODE">'
138            f'<text:span text:style-name="{span}">{preserve_spaces(xe(text))}</text:span>'
139            f'</text:p>'
140        )
141    return re.sub(rf'<text:list text:style-name="{re.escape(l)}">.*?</text:list>',
142                  ''.join(parts), s, flags=re.DOTALL)

Slide con comandos de shell.

def make_java(tpl: str, indicator: str, sec_title: str, lines: list, cfg=None) -> str:
145def make_java(tpl: str, indicator: str, sec_title: str, lines: list, cfg=None) -> str:
146    """Slide con código fuente Java con resaltado de sintaxis por tokens."""
147    l = cfg.lista_contenido if cfg else 'L4'
148    pt = _java_pt(len(lines))
149    s = _sec_header(tpl, indicator, sec_title, cfg)
150
151    in_block_cmt = False
152    parts = []
153    for line in lines:
154        if line == '':
155            parts.append(f'<text:p text:style-name="P_JAVA_{pt}"/>')
156            continue
157        tokens, in_block_cmt = _tokenize_java_line(line, in_block_cmt)
158        if not tokens:
159            parts.append(f'<text:p text:style-name="P_JAVA_{pt}"/>')
160            continue
161        spans = ''.join(
162            f'<text:span text:style-name="{_java_token_style(typ, pt)}">'
163            f'{preserve_spaces(xe(text))}</text:span>'
164            for typ, text in tokens
165        )
166        parts.append(f'<text:p text:style-name="P_JAVA_{pt}">{spans}</text:p>')
167
168    return re.sub(
169        rf'<text:list text:style-name="{re.escape(l)}">.*?</text:list>',
170        ''.join(parts), s, flags=re.DOTALL,
171    )

Slide con código fuente Java con resaltado de sintaxis por tokens.

def make_salida(tpl: str, indicator: str, sec_title: str, lines: list, cfg=None) -> str:
23def make_salida(tpl: str, indicator: str, sec_title: str, lines: list, cfg=None) -> str:
24    """Slide con salida de consola. Items: str | ('err', text) | ('ok', text)."""
25    l = cfg.lista_contenido if cfg else 'L4'
26    s = _sec_header(tpl, indicator, sec_title, cfg)
27    parts = []
28    for item in lines:
29        if isinstance(item, tuple):
30            kind, text = item[0], item[1]
31            span = 'T_SALIDA_ERR' if kind == 'err' else 'T_SALIDA_OK' if kind == 'ok' else 'T_SALIDA'
32        else:
33            text, span = item, 'T_SALIDA'
34        parts.append(
35            f'<text:p text:style-name="P_SALIDA">'
36            f'<text:span text:style-name="{span}">{preserve_spaces(xe(text))}</text:span>'
37            f'</text:p>'
38        )
39    return re.sub(rf'<text:list text:style-name="{re.escape(l)}">.*?</text:list>',
40                  ''.join(parts), s, flags=re.DOTALL)

Slide con salida de consola. Items: str | ('err', text) | ('ok', text).

def make_terminal(tpl: str, indicator: str, sec_title: str, items: list, cfg=None) -> str:
 61def make_terminal(tpl: str, indicator: str, sec_title: str, items: list, cfg=None) -> str:
 62    """Slide de sesión de terminal interactivo."""
 63    l = cfg.lista_contenido if cfg else 'L4'
 64    s = _sec_header(tpl, indicator, sec_title, cfg)
 65
 66    def _p(content: str) -> str:
 67        return f'<text:p text:style-name="P_TERM">{content}</text:p>'
 68
 69    def _span(style: str, text: str) -> str:
 70        return f'<text:span text:style-name="{style}">{preserve_spaces(xe(text))}</text:span>'
 71
 72    line_width = cfg.terminal_chars_por_linea if cfg else _TERM_LINE_WIDTH
 73
 74    def _render_cmd(prompt_style: str, prompt_char: str, cmd_text: str) -> list:
 75        segs = _split_cmd(cmd_text, line_width)
 76        lines = []
 77        for i, seg in enumerate(segs):
 78            is_last = (i == len(segs) - 1)
 79            cont = _span('T_TERM_CMT', ' \\') if not is_last else ''
 80            if i == 0:
 81                lines.append(_p(_span(prompt_style, prompt_char) + _span('T_TERM_CMD', seg) + cont))
 82            else:
 83                indent = ' ' * _TERM_CONT_INDENT
 84                lines.append(_p(_span('T_TERM_CMD', indent + seg) + cont))
 85        return lines
 86
 87    parts = []
 88    for item in items:
 89        if item == '' or item == {}:
 90            parts.append('<text:p text:style-name="P_TERM"/>')
 91            continue
 92        if isinstance(item, dict):
 93            if 'cmd' in item:
 94                parts.extend(_render_cmd('T_TERM_PROMPT', '$ ', item['cmd']))
 95            elif 'root' in item:
 96                parts.extend(_render_cmd('T_TERM_ROOT_P', '# ', item['root']))
 97            elif 'out' in item:
 98                parts.append(_p(_span('T_TERM_OUT', item['out'])))
 99            elif 'ok' in item:
100                parts.append(_p(_span('T_TERM_OK', item['ok'])))
101            elif 'err' in item:
102                parts.append(_p(_span('T_TERM_ERR', item['err'])))
103            elif 'cmt' in item:
104                parts.append(_p(_span('T_TERM_CMT', '# ' + item['cmt'])))
105        else:
106            parts.extend(_render_cmd('T_TERM_PROMPT', '$ ', item))
107
108    return re.sub(rf'<text:list text:style-name="{re.escape(l)}">.*?</text:list>',
109                  ''.join(parts), s, flags=re.DOTALL)

Slide de sesión de terminal interactivo.

def make_diagrama(tpl: str, indicator: str, sec_title: str, items: list, cfg=None) -> str:
135def make_diagrama(tpl: str, indicator: str, sec_title: str, items: list, cfg=None) -> str:
136    """Slide con diagrama en texto / jerarquía ASCII en monoespaciado."""
137    l = cfg.lista_contenido if cfg else 'L4'
138    s = _sec_header(tpl, indicator, sec_title, cfg)
139    parts = [
140        f'<text:p text:style-name="P_DIAG">'
141        f'<text:span text:style-name="T_DIAG">{preserve_spaces(xe(_force_text(item)))}</text:span>'
142        f'</text:p>'
143        for item in items
144    ]
145    return re.sub(rf'<text:list text:style-name="{re.escape(l)}">.*?</text:list>',
146                  ''.join(parts), s, flags=re.DOTALL)

Slide con diagrama en texto / jerarquía ASCII en monoespaciado.

def make_tabla( tpl: str, indicator: str, sec_title: str, cabeceras: list, filas: list, colores: dict = None, extra_styles: list = None, cfg=None) -> str:
175def make_tabla(tpl: str, indicator: str, sec_title: str,
176               cabeceras: list, filas: list,
177               colores: dict = None, extra_styles: list = None, cfg=None) -> str:
178    """Slide con tabla de datos. colores: {cabecera, texto_cabecera, texto, borde}."""
179    s = _sec_header(tpl, indicator, sec_title, cfg)
180
181    if colores:
182        bg      = colores.get('cabecera',       '#1e3a5f')
183        fg      = colores.get('texto_cabecera', '#ffffff')
184        cell_fg = colores.get('texto',          '#1a1a1a')
185        borde   = colores.get('borde',          '#cccccc')
186        key     = bg.lstrip('#')
187        hdr_cell_sty = f'GS_TC_H_{key}'
188        cell_sty     = f'GS_TC_{key}'
189        hdr_txt_sty  = f'T_TBL_H_{key}'
190        txt_sty      = f'T_TBL_{key}'
191        hdr_p_sty    = f'P_TBL_H_{key}'
192        p_sty        = f'P_TBL_{key}'
193        if extra_styles is not None and not any(key in s for s in extra_styles):
194            extra_styles.append(_table_color_styles(key, bg, fg, cell_fg, borde))
195    else:
196        hdr_cell_sty = 'GS_TC_H'
197        cell_sty     = 'GS_TC'
198        hdr_txt_sty  = 'T_TBL_H'
199        txt_sty      = 'T_TBL'
200        hdr_p_sty    = 'P_TBL_H'
201        p_sty        = 'P_TBL'
202
203    ncols    = max(len(cabeceras), max((len(f) for f in filas), default=1))
204    cols_xml = _tabla_col_xml(cabeceras, filas, ncols, extra_styles)
205
206    header_row = ''
207    if cabeceras:
208        cells = ''.join(
209            f'<table:table-cell table:style-name="{hdr_cell_sty}" office:value-type="string">'
210            f'<text:p text:style-name="{hdr_p_sty}">'
211            f'<text:span text:style-name="{hdr_txt_sty}">{xe(h)}</text:span>'
212            f'</text:p></table:table-cell>'
213            for h in cabeceras
214        )
215        header_row = f'<table:table-row>{cells}</table:table-row>'
216
217    data_rows = ''
218    for fila in filas:
219        cells = ''.join(
220            f'<table:table-cell table:style-name="{cell_sty}" office:value-type="string">'
221            f'<text:p text:style-name="{p_sty}">'
222            f'<text:span text:style-name="{txt_sty}">{xe(str(c))}</text:span>'
223            f'</text:p></table:table-cell>'
224            for c in fila
225        )
226        data_rows += f'<table:table-row>{cells}</table:table-row>'
227
228    table_xml = f'<table:table>{cols_xml}{header_row}{data_rows}</table:table>'
229    table_frame = _new_frame(_FRAME_W, _FRAME_H, _FRAME_X, _FRAME_Y, table_xml)
230    return _content_frame_re(cfg).sub(table_frame, s, count=1)

Slide con tabla de datos. colores: {cabecera, texto_cabecera, texto, borde}.

def make_aviso( tpl: str, indicator: str, sec_title: str, nivel: str, texto: str, cfg=None) -> str:
297def make_aviso(tpl: str, indicator: str, sec_title: str,
298               nivel: str, texto: str, cfg=None) -> str:
299    """Slide de aviso/callout. nivel: warning|info|tip."""
300    _LABELS  = {'warning': 'Atención', 'info': 'Nota', 'tip': 'Consejo'}
301    _ESTILOS = {'warning': 'T_AVISO_WARN', 'info': 'T_AVISO_INFO', 'tip': 'T_AVISO_TIP'}
302
303    label  = _LABELS.get(nivel, nivel.capitalize())
304    estilo = _ESTILOS.get(nivel, 'T_AVISO_WARN')
305    ind = cfg.indicador if cfg else 'T7'
306    tit = cfg.titulo if cfg else 'T10'
307    bod = cfg.intro_cuerpo if cfg else 'T9'
308
309    s = re.sub(rf'(<text:span text:style-name="{re.escape(ind)}">)[^<]*(</text:span>)',
310               rf'\g<1>{xe(indicator)}\2', tpl)
311    s = re.sub(rf'<text:span text:style-name="{re.escape(tit)}">[^<]*</text:span>',
312               f'<text:span text:style-name="{estilo}">{xe(label)}</text:span>', s)
313    s = re.sub(rf'(<text:span text:style-name="{re.escape(bod)}">).*?(</text:span>)',
314               rf'\g<1>{xe(texto)}\2', s, flags=re.DOTALL)
315    return s

Slide de aviso/callout. nivel: warning|info|tip.

def make_dos_columnas( tpl: str, indicator: str, sec_title: str, izquierda: list, derecha: list, cfg=None) -> str:
318def make_dos_columnas(tpl: str, indicator: str, sec_title: str,
319                      izquierda: list, derecha: list, cfg=None) -> str:
320    l = cfg.lista_contenido if cfg else 'L4'
321    p = cfg.parrafo_contenido if cfg else 'P27'
322    sp = cfg.span_contenido if cfg else 'T6'
323    s = _sec_header(tpl, indicator, sec_title, cfg)
324    left_xml  = build_list(l, p, sp, izquierda)
325    right_xml = build_list(l, p, sp, derecha)
326    frames = (
327        _new_frame(_COL_W_2, _FRAME_H, _FRAME_X, _FRAME_Y,
328                   f'<draw:text-box>{left_xml}</draw:text-box>')
329        + _new_frame(_COL_W_2, _FRAME_H, _COL_X2, _FRAME_Y,
330                     f'<draw:text-box>{right_xml}</draw:text-box>')
331    )
332    return _content_frame_re(cfg).sub(frames, s, count=1)
def make_comparar( tpl: str, indicator: str, sec_title: str, izq: list, der: list, cfg=None) -> str:
353def make_comparar(tpl: str, indicator: str, sec_title: str,
354                  izq: list, der: list, cfg=None) -> str:
355    pt = _java_pt(max(len(izq), len(der)) * 2)
356    return _make_comparar_cols(tpl, indicator, sec_title,
357                                _java_col_xml(izq, pt), _java_col_xml(der, pt), cfg)
def make_comparar_python( tpl: str, indicator: str, sec_title: str, izq: list, der: list, cfg=None) -> str:
360def make_comparar_python(tpl: str, indicator: str, sec_title: str,
361                         izq: list, der: list, cfg=None) -> str:
362    pt = _java_pt(max(len(izq), len(der)) * 2)
363    return _make_comparar_cols(tpl, indicator, sec_title,
364                                _col_xml_lang_stateful(izq, pt, 'PY', _tokenize_py_line),
365                                _col_xml_lang_stateful(der, pt, 'PY', _tokenize_py_line), cfg)
def make_comparar_js( tpl: str, indicator: str, sec_title: str, izq: list, der: list, cfg=None) -> str:
368def make_comparar_js(tpl: str, indicator: str, sec_title: str,
369                     izq: list, der: list, cfg=None) -> str:
370    pt = _java_pt(max(len(izq), len(der)) * 2)
371    return _make_comparar_cols(tpl, indicator, sec_title,
372                                _col_xml_lang_stateful(izq, pt, 'JS', _tokenize_js_line),
373                                _col_xml_lang_stateful(der, pt, 'JS', _tokenize_js_line), cfg)
def make_comparar_xml( tpl: str, indicator: str, sec_title: str, izq: list, der: list, cfg=None) -> str:
376def make_comparar_xml(tpl: str, indicator: str, sec_title: str,
377                      izq: list, der: list, cfg=None) -> str:
378    pt = _java_pt(max(len(izq), len(der)) * 2)
379    return _make_comparar_cols(tpl, indicator, sec_title,
380                                _col_xml_xml(izq, pt), _col_xml_xml(der, pt), cfg)
def make_comparar_jsp( tpl: str, indicator: str, sec_title: str, izq: list, der: list, cfg=None) -> str:
383def make_comparar_jsp(tpl: str, indicator: str, sec_title: str,
384                      izq: list, der: list, cfg=None) -> str:
385    pt = _java_pt(max(len(izq), len(der)) * 2)
386    return _make_comparar_cols(tpl, indicator, sec_title,
387                                _col_xml_jsp(izq, pt), _col_xml_jsp(der, pt), cfg)
def make_comparar_css( tpl: str, indicator: str, sec_title: str, izq: list, der: list, cfg=None) -> str:
390def make_comparar_css(tpl: str, indicator: str, sec_title: str,
391                      izq: list, der: list, cfg=None) -> str:
392    pt = _java_pt(max(len(izq), len(der)) * 2)
393    return _make_comparar_cols(tpl, indicator, sec_title,
394                                _col_xml_css(izq, pt), _col_xml_css(der, pt), cfg)
def make_comparar_json( tpl: str, indicator: str, sec_title: str, izq: list, der: list, cfg=None) -> str:
397def make_comparar_json(tpl: str, indicator: str, sec_title: str,
398                       izq: list, der: list, cfg=None) -> str:
399    pt = _java_pt(max(len(izq), len(der)) * 2)
400    return _make_comparar_cols(tpl, indicator, sec_title,
401                                _col_xml_json(izq, pt), _col_xml_json(der, pt), cfg)
def make_codigo(tpl: str, indicator: str, sec_title: str, items: list, cfg=None) -> str:
41def make_codigo(tpl: str, indicator: str, sec_title: str, items: list, cfg=None) -> str:
42    """Slide de código genérico (monoespaciado, sin resaltado de lenguaje)."""
43    l = cfg.lista_contenido if cfg else 'L4'
44    s = _sec_header(tpl, indicator, sec_title, cfg)
45    pt = _java_pt(len(items))
46    parts = [
47        f'<text:p text:style-name="P_JAVA_{pt}">'
48        f'<text:span text:style-name="T_JAVA_{pt}">{preserve_spaces(xe(line))}</text:span>'
49        f'</text:p>'
50        if line else f'<text:p text:style-name="P_JAVA_{pt}"/>'
51        for line in items
52    ]
53    return re.sub(rf'<text:list text:style-name="{re.escape(l)}">.*?</text:list>',
54                  ''.join(parts), s, flags=re.DOTALL)

Slide de código genérico (monoespaciado, sin resaltado de lenguaje).

def make_javascript(tpl: str, indicator: str, sec_title: str, lines: list, cfg=None) -> str:
57def make_javascript(tpl: str, indicator: str, sec_title: str, lines: list, cfg=None) -> str:
58    return _make_lang_slide(tpl, indicator, sec_title, lines, _tokenize_js_line, 'JS', cfg)
def make_python(tpl: str, indicator: str, sec_title: str, lines: list, cfg=None) -> str:
61def make_python(tpl: str, indicator: str, sec_title: str, lines: list, cfg=None) -> str:
62    return _make_lang_slide(tpl, indicator, sec_title, lines, _tokenize_py_line, 'PY', cfg)
def make_yaml(tpl: str, indicator: str, sec_title: str, lines: list, cfg=None) -> str:
117def make_yaml(tpl: str, indicator: str, sec_title: str, lines: list, cfg=None) -> str:
118    l = cfg.lista_contenido if cfg else 'L4'
119    pt = _java_pt(len(lines))
120    s = _sec_header(tpl, indicator, sec_title, cfg)
121    parts = []
122    for line in lines:
123        if line == '':
124            parts.append(f'<text:p text:style-name="P_YAML_{pt}"/>'); continue
125        tokens = _tokenize_yaml_line(line)
126        if not tokens:
127            parts.append(f'<text:p text:style-name="P_YAML_{pt}"/>'); continue
128        spans = ''.join(
129            f'<text:span text:style-name="'
130            f'T_YAML_{pt if typ == "default" else typ.upper() + "_" + str(pt)}">'
131            f'{preserve_spaces(xe(text))}</text:span>'
132            for typ, text in tokens
133        )
134        parts.append(f'<text:p text:style-name="P_YAML_{pt}">{spans}</text:p>')
135    return re.sub(
136        rf'<text:list text:style-name="{re.escape(l)}">.*?</text:list>',
137        ''.join(parts), s, flags=re.DOTALL,
138    )
def make_xml(tpl: str, indicator: str, sec_title: str, lines: list, cfg=None) -> str:
65def make_xml(tpl: str, indicator: str, sec_title: str, lines: list, cfg=None) -> str:
66    l = cfg.lista_contenido if cfg else 'L4'
67    pt = _java_pt(len(lines))
68    s = _sec_header(tpl, indicator, sec_title, cfg)
69    parts = []
70    for line in lines:
71        if line == '':
72            parts.append(f'<text:p text:style-name="P_XML_{pt}"/>'); continue
73        tokens = _tokenize_xml_line(line)
74        if not tokens:
75            parts.append(f'<text:p text:style-name="P_XML_{pt}"/>'); continue
76        spans = ''.join(
77            f'<text:span text:style-name="'
78            f'T_XML_{pt if typ == "default" else typ.upper() + "_" + str(pt)}">'
79            f'{preserve_spaces(xe(text))}</text:span>'
80            for typ, text in tokens
81        )
82        parts.append(f'<text:p text:style-name="P_XML_{pt}">{spans}</text:p>')
83    return re.sub(
84        rf'<text:list text:style-name="{re.escape(l)}">.*?</text:list>',
85        ''.join(parts), s, flags=re.DOTALL,
86    )
def make_json(tpl: str, indicator: str, sec_title: str, lines: list, cfg=None) -> str:
 93def make_json(tpl: str, indicator: str, sec_title: str, lines: list, cfg=None) -> str:
 94    l = cfg.lista_contenido if cfg else 'L4'
 95    pt = _java_pt(len(lines))
 96    s = _sec_header(tpl, indicator, sec_title, cfg)
 97    parts = []
 98    for line in lines:
 99        if line == '':
100            parts.append(f'<text:p text:style-name="P_JSON_{pt}"/>'); continue
101        tokens = _tokenize_json_line(line)
102        if not tokens:
103            parts.append(f'<text:p text:style-name="P_JSON_{pt}"/>'); continue
104        spans = ''.join(
105            f'<text:span text:style-name="'
106            f'T_JSON_{pt if typ == "default" else typ.upper() + "_" + str(pt)}">'
107            f'{preserve_spaces(xe(text))}</text:span>'
108            for typ, text in tokens
109        )
110        parts.append(f'<text:p text:style-name="P_JSON_{pt}">{spans}</text:p>')
111    return re.sub(
112        rf'<text:list text:style-name="{re.escape(l)}">.*?</text:list>',
113        ''.join(parts), s, flags=re.DOTALL,
114    )
def make_html(tpl: str, indicator: str, sec_title: str, lines: list, cfg=None) -> str:
89def make_html(tpl: str, indicator: str, sec_title: str, lines: list, cfg=None) -> str:
90    return make_xml(tpl, indicator, sec_title, lines, cfg)
def make_jsp(tpl: str, indicator: str, sec_title: str, lines: list, cfg=None) -> str:
141def make_jsp(tpl: str, indicator: str, sec_title: str, lines: list, cfg=None) -> str:
142    l = cfg.lista_contenido if cfg else 'L4'
143    pt = _java_pt(len(lines))
144    s = _sec_header(tpl, indicator, sec_title, cfg)
145    in_sc, in_jcmt = False, False
146    parts = []
147    for line in lines:
148        if line == '':
149            parts.append(f'<text:p text:style-name="P_JSP_{pt}"/>'); continue
150        tokens, in_sc, in_jcmt = _tokenize_jsp_line(line, in_sc, in_jcmt)
151        if not tokens:
152            parts.append(f'<text:p text:style-name="P_JSP_{pt}"/>'); continue
153        spans = ''.join(
154            f'<text:span text:style-name="'
155            f'T_JSP_{pt if typ == "default" else typ.upper() + "_" + str(pt)}">'
156            f'{preserve_spaces(xe(text))}</text:span>'
157            for typ, text in tokens
158        )
159        parts.append(f'<text:p text:style-name="P_JSP_{pt}">{spans}</text:p>')
160    return re.sub(
161        rf'<text:list text:style-name="{re.escape(l)}">.*?</text:list>',
162        ''.join(parts), s, flags=re.DOTALL,
163    )
def make_css(tpl: str, indicator: str, sec_title: str, lines: list, cfg=None) -> str:
166def make_css(tpl: str, indicator: str, sec_title: str, lines: list, cfg=None) -> str:
167    l = cfg.lista_contenido if cfg else 'L4'
168    pt = _java_pt(len(lines))
169    s = _sec_header(tpl, indicator, sec_title, cfg)
170    in_cmt, in_rule = False, False
171    parts = []
172    for line in lines:
173        if line == '':
174            parts.append(f'<text:p text:style-name="P_CSS_{pt}"/>'); continue
175        tokens, in_cmt, in_rule = _tokenize_css_line(line, in_cmt, in_rule)
176        if not tokens:
177            parts.append(f'<text:p text:style-name="P_CSS_{pt}"/>'); continue
178        spans = ''.join(
179            f'<text:span text:style-name="'
180            f'T_CSS_{pt if typ == "default" else typ.upper() + "_" + str(pt)}">'
181            f'{preserve_spaces(xe(text))}</text:span>'
182            for typ, text in tokens
183        )
184        parts.append(f'<text:p text:style-name="P_CSS_{pt}">{spans}</text:p>')
185    return re.sub(
186        rf'<text:list text:style-name="{re.escape(l)}">.*?</text:list>',
187        ''.join(parts), s, flags=re.DOTALL,
188    )
def make_imagen( tpl: str, indicator: str, sec_title: str, ruta: str, media: dict, course_dir, cfg=None) -> str:
271def make_imagen(tpl: str, indicator: str, sec_title: str,
272                ruta: str, media: dict, course_dir, cfg=None) -> str:
273    zip_name = _add_image(ruta, media, course_dir)
274    if zip_name is None:
275        return _placeholder_slide(tpl, indicator, sec_title, ruta, cfg)
276    s = _sec_header(tpl, indicator, sec_title, cfg)
277    img_frame = _image_frame(zip_name, _FRAME_W, _FRAME_H, _FRAME_X, _FRAME_Y)
278    return _content_frame_re(cfg).sub(img_frame, s, count=1)
def make_imagen_texto( tpl: str, indicator: str, sec_title: str, ruta: str, items: list, media: dict, course_dir, cfg=None) -> str:
281def make_imagen_texto(tpl: str, indicator: str, sec_title: str,
282                      ruta: str, items: list,
283                      media: dict, course_dir, cfg=None) -> str:
284    l = cfg.lista_contenido if cfg else 'L4'
285    p = cfg.parrafo_contenido if cfg else 'P27'
286    sp = cfg.span_contenido if cfg else 'T6'
287    zip_name = _add_image(ruta, media, course_dir)
288    if zip_name is None:
289        return _placeholder_slide(tpl, indicator, sec_title, ruta, cfg)
290    s = _sec_header(tpl, indicator, sec_title, cfg)
291    img_frame  = _image_frame(zip_name, _COL_W_2, _FRAME_H, _FRAME_X, _FRAME_Y)
292    text_frame = _new_frame(_COL_W_2, _FRAME_H, _COL_X2, _FRAME_Y,
293                            f'<draw:text-box>{build_list(l, p, sp, items)}</draw:text-box>')
294    return _content_frame_re(cfg).sub(img_frame + text_frame, s, count=1)
def build_slides_from_yaml( t: SlideTemplates, data: dict, ctx: dict = None) -> list:
122def build_slides_from_yaml(t: SlideTemplates, data: dict, ctx: dict = None) -> list:
123    """Build a complete slide list from a YAML content dict."""
124    ctx          = ctx or {}
125    media        = ctx.get('media', {})
126    course_dir   = ctx.get('course_dir')
127    extra_styles = ctx.get('extra_styles', [])
128    cfg          = ctx.get('cfg')
129
130    extra_styles.append(subtitle_text_style(
131        getattr(cfg, 'color_subtitulo', '#000000') if cfg else '#000000'
132    ))
133
134    slides = []
135    meta = data['meta']
136    secciones = data['secciones']
137    indice_list = data.get('indice', [])
138
139    index_titles = indice_list if indice_list else [s['titulo'] for s in secciones]
140
141    slides.append(make_portada(t.portada, meta['portada'], meta['unidad'], cfg=cfg))
142    _emit_list(slides, make_objectives, t.objectives, data['objetivos'], 'Objetivos', cfg=cfg)
143    _emit_list(slides, make_index,      t.index,      index_titles,      'Índice',   cfg=cfg)
144
145    for i, sec in enumerate(secciones, 1):
146        num = str(sec['indice']) if 'indice' in sec else str(i)
147        if 'titulo' in sec:
148            titulo = sec['titulo']
149        elif 'indice' in sec and indice_list:
150            idx = sec['indice'] - 1
151            titulo = indice_list[idx] if 0 <= idx < len(indice_list) else ''
152        else:
153            titulo = ''
154
155        if 'intro' in sec:
156            slides.append(make_single(t.single, num, titulo, sec['intro'], cfg=cfg))
157        for slide in sec.get('slides', []):
158            tipo    = slide['tipo']
159            texto   = slide.get('texto', '')
160            items   = texto.splitlines() if texto else slide.get('items', [])
161            fichero = slide.get('fichero', '')
162            notas   = slide.get('notas', '')
163            if tipo == 'bullets':
164                _emit_list(slides, make_bullets, t.bullets, items, titulo, num, titulo, cfg=cfg,
165                           notas=notas, subtitle=slide.get('titulo', ''))
166            elif tipo == 'code':
167                lines = [
168                    ('cmt', item['cmt']) if isinstance(item, dict) and 'cmt' in item
169                    else ('ok', item['ok']) if isinstance(item, dict) and 'ok' in item
170                    else item
171                    for item in items
172                ]
173                xml = make_code(t.bullets, num, titulo, lines, cfg=cfg)
174                slides.append(inject_notes(xml, notas) if notas else xml)
175            elif tipo == 'java':
176                _emit_code(slides, make_java, t.bullets, num, titulo, items, fichero, cfg, notas=notas)
177            elif tipo == 'salida':
178                lines = [
179                    ('err', item['err']) if isinstance(item, dict) and 'err' in item
180                    else ('ok', item['ok']) if isinstance(item, dict) and 'ok' in item
181                    else item
182                    for item in items
183                ]
184                xml = make_salida(t.bullets, num, titulo, lines, cfg=cfg)
185                slides.append(inject_notes(xml, notas) if notas else xml)
186            elif tipo == 'terminal':
187                xml = make_terminal(t.bullets, num, titulo, items, cfg=cfg)
188                slides.append(inject_notes(xml, notas) if notas else xml)
189            elif tipo == 'diagrama':
190                xml = make_diagrama(t.bullets, num, titulo, items, cfg=cfg)
191                slides.append(inject_notes(xml, notas) if notas else xml)
192            elif tipo == 'tabla':
193                xml = make_tabla(t.bullets, num, titulo,
194                                 slide.get('cabeceras', []),
195                                 slide.get('filas', []),
196                                 slide.get('colores'),
197                                 extra_styles, cfg=cfg)
198                slides.append(inject_notes(xml, notas) if notas else xml)
199            elif tipo == 'aviso':
200                xml = make_aviso(t.single, num, titulo,
201                                 slide['nivel'], slide['texto'], cfg=cfg)
202                slides.append(inject_notes(xml, notas) if notas else xml)
203            elif tipo == 'dos_columnas':
204                xml = make_dos_columnas(t.bullets, num, titulo,
205                                        slide['izquierda'], slide['derecha'], cfg=cfg)
206                slides.append(inject_notes(xml, notas) if notas else xml)
207            elif tipo == 'comparar':
208                _emit_comparar(slides, make_comparar, t.bullets, num, titulo,
209                               slide['izquierda'], slide['derecha'], cfg=cfg, notas=notas)
210            elif tipo == 'comparar_python':
211                _emit_comparar(slides, make_comparar_python, t.bullets, num, titulo,
212                               slide['izquierda'], slide['derecha'], cfg=cfg, notas=notas)
213            elif tipo == 'comparar_js':
214                _emit_comparar(slides, make_comparar_js, t.bullets, num, titulo,
215                               slide['izquierda'], slide['derecha'], cfg=cfg, notas=notas)
216            elif tipo in ('comparar_xml', 'comparar_html'):
217                _emit_comparar(slides, make_comparar_xml, t.bullets, num, titulo,
218                               slide['izquierda'], slide['derecha'], cfg=cfg, notas=notas)
219            elif tipo == 'comparar_jsp':
220                _emit_comparar(slides, make_comparar_jsp, t.bullets, num, titulo,
221                               slide['izquierda'], slide['derecha'], cfg=cfg, notas=notas)
222            elif tipo == 'comparar_css':
223                _emit_comparar(slides, make_comparar_css, t.bullets, num, titulo,
224                               slide['izquierda'], slide['derecha'], cfg=cfg, notas=notas)
225            elif tipo == 'comparar_json':
226                _emit_comparar(slides, make_comparar_json, t.bullets, num, titulo,
227                               slide['izquierda'], slide['derecha'], cfg=cfg, notas=notas)
228            elif tipo == 'codigo':
229                _emit_code(slides, make_codigo, t.bullets, num, titulo, items, fichero, cfg, notas=notas)
230            elif tipo == 'javascript':
231                _emit_code(slides, make_javascript, t.bullets, num, titulo, items, fichero, cfg, notas=notas)
232            elif tipo == 'python':
233                _emit_code(slides, make_python, t.bullets, num, titulo, items, fichero, cfg, notas=notas)
234            elif tipo == 'yaml':
235                _emit_code(slides, make_yaml, t.bullets, num, titulo, items, fichero, cfg, notas=notas)
236            elif tipo in ('xml', 'html'):
237                _emit_code(slides, make_xml, t.bullets, num, titulo, items, fichero, cfg, notas=notas)
238            elif tipo == 'jsp':
239                _emit_code(slides, make_jsp, t.bullets, num, titulo, items, fichero, cfg, notas=notas)
240            elif tipo == 'css':
241                _emit_code(slides, make_css, t.bullets, num, titulo, items, fichero, cfg, notas=notas)
242            elif tipo == 'json':
243                _emit_code(slides, make_json, t.bullets, num, titulo, items, fichero, cfg, notas=notas)
244            elif tipo == 'imagen':
245                xml = make_imagen(t.bullets, num, titulo,
246                                  slide['ruta'], media, course_dir, cfg=cfg)
247                slides.append(inject_notes(xml, notas) if notas else xml)
248            elif tipo == 'imagen_texto':
249                xml = make_imagen_texto(t.bullets, num, titulo,
250                                        slide['ruta'], items,
251                                        media, course_dir, cfg=cfg)
252                slides.append(inject_notes(xml, notas) if notas else xml)
253            elif tipo == 'texto':
254                xml = make_single(t.single, num,
255                                  slide.get('titulo', titulo),
256                                  slide.get('texto', ''), cfg=cfg)
257                slides.append(inject_notes(xml, notas) if notas else xml)
258            elif tipo == 'cita':
259                cita_txt = slide.get('cita', '')
260                autor    = slide.get('autor', '')
261                fuente   = slide.get('fuente', '')
262                atrib    = f'— {autor}' + (f', {fuente}' if fuente else '') if autor else ''
263                body     = f{cita_txt}»' + (f'\n\n{atrib}' if atrib else '')
264                xml = make_single(t.single, num,
265                                  slide.get('titulo', titulo), body, cfg=cfg)
266                slides.append(inject_notes(xml, notas) if notas else xml)
267
268    conclusion = data['conclusion']
269    if isinstance(conclusion, list):
270        _emit_list(slides, make_bullets, t.bullets, conclusion, 'Conclusiones',
271                   'C', 'Conclusiones', cfg=cfg)
272    else:
273        slides.append(make_single(t.conclusion, 'C', 'Conclusiones', conclusion, cfg=cfg))
274    refs = [tuple(r) if isinstance(r, list) else r for r in data['referencias']]
275    slides.append(make_refs(t.references, refs, cfg=cfg))
276    slides.append(t.contact)
277    slides.append(t.license)
278
279    return slides

Build a complete slide list from a YAML content dict.

def _java_pt(n: int) -> int:
27def _java_pt(n: int) -> int:
28    """Font size for a Java code slide with n lines."""
29    for max_n, pt in _JAVA_SIZES:
30        if n <= max_n:
31            return pt
32    return 9

Font size for a Java code slide with n lines.

def build_list(style_list: str, style_p: str, style_span: str, items: list) -> str:
64def build_list(style_list: str, style_p: str, style_span: str, items: list) -> str:
65    parts = [
66        f'<text:list-item>'
67        f'<text:p text:style-name="{style_p}">'
68        f'<text:span text:style-name="{style_span}">{_xe_line_with_links(item, style_span)}</text:span>'
69        f'</text:p>'
70        f'</text:list-item>'
71        for item in items
72    ]
73    return f'<text:list text:style-name="{style_list}">{"".join(parts)}</text:list>'
def extract_slides(content: str) -> list:
118def extract_slides(content: str) -> list:
119    slides, pos = [], 0
120    while True:
121        start = content.find('<draw:page ', pos)
122        if start == -1:
123            break
124        end = content.find('</draw:page>', start) + len('</draw:page>')
125        slides.append(content[start:end])
126        pos = end
127    return slides
def finalize(slide: str, n: int) -> str:
130def finalize(slide: str, n: int) -> str:
131    s = re.sub(r'(draw:page-number=")(\d+)(")', rf'\g<1>{n}\3', slide)
132    s = re.sub(r'(draw:page draw:name=")([^"]+)(")', rf'\g<1>page{n}\3', s, count=1)
133    return s
def inject_notes(slide_xml: str, notes_text: str) -> str:
101def inject_notes(slide_xml: str, notes_text: str) -> str:
102    """Inject speaker notes into the presentation:notes text-box of a slide."""
103    if not notes_text:
104        return slide_xml
105    paragraphs = ''.join(
106        f'<text:p>{xe(line)}</text:p>'
107        for line in notes_text.split('\n')
108    )
109    return re.sub(
110        r'(<presentation:notes\b.*?<draw:text-box)/>',
111        rf'\g<1>>{paragraphs}</draw:text-box>',
112        slide_xml,
113        count=1,
114        flags=re.DOTALL,
115    )

Inject speaker notes into the presentation:notes text-box of a slide.

def xe_multiline(text: str, span_style: str) -> str:
43def xe_multiline(text: str, span_style: str) -> str:
44    """Como xe() pero:
45    - convierte \\n en <text:line-break/> (cerrando y reabriendo el span)
46    - convierte URLs (https?://…) en <text:a> clicables
47    Devuelve contenido para insertar entre el span de apertura y cierre.
48    """
49    open_s  = f'<text:span text:style-name="{span_style}">'
50    close_s = '</text:span>'
51    lines = text.split('\n')
52    processed = [_xe_line_with_links(line, span_style) for line in lines]
53    return (close_s + '<text:line-break/>' + open_s).join(processed)

Como xe() pero:

  • convierte \n en (cerrando y reabriendo el span)
  • convierte URLs (https?://…) en clicables Devuelve contenido para insertar entre el span de apertura y cierre.