laporan-generator 2.7.0 → 2.9.0

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.
Files changed (58) hide show
  1. package/.agents/skills/laporan-generator/SKILL.md +30 -15
  2. package/.agents/skills/laporan-generator/references/campus-guide.md +8 -2
  3. package/.agents/skills/laporan-generator/references/metadata-schema.md +39 -11
  4. package/.agents/skills/laporan-generator/references/template-guide.md +20 -14
  5. package/.agents/skills/laporan-generator/resources/cover.md +18 -14
  6. package/.agents/skills/laporan-generator/resources/docx.lua +208 -5
  7. package/.agents/skills/laporan-generator/resources/laporan +2 -2
  8. package/.agents/skills/laporan-generator/resources/laporan.ps1 +55 -25
  9. package/.agents/skills/laporan-generator/resources/metadata.yml +13 -16
  10. package/.agents/skills/laporan-generator/resources/presets/ipb-ppki.yml +27 -0
  11. package/.agents/skills/laporan-generator/resources/presets/its-skripsi.yml +6 -6
  12. package/.agents/skills/laporan-generator/resources/presets/telkom-ta.yml +26 -0
  13. package/.agents/skills/laporan-generator/resources/presets/ub-skripsi.yml +26 -0
  14. package/.agents/skills/laporan-generator/resources/presets/ui-skripsi.yml +6 -6
  15. package/.agents/skills/laporan-generator/resources/presets/unair-skripsi.yml +26 -0
  16. package/.agents/skills/laporan-generator/resources/presets/undip-skripsi.yml +26 -0
  17. package/.agents/skills/laporan-generator/resources/scripts/bundle.py +7 -1
  18. package/.agents/skills/laporan-generator/resources/scripts/docx-pagenum.py +6 -2
  19. package/.agents/skills/laporan-generator/resources/scripts/finalize-docx.py +13 -0
  20. package/.agents/skills/laporan-generator/resources/scripts/setup-deps.sh +9 -3
  21. package/.agents/skills/laporan-generator/resources/scripts/test_scripts.py +130 -0
  22. package/.agents/skills/laporan-generator/resources/template.typ +3 -0
  23. package/.agents/skills/laporan-generator/scripts/build-docx.sh +122 -0
  24. package/.agents/skills/laporan-generator/scripts/build.sh +32 -8
  25. package/.agents/skills/laporan-generator/scripts/bundle.py +7 -1
  26. package/.agents/skills/laporan-generator/scripts/docx-pagenum.py +6 -2
  27. package/.agents/skills/laporan-generator/scripts/finalize-docx.py +13 -0
  28. package/.agents/skills/laporan-generator/scripts/laporan +55 -5
  29. package/.agents/skills/laporan-generator/scripts/laporan.ps1 +628 -529
  30. package/.agents/skills/laporan-generator/scripts/setup-deps.sh +9 -3
  31. package/.agents/skills/laporan-generator/scripts/test_scripts.py +130 -0
  32. package/README.md +29 -19
  33. package/bin/laporan-generator.js +34 -6
  34. package/cover.md +18 -14
  35. package/docs/campus-guide.md +8 -2
  36. package/docs/metadata-schema.md +39 -11
  37. package/docs/template-guide.md +20 -14
  38. package/docx.lua +208 -5
  39. package/laporan +2 -2
  40. package/laporan.ps1 +55 -25
  41. package/lib/init.js +28 -3
  42. package/lib/mcp.js +21 -9
  43. package/lib/sync-hosts.js +26 -2
  44. package/metadata.yml +13 -16
  45. package/package.json +3 -2
  46. package/presets/ipb-ppki.yml +27 -0
  47. package/presets/its-skripsi.yml +6 -6
  48. package/presets/telkom-ta.yml +26 -0
  49. package/presets/ub-skripsi.yml +26 -0
  50. package/presets/ui-skripsi.yml +6 -6
  51. package/presets/unair-skripsi.yml +26 -0
  52. package/presets/undip-skripsi.yml +26 -0
  53. package/scripts/bundle.py +7 -1
  54. package/scripts/docx-pagenum.py +6 -2
  55. package/scripts/finalize-docx.py +13 -0
  56. package/scripts/setup-deps.sh +9 -3
  57. package/scripts/test_scripts.py +130 -0
  58. package/template.typ +3 -0
@@ -40,7 +40,7 @@ function Get-PythonCommand {
40
40
 
41
41
  function Show-Banner {
42
42
  Write-Host " ========================================================" -ForegroundColor Cyan
43
- Write-Host " LAPORAN GENERATOR CLI v2.7.0 " -ForegroundColor Cyan
43
+ Write-Host " LAPORAN GENERATOR CLI v2.9.0 " -ForegroundColor Cyan
44
44
  Write-Host " Otomatisasi Dokumen Akademik (Typst + DOCX Engine) " -ForegroundColor Cyan
45
45
  Write-Host " ========================================================" -ForegroundColor Cyan
46
46
  Write-Host ""
@@ -60,7 +60,9 @@ function Show-Help {
60
60
  Write-Host " doctor Audit kesehatan proyek (broken images, sitasi hilang, dll.)" -ForegroundColor Green
61
61
  Write-Host " bundle Kemas seluruh laporan (PDF, DOCX, MD) menjadi arsip zip" -ForegroundColor Green
62
62
  Write-Host " setup Pasang dependensi sistem otomatis Windows (Typst, Pandoc, Magick)" -ForegroundColor Green
63
- Write-Host " sync-skills Sinkronkan skill AI agent ke Antigravity, Claude, Grok" -ForegroundColor Green
63
+ Write-Host " mcp Jalankan Model Context Protocol (MCP) Server untuk AI Agent" -ForegroundColor Green
64
+ Write-Host " sync-skills Sinkronkan skill AI agent & MCP ke Antigravity, Claude, OpenCode" -ForegroundColor Green
65
+ Write-Host " uninstall Hapus integrasi skill dan perintah AI agent dari seluruh sistem" -ForegroundColor Green
64
66
  Write-Host " check Audit dependensi sistem dan struktur proyek" -ForegroundColor Green
65
67
  Write-Host " test Jalankan suite pengujian otomatis" -ForegroundColor Green
66
68
  Write-Host " view Buka dokumen Laporan.pdf di PDF viewer" -ForegroundColor Green
@@ -336,29 +338,29 @@ preset: "$escPreset"
336
338
  Write-Host "Jalankan '.\laporan.ps1 build' untuk mulai menghasilkan laporan." -ForegroundColor Cyan
337
339
  }
338
340
 
339
- function Cmd-Test {
340
- Show-Banner
341
- Write-Host "Menjalankan Test Suite..." -ForegroundColor Blue
342
- $gitBash = "C:\Program Files\Git\bin\bash.exe"
343
- $bashCmd = if (Test-Path $gitBash) { $gitBash } elseif ((Get-Command "bash" -ErrorAction SilentlyContinue) -and (Get-Command "bash").Source -notmatch "system32\\bash\.exe") { "bash" } else { "" }
344
- if ($bashCmd -and (Test-Path "test.sh")) {
345
- & $bashCmd test.sh
346
- } else {
347
- $pyCmd = Get-PythonCommand
348
- if ($pyCmd) {
349
- Write-Host "Menjalankan validasi skema preset..." -ForegroundColor Blue
350
- if (Test-Path "scripts/validate-preset.py") {
351
- & $pyCmd scripts/validate-preset.py --all
352
- }
353
- if (Test-Path "scripts/test_scripts.py") {
354
- Write-Host ""
355
- Write-Host "Menjalankan unit test skrip Python..." -ForegroundColor Blue
356
- & $pyCmd scripts/test_scripts.py
357
- }
358
- } else {
359
- Write-Host "Test suite membutuhkan bash atau python3." -ForegroundColor Yellow
360
- }
361
- }
341
+ function Cmd-Test {
342
+ Show-Banner
343
+ Write-Host "Menjalankan Test Suite..." -ForegroundColor Blue
344
+ $gitBash = "C:\Program Files\Git\bin\bash.exe"
345
+ $bashCmd = if (Test-Path $gitBash) { $gitBash } elseif ((Get-Command "bash" -ErrorAction SilentlyContinue) -and (Get-Command "bash").Source -notmatch "system32\\bash\.exe") { "bash" } else { "" }
346
+ if ($bashCmd -and (Test-Path "test.sh")) {
347
+ & $bashCmd test.sh
348
+ } else {
349
+ $pyCmd = Get-PythonCommand
350
+ if ($pyCmd) {
351
+ Write-Host "Menjalankan validasi skema preset..." -ForegroundColor Blue
352
+ if (Test-Path "scripts/validate-preset.py") {
353
+ & $pyCmd scripts/validate-preset.py --all
354
+ }
355
+ if (Test-Path "scripts/test_scripts.py") {
356
+ Write-Host ""
357
+ Write-Host "Menjalankan unit test skrip Python..." -ForegroundColor Blue
358
+ & $pyCmd scripts/test_scripts.py
359
+ }
360
+ } else {
361
+ Write-Host "Test suite membutuhkan bash atau python3." -ForegroundColor Yellow
362
+ }
363
+ }
362
364
  }
363
365
 
364
366
  function Cmd-Preset {
@@ -587,6 +589,34 @@ switch ($Command.ToLower()) {
587
589
  Write-Host "Node.js diperlukan untuk sinkronisasi multi-host AI agent." -ForegroundColor Yellow
588
590
  }
589
591
  }
592
+ "uninstall" {
593
+ if (Get-Command "node" -ErrorAction SilentlyContinue) {
594
+ $genPath = Join-Path $PSScriptRoot "bin\laporan-generator.js"
595
+ if (Test-Path $genPath) {
596
+ node $genPath uninstall @extraArgs
597
+ } elseif (Get-Command "npx" -ErrorAction SilentlyContinue) {
598
+ npx -y laporan-generator uninstall @extraArgs
599
+ } else {
600
+ Write-Host "bin\laporan-generator.js tidak ditemukan dan npx tidak tersedia." -ForegroundColor Red
601
+ }
602
+ } else {
603
+ Write-Host "Node.js diperlukan untuk membersihkan integrasi AI agent." -ForegroundColor Yellow
604
+ }
605
+ }
606
+ "clean-skills" {
607
+ if (Get-Command "node" -ErrorAction SilentlyContinue) {
608
+ $genPath = Join-Path $PSScriptRoot "bin\laporan-generator.js"
609
+ if (Test-Path $genPath) {
610
+ node $genPath uninstall @extraArgs
611
+ } elseif (Get-Command "npx" -ErrorAction SilentlyContinue) {
612
+ npx -y laporan-generator uninstall @extraArgs
613
+ } else {
614
+ Write-Host "bin\laporan-generator.js tidak ditemukan dan npx tidak tersedia." -ForegroundColor Red
615
+ }
616
+ } else {
617
+ Write-Host "Node.js diperlukan untuk membersihkan integrasi AI agent." -ForegroundColor Yellow
618
+ }
619
+ }
590
620
  "check" { Cmd-Check }
591
621
  "clean" { Cmd-Clean }
592
622
  "view" { Cmd-View }
@@ -15,21 +15,18 @@ institution: "Universitas Example"
15
15
  faculty: "Program Studi Informatika"
16
16
  year: "2026/2027"
17
17
  date: "Agustus 2026"
18
- abstract_id: |
19
- Laporan ini membahas perancangan dan implementasi pipeline otomatisasi pembuatan dokumen akademik modern menggunakan Markdown, Pandoc, Typst, Lua Filter, dan multi-pass DOCX engine. Pipeline ini memungkinkan penulisan konten laporan dalam format Markdown yang secara otomatis dikonversi menjadi dokumen PDF berkualitas percetakan berbasis Typst serta dokumen Microsoft Word (.docx) berstruktur formal lengkap dengan penomoran bab, tabel, gambar, dan halaman otomatis yang mematuhi standar karya ilmiah perguruan tinggi di Indonesia.
20
- keywords_id:
21
- - Otomatisasi Dokumen
22
- - Pandoc
23
- - Typst
24
- - OpenXML
25
- - Rekayasa Perangkat Lunak
26
- abstract_en: |
27
- This report presents the design and implementation of a modern academic document automation pipeline utilizing Markdown, Pandoc, Typst, Lua Filters, and a multi-pass DOCX engine. This pipeline facilitates authoring scholarly reports in plain Markdown, seamlessly generating both print-quality Typst-based PDF documents and formally structured Microsoft Word (.docx) files. The generated outputs feature automated chapter, table, figure, and dual-pagination numbering compliant with standardized higher education guidelines.
28
- keywords_en:
29
- - Document Automation
30
- - Pandoc
31
- - Typst
32
- - OpenXML
33
- - Software Engineering
18
+ daftar_gambar: false
19
+ daftar_tabel: false
20
+ # Komponen halaman depan opsional (aktifkan jika diperlukan):
21
+ # abstract_id: |
22
+ # Ringkasan atau abstrak laporan dalam Bahasa Indonesia...
23
+ # keywords_id:
24
+ # - Kata Kunci 1
25
+ # - Kata Kunci 2
26
+ # abstract_en: |
27
+ # Summary or abstract of the report in English...
28
+ # keywords_en:
29
+ # - Keyword 1
30
+ # - Keyword 2
34
31
  preset: "standard"
35
32
  ...
@@ -0,0 +1,27 @@
1
+ # Preset: IPB University (Institut Pertanian Bogor)
2
+ preset_id: "ipb-ppki"
3
+ name: "IPB University (PPKI)"
4
+ description: "Format Tugas Akhir sesuai Pedoman Penulisan Karya Ilmiah (PPKI) IPB University (Margin Kiri 4cm, Atas/Kanan/Bawah 3cm)."
5
+
6
+ # 1. Geometri & Margin Halaman (Standar Resmi PPKI IPB: Kiri 4cm, Atas 3cm, Bawah 3cm, Kanan 3cm)
7
+ margin_top: 3cm
8
+ margin_bottom: 3cm
9
+ margin_left: 4cm
10
+ margin_right: 3cm
11
+
12
+ # 2. Tipografi & Spasi
13
+ font_family: "Libertinus Serif"
14
+ font_size: 12pt
15
+ line_spacing: 0.75em
16
+ first_line_indent: 1.0cm
17
+
18
+ # 3. Penomoran Heading
19
+ heading_chapter_prefix: "BAB "
20
+ heading_chapter_num_format: "roman"
21
+ heading_sub_dot: false
22
+ heading_subsub_dot: false
23
+
24
+ # 4. Pengaturan Halaman Sampul
25
+ cover_show_lecturer: false
26
+ cover_hide_lecturer: true
27
+ cover_logo_width: 4cm
@@ -1,13 +1,13 @@
1
1
  # Preset: Institut Teknologi Sepuluh Nopember (ITS)
2
2
  preset_id: "its-skripsi"
3
3
  name: "Institut Teknologi Sepuluh Nopember (ITS)"
4
- description: "Format Tugas Akhir & Skripsi Institut Teknologi Sepuluh Nopember (Margin 4-4-3-3, Standard Academic)."
4
+ description: "Format Tugas Akhir & Skripsi sesuai Pedoman Resmi ITS (Margin Atas 3cm, Kiri 3cm, Bawah 2.5cm, Kanan 2cm)."
5
5
 
6
- # Margin halaman (Standar ITS: Kiri 4cm, Atas 4cm, Bawah 3cm, Kanan 3cm)
7
- margin_top: 4cm
8
- margin_bottom: 3cm
9
- margin_left: 4cm
10
- margin_right: 3cm
6
+ # Margin halaman (Standar Resmi Pedoman TA ITS: Atas 3.0cm, Bawah 2.5cm, Kiri 3.0cm, Kanan 2.0cm)
7
+ margin_top: 3cm
8
+ margin_bottom: 2.5cm
9
+ margin_left: 3cm
10
+ margin_right: 2cm
11
11
 
12
12
  # Tipografi
13
13
  font_family: "Libertinus Serif"
@@ -0,0 +1,26 @@
1
+ # Preset: Telkom University (Tel-U)
2
+ preset_id: "telkom-ta"
3
+ name: "Telkom University (Tel-U)"
4
+ description: "Format Buku Tugas Akhir sesuai Pedoman Akademik Telkom University (Margin Kiri 4cm, Atas/Kanan/Bawah 3cm)."
5
+
6
+ # 1. Geometri & Margin Halaman (Standar TA Telkom University: Kiri 4cm, Atas 3cm, Bawah 3cm, Kanan 3cm)
7
+ margin_top: 3cm
8
+ margin_bottom: 3cm
9
+ margin_left: 4cm
10
+ margin_right: 3cm
11
+
12
+ # 2. Tipografi & Spasi
13
+ font_family: "Libertinus Serif"
14
+ font_size: 12pt
15
+ line_spacing: 0.75em
16
+ first_line_indent: 1.25cm
17
+
18
+ # 3. Penomoran Heading
19
+ heading_chapter_prefix: "BAB "
20
+ heading_chapter_num_format: "roman"
21
+ heading_sub_dot: true
22
+ heading_subsub_dot: false
23
+
24
+ # 4. Pengaturan Halaman Sampul
25
+ cover_show_lecturer: true
26
+ cover_logo_width: 4cm
@@ -0,0 +1,26 @@
1
+ # Preset: Universitas Brawijaya (UB)
2
+ preset_id: "ub-skripsi"
3
+ name: "Universitas Brawijaya (UB)"
4
+ description: "Format Skripsi & Tugas Akhir sesuai Pedoman Penulisan Karya Ilmiah Universitas Brawijaya (Margin 4-4-3-3)."
5
+
6
+ # 1. Geometri & Margin Halaman (Standar UB: Atas 4cm, Kiri 4cm, Bawah 3cm, Kanan 3cm)
7
+ margin_top: 4cm
8
+ margin_bottom: 3cm
9
+ margin_left: 4cm
10
+ margin_right: 3cm
11
+
12
+ # 2. Tipografi & Spasi
13
+ font_family: "Libertinus Serif"
14
+ font_size: 12pt
15
+ line_spacing: 0.75em
16
+ first_line_indent: 1.25cm
17
+
18
+ # 3. Penomoran Heading
19
+ heading_chapter_prefix: "BAB "
20
+ heading_chapter_num_format: "roman"
21
+ heading_sub_dot: true
22
+ heading_subsub_dot: false
23
+
24
+ # 4. Pengaturan Halaman Sampul
25
+ cover_show_lecturer: true
26
+ cover_logo_width: 4cm
@@ -1,13 +1,13 @@
1
1
  # Preset: Universitas Indonesia (UI)
2
2
  preset_id: "ui-skripsi"
3
3
  name: "Universitas Indonesia (UI)"
4
- description: "Format Tugas Akhir & Skripsi sesuai Pedoman Penulisan Karya Ilmiah Universitas Indonesia."
4
+ description: "Format Tugas Akhir & Skripsi sesuai Keputusan Rektor UI (Margin Kiri 4cm, Atas/Kanan/Bawah 3cm, TNR 12pt)."
5
5
 
6
- # Margin halaman (Standar UI: Top 2.5cm / 2cm, Left 2.5cm, Bottom 2.5cm / 3cm, Right 2.5cm)
7
- margin_top: 2.5cm
8
- margin_bottom: 2.5cm
9
- margin_left: 2.5cm
10
- margin_right: 2.5cm
6
+ # Margin halaman (Standar Resmi Pedoman Teknis TA UI: Kiri 4cm, Atas 3cm, Bawah 3cm, Kanan 3cm)
7
+ margin_top: 3cm
8
+ margin_bottom: 3cm
9
+ margin_left: 4cm
10
+ margin_right: 3cm
11
11
 
12
12
  # Tipografi
13
13
  font_family: "Libertinus Serif"
@@ -0,0 +1,26 @@
1
+ # Preset: Universitas Airlangga (UNAIR)
2
+ preset_id: "unair-skripsi"
3
+ name: "Universitas Airlangga (UNAIR)"
4
+ description: "Format Skripsi & Karya Ilmiah sesuai Pedoman Penulisan Universitas Airlangga (Margin 4-4-3-3)."
5
+
6
+ # 1. Geometri & Margin Halaman (Standar UNAIR: Atas 4cm, Kiri 4cm, Bawah 3cm, Kanan 3cm)
7
+ margin_top: 4cm
8
+ margin_bottom: 3cm
9
+ margin_left: 4cm
10
+ margin_right: 3cm
11
+
12
+ # 2. Tipografi & Spasi
13
+ font_family: "Libertinus Serif"
14
+ font_size: 12pt
15
+ line_spacing: 0.75em
16
+ first_line_indent: 1.25cm
17
+
18
+ # 3. Penomoran Heading
19
+ heading_chapter_prefix: "BAB "
20
+ heading_chapter_num_format: "roman"
21
+ heading_sub_dot: true
22
+ heading_subsub_dot: false
23
+
24
+ # 4. Pengaturan Halaman Sampul
25
+ cover_show_lecturer: true
26
+ cover_logo_width: 4cm
@@ -0,0 +1,26 @@
1
+ # Preset: Universitas Diponegoro (UNDIP)
2
+ preset_id: "undip-skripsi"
3
+ name: "Universitas Diponegoro (UNDIP)"
4
+ description: "Format Skripsi & Tugas Akhir sesuai Pedoman Akademik Universitas Diponegoro (Margin 4-4-3-3)."
5
+
6
+ # 1. Geometri & Margin Halaman (Standar UNDIP: Atas 4cm, Kiri 4cm, Bawah 3cm, Kanan 3cm)
7
+ margin_top: 4cm
8
+ margin_bottom: 3cm
9
+ margin_left: 4cm
10
+ margin_right: 3cm
11
+
12
+ # 2. Tipografi & Spasi
13
+ font_family: "Libertinus Serif"
14
+ font_size: 12pt
15
+ line_spacing: 0.75em
16
+ first_line_indent: 1.25cm
17
+
18
+ # 3. Penomoran Heading
19
+ heading_chapter_prefix: "BAB "
20
+ heading_chapter_num_format: "roman"
21
+ heading_sub_dot: true
22
+ heading_subsub_dot: false
23
+
24
+ # 4. Pengaturan Halaman Sampul
25
+ cover_show_lecturer: true
26
+ cover_logo_width: 4cm
@@ -36,7 +36,13 @@ def main():
36
36
  # Cek apakah PDF & DOCX sudah ada
37
37
  if not os.path.isfile("Laporan.pdf") or not os.path.isfile("Laporan.docx"):
38
38
  print(f" {BLUE}[1/3] Menjalankan build PDF dan DOCX...{NC}")
39
- ret = os.system("./build.sh && make docx")
39
+ import subprocess
40
+ if sys.platform == "win32":
41
+ ret = subprocess.run(["powershell", "-ExecutionPolicy", "Bypass", "-File", "laporan.ps1", "build"]).returncode
42
+ else:
43
+ ret_pdf = subprocess.run(["bash", "build.sh"]).returncode
44
+ ret_docx = subprocess.run(["bash", "build-docx.sh"]).returncode
45
+ ret = ret_pdf or ret_docx
40
46
  if ret != 0:
41
47
  print("Gagal mengompilasi dokumen untuk bundling.")
42
48
  return 1
@@ -73,6 +73,10 @@ def first_decimal_page(pages, start):
73
73
 
74
74
 
75
75
  def find_page(pages, needle, start, end):
76
+ for p in range(start, end + 1):
77
+ lines = pages.get(p, [])
78
+ if any(l == needle for l in lines):
79
+ return p
76
80
  for p in range(start, end + 1):
77
81
  text = "\n".join(pages.get(p, []))
78
82
  if needle in text:
@@ -160,8 +164,8 @@ def main():
160
164
  entries = parse_entries(doc)
161
165
  injected = 0
162
166
  for para, pid, text in entries:
163
- if pid == "kata-pengantar":
164
- page = find_page(pages, "KATA PENGANTAR", 1, toc_start - 1)
167
+ if pid in ("kata-pengantar", "lembar-pengesahan", "abstrak", "abstract"):
168
+ page = find_page(pages, text, 1, toc_start - 1)
165
169
  else:
166
170
  page = find_page(pages, needle_of(text), first_dec, max(pages))
167
171
  if not page:
@@ -43,6 +43,8 @@ def next_heading_text(doc, start):
43
43
  def is_front_matter_heading(t):
44
44
  norm = t.strip().upper()
45
45
  return norm in (
46
+ "LEMBAR PENGESAHAN",
47
+ "HALAMAN PENGESAHAN",
46
48
  "KATA PENGANTAR",
47
49
  "PRAKATA",
48
50
  "ABSTRAK",
@@ -103,7 +105,18 @@ def add_pgnum(sect, fmt, start):
103
105
  return sect + tag
104
106
 
105
107
 
108
+ def sync_section_margins(doc):
109
+ first_mar = re.search(
110
+ r'<w:sectPr[^>]*>.*?(<w:pgMar\b[^>]*/>).*?</w:sectPr>', doc, re.S
111
+ )
112
+ if not first_mar:
113
+ return doc
114
+ target_pgmar = first_mar.group(1)
115
+ return re.sub(r'<w:pgMar\b[^>]*/>', target_pgmar, doc)
116
+
117
+
106
118
  def process(doc):
119
+ doc = sync_section_margins(doc)
107
120
  matches = list(SECT_IN_PPR.finditer(doc))
108
121
  if not matches:
109
122
  return doc, 0
@@ -83,11 +83,17 @@ elif has_cmd apt-get; then
83
83
  # Install typst binary jika belum ada
84
84
  if ! has_cmd typst; then
85
85
  echo -e "--> Mengunduh Typst standalone binary..."
86
- TYPST_URL="https://github.com/typst/typst/releases/download/v0.13.0/typst-x86_64-unknown-linux-musl.tar.xz"
86
+ ARCH=$(uname -m)
87
+ if [ "$ARCH" = "aarch64" ] || [ "$ARCH" = "arm64" ]; then
88
+ TYPST_DIR="typst-aarch64-unknown-linux-musl"
89
+ else
90
+ TYPST_DIR="typst-x86_64-unknown-linux-musl"
91
+ fi
92
+ TYPST_URL="https://github.com/typst/typst/releases/download/v0.15.1/${TYPST_DIR}.tar.xz"
87
93
  wget -q "$TYPST_URL" -O /tmp/typst.tar.xz
88
94
  tar -xJf /tmp/typst.tar.xz -C /tmp
89
- sudo mv /tmp/typst-x86_64-unknown-linux-musl/typst /usr/local/bin/typst
90
- rm -rf /tmp/typst.tar.xz /tmp/typst-x86_64-unknown-linux-musl
95
+ sudo mv "/tmp/${TYPST_DIR}/typst" /usr/local/bin/typst
96
+ rm -rf /tmp/typst.tar.xz "/tmp/${TYPST_DIR}"
91
97
  fi
92
98
  elif has_cmd pacman; then
93
99
  echo -e "${CYAN}--> Menggunakan pacman (Arch Linux)...${NC}"
@@ -84,6 +84,13 @@ class TestDocxPagenum(unittest.TestCase):
84
84
  self.assertEqual(docx_pagenum.needle_of("1.1 Latar Belakang"), "1.1 Latar Belakang")
85
85
  self.assertEqual(docx_pagenum.needle_of("KATA PENGANTAR"), "KATA PENGANTAR")
86
86
 
87
+ def test_find_page_exact_line_priority(self):
88
+ pages = {
89
+ 10: ["Mencegah penomoran ganda pada KATA PENGANTAR dan DAFTAR PUSTAKA.", "6"],
90
+ 23: ["DAFTAR PUSTAKA", "American Psychological Association. (2020).", "19"],
91
+ }
92
+ self.assertEqual(docx_pagenum.find_page(pages, "DAFTAR PUSTAKA", 5, 25), 23)
93
+
87
94
  def test_roman_pattern(self):
88
95
  self.assertTrue(docx_pagenum.ROMAN.match("iv"))
89
96
  self.assertTrue(docx_pagenum.ROMAN.match("xii"))
@@ -97,10 +104,20 @@ class TestDocxPagenum(unittest.TestCase):
97
104
 
98
105
  class TestFinalizeDocx(unittest.TestCase):
99
106
  def test_classify_front_matter(self):
107
+ self.assertEqual(finalize_docx.classify("LEMBAR PENGESAHAN"), "cover")
100
108
  self.assertEqual(finalize_docx.classify("KATA PENGANTAR"), "cover")
101
109
  self.assertEqual(finalize_docx.classify("ABSTRAK"), "cover")
102
110
  self.assertEqual(finalize_docx.classify("DAFTAR ISI"), "cover")
103
111
 
112
+ def test_sync_section_margins(self):
113
+ raw_xml = (
114
+ '<w:sectPr><w:pgSz w:w="11906"/><w:pgMar w:top="2268" w:right="1701" w:bottom="1701" w:left="2268"/></w:sectPr>'
115
+ '<w:p><w:pPr><w:sectPr><w:pgMar w:top="1134" w:right="1417" w:bottom="1701" w:left="1417"/></w:sectPr></w:pPr></w:p>'
116
+ )
117
+ synced = finalize_docx.sync_section_margins(raw_xml)
118
+ self.assertEqual(synced.count('w:top="2268"'), 2)
119
+ self.assertEqual(synced.count('w:left="2268"'), 2)
120
+
104
121
  def test_classify_standard_thesis_headings(self):
105
122
  self.assertEqual(finalize_docx.classify("BAB I"), "roman")
106
123
  self.assertEqual(finalize_docx.classify("BAB I PENDAHULUAN"), "roman")
@@ -184,6 +201,119 @@ class TestReportDoctor(unittest.TestCase):
184
201
  self.assertIn("citations", data)
185
202
 
186
203
 
204
+ class TestFrontMatterOptIn(unittest.TestCase):
205
+ def test_default_metadata_structure(self):
206
+ meta_path = os.path.join(get_project_root(), "metadata.yml")
207
+ with open(meta_path, "r", encoding="utf-8") as f:
208
+ content = f.read()
209
+ # Verify abstract is commented or empty by default
210
+ self.assertNotIn("\nabstract_id: |", content, "abstract_id should not be active by default in metadata.yml")
211
+ self.assertNotIn("\nabstract_en: |", content, "abstract_en should not be active by default in metadata.yml")
212
+ # Verify opt-in flags for daftar_gambar & daftar_tabel are explicitly false or commented
213
+ has_dg = "daftar_gambar: false" in content
214
+ has_dt = "daftar_tabel: false" in content
215
+ self.assertTrue(has_dg, "metadata.yml must explicitly specify daftar_gambar: false by default")
216
+ self.assertTrue(has_dt, "metadata.yml must explicitly specify daftar_tabel: false by default")
217
+
218
+ def test_cover_md_gated_outlines(self):
219
+ cover_path = os.path.join(get_project_root(), "cover.md")
220
+ with open(cover_path, "r", encoding="utf-8") as f:
221
+ content = f.read()
222
+ # Unconditional outline queries must not exist
223
+ self.assertNotIn("let imgs = query(figure.where(kind: image))\n if imgs.len() > 0", content,
224
+ "cover.md must not unconditionally emit DAFTAR GAMBAR outline")
225
+ self.assertNotIn("let tbls = query(figure.where(kind: table))\n if tbls.len() > 0", content,
226
+ "cover.md must not unconditionally emit DAFTAR TABEL outline")
227
+ # Must be gated by opt-in check
228
+ self.assertIn("opt-daftar-gambar", content, "cover.md must gate DAFTAR GAMBAR with opt-daftar-gambar")
229
+ self.assertIn("opt-daftar-tabel", content, "cover.md must gate DAFTAR TABEL with opt-daftar-tabel")
230
+
231
+ def test_lua_meta_bool_coercion(self):
232
+ """Verify docx.lua handles YAML boolean scalars and string equivalents safely."""
233
+ import shutil
234
+ lua_bin = (
235
+ shutil.which("lua")
236
+ or shutil.which("luajit")
237
+ or shutil.which("lua5.4")
238
+ or shutil.which("lua5.3")
239
+ or shutil.which("lua5.2")
240
+ or shutil.which("lua5.1")
241
+ )
242
+ if not lua_bin:
243
+ self.skipTest("Standalone Lua interpreter not found in PATH")
244
+
245
+ lua_code = """
246
+ local pandoc = {
247
+ utils = {
248
+ stringify = function(x)
249
+ if type(x) ~= 'string' and type(x) ~= 'table' then
250
+ error('pandoc.utils.stringify expects table or string, got ' .. type(x))
251
+ end
252
+ return tostring(x)
253
+ end
254
+ }
255
+ }
256
+ local function meta_str(meta, key)
257
+ local v = meta[key]
258
+ if v == nil then return "" end
259
+ if type(v) == "boolean" then return tostring(v) end
260
+ return pandoc.utils.stringify(v)
261
+ end
262
+ local function meta_bool(meta, key)
263
+ local v = meta[key]
264
+ if v == nil then return false end
265
+ if type(v) == "boolean" then return v end
266
+ local s = meta_str(meta, key):lower():gsub("%s+", "")
267
+ return s == "true" or s == "1" or s == "yes"
268
+ end
269
+
270
+ local meta = {
271
+ dg_bool_false = false,
272
+ dg_bool_true = true,
273
+ dg_str_false = "false",
274
+ dg_str_true = "true",
275
+ dg_nil = nil
276
+ }
277
+
278
+ assert(meta_str(meta, "dg_bool_false") == "false")
279
+ assert(meta_bool(meta, "dg_bool_false") == false)
280
+ assert(meta_str(meta, "dg_bool_true") == "true")
281
+ assert(meta_bool(meta, "dg_bool_true") == true)
282
+ assert(meta_bool(meta, "dg_str_false") == false)
283
+ assert(meta_bool(meta, "dg_str_true") == true)
284
+ assert(meta_bool(meta, "dg_nil") == false)
285
+ print("LUA_OK")
286
+ """
287
+ proc = subprocess.run([lua_bin, "-e", lua_code], capture_output=True, text=True)
288
+ self.assertEqual(proc.returncode, 0, f"Lua helper test failed: {proc.stderr}")
289
+ self.assertIn("LUA_OK", proc.stdout)
290
+
291
+ def test_init_project_boolean_coercion(self):
292
+ """Verify lib/init.js does not treat string 'false' as truthy."""
293
+ import shutil
294
+ import tempfile
295
+ if not shutil.which("node"):
296
+ self.skipTest("Node.js runtime not found in PATH")
297
+
298
+ with tempfile.TemporaryDirectory() as tmpdir:
299
+ node_code = f"""
300
+ const path = require('path');
301
+ const {{ initProject }} = require('{os.path.join(ROOT_DIR, "lib/init.js")}');
302
+ initProject({{ targetDir: '{tmpdir}/test_f', daftar_gambar: 'false', daftar_tabel: false, silent: true }});
303
+ initProject({{ targetDir: '{tmpdir}/test_t', daftar_gambar: 'true', daftar_tabel: true, silent: true }});
304
+ """
305
+ proc = subprocess.run(["node", "-e", node_code], capture_output=True, text=True)
306
+ self.assertEqual(proc.returncode, 0, f"Node initProject failed: {proc.stderr}")
307
+ with open(os.path.join(tmpdir, "test_f", "metadata.yml"), "r", encoding="utf-8") as f:
308
+ meta_f = f.read()
309
+ with open(os.path.join(tmpdir, "test_t", "metadata.yml"), "r", encoding="utf-8") as f:
310
+ meta_t = f.read()
311
+ self.assertIn("daftar_gambar: false", meta_f)
312
+ self.assertIn("daftar_tabel: false", meta_f)
313
+ self.assertIn("daftar_gambar: true", meta_t)
314
+ self.assertIn("daftar_tabel: true", meta_t)
315
+
316
+
187
317
  if __name__ == "__main__":
188
318
  unittest.main(verbosity=2)
189
319
 
@@ -391,4 +391,7 @@ $endif$
391
391
  #pagebreak()
392
392
  $endif$
393
393
 
394
+ #let opt-daftar-gambar = $if(daftar_gambar)$$daftar_gambar$$else$$if(list_of_figures)$$list_of_figures$$else$false$endif$$endif$
395
+ #let opt-daftar-tabel = $if(daftar_tabel)$$daftar_tabel$$else$$if(list_of_tables)$$list_of_tables$$else$false$endif$$endif$
396
+
394
397
  $body$