- Script: scripts/generate-marp-slides.py - Output: slides/marp/slide-bab-01.md ... slide-bab-18.md - 14-15 slides per bab, struktur: Cover, Reader Outcome, Pemantik, Model Utama (Mermaid), Definisi Kunci, Konsep Inti (2 slides), Salah Kaprah, Studi Kasus, Template A.N, Rangkuman, Final Statement, Latihan + Menuju Bab - Theme: Gaia invert, warna: gold headers, teal h2, sky blue h3
615 lines
19 KiB
Python
615 lines
19 KiB
Python
#!/usr/bin/env python3
|
||
# -*- coding: utf-8 -*-
|
||
"""
|
||
Generate Marp slides (Gaia theme, invert) for all 18 chapters of:
|
||
"Sistem Informasi Manajemen di Era AI: Perspektif Strategis dan Pengambilan Keputusan"
|
||
Helmi Bahar Alim, S.Kom., M.Kom.
|
||
|
||
Output: slides/marp/slide-bab-NN.md (one file per chapter)
|
||
Run : python scripts/generate-marp-slides.py
|
||
"""
|
||
|
||
import re
|
||
import os
|
||
from pathlib import Path
|
||
|
||
# ─── Paths ────────────────────────────────────────────────────────────────────
|
||
ROOT = Path(__file__).parent.parent
|
||
CHAPTERS = ROOT / "chapters"
|
||
OUT_DIR = ROOT / "slides" / "marp"
|
||
OUT_DIR.mkdir(parents=True, exist_ok=True)
|
||
|
||
AUTHOR = "Helmi Bahar Alim, S.Kom., M.Kom."
|
||
INSTITUTION = "Universitas Putra Bangsa — Kebumen"
|
||
|
||
# ─── Extractors ──────────────────────────────────────────────────────────────
|
||
|
||
def get_text(content: str) -> str:
|
||
"""Strip all markdown formatting for plain text usage."""
|
||
txt = re.sub(r'\*+', '', content)
|
||
txt = re.sub(r'`+[^`]*`+', lambda m: m.group(0).strip('`'), txt)
|
||
return txt.strip()
|
||
|
||
|
||
def extract_title_bagian(content: str):
|
||
"""Return (bab_num, title, bagian, reader_outcome, level)."""
|
||
# Strip BOM and search multiline for H1
|
||
m = re.search(r'^[\ufeff]*# BAB (\d+)\s+[—\-]{1,3}\s+(.+)', content, re.MULTILINE)
|
||
bab_num = int(m.group(1)) if m else 0
|
||
title = m.group(2).strip() if m else "?"
|
||
|
||
bagian_m = re.search(r'Bagian\s*:\s*(.+)', content)
|
||
bagian = bagian_m.group(1).strip() if bagian_m else ""
|
||
|
||
# reader outcome may span multiple lines inside the code‑block
|
||
ro_m = re.search(
|
||
r'Reader Outcome\s*:\s*(.+?)(?=Level\s*:|\`\`\`)',
|
||
content, re.DOTALL
|
||
)
|
||
if ro_m:
|
||
ro = re.sub(r'\s+', ' ', ro_m.group(1)).strip().rstrip(',.')
|
||
else:
|
||
ro = ""
|
||
|
||
level_m = re.search(r'Level\s*:\s*(.+)', content)
|
||
level = level_m.group(1).strip() if level_m else "Menengah"
|
||
|
||
return bab_num, title, bagian, ro, level
|
||
|
||
|
||
def extract_opening(content: str, bab_num: int):
|
||
"""Return (hook_sentence, pemantik_question)."""
|
||
header = f'## {bab_num}.1 Pembuka'
|
||
next_h = f'## {bab_num}.2'
|
||
m = re.search(
|
||
re.escape(header) + r'\n(.+?)(?=' + re.escape(next_h) + ')',
|
||
content, re.DOTALL
|
||
)
|
||
if not m:
|
||
return ("", "")
|
||
body = m.group(1).strip()
|
||
# split into paragraphs (double newline separated)
|
||
paras = [p.strip() for p in re.split(r'\n{2,}', body) if p.strip()]
|
||
# First non-empty, non-'---' paragraph is the hook
|
||
hook = ""
|
||
for p in paras:
|
||
if p != '---' and not p.startswith('#') and len(p) > 30:
|
||
hook = p
|
||
break
|
||
# Last paragraph is usually the pemantik question
|
||
pemantik = ""
|
||
for p in reversed(paras):
|
||
if '?' in p and len(p) > 20 and not p.startswith('---'):
|
||
pemantik = p
|
||
break
|
||
# Trim hook to end at a sentence boundary (max 350 chars)
|
||
if len(hook) > 350:
|
||
hook = hook[:350]
|
||
last_dot = max(hook.rfind('. '), hook.rfind('! '), hook.rfind('? '))
|
||
if last_dot > 150:
|
||
hook = hook[:last_dot+1]
|
||
hook = hook.rstrip()
|
||
pemantik = pemantik[:300].rstrip()
|
||
return hook, pemantik
|
||
|
||
|
||
def extract_mermaid(content: str, bab_num: int):
|
||
"""Extract the first mermaid code block (Gambar N.1 — Signature Model)."""
|
||
m = re.search(r'```mermaid\n(.+?)```', content, re.DOTALL)
|
||
if m:
|
||
return m.group(1).strip()
|
||
return ""
|
||
|
||
|
||
def extract_gambar_caption(content: str, gambar_label: str):
|
||
"""Find **Gambar N.1 — ...** caption."""
|
||
m = re.search(
|
||
re.escape(f'**{gambar_label}') + r' — (.+?)\*\*',
|
||
content
|
||
)
|
||
return m.group(1).strip() if m else ""
|
||
|
||
|
||
def extract_definisi(content: str, bab_num: int):
|
||
"""Return list of (term, first_sentence_of_def, manajerial_relevance) up to 3."""
|
||
section_m = re.search(
|
||
rf'## {bab_num}\.3 Definisi Kunci\n(.+?)(?=\n## {bab_num}\.\d)',
|
||
content, re.DOTALL
|
||
)
|
||
if not section_m:
|
||
return []
|
||
body = section_m.group(1)
|
||
# Each definition starts with **Term (*English*)** or **Term**
|
||
entries = re.split(r'\n---\n', body)
|
||
results = []
|
||
for entry in entries:
|
||
term_m = re.match(r'\*\*(.+?)\*\*', entry.strip())
|
||
if not term_m:
|
||
continue
|
||
term = re.sub(r'\(.+?\)', '', term_m.group(1)).strip()
|
||
# Definition text: first sentence (up to first .)
|
||
def_text_m = re.search(r'\*\*\n(.+?)(?=\*\*Relevansi|\Z)', entry, re.DOTALL)
|
||
if not def_text_m:
|
||
# try alt: text right after term line
|
||
lines = entry.strip().split('\n')
|
||
def_text = ' '.join(l.strip() for l in lines[1:4] if l.strip() and not l.startswith('**'))
|
||
else:
|
||
def_text = def_text_m.group(1).strip().split('\n')[0]
|
||
rel_m = re.search(r'\*\*Relevansi manajerial:\*\*\s*(.+)', entry)
|
||
relevance = rel_m.group(1).strip()[:160] if rel_m else ""
|
||
results.append((
|
||
term.strip('* \n'),
|
||
re.sub(r'\s+', ' ', def_text)[:150],
|
||
re.sub(r'\s+', ' ', relevance)
|
||
))
|
||
if len(results) >= 3:
|
||
break
|
||
return results
|
||
|
||
|
||
def extract_konsep_subsections(content: str, bab_num: int):
|
||
"""Return list of (number, title) for all 3.4.x subsections."""
|
||
pattern = rf'### {bab_num}\.4\.(\d+)\s+(.+)'
|
||
matches = re.findall(pattern, content)
|
||
return [(n, t.strip()) for n, t in matches]
|
||
|
||
|
||
def extract_salah_kaprah(content: str, bab_num: int):
|
||
"""Return list of (myth, correction) up to 4."""
|
||
section_m = re.search(
|
||
rf'## {bab_num}\.7 Salah Kaprah\n(.+?)(?=\n## {bab_num}\.\d)',
|
||
content, re.DOTALL
|
||
)
|
||
if not section_m:
|
||
return []
|
||
body = section_m.group(1)
|
||
# Myths: ***"..."*** or ***...***
|
||
myths = re.findall(r'\*{3}(.+?)\*{3}', body)
|
||
corrections = re.findall(r'\*\*Koreksi:\*\*\s*(.+?)(?=\n\*{3}|\Z)', body, re.DOTALL)
|
||
pairs = []
|
||
for i, myth in enumerate(myths[:4]):
|
||
raw_corr = corrections[i].strip() if i < len(corrections) else ""
|
||
# Take first sentence only for slide
|
||
first_sent = re.split(r'(?<=[.!?])\s', raw_corr)[0]
|
||
corr = first_sent.strip()[:160]
|
||
pairs.append((myth.strip('"').strip()[:80], corr))
|
||
return pairs
|
||
|
||
|
||
def extract_studi_kasus_titles(content: str, bab_num: int):
|
||
"""Return (case_a_title, case_b_title)."""
|
||
a_m = re.search(
|
||
rf'### Studi Kasus A \(Dasar\): (.+?)(?:\n|$)',
|
||
content
|
||
)
|
||
b_m = re.search(
|
||
rf'### Studi Kasus B \(Lanjutan\): (.+?)(?:\n|$)',
|
||
content
|
||
)
|
||
a = a_m.group(1).strip() if a_m else ""
|
||
b = b_m.group(1).strip() if b_m else ""
|
||
return a, b
|
||
|
||
|
||
def extract_studi_kasus_body(content: str, label: str, bab_num: int):
|
||
"""Extract first ~300 chars body text from a studi kasus block."""
|
||
key = f"### Studi Kasus {label}"
|
||
m = re.search(
|
||
re.escape(key) + r'.+?\n\n(.+?)(?=\n###|\n## )',
|
||
content, re.DOTALL
|
||
)
|
||
if m:
|
||
body = re.sub(r'\*\*[^*]+\*\*\n', '', m.group(1)).strip()
|
||
body = re.sub(r'\s+', ' ', body)
|
||
return body[:280]
|
||
return ""
|
||
|
||
|
||
def extract_template(content: str, bab_num: int):
|
||
"""Return (template_name, template_body_first_section)."""
|
||
name_m = re.search(
|
||
rf'### Template A\.{bab_num} — (.+)',
|
||
content
|
||
)
|
||
name = name_m.group(1).strip() if name_m else f"Template A.{bab_num}"
|
||
# Get the template table / code block
|
||
tmpl_body_m = re.search(
|
||
rf'Template A\.{bab_num} — .+?\n(.+?)(?=\n## {bab_num}\.\d)',
|
||
content, re.DOTALL
|
||
)
|
||
if tmpl_body_m:
|
||
body = tmpl_body_m.group(1).strip()
|
||
# trim to first code block or first 10 lines
|
||
lines = body.split('\n')[:30]
|
||
return name, '\n'.join(lines)
|
||
return name, ""
|
||
|
||
|
||
def extract_rangkuman(content: str, bab_num: int):
|
||
"""Return list of rangkuman bullet points (up to 5)."""
|
||
section_m = re.search(
|
||
rf'## {bab_num}\.11 Rangkuman\n(.+?)(?=\n## {bab_num}\.\d|\n## Referensi)',
|
||
content, re.DOTALL
|
||
)
|
||
if not section_m:
|
||
return []
|
||
body = section_m.group(1)
|
||
items = re.findall(r'\d+\.\s+(.+?)(?=\n\d+\.|\n\n---|\Z)', body, re.DOTALL)
|
||
result = []
|
||
for item in items[:5]:
|
||
item_clean = re.sub(r'\s+', ' ', item).strip()
|
||
# keep first sentence
|
||
first = re.split(r'(?<=[.!?])\s', item_clean)[0]
|
||
result.append(first[:160])
|
||
return result
|
||
|
||
|
||
def extract_final_statement(content: str):
|
||
"""Return the 🔥 final statement quote."""
|
||
# Look for blockquote near the end of rangkuman section
|
||
# Pattern: > "..." standalone quote
|
||
m_list = re.findall(r'^\*"(.+?)"\*', content, re.MULTILINE)
|
||
if m_list:
|
||
return m_list[-1].strip()
|
||
# Try blockquote form
|
||
m2 = re.search(r'> "(.+?)"', content, re.DOTALL)
|
||
if m2:
|
||
return re.sub(r'\s+', ' ', m2.group(1)).strip()
|
||
return ""
|
||
|
||
|
||
def extract_latihan(content: str, bab_num: int):
|
||
"""Return latihan artefact title and first instruction line."""
|
||
lat_m = re.search(
|
||
rf'\*\*Latihan {bab_num}\.1 — (.+?)\*\*',
|
||
content
|
||
)
|
||
title = lat_m.group(1).strip() if lat_m else ""
|
||
instr_m = re.search(
|
||
rf'Gunakan Template A\.{bab_num} (.+?)(?=\n\n|\Z)',
|
||
content
|
||
)
|
||
instr = instr_m.group(1).strip()[:200] if instr_m else ""
|
||
return title, instr
|
||
|
||
|
||
def extract_menuju_bab(content: str):
|
||
"""Return (next_bab_num, transition_text)."""
|
||
m = re.search(
|
||
r'\*\*Menuju Bab (\d+):\*\*\n(.+?)(?=\n---|\n\*")',
|
||
content, re.DOTALL
|
||
)
|
||
if m:
|
||
nxt = int(m.group(1))
|
||
text = re.sub(r'\s+', ' ', m.group(2)).strip()[:280]
|
||
return nxt, text
|
||
return None, ""
|
||
|
||
# ─── Slide builder ────────────────────────────────────────────────────────────
|
||
|
||
MARP_HEADER = '''\
|
||
---
|
||
marp: true
|
||
theme: gaia
|
||
class: invert
|
||
paginate: true
|
||
header: "{header}"
|
||
footer: "{author} | {institution}"
|
||
style: |
|
||
section {{
|
||
font-family: 'Segoe UI', Helvetica, sans-serif;
|
||
font-size: 21px;
|
||
}}
|
||
h1 {{ color: #ffd166; }}
|
||
h2 {{
|
||
color: #06d6a0;
|
||
border-bottom: 2px solid #06d6a060;
|
||
padding-bottom: 4px;
|
||
}}
|
||
h3 {{ color: #8ecae6; }}
|
||
blockquote {{
|
||
border-left: 4px solid #ffd166;
|
||
background: #ffffff15;
|
||
padding: 0.5em 1em;
|
||
font-style: italic;
|
||
}}
|
||
table {{ font-size: 18px; width: 100%; }}
|
||
th {{ background: #06d6a040; }}
|
||
code {{ background: #ffffff20; }}
|
||
.lead h1 {{ font-size: 2em; color: #ffd166; }}
|
||
.lead h2 {{ font-size: 1.3em; border: none; color: #e0e0e0; }}
|
||
.bagian {{ font-size: 0.8em; color: #8ecae6; letter-spacing: 1px; }}
|
||
.lead p {{ font-size: 0.9em; color: #c0c0c0; }}
|
||
---
|
||
'''
|
||
|
||
SLIDE_SEP = '\n---\n'
|
||
|
||
def build_slides(bab_num: int, title: str, bagian: str, reader_outcome: str,
|
||
level: str, content: str) -> str:
|
||
pad = f'{bab_num:02d}'
|
||
header = f'BAB {bab_num}'
|
||
bagian_short = bagian.split('—')[0].strip() if '—' in bagian else bagian
|
||
bagian_full = bagian
|
||
|
||
# Extract all parts
|
||
hook, pemantik = extract_opening(content, bab_num)
|
||
mermaid = extract_mermaid(content, bab_num)
|
||
fig1_caption = extract_gambar_caption(content, f'Gambar {bab_num}.1')
|
||
defs = extract_definisi(content, bab_num)
|
||
konsep = extract_konsep_subsections(content, bab_num)
|
||
salah_kaprah = extract_salah_kaprah(content, bab_num)
|
||
case_a_title, case_b_title = extract_studi_kasus_titles(content, bab_num)
|
||
case_a_body = extract_studi_kasus_body(content, 'A (Dasar)', bab_num)
|
||
case_b_body = extract_studi_kasus_body(content, 'B (Lanjutan)', bab_num)
|
||
tmpl_name, tmpl_body = extract_template(content, bab_num)
|
||
rangkuman = extract_rangkuman(content, bab_num)
|
||
final = extract_final_statement(content)
|
||
lat_title, lat_instr = extract_latihan(content, bab_num)
|
||
next_bab, menuju_text = extract_menuju_bab(content)
|
||
|
||
slides_md = MARP_HEADER.format(
|
||
header=f'BAB {bab_num} — {title}',
|
||
author=AUTHOR,
|
||
institution=INSTITUTION
|
||
)
|
||
|
||
# ── Slide 1: Cover ──────────────────────────────────────────────────────
|
||
slides_md += f'''\
|
||
<!-- _class: lead invert -->
|
||
|
||
# BAB {bab_num}
|
||
## {title}
|
||
|
||
<p class="bagian">{bagian_full}</p>
|
||
|
||
**Level:** {level}
|
||
|
||
{SLIDE_SEP}'''
|
||
|
||
# ── Slide 2: Reader Outcome ──────────────────────────────────────────────
|
||
slides_md += f'''\
|
||
## Reader Outcome
|
||
|
||
> {reader_outcome}
|
||
|
||
| Info | Detail |
|
||
|------|--------|
|
||
| **Bagian** | {bagian_full} |
|
||
| **Level** | {level} |
|
||
| **Sub-topik** | {len(konsep)} konsep inti |
|
||
|
||
{SLIDE_SEP}'''
|
||
|
||
# ── Slide 3: Pertanyaan Pemantik ─────────────────────────────────────────
|
||
if hook:
|
||
slides_md += f'''\
|
||
<!-- _class: invert -->
|
||
|
||
## Pertanyaan Pemantik
|
||
|
||
{hook}
|
||
|
||
---
|
||
|
||
_{pemantik}_
|
||
|
||
{SLIDE_SEP}'''
|
||
else:
|
||
slides_md += f'''\
|
||
<!-- _class: invert -->
|
||
|
||
## Pertanyaan Pemantik
|
||
|
||
_{pemantik}_
|
||
|
||
{SLIDE_SEP}'''
|
||
|
||
# ── Slide 4: Signature Model ─────────────────────────────────────────────
|
||
if mermaid:
|
||
caption = fig1_caption if fig1_caption else f"Signature Model — Bab {bab_num}"
|
||
slides_md += f'''\
|
||
## Model Utama — Gambar {bab_num}.1
|
||
|
||
```mermaid
|
||
{mermaid}
|
||
```
|
||
|
||
**{caption}**
|
||
|
||
{SLIDE_SEP}'''
|
||
else:
|
||
slides_md += f'''\
|
||
## Model Utama
|
||
|
||
_(Lihat Gambar {bab_num}.1 di buku teks)_
|
||
|
||
{SLIDE_SEP}'''
|
||
|
||
# ── Slide 5: Definisi Kunci ──────────────────────────────────────────────
|
||
if defs:
|
||
defs_md = ""
|
||
for term, defi, relevance in defs:
|
||
defs_md += f'**{term}**\n{defi}\n\n> _{relevance}_\n\n'
|
||
slides_md += f'''\
|
||
## Definisi Kunci
|
||
|
||
{defs_md.strip()}
|
||
|
||
{SLIDE_SEP}'''
|
||
else:
|
||
slides_md += f'''\
|
||
## Definisi Kunci
|
||
|
||
_(Lihat Seksi {bab_num}.3 di buku teks)_
|
||
|
||
{SLIDE_SEP}'''
|
||
|
||
# ── Slide 6 & 7: Konsep Inti ─────────────────────────────────────────────
|
||
if konsep:
|
||
half = (len(konsep) + 1) // 2
|
||
part1 = konsep[:half]
|
||
part2 = konsep[half:]
|
||
|
||
list1 = '\n'.join(f'- **{int(n)}.** {t}' for n, t in part1)
|
||
slides_md += f'''\
|
||
## Konsep Inti — Bagian 1
|
||
|
||
{list1}
|
||
|
||
{SLIDE_SEP}'''
|
||
|
||
if part2:
|
||
list2 = '\n'.join(f'- **{int(n)}.** {t}' for n, t in part2)
|
||
slides_md += f'''\
|
||
## Konsep Inti — Bagian 2
|
||
|
||
{list2}
|
||
|
||
{SLIDE_SEP}'''
|
||
|
||
# ── Slide 8: Salah Kaprah ────────────────────────────────────────────────
|
||
if salah_kaprah:
|
||
traps_md = ""
|
||
for myth, correction in salah_kaprah:
|
||
traps_md += f'> ⚠️ _"{myth}"_\n\n↳ {correction}\n\n'
|
||
slides_md += f'''\
|
||
## ⚠️ Salah Kaprah
|
||
|
||
{traps_md.strip()}
|
||
|
||
{SLIDE_SEP}'''
|
||
|
||
# ── Slide 9: Studi Kasus ─────────────────────────────────────────────────
|
||
case_slides = ""
|
||
if case_a_title:
|
||
summary_a = case_a_body[:250] if case_a_body else ""
|
||
case_slides += f'''\
|
||
📊 **Dasar:** {case_a_title}
|
||
|
||
{summary_a}
|
||
|
||
'''
|
||
if case_b_title:
|
||
summary_b = case_b_body[:250] if case_b_body else ""
|
||
case_slides += f'''\
|
||
📊 **Lanjutan:** {case_b_title}
|
||
|
||
{summary_b}
|
||
|
||
'''
|
||
if case_slides:
|
||
slides_md += f'''\
|
||
## Studi Kasus
|
||
|
||
{case_slides.strip()}
|
||
|
||
{SLIDE_SEP}'''
|
||
|
||
# ── Slide 10: Template ───────────────────────────────────────────────────
|
||
# Show first ~15 lines of template body (code block or table)
|
||
tmpl_preview_lines = [ln for ln in tmpl_body.split('\n') if ln.strip()][:18]
|
||
tmpl_preview = '\n'.join(tmpl_preview_lines)
|
||
slides_md += f'''\
|
||
## 🔧 Template A.{bab_num}
|
||
### {tmpl_name}
|
||
|
||
```
|
||
{tmpl_preview}
|
||
```
|
||
|
||
{SLIDE_SEP}'''
|
||
|
||
# ── Slide 11: Rangkuman ──────────────────────────────────────────────────
|
||
if rangkuman:
|
||
rang_md = '\n'.join(f'{i+1}. {item}' for i, item in enumerate(rangkuman))
|
||
slides_md += f'''\
|
||
## Rangkuman
|
||
|
||
{rang_md}
|
||
|
||
{SLIDE_SEP}'''
|
||
|
||
# ── Slide 12: Final Statement ─────────────────────────────────────────────
|
||
if final:
|
||
slides_md += f'''\
|
||
<!-- _class: lead invert -->
|
||
|
||
## 🔥 Final Statement
|
||
|
||
> "{final}"
|
||
|
||
{SLIDE_SEP}'''
|
||
|
||
# ── Slide 13: Latihan + Menuju Bab ───────────────────────────────────────
|
||
latihan_block = ""
|
||
if lat_title:
|
||
latihan_block = f'''\
|
||
### 📝 Latihan {bab_num}.1 — {lat_title}
|
||
|
||
{lat_instr}
|
||
|
||
'''
|
||
menuju_block = ""
|
||
if next_bab and menuju_text:
|
||
menuju_block = f'''\
|
||
### ➡️ Menuju Bab {next_bab}
|
||
|
||
_{menuju_text[:200]}_
|
||
'''
|
||
slides_md += f'''\
|
||
## Latihan & Refleksi
|
||
|
||
{latihan_block}{menuju_block}
|
||
|
||
{SLIDE_SEP}'''
|
||
|
||
return slides_md
|
||
|
||
|
||
# ─── Main ─────────────────────────────────────────────────────────────────────
|
||
|
||
def main():
|
||
print("Generating Marp slides for 18 chapters...")
|
||
print(f"Output directory: {OUT_DIR}")
|
||
print()
|
||
|
||
success = 0
|
||
errors = []
|
||
|
||
for bab_num in range(1, 19):
|
||
pad = f'{bab_num:02d}'
|
||
src_path = CHAPTERS / f'bab-{pad}.md'
|
||
|
||
if not src_path.exists():
|
||
print(f" [SKIP] bab-{pad}.md not found")
|
||
errors.append(f"bab-{pad}.md not found")
|
||
continue
|
||
|
||
content = src_path.read_text(encoding='utf-8-sig')
|
||
bab_n, title, bagian, ro, level = extract_title_bagian(content)
|
||
|
||
if bab_n == 0:
|
||
print(f" [WARN] Could not parse title in bab-{pad}.md")
|
||
bab_n = bab_num
|
||
|
||
slides = build_slides(bab_n, title, bagian, ro, level, content)
|
||
|
||
out_path = OUT_DIR / f'slide-bab-{pad}.md'
|
||
out_path.write_text(slides, encoding='utf-8')
|
||
|
||
# Count slides in output
|
||
n_slides = slides.count('\n---\n')
|
||
print(f" [OK] bab-{pad:>2} — {title[:50]:<50} ({n_slides} slides)")
|
||
success += 1
|
||
|
||
print()
|
||
print(f"Done: {success}/18 chapters generated.")
|
||
if errors:
|
||
print(f"Errors: {errors}")
|
||
print(f"\nOpen any file in VS Code with Marp extension to preview.")
|
||
print(f"CLI export: marp slides/marp/slide-bab-NN.md --pdf")
|
||
|
||
|
||
if __name__ == '__main__':
|
||
main()
|