laporan-generator 2.6.2 → 2.7.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.
- package/.agents/skills/laporan-generator/SKILL.md +99 -109
- package/.agents/skills/laporan-generator/resources/laporan +37 -4
- package/.agents/skills/laporan-generator/resources/laporan.ps1 +74 -28
- package/.agents/skills/laporan-generator/resources/scripts/bundle.py +11 -2
- package/.agents/skills/laporan-generator/resources/scripts/report-doctor.py +121 -48
- package/.agents/skills/laporan-generator/resources/scripts/report-stats.py +53 -2
- package/.agents/skills/laporan-generator/resources/scripts/test_scripts.py +63 -2
- package/.agents/skills/laporan-generator/scripts/bundle.py +11 -2
- package/.agents/skills/laporan-generator/scripts/finalize-docx.py +140 -1
- package/.agents/skills/laporan-generator/scripts/make-reference-docx.py +42 -2
- package/.agents/skills/laporan-generator/scripts/report-doctor.py +121 -48
- package/.agents/skills/laporan-generator/scripts/report-stats.py +53 -2
- package/.agents/skills/laporan-generator/scripts/test_scripts.py +88 -2
- package/README.md +44 -0
- package/bin/laporan-generator.js +15 -3
- package/bin/mcp-server.js +4 -0
- package/laporan +37 -4
- package/laporan.ps1 +50 -11
- package/lib/init.js +115 -44
- package/lib/mcp.js +569 -0
- package/lib/sync-hosts.js +196 -68
- package/lib/uninstall.js +21 -0
- package/package.json +9 -3
- package/scripts/bundle.py +11 -2
- package/scripts/report-doctor.py +121 -48
- package/scripts/report-stats.py +46 -2
- package/scripts/test_scripts.py +50 -3
|
@@ -2,11 +2,19 @@
|
|
|
2
2
|
"""report-doctor.py: Audit komprehensif kesehatan dan integritas proyek laporan."""
|
|
3
3
|
|
|
4
4
|
import glob
|
|
5
|
+
import json
|
|
5
6
|
import os
|
|
6
7
|
import re
|
|
7
8
|
import shutil
|
|
8
9
|
import sys
|
|
9
10
|
|
|
11
|
+
if sys.platform == "win32":
|
|
12
|
+
try:
|
|
13
|
+
sys.stdout.reconfigure(encoding="utf-8", errors="replace")
|
|
14
|
+
sys.stderr.reconfigure(encoding="utf-8", errors="replace")
|
|
15
|
+
except Exception:
|
|
16
|
+
pass
|
|
17
|
+
|
|
10
18
|
GREEN = "\033[0;32m"
|
|
11
19
|
BLUE = "\033[0;34m"
|
|
12
20
|
CYAN = "\033[0;36m"
|
|
@@ -98,101 +106,166 @@ def check_style_anomalies(markdown_files):
|
|
|
98
106
|
|
|
99
107
|
|
|
100
108
|
def main():
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
+
json_mode = "--json" in sys.argv
|
|
110
|
+
pos_args = [a for a in sys.argv[1:] if not a.startswith("-")]
|
|
111
|
+
|
|
112
|
+
target_dir = os.getcwd()
|
|
113
|
+
if pos_args:
|
|
114
|
+
target_dir = os.path.abspath(pos_args[0])
|
|
115
|
+
elif not (os.path.isfile(os.path.join(target_dir, "metadata.yml")) or os.path.isdir(os.path.join(target_dir, "chapters"))):
|
|
116
|
+
pkg_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
117
|
+
if os.path.isfile(os.path.join(pkg_root, "metadata.yml")):
|
|
118
|
+
target_dir = pkg_root
|
|
119
|
+
|
|
120
|
+
os.chdir(target_dir)
|
|
121
|
+
|
|
122
|
+
if not json_mode:
|
|
123
|
+
print(f"{CYAN}{BOLD}")
|
|
124
|
+
print(" ========================================================")
|
|
125
|
+
print(" LAPORAN GENERATOR - DOCTOR & AUDIT SUITE ")
|
|
126
|
+
print(" ========================================================")
|
|
127
|
+
print(f"{NC}")
|
|
109
128
|
|
|
110
129
|
errors = 0
|
|
111
130
|
warnings = 0
|
|
112
131
|
|
|
113
132
|
# 1. Dependensi
|
|
114
|
-
|
|
133
|
+
if not json_mode:
|
|
134
|
+
print(f" {BLUE}{BOLD}[1/4] Audit Toolchain & Dependensi Sistem{NC}")
|
|
115
135
|
deps, has_im = check_dependencies()
|
|
136
|
+
dep_list = []
|
|
116
137
|
for cmd, name, req, found in deps:
|
|
117
138
|
if "ImageMagick" in name:
|
|
118
139
|
continue
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
print(f" {RED}[FAIL]{NC} {name:<24}: TIDAK DITEMUKAN (Wajib)")
|
|
124
|
-
errors += 1
|
|
140
|
+
dep_list.append({"command": cmd, "name": name, "required": req, "found": found})
|
|
141
|
+
if not json_mode:
|
|
142
|
+
if found:
|
|
143
|
+
print(f" {GREEN}[OK]{NC} {name:<26}: Terpasang ({cmd})")
|
|
125
144
|
else:
|
|
126
|
-
|
|
145
|
+
if req:
|
|
146
|
+
print(f" {RED}[FAIL]{NC} {name:<24}: TIDAK DITEMUKAN (Wajib)")
|
|
147
|
+
else:
|
|
148
|
+
print(f" {YELLOW}[INFO]{NC} {name:<24}: Tidak ditemukan (Opsional)")
|
|
149
|
+
if not found and req:
|
|
150
|
+
errors += 1
|
|
127
151
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
152
|
+
dep_list.append({"command": "magick/convert", "name": "ImageMagick", "required": False, "found": has_im})
|
|
153
|
+
if not json_mode:
|
|
154
|
+
if has_im:
|
|
155
|
+
print(f" {GREEN}[OK]{NC} {'ImageMagick':<26}: Terpasang")
|
|
156
|
+
else:
|
|
157
|
+
print(f" {YELLOW}[INFO]{NC} {'ImageMagick':<24}: Tidak ditemukan (Opsional untuk kompresi gambar)")
|
|
158
|
+
print("")
|
|
133
159
|
|
|
134
160
|
# 2. Berkas & Struktur
|
|
135
|
-
|
|
161
|
+
if not json_mode:
|
|
162
|
+
print(f" {BLUE}{BOLD}[2/4] Audit Struktur Direktori & Berkas Wajib{NC}")
|
|
136
163
|
req_files = [
|
|
137
164
|
"metadata.yml", "template.typ", "reference.docx",
|
|
138
165
|
"references.bib", "docx.lua", "cover.md"
|
|
139
166
|
]
|
|
167
|
+
file_status = []
|
|
140
168
|
for rf in req_files:
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
169
|
+
exists = os.path.isfile(rf)
|
|
170
|
+
file_status.append({"file": rf, "found": exists})
|
|
171
|
+
if not json_mode:
|
|
172
|
+
if exists:
|
|
173
|
+
print(f" {GREEN}[OK]{NC} Berkas {rf}")
|
|
174
|
+
else:
|
|
175
|
+
print(f" {RED}[FAIL]{NC} Berkas {rf} TIDAK DITEMUKAN")
|
|
176
|
+
if not exists:
|
|
145
177
|
errors += 1
|
|
146
178
|
|
|
147
179
|
chapter_files = sorted(glob.glob("chapters/*.md"))
|
|
148
|
-
if
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
180
|
+
if not json_mode:
|
|
181
|
+
if chapter_files:
|
|
182
|
+
print(f" {GREEN}[OK]{NC} Direktori chapters/ ({len(chapter_files)} bab ditemukan)")
|
|
183
|
+
else:
|
|
184
|
+
print(f" {RED}[FAIL]{NC} Tidak ada berkas markdown di chapters/")
|
|
185
|
+
if not chapter_files:
|
|
152
186
|
errors += 1
|
|
153
|
-
|
|
187
|
+
if not json_mode:
|
|
188
|
+
print("")
|
|
154
189
|
|
|
155
190
|
# 3. Sitasi & Referensi
|
|
156
|
-
|
|
191
|
+
if not json_mode:
|
|
192
|
+
print(f" {BLUE}{BOLD}[3/4] Audit Sitasi & Integritas Bibliografi{NC}")
|
|
157
193
|
bib_keys, cited_keys, missing_keys, orphan_keys = check_citations("references.bib", chapter_files)
|
|
158
|
-
|
|
159
|
-
|
|
194
|
+
if not json_mode:
|
|
195
|
+
print(f" * Total entri .bib : {len(bib_keys)}")
|
|
196
|
+
print(f" * Total sitasi di teks : {len(cited_keys)}")
|
|
160
197
|
|
|
161
198
|
if missing_keys:
|
|
162
199
|
for k, locs in missing_keys.items():
|
|
163
200
|
loc_str = ", ".join([f"{f}:{l}" for f, l in locs])
|
|
164
|
-
|
|
201
|
+
if not json_mode:
|
|
202
|
+
print(f" {RED}[ERROR]{NC} Sitasi '@{k}' TIDAK DITEMUKAN di references.bib ({loc_str})")
|
|
165
203
|
errors += 1
|
|
166
|
-
|
|
204
|
+
elif not json_mode:
|
|
167
205
|
print(f" {GREEN}[OK]{NC} Semua sitasi dalam teks valid terdaftar di references.bib")
|
|
168
206
|
|
|
169
207
|
if orphan_keys:
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
208
|
+
if not json_mode:
|
|
209
|
+
print(f" {YELLOW}[WARN]{NC} {len(orphan_keys)} entri di references.bib belum disitir dalam teks:")
|
|
210
|
+
for ok in list(orphan_keys)[:5]:
|
|
211
|
+
print(f" - @{ok}")
|
|
212
|
+
if len(orphan_keys) > 5:
|
|
213
|
+
print(f" - ... dan {len(orphan_keys) - 5} lainnya")
|
|
175
214
|
warnings += 1
|
|
176
|
-
|
|
215
|
+
if not json_mode:
|
|
216
|
+
print("")
|
|
177
217
|
|
|
178
218
|
# 4. Gambar & Format Teks
|
|
179
|
-
|
|
219
|
+
if not json_mode:
|
|
220
|
+
print(f" {BLUE}{BOLD}[4/4] Audit Media Citra & Anomali Tipografi{NC}")
|
|
180
221
|
total_imgs, broken_imgs = check_images(chapter_files)
|
|
181
222
|
if broken_imgs:
|
|
182
223
|
for p, f, l in broken_imgs:
|
|
183
|
-
|
|
224
|
+
if not json_mode:
|
|
225
|
+
print(f" {RED}[ERROR]{NC} Berkas gambar tidak ditemukan: '{p}' (di {f}:{l})")
|
|
184
226
|
errors += 1
|
|
185
|
-
|
|
227
|
+
elif not json_mode:
|
|
186
228
|
print(f" {GREEN}[OK]{NC} Semua tautan gambar ({total_imgs} gambar) valid dan ada di disk")
|
|
187
229
|
|
|
188
230
|
anomalies = check_style_anomalies(chapter_files)
|
|
189
231
|
if anomalies:
|
|
190
232
|
for f, l, msg in anomalies:
|
|
191
|
-
|
|
233
|
+
if not json_mode:
|
|
234
|
+
print(f" {YELLOW}[WARN]{NC} {f}:{l} - {msg}")
|
|
192
235
|
warnings += 1
|
|
193
|
-
|
|
236
|
+
elif not json_mode:
|
|
194
237
|
print(f" {GREEN}[OK]{NC} Bebas dari manual heading numbering dan box-drawing characters")
|
|
195
|
-
|
|
238
|
+
if not json_mode:
|
|
239
|
+
print("")
|
|
240
|
+
|
|
241
|
+
score = 100 if (errors == 0 and warnings == 0) else (95 if errors == 0 else max(0, 90 - errors * 15 - warnings * 5))
|
|
242
|
+
status_label = "HEALTHY" if errors == 0 and warnings == 0 else ("WARNING" if errors == 0 else "ERROR")
|
|
243
|
+
|
|
244
|
+
if json_mode:
|
|
245
|
+
result = {
|
|
246
|
+
"status": status_label,
|
|
247
|
+
"score": score,
|
|
248
|
+
"errors_count": errors,
|
|
249
|
+
"warnings_count": warnings,
|
|
250
|
+
"target_dir": os.getcwd(),
|
|
251
|
+
"dependencies": dep_list,
|
|
252
|
+
"required_files": file_status,
|
|
253
|
+
"chapters_count": len(chapter_files),
|
|
254
|
+
"chapters": [os.path.basename(c) for c in chapter_files],
|
|
255
|
+
"citations": {
|
|
256
|
+
"bib_count": len(bib_keys),
|
|
257
|
+
"cited_count": len(cited_keys),
|
|
258
|
+
"missing_keys": {k: [f"{f}:{l}" for f, l in locs] for k, locs in missing_keys.items()},
|
|
259
|
+
"orphan_keys": list(orphan_keys),
|
|
260
|
+
},
|
|
261
|
+
"images": {
|
|
262
|
+
"total": total_imgs,
|
|
263
|
+
"broken": [{"path": p, "file": f, "line": l} for p, f, l in broken_imgs],
|
|
264
|
+
},
|
|
265
|
+
"anomalies": [{"file": f, "line": l, "message": msg} for f, l, msg in anomalies],
|
|
266
|
+
}
|
|
267
|
+
print(json.dumps(result, indent=2))
|
|
268
|
+
return 1 if errors > 0 else 0
|
|
196
269
|
|
|
197
270
|
# Skor Kesehatan
|
|
198
271
|
print(" ========================================================")
|
|
@@ -2,10 +2,18 @@
|
|
|
2
2
|
"""report-stats.py: Menghitung statistik komprehensif dokumen laporan akademik."""
|
|
3
3
|
|
|
4
4
|
import glob
|
|
5
|
+
import json
|
|
5
6
|
import os
|
|
6
7
|
import re
|
|
7
8
|
import sys
|
|
8
9
|
|
|
10
|
+
if sys.platform == "win32":
|
|
11
|
+
try:
|
|
12
|
+
sys.stdout.reconfigure(encoding="utf-8", errors="replace")
|
|
13
|
+
sys.stderr.reconfigure(encoding="utf-8", errors="replace")
|
|
14
|
+
except Exception:
|
|
15
|
+
pass
|
|
16
|
+
|
|
9
17
|
GREEN = "\033[0;32m"
|
|
10
18
|
BLUE = "\033[0;34m"
|
|
11
19
|
CYAN = "\033[0;36m"
|
|
@@ -48,8 +56,18 @@ def count_file_stats(filepath):
|
|
|
48
56
|
|
|
49
57
|
|
|
50
58
|
def main():
|
|
51
|
-
|
|
52
|
-
|
|
59
|
+
json_mode = "--json" in sys.argv
|
|
60
|
+
pos_args = [a for a in sys.argv[1:] if not a.startswith("-")]
|
|
61
|
+
|
|
62
|
+
target_dir = os.getcwd()
|
|
63
|
+
if pos_args:
|
|
64
|
+
target_dir = os.path.abspath(pos_args[0])
|
|
65
|
+
elif not (os.path.isfile(os.path.join(target_dir, "metadata.yml")) or os.path.isdir(os.path.join(target_dir, "chapters"))):
|
|
66
|
+
pkg_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
67
|
+
if os.path.isfile(os.path.join(pkg_root, "metadata.yml")):
|
|
68
|
+
target_dir = pkg_root
|
|
69
|
+
|
|
70
|
+
os.chdir(target_dir)
|
|
53
71
|
|
|
54
72
|
title = "Laporan Akademik"
|
|
55
73
|
preset = "standard"
|
|
@@ -109,6 +127,39 @@ def main():
|
|
|
109
127
|
sz = os.path.getsize("Laporan.docx")
|
|
110
128
|
docx_size = f"{sz / 1024:.1f} KB"
|
|
111
129
|
|
|
130
|
+
if json_mode:
|
|
131
|
+
result = {
|
|
132
|
+
"title": title,
|
|
133
|
+
"preset": preset,
|
|
134
|
+
"target_dir": os.getcwd(),
|
|
135
|
+
"chapters_count": len(chapter_files),
|
|
136
|
+
"total_words": total_words,
|
|
137
|
+
"total_chars": total_chars,
|
|
138
|
+
"est_pages": est_pages,
|
|
139
|
+
"read_mins": read_mins,
|
|
140
|
+
"headings": {
|
|
141
|
+
"h1": total_h1,
|
|
142
|
+
"h2": total_h2,
|
|
143
|
+
"h3": total_h3,
|
|
144
|
+
},
|
|
145
|
+
"elements": {
|
|
146
|
+
"images": total_images,
|
|
147
|
+
"tables": total_tables,
|
|
148
|
+
"equations": total_equations,
|
|
149
|
+
},
|
|
150
|
+
"citations": {
|
|
151
|
+
"unique_cited": len(all_citations),
|
|
152
|
+
"bib_entries": bib_count,
|
|
153
|
+
},
|
|
154
|
+
"compiled_files": {
|
|
155
|
+
"pdf": pdf_size,
|
|
156
|
+
"docx": docx_size,
|
|
157
|
+
},
|
|
158
|
+
"file_breakdown": [{"filename": f, "words": c} for f, c in file_breakdown],
|
|
159
|
+
}
|
|
160
|
+
print(json.dumps(result, indent=2))
|
|
161
|
+
return
|
|
162
|
+
|
|
112
163
|
print(f"{CYAN}{BOLD}")
|
|
113
164
|
print(" ========================================================")
|
|
114
165
|
print(" LAPORAN GENERATOR - STATISTIK DOKUMEN ")
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
2
|
"""test_scripts.py: Automated unit tests for Laporan Generator Python helper scripts."""
|
|
3
3
|
|
|
4
|
+
import json
|
|
4
5
|
import os
|
|
6
|
+
import subprocess
|
|
5
7
|
import sys
|
|
6
8
|
import unittest
|
|
7
9
|
|
|
@@ -36,16 +38,39 @@ class TestScanPreset(unittest.TestCase):
|
|
|
36
38
|
self.assertIn("UNIVERSITAS INDONESIA", inst)
|
|
37
39
|
|
|
38
40
|
|
|
41
|
+
def get_presets_dir():
|
|
42
|
+
for c in [
|
|
43
|
+
os.path.join(ROOT_DIR, "presets"),
|
|
44
|
+
os.path.join(ROOT_DIR, "resources", "presets"),
|
|
45
|
+
os.path.join(ROOT_DIR, "..", "..", "..", "presets"),
|
|
46
|
+
]:
|
|
47
|
+
if os.path.isdir(c):
|
|
48
|
+
return os.path.abspath(c)
|
|
49
|
+
return os.path.join(ROOT_DIR, "presets")
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def get_project_root():
|
|
53
|
+
for c in [
|
|
54
|
+
ROOT_DIR,
|
|
55
|
+
os.path.join(ROOT_DIR, "resources"),
|
|
56
|
+
os.path.join(ROOT_DIR, "..", "..", ".."),
|
|
57
|
+
]:
|
|
58
|
+
if os.path.isfile(os.path.join(c, "metadata.yml")):
|
|
59
|
+
return os.path.abspath(c)
|
|
60
|
+
return ROOT_DIR
|
|
61
|
+
|
|
62
|
+
|
|
39
63
|
class TestValidatePreset(unittest.TestCase):
|
|
40
64
|
def test_parse_simple_yaml(self):
|
|
41
|
-
|
|
65
|
+
presets_dir = get_presets_dir()
|
|
66
|
+
test_yaml = os.path.join(presets_dir, "standard.yml")
|
|
42
67
|
self.assertTrue(os.path.exists(test_yaml))
|
|
43
68
|
data = validate_preset.parse_simple_yaml(test_yaml)
|
|
44
69
|
self.assertEqual(data.get("preset_id"), "standard")
|
|
45
70
|
self.assertEqual(data.get("margin_top"), "2cm")
|
|
46
71
|
|
|
47
72
|
def test_validate_all_presets(self):
|
|
48
|
-
presets_dir =
|
|
73
|
+
presets_dir = get_presets_dir()
|
|
49
74
|
for f in os.listdir(presets_dir):
|
|
50
75
|
if f.endswith(".yml"):
|
|
51
76
|
path = os.path.join(presets_dir, f)
|
|
@@ -123,6 +148,42 @@ class TestFinalizeDocx(unittest.TestCase):
|
|
|
123
148
|
self.assertIn('w:left="720"', res)
|
|
124
149
|
|
|
125
150
|
|
|
151
|
+
report_stats = __import__("report-stats")
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
class TestReportStats(unittest.TestCase):
|
|
155
|
+
def test_count_file_stats_structure(self):
|
|
156
|
+
sample_md = os.path.join(get_project_root(), "cover.md")
|
|
157
|
+
if os.path.exists(sample_md):
|
|
158
|
+
stats = report_stats.count_file_stats(sample_md)
|
|
159
|
+
self.assertIsInstance(stats, dict)
|
|
160
|
+
self.assertIsInstance(stats["words"], int)
|
|
161
|
+
self.assertIsInstance(stats["chars"], int)
|
|
162
|
+
self.assertGreaterEqual(stats["words"], 0)
|
|
163
|
+
|
|
164
|
+
def test_json_stats_execution(self):
|
|
165
|
+
proj_root = get_project_root()
|
|
166
|
+
cmd = [sys.executable, os.path.join(SCRIPTS_DIR, "report-stats.py"), proj_root, "--json"]
|
|
167
|
+
proc = subprocess.run(cmd, capture_output=True, text=True, cwd=proj_root)
|
|
168
|
+
self.assertEqual(proc.returncode, 0)
|
|
169
|
+
data = json.loads(proc.stdout)
|
|
170
|
+
self.assertIn("total_words", data)
|
|
171
|
+
self.assertIn("preset", data)
|
|
172
|
+
self.assertIn("target_dir", data)
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
class TestReportDoctor(unittest.TestCase):
|
|
176
|
+
def test_json_doctor_execution(self):
|
|
177
|
+
proj_root = get_project_root()
|
|
178
|
+
cmd = [sys.executable, os.path.join(SCRIPTS_DIR, "report-doctor.py"), proj_root, "--json"]
|
|
179
|
+
proc = subprocess.run(cmd, capture_output=True, text=True, cwd=proj_root)
|
|
180
|
+
data = json.loads(proc.stdout)
|
|
181
|
+
self.assertIn("status", data)
|
|
182
|
+
self.assertIn("dependencies", data)
|
|
183
|
+
self.assertIn("required_files", data)
|
|
184
|
+
self.assertIn("citations", data)
|
|
185
|
+
|
|
186
|
+
|
|
126
187
|
if __name__ == "__main__":
|
|
127
188
|
unittest.main(verbosity=2)
|
|
128
189
|
|
|
@@ -15,8 +15,17 @@ NC = "\033[0m"
|
|
|
15
15
|
|
|
16
16
|
|
|
17
17
|
def main():
|
|
18
|
-
|
|
19
|
-
|
|
18
|
+
pos_args = [a for a in sys.argv[1:] if not a.startswith("-")]
|
|
19
|
+
|
|
20
|
+
target_dir = os.getcwd()
|
|
21
|
+
if pos_args:
|
|
22
|
+
target_dir = os.path.abspath(pos_args[0])
|
|
23
|
+
elif not (os.path.isfile(os.path.join(target_dir, "metadata.yml")) or os.path.isdir(os.path.join(target_dir, "chapters"))):
|
|
24
|
+
pkg_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
25
|
+
if os.path.isfile(os.path.join(pkg_root, "metadata.yml")):
|
|
26
|
+
target_dir = pkg_root
|
|
27
|
+
|
|
28
|
+
os.chdir(target_dir)
|
|
20
29
|
|
|
21
30
|
print(f"{CYAN}{BOLD}")
|
|
22
31
|
print(" ========================================================")
|
|
@@ -129,6 +129,133 @@ def process(doc):
|
|
|
129
129
|
return "".join(out), changed
|
|
130
130
|
|
|
131
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
|
+
|
|
132
259
|
def main():
|
|
133
260
|
src = sys.argv[1]
|
|
134
261
|
dst = sys.argv[2]
|
|
@@ -138,14 +265,26 @@ def main():
|
|
|
138
265
|
items = {n: zin.read(n) for n in names}
|
|
139
266
|
|
|
140
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
|
+
)
|
|
141
276
|
new_doc, changed = process(doc)
|
|
142
277
|
items["word/document.xml"] = new_doc.encode("utf-8")
|
|
143
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
|
+
|
|
144
283
|
with zipfile.ZipFile(dst, "w", zipfile.ZIP_DEFLATED) as zout:
|
|
145
284
|
for n in items:
|
|
146
285
|
zout.writestr(n, items[n])
|
|
147
286
|
|
|
148
|
-
print(f"[OK] finalize-docx: {changed} sectPr disesuaikan -> {dst}")
|
|
287
|
+
print(f"[OK] finalize-docx: {changed} sectPr disesuaikan (tab={tab_pos}) -> {dst}")
|
|
149
288
|
|
|
150
289
|
|
|
151
290
|
if __name__ == "__main__":
|
|
@@ -329,6 +329,26 @@ STYLES = {
|
|
|
329
329
|
<w:szCs w:val="24"/>
|
|
330
330
|
<w:i/>
|
|
331
331
|
</w:rPr>
|
|
332
|
+
</w:style>""",
|
|
333
|
+
"TOCHeading": f"""<w:style w:type="paragraph" w:styleId="TOCHeading">
|
|
334
|
+
<w:name w:val="TOC Heading"/>
|
|
335
|
+
<w:basedOn w:val="Normal"/>
|
|
336
|
+
<w:next w:val="Normal"/>
|
|
337
|
+
<w:uiPriority w:val="39"/>
|
|
338
|
+
<w:qFormat/>
|
|
339
|
+
<w:pPr>
|
|
340
|
+
<w:spacing w:before="360" w:after="360" w:line="240" w:lineRule="auto"/>
|
|
341
|
+
<w:jc w:val="center"/>
|
|
342
|
+
<w:outlineLvl w:val="9"/>
|
|
343
|
+
</w:pPr>
|
|
344
|
+
<w:rPr>
|
|
345
|
+
{TNR}
|
|
346
|
+
<w:color w:val="000000"/>
|
|
347
|
+
<w:sz w:val="28"/>
|
|
348
|
+
<w:szCs w:val="28"/>
|
|
349
|
+
<w:b/>
|
|
350
|
+
<w:bCs/>
|
|
351
|
+
</w:rPr>
|
|
332
352
|
</w:style>""",
|
|
333
353
|
"TOC1": f"""<w:style w:type="paragraph" w:styleId="TOC1">
|
|
334
354
|
<w:name w:val="toc 1"/>
|
|
@@ -336,7 +356,11 @@ STYLES = {
|
|
|
336
356
|
<w:next w:val="Normal"/>
|
|
337
357
|
<w:qFormat/>
|
|
338
358
|
<w:pPr>
|
|
339
|
-
<w:
|
|
359
|
+
<w:tabs>
|
|
360
|
+
<w:tab w:val="right" w:leader="dot" w:pos="9072"/>
|
|
361
|
+
</w:tabs>
|
|
362
|
+
<w:spacing w:before="120" w:after="40" w:line="260" w:lineRule="auto"/>
|
|
363
|
+
<w:ind w:left="0" w:right="0"/>
|
|
340
364
|
<w:jc w:val="left"/>
|
|
341
365
|
</w:pPr>
|
|
342
366
|
<w:rPr>
|
|
@@ -354,10 +378,18 @@ STYLES = {
|
|
|
354
378
|
<w:next w:val="Normal"/>
|
|
355
379
|
<w:qFormat/>
|
|
356
380
|
<w:pPr>
|
|
357
|
-
<w:
|
|
381
|
+
<w:tabs>
|
|
382
|
+
<w:tab w:val="right" w:leader="dot" w:pos="9072"/>
|
|
383
|
+
</w:tabs>
|
|
384
|
+
<w:spacing w:before="20" w:after="20" w:line="240" w:lineRule="auto"/>
|
|
385
|
+
<w:ind w:left="360" w:right="0"/>
|
|
358
386
|
<w:jc w:val="left"/>
|
|
359
387
|
</w:pPr>
|
|
360
388
|
<w:rPr>
|
|
389
|
+
{TNR}
|
|
390
|
+
<w:color w:val="000000"/>
|
|
391
|
+
<w:sz w:val="24"/>
|
|
392
|
+
<w:szCs w:val="24"/>
|
|
361
393
|
<w:b w:val="0"/>
|
|
362
394
|
<w:bCs w:val="0"/>
|
|
363
395
|
</w:rPr>
|
|
@@ -368,10 +400,18 @@ STYLES = {
|
|
|
368
400
|
<w:next w:val="Normal"/>
|
|
369
401
|
<w:qFormat/>
|
|
370
402
|
<w:pPr>
|
|
403
|
+
<w:tabs>
|
|
404
|
+
<w:tab w:val="right" w:leader="dot" w:pos="9072"/>
|
|
405
|
+
</w:tabs>
|
|
371
406
|
<w:spacing w:before="0" w:after="0" w:line="240" w:lineRule="auto"/>
|
|
407
|
+
<w:ind w:left="720" w:right="0"/>
|
|
372
408
|
<w:jc w:val="left"/>
|
|
373
409
|
</w:pPr>
|
|
374
410
|
<w:rPr>
|
|
411
|
+
{TNR}
|
|
412
|
+
<w:color w:val="000000"/>
|
|
413
|
+
<w:sz w:val="24"/>
|
|
414
|
+
<w:szCs w:val="24"/>
|
|
375
415
|
<w:b w:val="0"/>
|
|
376
416
|
<w:bCs w:val="0"/>
|
|
377
417
|
</w:rPr>
|