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.
- package/.agents/skills/laporan-generator/SKILL.md +132 -0
- package/.agents/skills/laporan-generator/agents/openai.yaml +3 -0
- package/.agents/skills/laporan-generator/references/campus-guide.md +120 -0
- package/.agents/skills/laporan-generator/references/metadata-schema.md +76 -0
- package/.agents/skills/laporan-generator/references/preset-schema.md +79 -0
- package/.agents/skills/laporan-generator/references/syntax-cheatsheet.md +122 -0
- package/.agents/skills/laporan-generator/references/template-guide.md +100 -0
- package/.agents/skills/laporan-generator/references/troubleshooting.md +105 -0
- package/.agents/skills/laporan-generator/resources/Makefile +90 -0
- package/.agents/skills/laporan-generator/resources/apa.csl +2273 -0
- package/.agents/skills/laporan-generator/resources/build-docx.sh +122 -0
- package/.agents/skills/laporan-generator/resources/build.sh +102 -0
- package/.agents/skills/laporan-generator/resources/chapters/bab1-pendahuluan.md +60 -0
- package/.agents/skills/laporan-generator/resources/chapters/bab2-tinjauan-pustaka.md +83 -0
- package/.agents/skills/laporan-generator/resources/chapters/bab3-metodologi.md +116 -0
- package/.agents/skills/laporan-generator/resources/chapters/bab4-hasil-dan-pembahasan.md +93 -0
- package/.agents/skills/laporan-generator/resources/chapters/bab5-penutup.md +27 -0
- package/.agents/skills/laporan-generator/resources/cover.md +50 -0
- package/.agents/skills/laporan-generator/resources/docx.lua +416 -0
- package/.agents/skills/laporan-generator/resources/laporan +464 -0
- package/.agents/skills/laporan-generator/resources/laporan.ps1 +552 -0
- package/.agents/skills/laporan-generator/resources/logo.jpg +0 -0
- package/.agents/skills/laporan-generator/resources/metadata.yml +35 -0
- package/.agents/skills/laporan-generator/resources/presets/itb-ta.yml +27 -0
- package/.agents/skills/laporan-generator/resources/presets/its-skripsi.yml +26 -0
- package/.agents/skills/laporan-generator/resources/presets/skripsi-4433.yml +26 -0
- package/.agents/skills/laporan-generator/resources/presets/standard.yml +26 -0
- package/.agents/skills/laporan-generator/resources/presets/ugm-skripsi.yml +26 -0
- package/.agents/skills/laporan-generator/resources/presets/ui-skripsi.yml +26 -0
- package/.agents/skills/laporan-generator/resources/presets/unpad-skripsi.yml +27 -0
- package/.agents/skills/laporan-generator/resources/reference.docx +0 -0
- package/.agents/skills/laporan-generator/resources/references.bib +115 -0
- package/.agents/skills/laporan-generator/resources/scripts/bundle.py +75 -0
- package/.agents/skills/laporan-generator/resources/scripts/docx-pagenum.py +192 -0
- package/.agents/skills/laporan-generator/resources/scripts/finalize-docx.py +291 -0
- package/.agents/skills/laporan-generator/resources/scripts/make-reference-docx.py +670 -0
- package/.agents/skills/laporan-generator/resources/scripts/report-doctor.py +211 -0
- package/.agents/skills/laporan-generator/resources/scripts/report-stats.py +149 -0
- package/.agents/skills/laporan-generator/resources/scripts/scan-preset.py +311 -0
- package/.agents/skills/laporan-generator/resources/scripts/setup-deps.ps1 +152 -0
- package/.agents/skills/laporan-generator/resources/scripts/setup-deps.sh +113 -0
- package/.agents/skills/laporan-generator/resources/scripts/test_scripts.py +128 -0
- package/.agents/skills/laporan-generator/resources/scripts/validate-preset.py +166 -0
- package/.agents/skills/laporan-generator/resources/template.typ +394 -0
- package/.agents/skills/laporan-generator/scripts/build.sh +78 -0
- package/.agents/skills/laporan-generator/scripts/bundle.py +75 -0
- package/.agents/skills/laporan-generator/scripts/docx-pagenum.py +192 -0
- package/.agents/skills/laporan-generator/scripts/finalize-docx.py +152 -0
- package/.agents/skills/laporan-generator/scripts/laporan +447 -0
- package/.agents/skills/laporan-generator/scripts/laporan.ps1 +529 -0
- package/.agents/skills/laporan-generator/scripts/make-reference-docx.py +630 -0
- package/.agents/skills/laporan-generator/scripts/report-doctor.py +211 -0
- package/.agents/skills/laporan-generator/scripts/report-stats.py +149 -0
- package/.agents/skills/laporan-generator/scripts/scan-preset.py +311 -0
- package/.agents/skills/laporan-generator/scripts/setup-deps.ps1 +152 -0
- package/.agents/skills/laporan-generator/scripts/setup-deps.sh +113 -0
- package/.agents/skills/laporan-generator/scripts/test_scripts.py +103 -0
- package/.agents/skills/laporan-generator/scripts/validate-preset.py +166 -0
- package/.agents/workflows/laporan-generator.md +8 -0
- package/LICENSE +21 -0
- package/Makefile +90 -0
- package/README.md +389 -0
- package/apa.csl +2273 -0
- package/bin/laporan-generator.js +71 -0
- package/build-docx.sh +122 -0
- package/build.sh +102 -0
- package/chapters/bab1-pendahuluan.md +60 -0
- package/chapters/bab2-tinjauan-pustaka.md +83 -0
- package/chapters/bab3-metodologi.md +116 -0
- package/chapters/bab4-hasil-dan-pembahasan.md +93 -0
- package/chapters/bab5-penutup.md +27 -0
- package/cover.md +50 -0
- package/docs/campus-guide.md +120 -0
- package/docs/metadata-schema.md +76 -0
- package/docs/preset-schema.md +79 -0
- package/docs/syntax-cheatsheet.md +122 -0
- package/docs/template-guide.md +100 -0
- package/docs/troubleshooting.md +105 -0
- package/docx.lua +416 -0
- package/laporan +464 -0
- package/laporan.ps1 +552 -0
- package/lib/init.js +118 -0
- package/lib/setup.js +33 -0
- package/lib/sync-hosts.js +194 -0
- package/lib/uninstall.js +123 -0
- package/logo.jpg +0 -0
- package/metadata.yml +35 -0
- package/package.json +65 -0
- package/presets/itb-ta.yml +27 -0
- package/presets/its-skripsi.yml +26 -0
- package/presets/skripsi-4433.yml +26 -0
- package/presets/standard.yml +26 -0
- package/presets/ugm-skripsi.yml +26 -0
- package/presets/ui-skripsi.yml +26 -0
- package/presets/unpad-skripsi.yml +27 -0
- package/reference.docx +0 -0
- package/references.bib +115 -0
- package/scripts/bundle.py +75 -0
- package/scripts/docx-pagenum.py +192 -0
- package/scripts/finalize-docx.py +291 -0
- package/scripts/make-reference-docx.py +670 -0
- package/scripts/report-doctor.py +211 -0
- package/scripts/report-stats.py +149 -0
- package/scripts/scan-preset.py +311 -0
- package/scripts/setup-deps.ps1 +152 -0
- package/scripts/setup-deps.sh +113 -0
- package/scripts/test_scripts.py +128 -0
- package/scripts/validate-preset.py +166 -0
- package/template.typ +394 -0
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""report-doctor.py: Audit komprehensif kesehatan dan integritas proyek laporan."""
|
|
3
|
+
|
|
4
|
+
import glob
|
|
5
|
+
import os
|
|
6
|
+
import re
|
|
7
|
+
import shutil
|
|
8
|
+
import sys
|
|
9
|
+
|
|
10
|
+
GREEN = "\033[0;32m"
|
|
11
|
+
BLUE = "\033[0;34m"
|
|
12
|
+
CYAN = "\033[0;36m"
|
|
13
|
+
YELLOW = "\033[1;33m"
|
|
14
|
+
RED = "\033[0;31m"
|
|
15
|
+
BOLD = "\033[1m"
|
|
16
|
+
NC = "\033[0m"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def check_dependencies():
|
|
20
|
+
tools = [
|
|
21
|
+
("pandoc", "Pandoc Document Engine", True),
|
|
22
|
+
("typst", "Typst Typesetting Engine", True),
|
|
23
|
+
("python3", "Python 3 Runtime", True),
|
|
24
|
+
("magick", "ImageMagick 7", False),
|
|
25
|
+
("convert", "ImageMagick 6", False),
|
|
26
|
+
("pdftotext", "Poppler PDF Utils", False),
|
|
27
|
+
("soffice", "LibreOffice", False),
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
results = []
|
|
31
|
+
has_im = False
|
|
32
|
+
for cmd, name, req in tools:
|
|
33
|
+
found = shutil.which(cmd) is not None
|
|
34
|
+
if "ImageMagick" in name and found:
|
|
35
|
+
has_im = True
|
|
36
|
+
results.append((cmd, name, req, found))
|
|
37
|
+
|
|
38
|
+
return results, has_im
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def check_citations(bib_path, markdown_files):
|
|
42
|
+
bib_keys = set()
|
|
43
|
+
if os.path.isfile(bib_path):
|
|
44
|
+
with open(bib_path, "r", encoding="utf-8", errors="ignore") as f:
|
|
45
|
+
bib_keys = set(re.findall(r"@\w+\s*\{\s*([a-zA-Z0-9_:-]+)\s*,", f.read()))
|
|
46
|
+
|
|
47
|
+
cited_keys = {}
|
|
48
|
+
for mf in markdown_files:
|
|
49
|
+
with open(mf, "r", encoding="utf-8", errors="ignore") as f:
|
|
50
|
+
for line_no, line in enumerate(f, 1):
|
|
51
|
+
# Abaikan contoh dalam inline code
|
|
52
|
+
clean_line = re.sub(r"`.*?`", "", line)
|
|
53
|
+
matches = re.findall(r"@([a-zA-Z0-9_:-]+)", clean_line)
|
|
54
|
+
for m in matches:
|
|
55
|
+
if m not in cited_keys:
|
|
56
|
+
cited_keys[m] = []
|
|
57
|
+
cited_keys[m].append((os.path.basename(mf), line_no))
|
|
58
|
+
|
|
59
|
+
missing_keys = {}
|
|
60
|
+
for key, locs in cited_keys.items():
|
|
61
|
+
if key not in bib_keys:
|
|
62
|
+
missing_keys[key] = locs
|
|
63
|
+
|
|
64
|
+
orphan_keys = bib_keys - set(cited_keys.keys())
|
|
65
|
+
|
|
66
|
+
return bib_keys, cited_keys, missing_keys, orphan_keys
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def check_images(markdown_files):
|
|
70
|
+
broken_images = []
|
|
71
|
+
total_images = 0
|
|
72
|
+
for mf in markdown_files:
|
|
73
|
+
with open(mf, "r", encoding="utf-8", errors="ignore") as f:
|
|
74
|
+
for line_no, line in enumerate(f, 1):
|
|
75
|
+
clean_line = re.sub(r"`.*?`", "", line)
|
|
76
|
+
matches = re.findall(r"!\[.*?\]\((.*?)\)", clean_line)
|
|
77
|
+
for img_path in matches:
|
|
78
|
+
total_images += 1
|
|
79
|
+
clean_path = img_path.split()[0].strip("<>\"'")
|
|
80
|
+
if not os.path.isfile(clean_path):
|
|
81
|
+
broken_images.append((clean_path, os.path.basename(mf), line_no))
|
|
82
|
+
return total_images, broken_images
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def check_style_anomalies(markdown_files):
|
|
86
|
+
anomalies = []
|
|
87
|
+
box_draw_regex = re.compile(r"[┌─┐│└┘├┤┬┴┼═║╔╗╚╝╠╣╦╩╬]")
|
|
88
|
+
manual_num_regex = re.compile(r"^#{2,4}\s+[0-9]+(\.[0-9]+)+")
|
|
89
|
+
|
|
90
|
+
for mf in markdown_files:
|
|
91
|
+
with open(mf, "r", encoding="utf-8", errors="ignore") as f:
|
|
92
|
+
for line_no, line in enumerate(f, 1):
|
|
93
|
+
if box_draw_regex.search(line):
|
|
94
|
+
anomalies.append((os.path.basename(mf), line_no, "Karakter box-drawing terdeteksi (gunakan tabel/ASCII)"))
|
|
95
|
+
if manual_num_regex.search(line):
|
|
96
|
+
anomalies.append((os.path.basename(mf), line_no, f"Manual numbering di heading: '{line.strip()}'"))
|
|
97
|
+
return anomalies
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def main():
|
|
101
|
+
root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
102
|
+
os.chdir(root)
|
|
103
|
+
|
|
104
|
+
print(f"{CYAN}{BOLD}")
|
|
105
|
+
print(" ========================================================")
|
|
106
|
+
print(" LAPORAN GENERATOR - DOCTOR & AUDIT SUITE ")
|
|
107
|
+
print(" ========================================================")
|
|
108
|
+
print(f"{NC}")
|
|
109
|
+
|
|
110
|
+
errors = 0
|
|
111
|
+
warnings = 0
|
|
112
|
+
|
|
113
|
+
# 1. Dependensi
|
|
114
|
+
print(f" {BLUE}{BOLD}[1/4] Audit Toolchain & Dependensi Sistem{NC}")
|
|
115
|
+
deps, has_im = check_dependencies()
|
|
116
|
+
for cmd, name, req, found in deps:
|
|
117
|
+
if "ImageMagick" in name:
|
|
118
|
+
continue
|
|
119
|
+
if found:
|
|
120
|
+
print(f" {GREEN}[OK]{NC} {name:<26}: Terpasang ({cmd})")
|
|
121
|
+
else:
|
|
122
|
+
if req:
|
|
123
|
+
print(f" {RED}[FAIL]{NC} {name:<24}: TIDAK DITEMUKAN (Wajib)")
|
|
124
|
+
errors += 1
|
|
125
|
+
else:
|
|
126
|
+
print(f" {YELLOW}[INFO]{NC} {name:<24}: Tidak ditemukan (Opsional)")
|
|
127
|
+
|
|
128
|
+
if has_im:
|
|
129
|
+
print(f" {GREEN}[OK]{NC} {'ImageMagick':<26}: Terpasang")
|
|
130
|
+
else:
|
|
131
|
+
print(f" {YELLOW}[INFO]{NC} {'ImageMagick':<24}: Tidak ditemukan (Opsional untuk kompresi gambar)")
|
|
132
|
+
print("")
|
|
133
|
+
|
|
134
|
+
# 2. Berkas & Struktur
|
|
135
|
+
print(f" {BLUE}{BOLD}[2/4] Audit Struktur Direktori & Berkas Wajib{NC}")
|
|
136
|
+
req_files = [
|
|
137
|
+
"metadata.yml", "template.typ", "reference.docx",
|
|
138
|
+
"references.bib", "docx.lua", "cover.md"
|
|
139
|
+
]
|
|
140
|
+
for rf in req_files:
|
|
141
|
+
if os.path.isfile(rf):
|
|
142
|
+
print(f" {GREEN}[OK]{NC} Berkas {rf}")
|
|
143
|
+
else:
|
|
144
|
+
print(f" {RED}[FAIL]{NC} Berkas {rf} TIDAK DITEMUKAN")
|
|
145
|
+
errors += 1
|
|
146
|
+
|
|
147
|
+
chapter_files = sorted(glob.glob("chapters/*.md"))
|
|
148
|
+
if chapter_files:
|
|
149
|
+
print(f" {GREEN}[OK]{NC} Direktori chapters/ ({len(chapter_files)} bab ditemukan)")
|
|
150
|
+
else:
|
|
151
|
+
print(f" {RED}[FAIL]{NC} Tidak ada berkas markdown di chapters/")
|
|
152
|
+
errors += 1
|
|
153
|
+
print("")
|
|
154
|
+
|
|
155
|
+
# 3. Sitasi & Referensi
|
|
156
|
+
print(f" {BLUE}{BOLD}[3/4] Audit Sitasi & Integritas Bibliografi{NC}")
|
|
157
|
+
bib_keys, cited_keys, missing_keys, orphan_keys = check_citations("references.bib", chapter_files)
|
|
158
|
+
print(f" * Total entri .bib : {len(bib_keys)}")
|
|
159
|
+
print(f" * Total sitasi di teks : {len(cited_keys)}")
|
|
160
|
+
|
|
161
|
+
if missing_keys:
|
|
162
|
+
for k, locs in missing_keys.items():
|
|
163
|
+
loc_str = ", ".join([f"{f}:{l}" for f, l in locs])
|
|
164
|
+
print(f" {RED}[ERROR]{NC} Sitasi '@{k}' TIDAK DITEMUKAN di references.bib ({loc_str})")
|
|
165
|
+
errors += 1
|
|
166
|
+
else:
|
|
167
|
+
print(f" {GREEN}[OK]{NC} Semua sitasi dalam teks valid terdaftar di references.bib")
|
|
168
|
+
|
|
169
|
+
if orphan_keys:
|
|
170
|
+
print(f" {YELLOW}[WARN]{NC} {len(orphan_keys)} entri di references.bib belum disitir dalam teks:")
|
|
171
|
+
for ok in list(orphan_keys)[:5]:
|
|
172
|
+
print(f" - @{ok}")
|
|
173
|
+
if len(orphan_keys) > 5:
|
|
174
|
+
print(f" - ... dan {len(orphan_keys) - 5} lainnya")
|
|
175
|
+
warnings += 1
|
|
176
|
+
print("")
|
|
177
|
+
|
|
178
|
+
# 4. Gambar & Format Teks
|
|
179
|
+
print(f" {BLUE}{BOLD}[4/4] Audit Media Citra & Anomali Tipografi{NC}")
|
|
180
|
+
total_imgs, broken_imgs = check_images(chapter_files)
|
|
181
|
+
if broken_imgs:
|
|
182
|
+
for p, f, l in broken_imgs:
|
|
183
|
+
print(f" {RED}[ERROR]{NC} Berkas gambar tidak ditemukan: '{p}' (di {f}:{l})")
|
|
184
|
+
errors += 1
|
|
185
|
+
else:
|
|
186
|
+
print(f" {GREEN}[OK]{NC} Semua tautan gambar ({total_imgs} gambar) valid dan ada di disk")
|
|
187
|
+
|
|
188
|
+
anomalies = check_style_anomalies(chapter_files)
|
|
189
|
+
if anomalies:
|
|
190
|
+
for f, l, msg in anomalies:
|
|
191
|
+
print(f" {YELLOW}[WARN]{NC} {f}:{l} - {msg}")
|
|
192
|
+
warnings += 1
|
|
193
|
+
else:
|
|
194
|
+
print(f" {GREEN}[OK]{NC} Bebas dari manual heading numbering dan box-drawing characters")
|
|
195
|
+
print("")
|
|
196
|
+
|
|
197
|
+
# Skor Kesehatan
|
|
198
|
+
print(" ========================================================")
|
|
199
|
+
if errors == 0 and warnings == 0:
|
|
200
|
+
print(f" {GREEN}{BOLD}STATUS: SEHAT SEMPURNA (100/100) - SIAP DIPUBLIKASIKAN!{NC}")
|
|
201
|
+
elif errors == 0:
|
|
202
|
+
print(f" {YELLOW}{BOLD}STATUS: SEHAT DENGAN CATATAN (95/100) - {warnings} Peringatan Terdeteksi{NC}")
|
|
203
|
+
else:
|
|
204
|
+
print(f" {RED}{BOLD}STATUS: DITEMUKAN MASALAH - {errors} Error, {warnings} Warning{NC}")
|
|
205
|
+
print(" ========================================================")
|
|
206
|
+
|
|
207
|
+
return 1 if errors > 0 else 0
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
if __name__ == "__main__":
|
|
211
|
+
sys.exit(main())
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""report-stats.py: Menghitung statistik komprehensif dokumen laporan akademik."""
|
|
3
|
+
|
|
4
|
+
import glob
|
|
5
|
+
import os
|
|
6
|
+
import re
|
|
7
|
+
import sys
|
|
8
|
+
|
|
9
|
+
GREEN = "\033[0;32m"
|
|
10
|
+
BLUE = "\033[0;34m"
|
|
11
|
+
CYAN = "\033[0;36m"
|
|
12
|
+
YELLOW = "\033[1;33m"
|
|
13
|
+
BOLD = "\033[1m"
|
|
14
|
+
NC = "\033[0m"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def count_file_stats(filepath):
|
|
18
|
+
with open(filepath, "r", encoding="utf-8", errors="ignore") as f:
|
|
19
|
+
content = f.read()
|
|
20
|
+
|
|
21
|
+
clean_text = re.sub(r"```.*?```", "", content, flags=re.S)
|
|
22
|
+
clean_text = re.sub(r"<!--.*?-->", "", clean_text, flags=re.S)
|
|
23
|
+
clean_text = re.sub(r"[#*_`~>|]", " ", clean_text)
|
|
24
|
+
words = clean_text.split()
|
|
25
|
+
|
|
26
|
+
h1 = len(re.findall(r"^#[^#].*$", content, re.M))
|
|
27
|
+
h2 = len(re.findall(r"^##[^#].*$", content, re.M))
|
|
28
|
+
h3 = len(re.findall(r"^###[^#].*$", content, re.M))
|
|
29
|
+
|
|
30
|
+
images = len(re.findall(r"!\[.*?\]\(.*?\)", content))
|
|
31
|
+
tables = len(re.findall(r"^\|.*\|[ \t]*$", content, re.M))
|
|
32
|
+
equations = len(re.findall(r"\$\$.*?\$\$", content, re.S)) + len(
|
|
33
|
+
re.findall(r"(?<!\$)\$(?!\$).+?(?<!\$)\$(?!\$)", content)
|
|
34
|
+
)
|
|
35
|
+
citations = re.findall(r"@([a-zA-Z0-9_:-]+)", content)
|
|
36
|
+
|
|
37
|
+
return {
|
|
38
|
+
"words": len(words),
|
|
39
|
+
"chars": len(content),
|
|
40
|
+
"h1": h1,
|
|
41
|
+
"h2": h2,
|
|
42
|
+
"h3": h3,
|
|
43
|
+
"images": images,
|
|
44
|
+
"tables": max(0, tables // 3),
|
|
45
|
+
"equations": equations,
|
|
46
|
+
"citations": citations,
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def main():
|
|
51
|
+
root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
52
|
+
os.chdir(root)
|
|
53
|
+
|
|
54
|
+
title = "Laporan Akademik"
|
|
55
|
+
preset = "standard"
|
|
56
|
+
|
|
57
|
+
if os.path.isfile("metadata.yml"):
|
|
58
|
+
with open("metadata.yml", "r", encoding="utf-8") as f:
|
|
59
|
+
for line in f:
|
|
60
|
+
if line.startswith("title:"):
|
|
61
|
+
title = line.split(":", 1)[1].strip().strip('"\'')
|
|
62
|
+
elif line.startswith("preset:"):
|
|
63
|
+
preset = line.split(":", 1)[1].strip().strip('"\'')
|
|
64
|
+
|
|
65
|
+
chapter_files = sorted(glob.glob("chapters/*.md"))
|
|
66
|
+
all_files = (["cover.md"] if os.path.isfile("cover.md") else []) + chapter_files
|
|
67
|
+
|
|
68
|
+
total_words = 0
|
|
69
|
+
total_chars = 0
|
|
70
|
+
total_h1 = 0
|
|
71
|
+
total_h2 = 0
|
|
72
|
+
total_h3 = 0
|
|
73
|
+
total_images = 0
|
|
74
|
+
total_tables = 0
|
|
75
|
+
total_equations = 0
|
|
76
|
+
all_citations = set()
|
|
77
|
+
|
|
78
|
+
file_breakdown = []
|
|
79
|
+
|
|
80
|
+
for f in all_files:
|
|
81
|
+
if os.path.isfile(f):
|
|
82
|
+
st = count_file_stats(f)
|
|
83
|
+
total_words += st["words"]
|
|
84
|
+
total_chars += st["chars"]
|
|
85
|
+
total_h1 += st["h1"]
|
|
86
|
+
total_h2 += st["h2"]
|
|
87
|
+
total_h3 += st["h3"]
|
|
88
|
+
total_images += st["images"]
|
|
89
|
+
total_tables += st["tables"]
|
|
90
|
+
total_equations += st["equations"]
|
|
91
|
+
all_citations.update(st["citations"])
|
|
92
|
+
file_breakdown.append((os.path.basename(f), st["words"]))
|
|
93
|
+
|
|
94
|
+
bib_count = 0
|
|
95
|
+
if os.path.isfile("references.bib"):
|
|
96
|
+
with open("references.bib", "r", encoding="utf-8") as f:
|
|
97
|
+
bib_count = len(re.findall(r"@\w+\s*\{", f.read()))
|
|
98
|
+
|
|
99
|
+
est_pages = max(1, round(total_words / 280))
|
|
100
|
+
read_mins = max(1, round(total_words / 200))
|
|
101
|
+
|
|
102
|
+
pdf_size = "-"
|
|
103
|
+
if os.path.isfile("Laporan.pdf"):
|
|
104
|
+
sz = os.path.getsize("Laporan.pdf")
|
|
105
|
+
pdf_size = f"{sz / 1024:.1f} KB"
|
|
106
|
+
|
|
107
|
+
docx_size = "-"
|
|
108
|
+
if os.path.isfile("Laporan.docx"):
|
|
109
|
+
sz = os.path.getsize("Laporan.docx")
|
|
110
|
+
docx_size = f"{sz / 1024:.1f} KB"
|
|
111
|
+
|
|
112
|
+
print(f"{CYAN}{BOLD}")
|
|
113
|
+
print(" ========================================================")
|
|
114
|
+
print(" LAPORAN GENERATOR - STATISTIK DOKUMEN ")
|
|
115
|
+
print(" ========================================================")
|
|
116
|
+
print(f"{NC}")
|
|
117
|
+
print(f" {BOLD}Judul Laporan :{NC} {title}")
|
|
118
|
+
print(f" {BOLD}Preset Aktif :{NC} {preset}")
|
|
119
|
+
print(f" {BOLD}Total Berkas Bab :{NC} {len(chapter_files)} berkas")
|
|
120
|
+
print("")
|
|
121
|
+
print(f" {BLUE}{BOLD}[Metrik Konten]{NC}")
|
|
122
|
+
print(f" * Total Kata : {GREEN}{BOLD}{total_words:,}{NC} kata")
|
|
123
|
+
print(f" * Total Karakter : {total_chars:,} karakter")
|
|
124
|
+
print(f" * Estimasi Halaman : {CYAN}{BOLD}~{est_pages}{NC} halaman (standar A4 1.5 spasi)")
|
|
125
|
+
print(f" * Estimasi Durasi Baca : ~{read_mins} menit")
|
|
126
|
+
print("")
|
|
127
|
+
print(f" {BLUE}{BOLD}[Struktur & Elemen Akademik]{NC}")
|
|
128
|
+
print(f" * Bab Utama (Heading 1) : {total_h1}")
|
|
129
|
+
print(f" * Sub-Bab (Heading 2) : {total_h2}")
|
|
130
|
+
print(f" * Sub-Sub-Bab (Heading 3): {total_h3}")
|
|
131
|
+
print(f" * Gambar / Diagram : {total_images}")
|
|
132
|
+
print(f" * Tabel Terdeteksi : {total_tables}")
|
|
133
|
+
print(f" * Persamaan Matematika : {total_equations}")
|
|
134
|
+
print(f" * Sitasi Digunakan : {len(all_citations)} sitasi unik")
|
|
135
|
+
print(f" * Entri Daftar Pustaka : {bib_count} referensi di .bib")
|
|
136
|
+
print("")
|
|
137
|
+
print(f" {BLUE}{BOLD}[Ukuran Berkas Terkompilasi]{NC}")
|
|
138
|
+
print(f" * Laporan.pdf : {pdf_size}")
|
|
139
|
+
print(f" * Laporan.docx : {docx_size}")
|
|
140
|
+
print("")
|
|
141
|
+
print(f" {BLUE}{BOLD}[Rincian Kata per Berkas]{NC}")
|
|
142
|
+
for fname, cnt in file_breakdown:
|
|
143
|
+
bar = "■" * max(1, min(25, cnt // 100))
|
|
144
|
+
print(f" {fname:<28} : {cnt:>5} kata {CYAN}{bar}{NC}")
|
|
145
|
+
print(" ========================================================")
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
if __name__ == "__main__":
|
|
149
|
+
main()
|
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""scan-preset.py: Ekstraktor otomatis aturan format & preset kampus dari berkas PDF.
|
|
3
|
+
|
|
4
|
+
Mengekstrak aturan tata letak (margin, font, spasi baris, penomoran heading,
|
|
5
|
+
dan nama institusi) dari dokumen PDF Buku Pedoman Penulisan Skripsi / Tugas Akhir.
|
|
6
|
+
|
|
7
|
+
Usage:
|
|
8
|
+
python3 scripts/scan-preset.py <input.pdf> [options]
|
|
9
|
+
|
|
10
|
+
Options:
|
|
11
|
+
--preset-id <id> ID unik preset (default: slug nama kampus/file)
|
|
12
|
+
--name <name> Nama lengkap preset/institusi
|
|
13
|
+
--output-dir <dir> Direktori output (default: presets/)
|
|
14
|
+
--non-interactive Jalankan tanpa konfirmasi interaktif
|
|
15
|
+
--apply Otomatis terapkan ke metadata.yml setelah scan
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
import argparse
|
|
19
|
+
import os
|
|
20
|
+
import re
|
|
21
|
+
import subprocess
|
|
22
|
+
import sys
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def extract_text_from_pdf(pdf_path):
|
|
26
|
+
"""Ekstraksi teks dari PDF menggunakan pdftotext (poppler-utils) atau fallback."""
|
|
27
|
+
if not os.path.exists(pdf_path):
|
|
28
|
+
print(f"[ERROR] Berkas '{pdf_path}' tidak ditemukan.", file=sys.stderr)
|
|
29
|
+
sys.exit(1)
|
|
30
|
+
|
|
31
|
+
# Jika file teks/mock markdown diberikan langsung
|
|
32
|
+
if pdf_path.endswith(".txt") or pdf_path.endswith(".md"):
|
|
33
|
+
with open(pdf_path, "r", encoding="utf-8", errors="ignore") as f:
|
|
34
|
+
return f.read()
|
|
35
|
+
|
|
36
|
+
# Cek pdftotext
|
|
37
|
+
if subprocess.run(["which", "pdftotext"], capture_output=True).returncode == 0:
|
|
38
|
+
try:
|
|
39
|
+
# Ambil maksimal 40 halaman pertama (bab pedoman format biasanya di bagian awal)
|
|
40
|
+
res = subprocess.run(
|
|
41
|
+
["pdftotext", "-f", "1", "-l", "40", "-layout", pdf_path, "-"],
|
|
42
|
+
capture_output=True,
|
|
43
|
+
text=True,
|
|
44
|
+
check=True,
|
|
45
|
+
)
|
|
46
|
+
return res.stdout
|
|
47
|
+
except Exception as e:
|
|
48
|
+
print(f"[WARN] pdftotext gagal: {e}", file=sys.stderr)
|
|
49
|
+
|
|
50
|
+
# Fallback jika file binary PDF tidak bisa diurai via pdftotext
|
|
51
|
+
print("[WARN] pdftotext tidak tersedia, membaca teks mentah...", file=sys.stderr)
|
|
52
|
+
try:
|
|
53
|
+
with open(pdf_path, "rb") as f:
|
|
54
|
+
raw = f.read().decode("latin1", errors="ignore")
|
|
55
|
+
# Ekstraksi string teks dalam kurung PDF (Tj / TJ)
|
|
56
|
+
strings = re.findall(r"\(([^\)]+)\)\s*Tj", raw)
|
|
57
|
+
return "\n".join(strings) if strings else raw
|
|
58
|
+
except Exception as e:
|
|
59
|
+
print(f"[ERROR] Gagal membaca berkas: {e}", file=sys.stderr)
|
|
60
|
+
sys.exit(1)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def detect_institution(text, filename):
|
|
64
|
+
"""Deteksi nama universitas / institut / politeknik."""
|
|
65
|
+
patterns = [
|
|
66
|
+
r"(UNIVERSITAS\s+[A-Z0-9\s]+)",
|
|
67
|
+
r"(INSTITUT\s+[A-Z0-9\s]+)",
|
|
68
|
+
r"(POLITEKNIK\s+[A-Z0-9\s]+)",
|
|
69
|
+
r"(SEKOLAH\s+TINGGI\s+[A-Z0-9\s]+)",
|
|
70
|
+
r"(Universitas\s+[A-Za-z0-9\s]+)",
|
|
71
|
+
r"(Institut\s+[A-Za-z0-9\s]+)",
|
|
72
|
+
r"(Politeknik\s+[A-Za-z0-9\s]+)",
|
|
73
|
+
]
|
|
74
|
+
for pat in patterns:
|
|
75
|
+
m = re.search(pat, text)
|
|
76
|
+
if m:
|
|
77
|
+
found = m.group(1).strip()
|
|
78
|
+
# Bersihkan baris berlebih
|
|
79
|
+
found = found.split("\n")[0].strip()
|
|
80
|
+
if len(found) > 8 and len(found) < 60:
|
|
81
|
+
return found
|
|
82
|
+
|
|
83
|
+
# Fallback dari nama file (contoh: pedoman-its.pdf -> ITS)
|
|
84
|
+
base = os.path.basename(pdf_slug(filename)).replace("-", " ").title()
|
|
85
|
+
return base
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def pdf_slug(path):
|
|
89
|
+
"""Buat slug ID dari nama berkas atau nama institusi."""
|
|
90
|
+
base = os.path.basename(path).rsplit(".", 1)[0]
|
|
91
|
+
base = re.sub(r"^(pedoman|buku|panduan|skripsi|ta|format)[-_]?", "", base, flags=re.I)
|
|
92
|
+
base = re.sub(r"[^a-zA-Z0-9]+", "-", base).strip("-").lower()
|
|
93
|
+
return base if base else "custom-kampus"
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def detect_margins(text):
|
|
97
|
+
"""Deteksi batas margin (top, bottom, left, right)."""
|
|
98
|
+
# Pola shorthand 4-4-3-3 atau 4 4 3 3 (kiri-atas-kanan-bawah atau atas-kiri-bawah-kanan)
|
|
99
|
+
if re.search(r"4\s*[-–,]\s*4\s*[-–,]\s*3\s*[-–,]\s*3", text):
|
|
100
|
+
return {"top": "4cm", "bottom": "3cm", "left": "4cm", "right": "3cm"}
|
|
101
|
+
if re.search(r"3\s*[-–,]\s*3\s*[-–,]\s*3\s*[-–,]\s*3", text):
|
|
102
|
+
return {"top": "3cm", "bottom": "3cm", "left": "3cm", "right": "3cm"}
|
|
103
|
+
|
|
104
|
+
margins = {"top": "2.5cm", "bottom": "2.5cm", "left": "2.5cm", "right": "2.5cm"}
|
|
105
|
+
|
|
106
|
+
# Pola spesifik bahasa Indonesia
|
|
107
|
+
m_left = re.search(r"(?:kiri|left)\s*[:=]?\s*([0-9]+(?:[.,][0-9]+)?)\s*cm", text, re.I)
|
|
108
|
+
m_top = re.search(r"(?:atas|top)\s*[:=]?\s*([0-9]+(?:[.,][0-9]+)?)\s*cm", text, re.I)
|
|
109
|
+
m_right = re.search(r"(?:kanan|right)\s*[:=]?\s*([0-9]+(?:[.,][0-9]+)?)\s*cm", text, re.I)
|
|
110
|
+
m_bottom = re.search(r"(?:bawah|bottom)\s*[:=]?\s*([0-9]+(?:[.,][0-9]+)?)\s*cm", text, re.I)
|
|
111
|
+
|
|
112
|
+
if m_left:
|
|
113
|
+
margins["left"] = f"{m_left.group(1).replace(',', '.')}cm"
|
|
114
|
+
if m_top:
|
|
115
|
+
margins["top"] = f"{m_top.group(1).replace(',', '.')}cm"
|
|
116
|
+
if m_right:
|
|
117
|
+
margins["right"] = f"{m_right.group(1).replace(',', '.')}cm"
|
|
118
|
+
if m_bottom:
|
|
119
|
+
margins["bottom"] = f"{m_bottom.group(1).replace(',', '.')}cm"
|
|
120
|
+
|
|
121
|
+
# Deteksi format skripsi jilid umum jika terdeteksi teks jilid
|
|
122
|
+
if re.search(r"(jilid|hardcover|penjilidan)", text, re.I) and not m_left:
|
|
123
|
+
margins["left"] = "4cm"
|
|
124
|
+
margins["top"] = "4cm"
|
|
125
|
+
margins["right"] = "3cm"
|
|
126
|
+
margins["bottom"] = "3cm"
|
|
127
|
+
|
|
128
|
+
return margins
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def detect_typography(text):
|
|
132
|
+
"""Deteksi font family, size, line spacing, dan indent."""
|
|
133
|
+
typo = {
|
|
134
|
+
"font_family": "Libertinus Serif",
|
|
135
|
+
"font_size": "12pt",
|
|
136
|
+
"line_spacing": "0.75em", # 0.75em = 1.5 spasi di Typst
|
|
137
|
+
"first_line_indent": "1.25cm",
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
# Deteksi Font
|
|
141
|
+
if re.search(r"Arial", text, re.I):
|
|
142
|
+
typo["font_family"] = "Arial"
|
|
143
|
+
elif re.search(r"Calibri", text, re.I):
|
|
144
|
+
typo["font_family"] = "Calibri"
|
|
145
|
+
elif re.search(r"Times\s*New\s*Roman", text, re.I):
|
|
146
|
+
typo["font_family"] = "Libertinus Serif"
|
|
147
|
+
|
|
148
|
+
# Deteksi Ukuran Font
|
|
149
|
+
m_sz = re.search(r"(?:ukuran|font\s*size|size)\s*[:=]?\s*([0-9]+)\s*(?:pt|point)?", text, re.I)
|
|
150
|
+
if m_sz:
|
|
151
|
+
typo["font_size"] = f"{m_sz.group(1)}pt"
|
|
152
|
+
|
|
153
|
+
# Deteksi Spasi Baris
|
|
154
|
+
if re.search(r"(?:spasi|line\s*spacing)\s*[:=]?\s*(?:ganda|2(?:\.0)?|double)", text, re.I):
|
|
155
|
+
typo["line_spacing"] = "1.0em" # double spacing
|
|
156
|
+
elif re.search(r"(?:spasi|line\s*spacing)\s*[:=]?\s*1[,.]15", text, re.I):
|
|
157
|
+
typo["line_spacing"] = "0.55em"
|
|
158
|
+
elif re.search(r"(?:spasi|line\s*spacing)\s*[:=]?\s*(?:1[,.]5|satu\s*setengah)", text, re.I):
|
|
159
|
+
typo["line_spacing"] = "0.75em"
|
|
160
|
+
|
|
161
|
+
# Deteksi Indentasi
|
|
162
|
+
m_ind = re.search(r"(?:indentasi|alinea|awal\s*paragraf)\s*[:=]?\s*([0-9]+(?:[.,][0-9]+)?)\s*cm", text, re.I)
|
|
163
|
+
if m_ind:
|
|
164
|
+
typo["first_line_indent"] = f"{m_ind.group(1).replace(',', '.')}cm"
|
|
165
|
+
|
|
166
|
+
return typo
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def detect_headings(text):
|
|
170
|
+
"""Deteksi format penomoran bab dan sub-bab."""
|
|
171
|
+
headings = {
|
|
172
|
+
"chapter_prefix": "BAB ",
|
|
173
|
+
"chapter_num_format": "roman",
|
|
174
|
+
"sub_dot": True,
|
|
175
|
+
"subsub_dot": False,
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
# Cek BAB 1 vs BAB I
|
|
179
|
+
if re.search(r"BAB\s+[0-9]+\b", text) and not re.search(r"BAB\s+[IVXLCDM]+\b", text):
|
|
180
|
+
headings["chapter_num_format"] = "arabic"
|
|
181
|
+
|
|
182
|
+
# Cek sub-bab trailing dot (1.1. vs 1.1)
|
|
183
|
+
if re.search(r"\b1\.1\s+[A-Z]", text):
|
|
184
|
+
headings["sub_dot"] = False
|
|
185
|
+
elif re.search(r"\b1\.1\.\s+[A-Z]", text):
|
|
186
|
+
headings["sub_dot"] = True
|
|
187
|
+
|
|
188
|
+
return headings
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def detect_cover(text):
|
|
192
|
+
"""Deteksi opsi sampul depan (apakah dosen pembimbing dicantumkan)."""
|
|
193
|
+
cover = {
|
|
194
|
+
"show_lecturer": True,
|
|
195
|
+
"logo_width": "4cm",
|
|
196
|
+
}
|
|
197
|
+
# Jika ada klausul eksplisit bahwa sampul luar tidak mencantumkan pembimbing (seperti ITB)
|
|
198
|
+
if re.search(r"sampul\s*(?:luar|depan)?\s*tanpa\s*(?:nama)?\s*(?:dosen|pembimbing)", text, re.I):
|
|
199
|
+
cover["show_lecturer"] = False
|
|
200
|
+
return cover
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def build_preset_yaml(preset_id, name, margins, typo, headings, cover):
|
|
204
|
+
"""Hasilkan string YAML preset standar."""
|
|
205
|
+
hide_lecturer_yaml = ""
|
|
206
|
+
if not cover["show_lecturer"]:
|
|
207
|
+
hide_lecturer_yaml = "\ncover_hide_lecturer: true"
|
|
208
|
+
|
|
209
|
+
return f"""# Preset: {name}
|
|
210
|
+
# Dihasilkan secara otomatis oleh scan-preset.py
|
|
211
|
+
preset_id: "{preset_id}"
|
|
212
|
+
name: "{name}"
|
|
213
|
+
description: "Format otomatis hasil pemindaian pedoman akademik {name}."
|
|
214
|
+
|
|
215
|
+
# 1. Geometri & Margin Halaman
|
|
216
|
+
margin_top: {margins['top']}
|
|
217
|
+
margin_bottom: {margins['bottom']}
|
|
218
|
+
margin_left: {margins['left']}
|
|
219
|
+
margin_right: {margins['right']}
|
|
220
|
+
|
|
221
|
+
# 2. Tipografi & Spasi
|
|
222
|
+
font_family: "{typo['font_family']}"
|
|
223
|
+
font_size: {typo['font_size']}
|
|
224
|
+
line_spacing: {typo['line_spacing']}
|
|
225
|
+
first_line_indent: {typo['first_line_indent']}
|
|
226
|
+
|
|
227
|
+
# 3. Penomoran Heading
|
|
228
|
+
heading_chapter_prefix: "{headings['chapter_prefix']}"
|
|
229
|
+
heading_chapter_num_format: "{headings['chapter_num_format']}"
|
|
230
|
+
heading_sub_dot: {str(headings['sub_dot']).lower()}
|
|
231
|
+
heading_subsub_dot: {str(headings['subsub_dot']).lower()}
|
|
232
|
+
|
|
233
|
+
# 4. Pengaturan Halaman Sampul
|
|
234
|
+
cover_show_lecturer: {str(cover['show_lecturer']).lower()}{hide_lecturer_yaml}
|
|
235
|
+
cover_logo_width: {cover['logo_width']}
|
|
236
|
+
"""
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def main():
|
|
240
|
+
parser = argparse.ArgumentParser(description="Scan pedoman penulisan PDF dan buat preset kampus otomatis.")
|
|
241
|
+
parser.add_argument("pdf_path", help="Path ke berkas PDF pedoman penulisan kampus")
|
|
242
|
+
parser.add_argument("--preset-id", help="ID unik preset (contoh: unpad-skripsi)")
|
|
243
|
+
parser.add_argument("--name", help="Nama lengkap institusi / preset")
|
|
244
|
+
parser.add_argument("--output-dir", default="presets", help="Direktori output penyimpanan preset")
|
|
245
|
+
parser.add_argument("--non-interactive", action="store_true", help="Jalankan otomatis tanpa prompt interaktif")
|
|
246
|
+
parser.add_argument("--apply", action="store_true", help="Terapkan preset langsung ke metadata.yml")
|
|
247
|
+
|
|
248
|
+
args = parser.parse_args()
|
|
249
|
+
|
|
250
|
+
print(f"\n[1/3] Membaca dan menganalisis berkas: {args.pdf_path}...")
|
|
251
|
+
text = extract_text_from_pdf(args.pdf_path)
|
|
252
|
+
print(f" Total teks terdeteksi: {len(text.split())} kata")
|
|
253
|
+
|
|
254
|
+
print("[2/3] Mengekstraksi aturan penulisan akademik...")
|
|
255
|
+
inst_name = args.name or detect_institution(text, args.pdf_path)
|
|
256
|
+
pid = args.preset_id or pdf_slug(inst_name or args.pdf_path)
|
|
257
|
+
margins = detect_margins(text)
|
|
258
|
+
typo = detect_typography(text)
|
|
259
|
+
headings = detect_headings(text)
|
|
260
|
+
cover = detect_cover(text)
|
|
261
|
+
|
|
262
|
+
print("\n" + "=" * 56)
|
|
263
|
+
print(" HASIL DETEKSI ATURAN FORMAT KAMPUS")
|
|
264
|
+
print("=" * 56)
|
|
265
|
+
print(f" * ID Preset : {pid}")
|
|
266
|
+
print(f" * Nama Institusi : {inst_name}")
|
|
267
|
+
print(f" * Margin Halaman : Atas {margins['top']}, Bawah {margins['bottom']}, Kiri {margins['left']}, Kanan {margins['right']}")
|
|
268
|
+
print(f" * Tipografi : {typo['font_family']} {typo['font_size']} (Spasi: {typo['line_spacing']}, Indent: {typo['first_line_indent']})")
|
|
269
|
+
print(f" * Penomoran Bab : {headings['chapter_prefix']}{'I' if headings['chapter_num_format'] == 'roman' else '1'} (Sub-bab dot: {headings['sub_dot']})")
|
|
270
|
+
print(f" * Sampul Depan : Dosen={'Ya' if cover['show_lecturer'] else 'Tidak'}, Lebar Logo={cover['logo_width']}")
|
|
271
|
+
print("=" * 56 + "\n")
|
|
272
|
+
|
|
273
|
+
if not args.non_interactive:
|
|
274
|
+
confirm = input("Simpan konfigurasi preset di atas? [Y/n]: ").strip().lower()
|
|
275
|
+
if confirm in ("n", "no"):
|
|
276
|
+
print("[INFO] Pembuatan preset dibatalkan.")
|
|
277
|
+
sys.exit(0)
|
|
278
|
+
|
|
279
|
+
os.makedirs(args.output_dir, exist_ok=True)
|
|
280
|
+
out_file = os.path.join(args.output_dir, f"{pid}.yml")
|
|
281
|
+
yaml_content = build_preset_yaml(pid, inst_name, margins, typo, headings, cover)
|
|
282
|
+
|
|
283
|
+
with open(out_file, "w", encoding="utf-8") as f:
|
|
284
|
+
f.write(yaml_content)
|
|
285
|
+
|
|
286
|
+
print(f"[3/3] [OK] Preset berhasil disimpan ke: {out_file}")
|
|
287
|
+
|
|
288
|
+
if args.apply:
|
|
289
|
+
meta_file = "metadata.yml"
|
|
290
|
+
if os.path.exists(meta_file):
|
|
291
|
+
with open(meta_file, "r", encoding="utf-8") as f:
|
|
292
|
+
content = f.read()
|
|
293
|
+
if re.search(r"^\s*preset:", content, re.M):
|
|
294
|
+
content = re.sub(r"^\s*preset:.*", f'preset: "{pid}"', content, flags=re.M)
|
|
295
|
+
elif re.search(r"^\s*margin_preset:", content, re.M):
|
|
296
|
+
content = re.sub(r"^\s*margin_preset:.*", f'preset: "{pid}"', content, flags=re.M)
|
|
297
|
+
elif "..." in content:
|
|
298
|
+
content = content.replace("...", f'preset: "{pid}"\n...')
|
|
299
|
+
else:
|
|
300
|
+
content += f'\npreset: "{pid}"\n'
|
|
301
|
+
with open(meta_file, "w", encoding="utf-8") as f:
|
|
302
|
+
f.write(content)
|
|
303
|
+
print(f"[OK] Preset '{pid}' telah diterapkan ke metadata.yml!")
|
|
304
|
+
|
|
305
|
+
print(f"\nUntuk menggunakan preset ini, jalankan:")
|
|
306
|
+
print(f" ./laporan preset apply {pid}")
|
|
307
|
+
print(f" ./laporan build\n")
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
if __name__ == "__main__":
|
|
311
|
+
main()
|