sim-manajement-book/scripts/generate-marp-slides.py
hb_alim 54f974f95f feat: generate Marp slides (Gaia theme) untuk semua 18 bab
- 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
2026-04-25 13:19:47 +07:00

615 lines
19 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/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 codeblock
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} &nbsp;|&nbsp; {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()