laporan-generator 2.6.1

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 (109) hide show
  1. package/.agents/skills/laporan-generator/SKILL.md +132 -0
  2. package/.agents/skills/laporan-generator/agents/openai.yaml +3 -0
  3. package/.agents/skills/laporan-generator/references/campus-guide.md +120 -0
  4. package/.agents/skills/laporan-generator/references/metadata-schema.md +76 -0
  5. package/.agents/skills/laporan-generator/references/preset-schema.md +79 -0
  6. package/.agents/skills/laporan-generator/references/syntax-cheatsheet.md +122 -0
  7. package/.agents/skills/laporan-generator/references/template-guide.md +100 -0
  8. package/.agents/skills/laporan-generator/references/troubleshooting.md +105 -0
  9. package/.agents/skills/laporan-generator/resources/Makefile +90 -0
  10. package/.agents/skills/laporan-generator/resources/apa.csl +2273 -0
  11. package/.agents/skills/laporan-generator/resources/build-docx.sh +122 -0
  12. package/.agents/skills/laporan-generator/resources/build.sh +102 -0
  13. package/.agents/skills/laporan-generator/resources/chapters/bab1-pendahuluan.md +60 -0
  14. package/.agents/skills/laporan-generator/resources/chapters/bab2-tinjauan-pustaka.md +83 -0
  15. package/.agents/skills/laporan-generator/resources/chapters/bab3-metodologi.md +116 -0
  16. package/.agents/skills/laporan-generator/resources/chapters/bab4-hasil-dan-pembahasan.md +93 -0
  17. package/.agents/skills/laporan-generator/resources/chapters/bab5-penutup.md +27 -0
  18. package/.agents/skills/laporan-generator/resources/cover.md +50 -0
  19. package/.agents/skills/laporan-generator/resources/docx.lua +416 -0
  20. package/.agents/skills/laporan-generator/resources/laporan +464 -0
  21. package/.agents/skills/laporan-generator/resources/laporan.ps1 +552 -0
  22. package/.agents/skills/laporan-generator/resources/logo.jpg +0 -0
  23. package/.agents/skills/laporan-generator/resources/metadata.yml +35 -0
  24. package/.agents/skills/laporan-generator/resources/presets/itb-ta.yml +27 -0
  25. package/.agents/skills/laporan-generator/resources/presets/its-skripsi.yml +26 -0
  26. package/.agents/skills/laporan-generator/resources/presets/skripsi-4433.yml +26 -0
  27. package/.agents/skills/laporan-generator/resources/presets/standard.yml +26 -0
  28. package/.agents/skills/laporan-generator/resources/presets/ugm-skripsi.yml +26 -0
  29. package/.agents/skills/laporan-generator/resources/presets/ui-skripsi.yml +26 -0
  30. package/.agents/skills/laporan-generator/resources/presets/unpad-skripsi.yml +27 -0
  31. package/.agents/skills/laporan-generator/resources/reference.docx +0 -0
  32. package/.agents/skills/laporan-generator/resources/references.bib +115 -0
  33. package/.agents/skills/laporan-generator/resources/scripts/bundle.py +75 -0
  34. package/.agents/skills/laporan-generator/resources/scripts/docx-pagenum.py +192 -0
  35. package/.agents/skills/laporan-generator/resources/scripts/finalize-docx.py +291 -0
  36. package/.agents/skills/laporan-generator/resources/scripts/make-reference-docx.py +670 -0
  37. package/.agents/skills/laporan-generator/resources/scripts/report-doctor.py +211 -0
  38. package/.agents/skills/laporan-generator/resources/scripts/report-stats.py +149 -0
  39. package/.agents/skills/laporan-generator/resources/scripts/scan-preset.py +311 -0
  40. package/.agents/skills/laporan-generator/resources/scripts/setup-deps.ps1 +152 -0
  41. package/.agents/skills/laporan-generator/resources/scripts/setup-deps.sh +113 -0
  42. package/.agents/skills/laporan-generator/resources/scripts/test_scripts.py +128 -0
  43. package/.agents/skills/laporan-generator/resources/scripts/validate-preset.py +166 -0
  44. package/.agents/skills/laporan-generator/resources/template.typ +394 -0
  45. package/.agents/skills/laporan-generator/scripts/build.sh +78 -0
  46. package/.agents/skills/laporan-generator/scripts/bundle.py +75 -0
  47. package/.agents/skills/laporan-generator/scripts/docx-pagenum.py +192 -0
  48. package/.agents/skills/laporan-generator/scripts/finalize-docx.py +152 -0
  49. package/.agents/skills/laporan-generator/scripts/laporan +447 -0
  50. package/.agents/skills/laporan-generator/scripts/laporan.ps1 +529 -0
  51. package/.agents/skills/laporan-generator/scripts/make-reference-docx.py +630 -0
  52. package/.agents/skills/laporan-generator/scripts/report-doctor.py +211 -0
  53. package/.agents/skills/laporan-generator/scripts/report-stats.py +149 -0
  54. package/.agents/skills/laporan-generator/scripts/scan-preset.py +311 -0
  55. package/.agents/skills/laporan-generator/scripts/setup-deps.ps1 +152 -0
  56. package/.agents/skills/laporan-generator/scripts/setup-deps.sh +113 -0
  57. package/.agents/skills/laporan-generator/scripts/test_scripts.py +103 -0
  58. package/.agents/skills/laporan-generator/scripts/validate-preset.py +166 -0
  59. package/.agents/workflows/laporan-generator.md +8 -0
  60. package/LICENSE +21 -0
  61. package/Makefile +90 -0
  62. package/README.md +389 -0
  63. package/apa.csl +2273 -0
  64. package/bin/laporan-generator.js +71 -0
  65. package/build-docx.sh +122 -0
  66. package/build.sh +102 -0
  67. package/chapters/bab1-pendahuluan.md +60 -0
  68. package/chapters/bab2-tinjauan-pustaka.md +83 -0
  69. package/chapters/bab3-metodologi.md +116 -0
  70. package/chapters/bab4-hasil-dan-pembahasan.md +93 -0
  71. package/chapters/bab5-penutup.md +27 -0
  72. package/cover.md +50 -0
  73. package/docs/campus-guide.md +120 -0
  74. package/docs/metadata-schema.md +76 -0
  75. package/docs/preset-schema.md +79 -0
  76. package/docs/syntax-cheatsheet.md +122 -0
  77. package/docs/template-guide.md +100 -0
  78. package/docs/troubleshooting.md +105 -0
  79. package/docx.lua +416 -0
  80. package/laporan +464 -0
  81. package/laporan.ps1 +552 -0
  82. package/lib/init.js +118 -0
  83. package/lib/setup.js +33 -0
  84. package/lib/sync-hosts.js +194 -0
  85. package/lib/uninstall.js +123 -0
  86. package/logo.jpg +0 -0
  87. package/metadata.yml +35 -0
  88. package/package.json +65 -0
  89. package/presets/itb-ta.yml +27 -0
  90. package/presets/its-skripsi.yml +26 -0
  91. package/presets/skripsi-4433.yml +26 -0
  92. package/presets/standard.yml +26 -0
  93. package/presets/ugm-skripsi.yml +26 -0
  94. package/presets/ui-skripsi.yml +26 -0
  95. package/presets/unpad-skripsi.yml +27 -0
  96. package/reference.docx +0 -0
  97. package/references.bib +115 -0
  98. package/scripts/bundle.py +75 -0
  99. package/scripts/docx-pagenum.py +192 -0
  100. package/scripts/finalize-docx.py +291 -0
  101. package/scripts/make-reference-docx.py +670 -0
  102. package/scripts/report-doctor.py +211 -0
  103. package/scripts/report-stats.py +149 -0
  104. package/scripts/scan-preset.py +311 -0
  105. package/scripts/setup-deps.ps1 +152 -0
  106. package/scripts/setup-deps.sh +113 -0
  107. package/scripts/test_scripts.py +128 -0
  108. package/scripts/validate-preset.py +166 -0
  109. package/template.typ +394 -0
@@ -0,0 +1,26 @@
1
+ # Preset: Standar Modern
2
+ preset_id: "standard"
3
+ name: "Standar Modern"
4
+ description: "Format standar modern untuk laporan proyek, tugas kuliah, praktikum, dan makalah."
5
+
6
+ # Margin halaman
7
+ margin_top: 2cm
8
+ margin_bottom: 3cm
9
+ margin_left: 2.5cm
10
+ margin_right: 2.5cm
11
+
12
+ # Tipografi
13
+ font_family: "Libertinus Serif"
14
+ font_size: 12pt
15
+ line_spacing: 0.75em
16
+ first_line_indent: 1.25cm
17
+
18
+ # 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
+ # Pengaturan Cover
25
+ cover_show_lecturer: true
26
+ cover_logo_width: 4cm
@@ -0,0 +1,26 @@
1
+ # Preset: Universitas Gadjah Mada (UGM)
2
+ preset_id: "ugm-skripsi"
3
+ name: "Universitas Gadjah Mada (UGM)"
4
+ description: "Format Skripsi / Laporan Tugas Akhir Universitas Gadjah Mada (Margin 4-4-3-3, Full Academic Cover)."
5
+
6
+ # Margin halaman (Standar UGM: Kiri 4cm, Atas 4cm, Bawah 3cm, Kanan 3cm)
7
+ margin_top: 4cm
8
+ margin_bottom: 3cm
9
+ margin_left: 4cm
10
+ margin_right: 3cm
11
+
12
+ # Tipografi
13
+ font_family: "Libertinus Serif"
14
+ font_size: 12pt
15
+ line_spacing: 0.75em
16
+ first_line_indent: 1.25cm
17
+
18
+ # 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
+ # Pengaturan Cover
25
+ cover_show_lecturer: true
26
+ cover_logo_width: 4cm
@@ -0,0 +1,26 @@
1
+ # Preset: Universitas Indonesia (UI)
2
+ preset_id: "ui-skripsi"
3
+ name: "Universitas Indonesia (UI)"
4
+ description: "Format Tugas Akhir & Skripsi sesuai Pedoman Penulisan Karya Ilmiah Universitas Indonesia."
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
11
+
12
+ # Tipografi
13
+ font_family: "Libertinus Serif"
14
+ font_size: 12pt
15
+ line_spacing: 0.75em
16
+ first_line_indent: 1.25cm
17
+
18
+ # 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
+ # Pengaturan Cover
25
+ cover_show_lecturer: true
26
+ cover_logo_width: 4cm
@@ -0,0 +1,27 @@
1
+ # Preset: UNIVERSITAS PADJADJARAN
2
+ # Dihasilkan secara otomatis oleh scan-preset.py
3
+ preset_id: "unpad-skripsi"
4
+ name: "UNIVERSITAS PADJADJARAN"
5
+ description: "Format otomatis hasil pemindaian pedoman akademik UNIVERSITAS PADJADJARAN."
6
+
7
+ # 1. Geometri & Margin Halaman
8
+ margin_top: 4cm
9
+ margin_bottom: 3cm
10
+ margin_left: 4cm
11
+ margin_right: 3cm
12
+
13
+ # 2. Tipografi & Spasi
14
+ font_family: "Libertinus Serif"
15
+ font_size: 12pt
16
+ line_spacing: 0.75em
17
+ first_line_indent: 1.25cm
18
+
19
+ # 3. Penomoran Heading
20
+ heading_chapter_prefix: "BAB "
21
+ heading_chapter_num_format: "roman"
22
+ heading_sub_dot: true
23
+ heading_subsub_dot: false
24
+
25
+ # 4. Pengaturan Halaman Sampul
26
+ cover_show_lecturer: true
27
+ cover_logo_width: 4cm
@@ -0,0 +1,115 @@
1
+ @manual{typst2024,
2
+ author = {M{\"a}dje, Laurenz and Haug, Martin},
3
+ title = {Typst: A Programmable Markup-Based Typesetting System},
4
+ year = {2024},
5
+ organization = {Typst GmbH},
6
+ url = {https://typst.app/docs}
7
+ }
8
+
9
+ @manual{pandoc2024,
10
+ author = {MacFarlane, John},
11
+ title = {Pandoc: A Universal Document Converter},
12
+ year = {2024},
13
+ url = {https://pandoc.org/MANUAL.html}
14
+ }
15
+
16
+ @article{krewinkel2020pandoc,
17
+ author = {Krewinkel, Albert and MacFarlane, John},
18
+ title = {Pandoc Lua Filters: Extensible Document Processing},
19
+ year = {2020},
20
+ journal = {Journal of Open Source Software},
21
+ url = {https://pandoc.org/lua-filters.html}
22
+ }
23
+
24
+ @misc{gruber2004markdown,
25
+ author = {Gruber, John},
26
+ title = {Markdown: A Text-to-HTML Conversion Tool for Web Writers},
27
+ year = {2004},
28
+ url = {https://daringfireball.net/projects/markdown/}
29
+ }
30
+
31
+ @manual{commonmark2021,
32
+ author = {MacFarlane, John and Lehmann, David and Krewinkel, Albert and others},
33
+ title = {CommonMark Spec (Version 0.30)},
34
+ year = {2021},
35
+ url = {https://spec.commonmark.org/0.30/}
36
+ }
37
+
38
+ @manual{imagemagick2024,
39
+ author = {{ImageMagick Studio LLC}},
40
+ title = {ImageMagick: Master Software Suite for Image Processing},
41
+ year = {2024},
42
+ url = {https://imagemagick.org/}
43
+ }
44
+
45
+ @manual{gnu2024bash,
46
+ author = {Fox, Brian and Ramey, Chet},
47
+ title = {GNU Bash Reference Manual (Version 5.2)},
48
+ year = {2024},
49
+ organization = {Free Software Foundation},
50
+ url = {https://www.gnu.org/software/bash/manual/}
51
+ }
52
+
53
+ @inproceedings{dolstra2004nix,
54
+ author = {Dolstra, Eelco and de Jonge, Merijn and Visser, Eelco},
55
+ title = {Nix: A Safe and Policy-Free System for Software Deployment},
56
+ booktitle = {Proceedings of the 18th Large Installation System Administration Conference (LISA '04)},
57
+ pages = {23--35},
58
+ year = {2004},
59
+ organization = {USENIX Association}
60
+ }
61
+
62
+ @article{merkel2014docker,
63
+ author = {Merkel, Dirk},
64
+ title = {Docker: Lightweight Linux Containers for Consistent Development and Deployment},
65
+ journal = {Linux Journal},
66
+ volume = {2014},
67
+ number = {239},
68
+ pages = {2},
69
+ year = {2014}
70
+ }
71
+
72
+ @book{apa2020manual,
73
+ author = {{American Psychological Association}},
74
+ title = {Publication Manual of the American Psychological Association},
75
+ edition = {7th},
76
+ year = {2020},
77
+ publisher = {American Psychological Association},
78
+ address = {Washington, DC},
79
+ isbn = {978-1-4338-3216-1},
80
+ doi = {10.1037/0000165-000}
81
+ }
82
+
83
+ @standard{iso2021openxml,
84
+ author = {{International Organization for Standardization}},
85
+ title = {ISO/IEC 29500:2021 Information technology -- Document description and processing languages -- Office Open XML File Formats},
86
+ year = {2021},
87
+ organization = {ISO/IEC}
88
+ }
89
+
90
+ @book{knuth1984texbook,
91
+ author = {Knuth, Donald E.},
92
+ title = {The TeXbook},
93
+ year = {1984},
94
+ publisher = {Addison-Wesley},
95
+ address = {Reading, Massachusetts},
96
+ isbn = {0-201-13448-9}
97
+ }
98
+
99
+ @book{lamport1994latex,
100
+ author = {Lamport, Leslie},
101
+ title = {LaTeX: A Document Preparation System},
102
+ year = {1994},
103
+ edition = {2nd},
104
+ publisher = {Addison-Wesley},
105
+ isbn = {0-201-52983-1}
106
+ }
107
+
108
+ @book{love2010kernel,
109
+ author = {Love, Robert},
110
+ title = {Linux Kernel Development},
111
+ year = {2010},
112
+ edition = {3rd},
113
+ publisher = {Addison-Wesley Professional},
114
+ isbn = {978-0-672-32946-3}
115
+ }
@@ -0,0 +1,75 @@
1
+ #!/usr/bin/env python3
2
+ """bundle.py: Mengemas hasil laporan PDF, DOCX, dan sumber Markdown menjadi arsip rilis zip."""
3
+
4
+ import glob
5
+ import os
6
+ import shutil
7
+ import sys
8
+ import zipfile
9
+
10
+ GREEN = "\033[0;32m"
11
+ BLUE = "\033[0;34m"
12
+ CYAN = "\033[0;36m"
13
+ BOLD = "\033[1m"
14
+ NC = "\033[0m"
15
+
16
+
17
+ def main():
18
+ root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
19
+ os.chdir(root)
20
+
21
+ print(f"{CYAN}{BOLD}")
22
+ print(" ========================================================")
23
+ print(" LAPORAN GENERATOR - BUNDLE PACKAGER ")
24
+ print(" ========================================================")
25
+ print(f"{NC}")
26
+
27
+ # Cek apakah PDF & DOCX sudah ada
28
+ if not os.path.isfile("Laporan.pdf") or not os.path.isfile("Laporan.docx"):
29
+ print(f" {BLUE}[1/3] Menjalankan build PDF dan DOCX...{NC}")
30
+ ret = os.system("./build.sh && make docx")
31
+ if ret != 0:
32
+ print("Gagal mengompilasi dokumen untuk bundling.")
33
+ return 1
34
+ else:
35
+ print(f" {GREEN}[OK]{NC} Berkas Laporan.pdf dan Laporan.docx siap dikemas.")
36
+
37
+ out_dir = "dist"
38
+ os.makedirs(out_dir, exist_ok=True)
39
+ zip_path = os.path.join(out_dir, "Laporan-Akademik-Lengkap.zip")
40
+
41
+ files_to_pack = [
42
+ "Laporan.pdf",
43
+ "Laporan.docx",
44
+ "metadata.yml",
45
+ "references.bib",
46
+ "cover.md",
47
+ ]
48
+
49
+ for f in glob.glob("chapters/*.md"):
50
+ files_to_pack.append(f)
51
+
52
+ if os.path.isdir("gambar"):
53
+ for f in glob.glob("gambar/**/*", recursive=True):
54
+ if os.path.isfile(f):
55
+ files_to_pack.append(f)
56
+
57
+ print(f" {BLUE}[2/3] Mengompresi berkas ke dalam {zip_path}...{NC}")
58
+ with zipfile.ZipFile(zip_path, "w", zipfile.ZIP_DEFLATED) as zf:
59
+ for f in files_to_pack:
60
+ if os.path.isfile(f):
61
+ zf.write(f, arcname=f)
62
+ sz = os.path.getsize(f) / 1024
63
+ print(f" + {f:<30} ({sz:.1f} KB)")
64
+
65
+ total_sz = os.path.getsize(zip_path) / 1024
66
+ print("")
67
+ print(f" {GREEN}{BOLD}[3/3] BUNDLE SELESAI DIBUAT!{NC}")
68
+ print(f" * Lokasi Arsip : {CYAN}{BOLD}{os.path.abspath(zip_path)}{NC}")
69
+ print(f" * Ukuran Arsip : {total_sz:.1f} KB ({len(files_to_pack)} berkas)")
70
+ print(" ========================================================")
71
+ return 0
72
+
73
+
74
+ if __name__ == "__main__":
75
+ sys.exit(main())
@@ -0,0 +1,192 @@
1
+ #!/usr/bin/env python3
2
+ """Isi nilai halaman asli ke field PAGEREF di daftar isi DOCX.
3
+
4
+ Dua-pass:
5
+ 1. Render DOCX (yang sudah di-finalize section-nya) via LibreOffice
6
+ (soffice --headless) menjadi PDF.
7
+ 2. Baca nomor halaman setiap heading dari teks PDF (footer per halaman),
8
+ lalu tulis sebagai nilai cache PAGEREF di daftar isi.
9
+
10
+ Hasil: daftar isi menampilkan nomor halaman asli di aplikasi manapun
11
+ (Word, LibreOffice, Google Docs) tanpa perlu update field manual.
12
+
13
+ Kalau soffice/pdftotext tidak tersedia, output = input (Word tetap
14
+ memperbarui field saat dibuka karena updateFields=true).
15
+
16
+ Usage:
17
+ python3 scripts/docx-pagenum.py input.docx output.docx
18
+ """
19
+
20
+ import os
21
+ import re
22
+ import shutil
23
+ import subprocess
24
+ import sys
25
+ import tempfile
26
+ import zipfile
27
+
28
+ ROMAN = re.compile(r"^[ivxlcdm]+$", re.I)
29
+ DECIMAL = re.compile(r"^\d+$")
30
+
31
+
32
+ def page_count(pdf):
33
+ out = subprocess.check_output(["pdfinfo", pdf], text=True, timeout=15)
34
+ m = re.search(r"^Pages:\s+(\d+)", out, re.M)
35
+ return int(m.group(1)) if m else 0
36
+
37
+
38
+ def page_texts(pdf):
39
+ n = page_count(pdf)
40
+ pages = {}
41
+ for p in range(1, n + 1):
42
+ out = subprocess.check_output(
43
+ ["pdftotext", "-f", str(p), "-l", str(p), "-layout", pdf, "-"],
44
+ text=True,
45
+ timeout=15,
46
+ )
47
+ lines = [l.strip() for l in out.splitlines() if l.strip()]
48
+ pages[p] = lines
49
+ return pages
50
+
51
+
52
+ def footer_of(lines):
53
+ if not lines:
54
+ return ""
55
+ return lines[-1]
56
+
57
+
58
+ def find_toc_start(pages):
59
+ for p, lines in sorted(pages.items()):
60
+ for l in lines:
61
+ if l == "DAFTAR ISI":
62
+ return p
63
+ return None
64
+
65
+
66
+ def first_decimal_page(pages, start):
67
+ for p, lines in sorted(pages.items()):
68
+ if p <= start:
69
+ continue
70
+ if DECIMAL.match(footer_of(lines)):
71
+ return p
72
+ return None
73
+
74
+
75
+ def find_page(pages, needle, start, end):
76
+ for p in range(start, end + 1):
77
+ text = "\n".join(pages.get(p, []))
78
+ if needle in text:
79
+ return p
80
+ return None
81
+
82
+
83
+ def parse_entries(doc):
84
+ entries = []
85
+ par_pat = re.compile(r"<w:p>(?:(?!</w:p>).)*?</w:p>", re.S)
86
+ for pm in par_pat.finditer(doc):
87
+ p = pm.group(0)
88
+ m = re.search(
89
+ r'<w:hyperlink w:anchor="([^"]+)"[^>]*>.*?<w:t[^>]*>([^<]*)</w:t>', p, re.S
90
+ )
91
+ r = re.search(
92
+ r"<w:instrText[^>]*>\s*PAGEREF\s+(\S+)\s+\\h\s*</w:instrText>", p
93
+ )
94
+ if m and r and m.group(1) == r.group(1):
95
+ entries.append((p, m.group(1), m.group(2)))
96
+ return entries
97
+
98
+
99
+ def needle_of(entry_text):
100
+ if entry_text.startswith("BAB "):
101
+ parts = entry_text.split(" ", 2)
102
+ return parts[2] if len(parts) == 3 else parts[1]
103
+ return entry_text
104
+
105
+
106
+ def render_pdf(docx):
107
+ tmp = tempfile.mkdtemp(prefix="lo_pagenum_")
108
+ profile = os.path.join(tmp, "profile")
109
+ subprocess.run(
110
+ [
111
+ "soffice", "--headless", "--norestore",
112
+ "-env:UserInstallation=file://" + profile,
113
+ "--convert-to", "pdf", "--outdir", tmp, docx,
114
+ ],
115
+ check=True,
116
+ capture_output=True,
117
+ timeout=45,
118
+ )
119
+ base = os.path.basename(docx).rsplit(".", 1)[0] + ".pdf"
120
+ return os.path.join(tmp, base)
121
+
122
+
123
+ def main():
124
+ src, dst = sys.argv[1], sys.argv[2]
125
+ shutil.copy(src, dst)
126
+
127
+ if not shutil.which("soffice") or not shutil.which("pdftotext"):
128
+ print("[warn] docx-pagenum: soffice/pdftotext tidak ada, nomor halaman "
129
+ "daftar isi memakai nilai cache (Word tetap update saat dibuka)")
130
+ return
131
+
132
+ with zipfile.ZipFile(src, "r") as zin:
133
+ names = zin.namelist()
134
+ items = {n: zin.read(n) for n in names}
135
+ doc = items["word/document.xml"].decode("utf-8")
136
+
137
+ try:
138
+ pdf = render_pdf(src)
139
+ except (subprocess.TimeoutExpired, subprocess.SubprocessError) as err:
140
+ print(f"[warn] docx-pagenum: render LibreOffice gagal/timeout ({err}), nomor halaman memakai nilai cache default.")
141
+ return
142
+
143
+ try:
144
+ pages = page_texts(pdf)
145
+ except (subprocess.TimeoutExpired, subprocess.SubprocessError) as err:
146
+ print(f"[warn] docx-pagenum: ekstraksi teks PDF gagal/timeout ({err}), nomor halaman memakai nilai cache default.")
147
+ return
148
+ finally:
149
+ shutil.rmtree(os.path.dirname(pdf), ignore_errors=True)
150
+
151
+ toc_start = find_toc_start(pages)
152
+ if not toc_start:
153
+ print("[warn] docx-pagenum: halaman DAFTAR ISI tidak ditemukan di render")
154
+ return
155
+ first_dec = first_decimal_page(pages, toc_start)
156
+ if not first_dec:
157
+ print("[warn] docx-pagenum: halaman body (decimal) tidak ditemukan")
158
+ return
159
+
160
+ entries = parse_entries(doc)
161
+ injected = 0
162
+ for para, pid, text in entries:
163
+ if pid == "kata-pengantar":
164
+ page = find_page(pages, "KATA PENGANTAR", 1, toc_start - 1)
165
+ else:
166
+ page = find_page(pages, needle_of(text), first_dec, max(pages))
167
+ if not page:
168
+ print(f"[warn] docx-pagenum: heading '{text}' tidak ditemukan di render")
169
+ continue
170
+ value = footer_of(pages[page])
171
+ if not (ROMAN.match(value) or DECIMAL.match(value)):
172
+ print(f"[warn] docx-pagenum: footer halaman {page} tidak valid: {value!r}")
173
+ continue
174
+ new_para = re.sub(
175
+ r'(<w:fldChar w:fldCharType="separate"/></w:r><w:r><w:t[^>]*>)[^<]*(</w:t>)',
176
+ r"\g<1>" + value + r"\g<2>",
177
+ para,
178
+ count=1,
179
+ )
180
+ doc = doc.replace(para, new_para)
181
+ injected += 1
182
+ print(f"[OK] {text} -> halaman {page} (nomor {value})")
183
+
184
+ items["word/document.xml"] = doc.encode("utf-8")
185
+ with zipfile.ZipFile(dst, "w", zipfile.ZIP_DEFLATED) as zout:
186
+ for n, content in items.items():
187
+ zout.writestr(n, content)
188
+ print(f"[OK] docx-pagenum: {injected} nomor halaman diisi -> {dst}")
189
+
190
+
191
+ if __name__ == "__main__":
192
+ main()