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
package/references.bib ADDED
@@ -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()
@@ -0,0 +1,291 @@
1
+ #!/usr/bin/env python3
2
+ """Finalisasi document.xml hasil pandoc agar penomoran halaman DOCX
3
+ mengikuti PDF:
4
+
5
+ - Cover : tanpa nomor halaman (footerReference dihapus)
6
+ - KATA PENGANTAR + ISI : i, ii (lowerRoman, start 1)
7
+ - BAB I : 1, 2, 3 ... (decimal, start 1)
8
+ - BAB II dst + PUSTAKA : lanjut (tanpa restart)
9
+
10
+ Pandoc otomatis menyisipkan paragraf `w:sectPr` (berisi footerReference
11
+ dari reference.docx) sebelum setiap Heading1. Script ini mengklasifikasikan
12
+ setiap sectPr berdasarkan heading berikutnya dan menyesuaikan isinya.
13
+
14
+ Usage:
15
+ python3 scripts/finalize-docx.py input.docx output.docx
16
+ """
17
+
18
+ import re
19
+ import sys
20
+ import zipfile
21
+
22
+ if sys.platform == "win32":
23
+ try:
24
+ sys.stdout.reconfigure(encoding="utf-8", errors="replace")
25
+ sys.stderr.reconfigure(encoding="utf-8", errors="replace")
26
+ except Exception:
27
+ pass
28
+
29
+ SECT_IN_PPR = re.compile(r"<w:pPr><w:sectPr>(.*?)</w:sectPr></w:pPr>", re.S)
30
+ FOOTER_REF = re.compile(r"<w:footerReference[^/]*/>")
31
+
32
+
33
+ def next_heading_text(doc, start):
34
+ nxt = doc[start:]
35
+ m = re.search(r'<w:pStyle w:val="Heading1"[^>]*/>', nxt)
36
+ if not m:
37
+ return ""
38
+ hdr = nxt[m.end():]
39
+ t = re.search(r"<w:t[^>]*>([^<]*)", hdr)
40
+ return t.group(1) if t else ""
41
+
42
+
43
+ def is_front_matter_heading(t):
44
+ norm = t.strip().upper()
45
+ return norm in (
46
+ "KATA PENGANTAR",
47
+ "PRAKATA",
48
+ "ABSTRAK",
49
+ "ABSTRACT",
50
+ "DAFTAR ISI",
51
+ "DAFTAR TABEL",
52
+ "DAFTAR GAMBAR",
53
+ "DAFTAR LAMPIRAN",
54
+ )
55
+
56
+
57
+ def is_chapter_1(t):
58
+ norm = t.strip().upper()
59
+ if re.match(r"^BAB\s+(?:I|1|01)\b", norm):
60
+ return True
61
+ if re.match(r"^(?:1\.|1\s+|I\.)", norm):
62
+ return True
63
+ if norm.startswith("PENDAHULUAN"):
64
+ return True
65
+ return False
66
+
67
+
68
+ def is_chapter_2(t):
69
+ norm = t.strip().upper()
70
+ if re.match(r"^BAB\s+(?:II|2|02)\b", norm):
71
+ return True
72
+ if re.match(r"^(?:2\.|2\s+|II\.)", norm):
73
+ return True
74
+ if any(
75
+ norm.startswith(k)
76
+ for k in (
77
+ "PEMBAHASAN",
78
+ "TINJAUAN PUSTAKA",
79
+ "LANDASAN TEORI",
80
+ "METODOLOGI",
81
+ "METODE PENELITIAN",
82
+ )
83
+ ):
84
+ return True
85
+ return False
86
+
87
+
88
+ def classify(text):
89
+ t = text.strip()
90
+ if is_front_matter_heading(t):
91
+ return "cover"
92
+ if is_chapter_1(t):
93
+ return "roman"
94
+ if is_chapter_2(t):
95
+ return "decimal-start"
96
+ return "body"
97
+
98
+
99
+ def add_pgnum(sect, fmt, start):
100
+ if "<w:pgNumType" in sect:
101
+ return sect
102
+ tag = f'<w:pgNumType w:fmt="{fmt}" w:start="{start}"/>'
103
+ return sect + tag
104
+
105
+
106
+ def process(doc):
107
+ matches = list(SECT_IN_PPR.finditer(doc))
108
+ if not matches:
109
+ return doc, 0
110
+ out = []
111
+ last = 0
112
+ changed = 0
113
+ for m in matches:
114
+ out.append(doc[last:m.start()])
115
+ sect = m.group(1)
116
+ kind = classify(next_heading_text(doc, m.end()))
117
+ if kind == "cover":
118
+ sect = FOOTER_REF.sub("", sect)
119
+ changed += 1
120
+ elif kind == "roman":
121
+ sect = add_pgnum(sect, "lowerRoman", "1")
122
+ changed += 1
123
+ elif kind == "decimal-start":
124
+ sect = add_pgnum(sect, "decimal", "1")
125
+ changed += 1
126
+ out.append(f"<w:pPr><w:sectPr>{sect}</w:sectPr></w:pPr>")
127
+ last = m.end()
128
+ out.append(doc[last:])
129
+ return "".join(out), changed
130
+
131
+
132
+ def get_tab_pos(doc):
133
+ sz = re.search(r'<w:pgSz\b[^>]*w:w="(\d+)"', doc)
134
+ mar_left = re.search(r'<w:pgMar\b[^>]*w:left="(\d+)"', doc)
135
+ mar_right = re.search(r'<w:pgMar\b[^>]*w:right="(\d+)"', doc)
136
+ if sz and mar_left and mar_right:
137
+ try:
138
+ w = int(sz.group(1))
139
+ left = int(mar_left.group(1))
140
+ right = int(mar_right.group(1))
141
+ pos = w - left - right
142
+ if pos > 1000:
143
+ return pos
144
+ except (ValueError, TypeError):
145
+ pass
146
+ return 9072
147
+
148
+
149
+ def fix_daftar_isi_title(doc):
150
+ pattern = re.compile(
151
+ r'(<w:p\b[^>]*><w:pPr>(?:(?!</w:p>).)*?<w:pStyle\s+w:val=["\x27])Heading1(["\x27](?:(?!</w:p>).)*?<w:t[^>]*>\s*DAFTAR ISI\s*</w:t>(?:(?!</w:p>).)*?</w:p>)',
152
+ re.S,
153
+ )
154
+ return pattern.sub(r"\g<1>TOCHeading\g<2>", doc)
155
+
156
+
157
+ def fix_toc_styles(styles, tab_pos):
158
+ tnr = '<w:rFonts w:ascii="Times New Roman" w:hAnsi="Times New Roman" w:cs="Times New Roman"/>'
159
+ defs = {
160
+ "TOCHeading": f"""<w:style w:type="paragraph" w:styleId="TOCHeading">
161
+ <w:name w:val="TOC Heading"/>
162
+ <w:basedOn w:val="Normal"/>
163
+ <w:next w:val="Normal"/>
164
+ <w:uiPriority w:val="39"/>
165
+ <w:qFormat/>
166
+ <w:pPr>
167
+ <w:spacing w:before="360" w:after="360" w:line="240" w:lineRule="auto"/>
168
+ <w:jc w:val="center"/>
169
+ <w:outlineLvl w:val="9"/>
170
+ </w:pPr>
171
+ <w:rPr>
172
+ {tnr}
173
+ <w:color w:val="000000"/>
174
+ <w:sz w:val="28"/>
175
+ <w:szCs w:val="28"/>
176
+ <w:b/>
177
+ <w:bCs/>
178
+ </w:rPr>
179
+ </w:style>""",
180
+ "TOC1": f"""<w:style w:type="paragraph" w:styleId="TOC1">
181
+ <w:name w:val="toc 1"/>
182
+ <w:basedOn w:val="Normal"/>
183
+ <w:next w:val="Normal"/>
184
+ <w:qFormat/>
185
+ <w:pPr>
186
+ <w:tabs>
187
+ <w:tab w:val="right" w:leader="dot" w:pos="{tab_pos}"/>
188
+ </w:tabs>
189
+ <w:spacing w:before="120" w:after="40" w:line="260" w:lineRule="auto"/>
190
+ <w:ind w:left="0" w:right="0"/>
191
+ <w:jc w:val="left"/>
192
+ </w:pPr>
193
+ <w:rPr>
194
+ {tnr}
195
+ <w:color w:val="000000"/>
196
+ <w:sz w:val="24"/>
197
+ <w:szCs w:val="24"/>
198
+ <w:b/>
199
+ <w:bCs/>
200
+ </w:rPr>
201
+ </w:style>""",
202
+ "TOC2": f"""<w:style w:type="paragraph" w:styleId="TOC2">
203
+ <w:name w:val="toc 2"/>
204
+ <w:basedOn w:val="TOC1"/>
205
+ <w:next w:val="Normal"/>
206
+ <w:qFormat/>
207
+ <w:pPr>
208
+ <w:tabs>
209
+ <w:tab w:val="right" w:leader="dot" w:pos="{tab_pos}"/>
210
+ </w:tabs>
211
+ <w:spacing w:before="20" w:after="20" w:line="240" w:lineRule="auto"/>
212
+ <w:ind w:left="360" w:right="0"/>
213
+ <w:jc w:val="left"/>
214
+ </w:pPr>
215
+ <w:rPr>
216
+ {tnr}
217
+ <w:color w:val="000000"/>
218
+ <w:sz w:val="24"/>
219
+ <w:szCs w:val="24"/>
220
+ <w:b w:val="0"/>
221
+ <w:bCs w:val="0"/>
222
+ </w:rPr>
223
+ </w:style>""",
224
+ "TOC3": f"""<w:style w:type="paragraph" w:styleId="TOC3">
225
+ <w:name w:val="toc 3"/>
226
+ <w:basedOn w:val="TOC2"/>
227
+ <w:next w:val="Normal"/>
228
+ <w:qFormat/>
229
+ <w:pPr>
230
+ <w:tabs>
231
+ <w:tab w:val="right" w:leader="dot" w:pos="{tab_pos}"/>
232
+ </w:tabs>
233
+ <w:spacing w:before="0" w:after="0" w:line="240" w:lineRule="auto"/>
234
+ <w:ind w:left="720" w:right="0"/>
235
+ <w:jc w:val="left"/>
236
+ </w:pPr>
237
+ <w:rPr>
238
+ {tnr}
239
+ <w:color w:val="000000"/>
240
+ <w:sz w:val="24"/>
241
+ <w:szCs w:val="24"/>
242
+ <w:b w:val="0"/>
243
+ <w:bCs w:val="0"/>
244
+ </w:rPr>
245
+ </w:style>""",
246
+ }
247
+ missing = []
248
+ for sid, block in defs.items():
249
+ pattern = re.compile(rf'<w:style\b[^>]*w:styleId="{sid}"[^>]*>.*?</w:style>', re.S)
250
+ if pattern.search(styles):
251
+ styles = pattern.sub(block, styles)
252
+ else:
253
+ missing.append(block)
254
+ if missing:
255
+ styles = styles.replace("</w:styles>", "".join(missing) + "</w:styles>")
256
+ return styles
257
+
258
+
259
+ def main():
260
+ src = sys.argv[1]
261
+ dst = sys.argv[2]
262
+
263
+ with zipfile.ZipFile(src, "r") as zin:
264
+ names = zin.namelist()
265
+ items = {n: zin.read(n) for n in names}
266
+
267
+ doc = items["word/document.xml"].decode("utf-8")
268
+ tab_pos = get_tab_pos(doc)
269
+ doc = fix_daftar_isi_title(doc)
270
+ # Sesuaikan tab leader pada cached TOC entries agar tepat di margin kanan
271
+ doc = re.sub(
272
+ r'(<w:pStyle w:val="TOC[123]"/>.*?<w:tab w:val="right" w:leader="dot" w:pos=)"\d+"',
273
+ rf'\g<1>"{tab_pos}"',
274
+ doc,
275
+ )
276
+ new_doc, changed = process(doc)
277
+ items["word/document.xml"] = new_doc.encode("utf-8")
278
+
279
+ if "word/styles.xml" in items:
280
+ styles = items["word/styles.xml"].decode("utf-8")
281
+ items["word/styles.xml"] = fix_toc_styles(styles, tab_pos).encode("utf-8")
282
+
283
+ with zipfile.ZipFile(dst, "w", zipfile.ZIP_DEFLATED) as zout:
284
+ for n in items:
285
+ zout.writestr(n, items[n])
286
+
287
+ print(f"[OK] finalize-docx: {changed} sectPr disesuaikan (tab={tab_pos}) -> {dst}")
288
+
289
+
290
+ if __name__ == "__main__":
291
+ main()