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,152 @@
1
+ # setup-deps.ps1: Universal Dependency Installer untuk Windows
2
+ # Mendukung: Windows 10/11 (Winget) dan Fallback Portable Binary
3
+
4
+ [CmdletBinding()]
5
+ param (
6
+ [switch]$Force,
7
+ [switch]$CheckOnly
8
+ )
9
+
10
+ $ErrorActionPreference = "Stop"
11
+
12
+ # Pastikan folder localBin ada di PATH sesi saat ini jika ada
13
+ $localBin = Join-Path $env:LOCALAPPDATA "laporan-generator\bin"
14
+ if ((Test-Path $localBin) -and ($env:Path -notlike "*$localBin*")) {
15
+ $env:Path = "$localBin;$env:Path"
16
+ }
17
+
18
+ Write-Host "==========================================================" -ForegroundColor Cyan
19
+ Write-Host " LAPORAN GENERATOR - WINDOWS DEPENDENCY SETUP " -ForegroundColor Cyan
20
+ Write-Host "==========================================================" -ForegroundColor Cyan
21
+ Write-Host ""
22
+
23
+ $packages = @(
24
+ @{ Id = "Typst.Typst"; Name = "Typst Typesetting Engine"; Cmd = "typst" },
25
+ @{ Id = "JohnMacFarlane.Pandoc"; Name = "Pandoc Document Converter"; Cmd = "pandoc" },
26
+ @{ Id = "ImageMagick.ImageMagick"; Name = "ImageMagick"; Cmd = "magick" }
27
+ )
28
+
29
+ Write-Host "[1/3] Memeriksa dependensi saat ini..." -ForegroundColor Yellow
30
+ $missing = @()
31
+ foreach ($pkg in $packages) {
32
+ $found = Get-Command $pkg.Cmd -ErrorAction SilentlyContinue
33
+ if (-not $found -and $pkg.Cmd -eq "magick" -and -not ($env:OS -match "Windows")) {
34
+ $found = Get-Command "convert" -ErrorAction SilentlyContinue
35
+ }
36
+
37
+ if ($found) {
38
+ Write-Host " [OK] $($pkg.Name) ($($pkg.Cmd)) terpasang." -ForegroundColor Green
39
+ } else {
40
+ Write-Host " [MISSING] $($pkg.Name) belum terpasang." -ForegroundColor Red
41
+ $missing += $pkg
42
+ }
43
+ }
44
+
45
+ if ($missing.Count -eq 0 -and -not $Force) {
46
+ Write-Host ""
47
+ Write-Host "[OK] Semua dependensi utama sudah lengkap dan siap digunakan!" -ForegroundColor Green
48
+ exit 0
49
+ }
50
+
51
+ if ($CheckOnly) {
52
+ Write-Host ""
53
+ Write-Host "Ada $($missing.Count) dependensi yang belum terpasang." -ForegroundColor Red
54
+ exit 1
55
+ }
56
+
57
+ Write-Host ""
58
+ Write-Host "[2/3] Memasang dependensi yang dibutuhkan..." -ForegroundColor Yellow
59
+
60
+ $hasWinget = Get-Command "winget" -ErrorAction SilentlyContinue
61
+
62
+ if ($hasWinget) {
63
+ Write-Host "Menggunakan Windows Package Manager (Winget)..." -ForegroundColor Cyan
64
+ foreach ($pkg in $missing) {
65
+ Write-Host "--> Memasang $($pkg.Name) [$($pkg.Id)]..." -ForegroundColor White
66
+ try {
67
+ $process = Start-Process -FilePath "winget" -ArgumentList "install --id $($pkg.Id) --silent --accept-package-agreements --accept-source-agreements" -Wait -PassThru -NoNewWindow
68
+ if ($process.ExitCode -eq 0) {
69
+ Write-Host " [SUKSES] $($pkg.Name) berhasil dipasang." -ForegroundColor Green
70
+ } else {
71
+ Write-Host " [INFO] Winget exit code: $($process.ExitCode)." -ForegroundColor Yellow
72
+ }
73
+ } catch {
74
+ Write-Host " [GAGAL] Gagal memasang $($pkg.Name): $_" -ForegroundColor Red
75
+ }
76
+ }
77
+ }
78
+
79
+ # Periksa kembali apakah dependensi wajib (typst, pandoc) sudah tersedia setelah winget.
80
+ # Jika belum, unduh binary portable resmi dari GitHub release secara otomatis.
81
+ $typstAvailable = (Get-Command "typst" -ErrorAction SilentlyContinue) -or (Test-Path (Join-Path $localBin "typst.exe"))
82
+ $pandocAvailable = (Get-Command "pandoc" -ErrorAction SilentlyContinue) -or (Test-Path (Join-Path $localBin "pandoc.exe"))
83
+
84
+ if (-not $typstAvailable -or -not $pandocAvailable) {
85
+ Write-Host ""
86
+ Write-Host "Memulai pemasangan portable ke folder lokal ($localBin)..." -ForegroundColor Cyan
87
+ if (-not (Test-Path $localBin)) {
88
+ New-Item -ItemType Directory -Path $localBin -Force | Out-Null
89
+ }
90
+
91
+ [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
92
+
93
+ # Unduh Typst standalone jika belum ada
94
+ if (-not $typstAvailable) {
95
+ Write-Host "--> Mengunduh Typst standalone release..." -ForegroundColor White
96
+ $typstZip = Join-Path $env:TEMP "typst-x86_64-pc-windows-msvc.zip"
97
+ $typstUrl = "https://github.com/typst/typst/releases/latest/download/typst-x86_64-pc-windows-msvc.zip"
98
+ try {
99
+ Invoke-WebRequest -Uri $typstUrl -OutFile $typstZip -UseBasicParsing
100
+ Expand-Archive -Path $typstZip -DestinationPath (Join-Path $env:TEMP "typst-extracted") -Force
101
+ $typstExe = Get-ChildItem -Path (Join-Path $env:TEMP "typst-extracted") -Filter "typst.exe" -Recurse | Select-Object -First 1
102
+ if ($typstExe) {
103
+ Copy-Item -Path $typstExe.FullName -Destination (Join-Path $localBin "typst.exe") -Force
104
+ Write-Host " [SUKSES] Typst tersimpan di $localBin" -ForegroundColor Green
105
+ }
106
+ } catch {
107
+ Write-Host " [GAGAL] Mengunduh Typst: $_" -ForegroundColor Red
108
+ } finally {
109
+ Remove-Item -Path $typstZip -Force -ErrorAction SilentlyContinue
110
+ Remove-Item -Path (Join-Path $env:TEMP "typst-extracted") -Recurse -Force -ErrorAction SilentlyContinue
111
+ }
112
+ }
113
+
114
+ # Unduh Pandoc standalone jika belum ada
115
+ if (-not $pandocAvailable) {
116
+ Write-Host "--> Mengunduh Pandoc standalone release..." -ForegroundColor White
117
+ $pandocZip = Join-Path $env:TEMP "pandoc-windows-x86_64.zip"
118
+ $pandocUrl = "https://github.com/jgm/pandoc/releases/download/3.11/pandoc-3.11-windows-x86_64.zip"
119
+ try {
120
+ Invoke-WebRequest -Uri $pandocUrl -OutFile $pandocZip -UseBasicParsing
121
+ Expand-Archive -Path $pandocZip -DestinationPath (Join-Path $env:TEMP "pandoc-extracted") -Force
122
+ $pandocExe = Get-ChildItem -Path (Join-Path $env:TEMP "pandoc-extracted") -Filter "pandoc.exe" -Recurse | Select-Object -First 1
123
+ if ($pandocExe) {
124
+ Copy-Item -Path $pandocExe.FullName -Destination (Join-Path $localBin "pandoc.exe") -Force
125
+ Write-Host " [SUKSES] Pandoc tersimpan di $localBin" -ForegroundColor Green
126
+ }
127
+ } catch {
128
+ Write-Host " [GAGAL] Mengunduh Pandoc: $_" -ForegroundColor Red
129
+ } finally {
130
+ Remove-Item -Path $pandocZip -Force -ErrorAction SilentlyContinue
131
+ Remove-Item -Path (Join-Path $env:TEMP "pandoc-extracted") -Recurse -Force -ErrorAction SilentlyContinue
132
+ }
133
+ }
134
+
135
+ # Tambahkan localBin ke User PATH jika belum terdaftar
136
+ $userPath = [Environment]::GetEnvironmentVariable("Path", "User")
137
+ if ($userPath -notlike "*$localBin*") {
138
+ [Environment]::SetEnvironmentVariable("Path", "$userPath;$localBin", "User")
139
+ Write-Host " [OK] Ditambahkan ke User PATH: $localBin" -ForegroundColor Green
140
+ }
141
+ }
142
+
143
+ Write-Host ""
144
+ Write-Host "[3/3] Memperbarui sesi Environment PATH..." -ForegroundColor Yellow
145
+ $machinePath = [Environment]::GetEnvironmentVariable("Path", "Machine")
146
+ $userPath = [Environment]::GetEnvironmentVariable("Path", "User")
147
+ $env:Path = "$localBin;$machinePath;$userPath"
148
+
149
+ Write-Host ""
150
+ Write-Host "==========================================================" -ForegroundColor Green
151
+ Write-Host " PEMASANGAN SELESAI! SILAKAN JALANKAN BUILD DOKUMEN " -ForegroundColor Green
152
+ Write-Host "==========================================================" -ForegroundColor Green
@@ -0,0 +1,113 @@
1
+ #!/usr/bin/env bash
2
+ # setup-deps.sh: Universal Dependency Installer untuk Linux & macOS
3
+ # Mendukung: apt (Debian/Ubuntu), pacman (Arch), dnf (Fedora), brew (macOS), nix, dan docker
4
+
5
+ set -euo pipefail
6
+
7
+ RED='\033[0;31m'
8
+ GREEN='\033[0;32m'
9
+ YELLOW='\033[1;33m'
10
+ CYAN='\033[0;36m'
11
+ BOLD='\033[1m'
12
+ NC='\033[0m'
13
+
14
+ echo -e "${CYAN}${BOLD}==========================================================${NC}"
15
+ echo -e "${CYAN}${BOLD} LAPORAN GENERATOR - DEPENDENCY SETUP (LINUX/MAC) ${NC}"
16
+ echo -e "${CYAN}${BOLD}==========================================================${NC}"
17
+ echo ""
18
+
19
+ # 1. Cek dependensi saat ini
20
+ has_cmd() { command -v "$1" >/dev/null 2>&1; }
21
+
22
+ echo -e "${YELLOW}[1/3] Memeriksa dependensi sistem...${NC}"
23
+ missing=()
24
+
25
+ if has_cmd typst; then
26
+ echo -e " ${GREEN}[OK]${NC} Typst Engine: $(typst --version 2>/dev/null || echo 'terpasang')"
27
+ else
28
+ echo -e " ${RED}[MISSING]${NC} Typst Engine belum terpasang."
29
+ missing+=("typst")
30
+ fi
31
+
32
+ if has_cmd pandoc; then
33
+ echo -e " ${GREEN}[OK]${NC} Pandoc Converter: $(pandoc --version 2>/dev/null | head -n1 || echo 'terpasang')"
34
+ else
35
+ echo -e " ${RED}[MISSING]${NC} Pandoc Converter belum terpasang."
36
+ missing+=("pandoc")
37
+ fi
38
+
39
+ if has_cmd magick || has_cmd convert; then
40
+ echo -e " ${GREEN}[OK]${NC} ImageMagick terpasang."
41
+ else
42
+ echo -e " ${RED}[MISSING]${NC} ImageMagick belum terpasang."
43
+ missing+=("imagemagick")
44
+ fi
45
+
46
+ if [ ${#missing[@]} -eq 0 ]; then
47
+ echo ""
48
+ echo -e "${GREEN}✔ Semua dependensi utama sudah lengkap dan siap digunakan!${NC}"
49
+ exit 0
50
+ fi
51
+
52
+ echo ""
53
+ echo -e "${YELLOW}[2/3] Memasang dependensi: ${missing[*]}...${NC}"
54
+
55
+ # Deteksi OS / Package Manager
56
+ if [[ "$OSTYPE" == "darwin"* ]]; then
57
+ if has_cmd brew; then
58
+ echo -e "${CYAN}--> Menggunakan Homebrew (macOS)...${NC}"
59
+ brew install typst pandoc imagemagick
60
+ else
61
+ echo -e "${RED}[ERROR] Homebrew tidak ditemukan. Silakan pasang brew (https://brew.sh) terlebih dahulu.${NC}"
62
+ exit 1
63
+ fi
64
+ elif has_cmd nix; then
65
+ echo -e "${CYAN}--> Lingkungan Nix terdeteksi! Kamu bisa langsung menjalankan:${NC}"
66
+ echo -e " ${BOLD}nix develop${NC}"
67
+ echo -e " Semua dependensi sudah otomatis terisolasi di flake.nix."
68
+ exit 0
69
+ elif has_cmd apt-get; then
70
+ echo -e "${CYAN}--> Menggunakan apt-get (Debian/Ubuntu)...${NC}"
71
+ sudo apt-get update -qq
72
+ sudo apt-get install -y imagemagick poppler-utils xz-utils unzip wget
73
+
74
+ # Install pandoc deb terbaru jika belum ada
75
+ if ! has_cmd pandoc; then
76
+ echo -e "--> Mengunduh Pandoc official deb..."
77
+ PANDOC_URL="https://github.com/jgm/pandoc/releases/download/3.7.0.2/pandoc-3.7.0.2-1-amd64.deb"
78
+ wget -q "$PANDOC_URL" -O /tmp/pandoc.deb
79
+ sudo dpkg -i /tmp/pandoc.deb
80
+ rm -f /tmp/pandoc.deb
81
+ fi
82
+
83
+ # Install typst binary jika belum ada
84
+ if ! has_cmd typst; then
85
+ echo -e "--> Mengunduh Typst standalone binary..."
86
+ TYPST_URL="https://github.com/typst/typst/releases/download/v0.13.0/typst-x86_64-unknown-linux-musl.tar.xz"
87
+ wget -q "$TYPST_URL" -O /tmp/typst.tar.xz
88
+ tar -xJf /tmp/typst.tar.xz -C /tmp
89
+ sudo mv /tmp/typst-x86_64-unknown-linux-musl/typst /usr/local/bin/typst
90
+ rm -rf /tmp/typst.tar.xz /tmp/typst-x86_64-unknown-linux-musl
91
+ fi
92
+ elif has_cmd pacman; then
93
+ echo -e "${CYAN}--> Menggunakan pacman (Arch Linux)...${NC}"
94
+ sudo pacman -S --noconfirm typst pandoc imagemagick
95
+ elif has_cmd dnf; then
96
+ echo -e "${CYAN}--> Menggunakan dnf (Fedora)...${NC}"
97
+ sudo dnf install -y typst pandoc ImageMagick
98
+ else
99
+ echo -e "${YELLOW}[WARN] Package manager native tidak dikenali.${NC}"
100
+ echo -e "Kamu dapat menggunakan Docker container bawaan:"
101
+ echo -e " ${BOLD}docker run --rm -v \"\$(pwd):/workspace\" muadzhdz/laporan-generator${NC}"
102
+ exit 1
103
+ fi
104
+
105
+ echo ""
106
+ echo -e "${YELLOW}[3/3] Memverifikasi instalasi...${NC}"
107
+ if has_cmd typst && has_cmd pandoc; then
108
+ echo -e "${GREEN}==========================================================${NC}"
109
+ echo -e "${GREEN} PEMASANGAN SELESAI! SEMUA DEPENDENSI SIAP PAKAI! ${NC}"
110
+ echo -e "${GREEN}==========================================================${NC}"
111
+ else
112
+ echo -e "${RED}[WARN] Beberapa dependensi belum terdeteksi di PATH.${NC}"
113
+ fi
@@ -0,0 +1,128 @@
1
+ #!/usr/bin/env python3
2
+ """test_scripts.py: Automated unit tests for Laporan Generator Python helper scripts."""
3
+
4
+ import os
5
+ import sys
6
+ import unittest
7
+
8
+ # Tambahkan direktori scripts ke sys.path
9
+ SCRIPTS_DIR = os.path.dirname(os.path.abspath(__file__))
10
+ ROOT_DIR = os.path.dirname(SCRIPTS_DIR)
11
+ sys.path.insert(0, SCRIPTS_DIR)
12
+
13
+ scan_preset = __import__("scan-preset")
14
+ validate_preset = __import__("validate-preset")
15
+ docx_pagenum = __import__("docx-pagenum")
16
+ finalize_docx = __import__("finalize-docx")
17
+
18
+
19
+ class TestScanPreset(unittest.TestCase):
20
+ def test_pdf_slug(self):
21
+ self.assertEqual(scan_preset.pdf_slug("pedoman-itb.pdf"), "itb")
22
+ self.assertEqual(scan_preset.pdf_slug("panduan-ugm.pdf"), "ugm")
23
+ self.assertEqual(scan_preset.pdf_slug("buku-ui.pdf"), "ui")
24
+
25
+ def test_detect_margins(self):
26
+ text_4433 = "Batas pengetikan adalah 4 - 4 - 3 - 3 cm."
27
+ margins = scan_preset.detect_margins(text_4433)
28
+ self.assertEqual(margins["top"], "4cm")
29
+ self.assertEqual(margins["bottom"], "3cm")
30
+ self.assertEqual(margins["left"], "4cm")
31
+ self.assertEqual(margins["right"], "3cm")
32
+
33
+ def test_detect_institution(self):
34
+ sample = "KEMENTERIAN PENDIDIKAN\nUNIVERSITAS INDONESIA\nFAKULTAS ILMU KOMPUTER"
35
+ inst = scan_preset.detect_institution(sample, "ui.pdf")
36
+ self.assertIn("UNIVERSITAS INDONESIA", inst)
37
+
38
+
39
+ class TestValidatePreset(unittest.TestCase):
40
+ def test_parse_simple_yaml(self):
41
+ test_yaml = os.path.join(ROOT_DIR, "presets", "standard.yml")
42
+ self.assertTrue(os.path.exists(test_yaml))
43
+ data = validate_preset.parse_simple_yaml(test_yaml)
44
+ self.assertEqual(data.get("preset_id"), "standard")
45
+ self.assertEqual(data.get("margin_top"), "2cm")
46
+
47
+ def test_validate_all_presets(self):
48
+ presets_dir = os.path.join(ROOT_DIR, "presets")
49
+ for f in os.listdir(presets_dir):
50
+ if f.endswith(".yml"):
51
+ path = os.path.join(presets_dir, f)
52
+ errors = validate_preset.validate_preset_file(path)
53
+ self.assertEqual(errors, [], f"Preset {f} gagal validasi: {errors}")
54
+
55
+
56
+ class TestDocxPagenum(unittest.TestCase):
57
+ def test_needle_of(self):
58
+ self.assertEqual(docx_pagenum.needle_of("BAB I PENDAHULUAN"), "PENDAHULUAN")
59
+ self.assertEqual(docx_pagenum.needle_of("1.1 Latar Belakang"), "1.1 Latar Belakang")
60
+ self.assertEqual(docx_pagenum.needle_of("KATA PENGANTAR"), "KATA PENGANTAR")
61
+
62
+ def test_roman_pattern(self):
63
+ self.assertTrue(docx_pagenum.ROMAN.match("iv"))
64
+ self.assertTrue(docx_pagenum.ROMAN.match("xii"))
65
+ self.assertFalse(docx_pagenum.ROMAN.match("123"))
66
+
67
+ def test_decimal_pattern(self):
68
+ self.assertTrue(docx_pagenum.DECIMAL.match("1"))
69
+ self.assertTrue(docx_pagenum.DECIMAL.match("42"))
70
+ self.assertFalse(docx_pagenum.DECIMAL.match("iv"))
71
+
72
+
73
+ class TestFinalizeDocx(unittest.TestCase):
74
+ def test_classify_front_matter(self):
75
+ self.assertEqual(finalize_docx.classify("KATA PENGANTAR"), "cover")
76
+ self.assertEqual(finalize_docx.classify("ABSTRAK"), "cover")
77
+ self.assertEqual(finalize_docx.classify("DAFTAR ISI"), "cover")
78
+
79
+ def test_classify_standard_thesis_headings(self):
80
+ self.assertEqual(finalize_docx.classify("BAB I"), "roman")
81
+ self.assertEqual(finalize_docx.classify("BAB I PENDAHULUAN"), "roman")
82
+ self.assertEqual(finalize_docx.classify("BAB II"), "decimal-start")
83
+ self.assertEqual(
84
+ finalize_docx.classify("BAB II TINJAUAN PUSTAKA"), "decimal-start"
85
+ )
86
+ self.assertEqual(finalize_docx.classify("BAB III METODOLOGI"), "body")
87
+ self.assertEqual(finalize_docx.classify("DAFTAR PUSTAKA"), "body")
88
+
89
+ def test_classify_makalah_headings(self):
90
+ self.assertEqual(finalize_docx.classify("1. PENDAHULUAN"), "roman")
91
+ self.assertEqual(finalize_docx.classify("1 PENDAHULUAN"), "roman")
92
+ self.assertEqual(finalize_docx.classify("2. PEMBAHASAN"), "decimal-start")
93
+ self.assertEqual(finalize_docx.classify("2 PEMBAHASAN"), "decimal-start")
94
+ self.assertEqual(finalize_docx.classify("3. PENUTUP"), "body")
95
+
96
+ def test_classify_arabic_chapter_headings(self):
97
+ self.assertEqual(finalize_docx.classify("BAB 1 PENDAHULUAN"), "roman")
98
+ self.assertEqual(finalize_docx.classify("BAB 2 PEMBAHASAN"), "decimal-start")
99
+ self.assertEqual(finalize_docx.classify("BAB 3 PENUTUP"), "body")
100
+
101
+ def test_get_tab_pos(self):
102
+ sample_standard = '<w:sectPr><w:pgSz w:w="11906"/><w:pgMar w:left="1417" w:right="1417"/></w:sectPr>'
103
+ self.assertEqual(finalize_docx.get_tab_pos(sample_standard), 9072)
104
+ sample_4433 = '<w:sectPr><w:pgSz w:w="11906"/><w:pgMar w:left="2268" w:right="1701"/></w:sectPr>'
105
+ self.assertEqual(finalize_docx.get_tab_pos(sample_4433), 7937)
106
+ self.assertEqual(finalize_docx.get_tab_pos(""), 9072)
107
+
108
+ def test_fix_daftar_isi_title(self):
109
+ raw = '<w:p><w:pPr><w:pStyle w:val="Heading1"/><w:outlineLvl w:val="-1"/></w:pPr><w:r><w:t>DAFTAR ISI</w:t></w:r></w:p>'
110
+ fixed = finalize_docx.fix_daftar_isi_title(raw)
111
+ self.assertIn('w:val="TOCHeading"', fixed)
112
+ self.assertNotIn('w:val="Heading1"', fixed)
113
+
114
+ def test_fix_toc_styles(self):
115
+ styles = '<w:styles></w:styles>'
116
+ res = finalize_docx.fix_toc_styles(styles, 7937)
117
+ self.assertIn('w:styleId="TOCHeading"', res)
118
+ self.assertIn('w:styleId="TOC1"', res)
119
+ self.assertIn('w:styleId="TOC2"', res)
120
+ self.assertIn('w:styleId="TOC3"', res)
121
+ self.assertIn('w:pos="7937"', res)
122
+ self.assertIn('w:left="360"', res)
123
+ self.assertIn('w:left="720"', res)
124
+
125
+
126
+ if __name__ == "__main__":
127
+ unittest.main(verbosity=2)
128
+
@@ -0,0 +1,166 @@
1
+ #!/usr/bin/env python3
2
+ """validate-preset.py: Validator dan Linter untuk berkas preset format kampus (YAML).
3
+
4
+ Memeriksa integritas berkas preset terhadap spesifikasi skema resmi (docs/preset-schema.md).
5
+
6
+ Usage:
7
+ python3 scripts/validate-preset.py [path-to-preset.yml ...]
8
+ python3 scripts/validate-preset.py --all
9
+ """
10
+
11
+ import argparse
12
+ import glob
13
+ import os
14
+ import re
15
+ import sys
16
+
17
+ DIM_REGEX = re.compile(r"^[0-9]+(\.[0-9]+)?(cm|mm|in|pt)$", re.I)
18
+ PT_REGEX = re.compile(r"^[0-9]+(\.[0-9]+)?pt$", re.I)
19
+
20
+
21
+ def parse_simple_yaml(filepath):
22
+ """Parser YAML yang tangguh dengan PyYAML dan fallback aman tanpa dependensi."""
23
+ try:
24
+ import yaml
25
+ with open(filepath, "r", encoding="utf-8") as f:
26
+ content = yaml.safe_load(f)
27
+ if isinstance(content, dict):
28
+ return {str(k): ("" if v is None else str(v)) for k, v in content.items()}
29
+ except (ImportError, ModuleNotFoundError):
30
+ pass
31
+
32
+ data = {}
33
+ with open(filepath, "r", encoding="utf-8") as f:
34
+ for line in f:
35
+ line = line.strip()
36
+ if not line or line.startswith("#"):
37
+ continue
38
+ if ":" in line:
39
+ key, val = line.split(":", 1)
40
+ key = key.strip()
41
+ val = val.strip()
42
+ if val.startswith('"'):
43
+ end_quote = val.find('"', 1)
44
+ val = val[1:end_quote] if end_quote != -1 else val.strip('"')
45
+ elif val.startswith("'"):
46
+ end_quote = val.find("'", 1)
47
+ val = val[1:end_quote] if end_quote != -1 else val.strip("'")
48
+ else:
49
+ val = val.split("#")[0].strip()
50
+ data[key] = val
51
+ return data
52
+
53
+
54
+ def validate_preset_file(filepath):
55
+ """Validasi sebuah file preset dan kembalikan daftar error."""
56
+ errors = []
57
+ if not os.path.isfile(filepath):
58
+ return [f"Berkas '{filepath}' tidak ditemukan."]
59
+
60
+ try:
61
+ data = parse_simple_yaml(filepath)
62
+ except Exception as e:
63
+ return [f"Gagal mengurai YAML: {e}"]
64
+
65
+ # 1. Identitas
66
+ expected_id = os.path.basename(filepath).rsplit(".", 1)[0]
67
+ pid = data.get("preset_id")
68
+ if not pid:
69
+ errors.append("Field wajib 'preset_id' tidak ditemukan.")
70
+ elif pid != expected_id:
71
+ errors.append(f"'preset_id' ('{pid}') tidak cocok dengan nama berkas ('{expected_id}').")
72
+
73
+ if not data.get("name"):
74
+ errors.append("Field wajib 'name' tidak ditemukan atau kosong.")
75
+
76
+ # 2. Margin
77
+ for m in ["margin_top", "margin_bottom", "margin_left", "margin_right"]:
78
+ val = data.get(m)
79
+ if not val:
80
+ errors.append(f"Field margin '{m}' wajib diisi.")
81
+ elif not DIM_REGEX.match(val):
82
+ errors.append(f"Format satuan '{m}' tidak valid: '{val}' (contoh valid: '4cm', '2.5cm', '12pt').")
83
+
84
+ # 3. Tipografi
85
+ if not data.get("font_family"):
86
+ errors.append("Field 'font_family' wajib diisi.")
87
+
88
+ fs = data.get("font_size")
89
+ if not fs:
90
+ errors.append("Field 'font_size' wajib diisi.")
91
+ elif not PT_REGEX.match(fs):
92
+ errors.append(f"Format 'font_size' tidak valid: '{fs}' (contoh: '12pt').")
93
+
94
+ ls = data.get("line_spacing")
95
+ if not ls:
96
+ errors.append("Field 'line_spacing' wajib diisi.")
97
+
98
+ ind = data.get("first_line_indent")
99
+ if not ind:
100
+ errors.append("Field 'first_line_indent' wajib diisi.")
101
+ elif not DIM_REGEX.match(ind):
102
+ errors.append(f"Format 'first_line_indent' tidak valid: '{ind}' (contoh: '1.25cm').")
103
+
104
+ # 4. Heading
105
+ fmt = data.get("heading_chapter_num_format")
106
+ if fmt and fmt not in ("roman", "arabic"):
107
+ errors.append(f"'heading_chapter_num_format' harus 'roman' atau 'arabic', bukan '{fmt}'.")
108
+
109
+ # 5. Booleans
110
+ for b in ["heading_sub_dot", "heading_subsub_dot", "cover_show_lecturer"]:
111
+ if b in data and str(data[b]).lower() not in ("true", "false"):
112
+ errors.append(f"Field '{b}' harus berupa boolean (true/false), bukan '{data[b]}'.")
113
+
114
+ return errors
115
+
116
+
117
+ def main():
118
+ parser = argparse.ArgumentParser(description="Linter dan validator berkas preset format kampus.")
119
+ parser.add_argument("files", nargs="*", help="Path ke berkas preset YAML yang ingin divalidasi")
120
+ parser.add_argument("--all", action="store_true", help="Validasi seluruh preset di folder presets/")
121
+ parser.add_argument("--dir", default="presets", help="Direktori preset (default: presets/)")
122
+
123
+ args = parser.parse_args()
124
+
125
+ target_files = []
126
+ if args.all or not args.files:
127
+ target_files = sorted(glob.glob(os.path.join(args.dir, "*.yml")))
128
+ else:
129
+ for f in args.files:
130
+ if not f.endswith(".yml"):
131
+ f = os.path.join(args.dir, f"{f}.yml")
132
+ target_files.append(f)
133
+
134
+ if not target_files:
135
+ print(f"[WARN] Tidak ada berkas preset YAML yang ditemukan di '{args.dir}'.")
136
+ sys.exit(0)
137
+
138
+ print("=" * 60)
139
+ print(" LAPORAN GENERATOR PRESET VALIDATOR")
140
+ print("=" * 60)
141
+
142
+ total = len(target_files)
143
+ passed = 0
144
+ failed = 0
145
+
146
+ for f in target_files:
147
+ rel_path = os.path.relpath(f)
148
+ errors = validate_preset_file(f)
149
+ if not errors:
150
+ print(f" [OK] {rel_path}")
151
+ passed += 1
152
+ else:
153
+ print(f" [FAIL] {rel_path}:")
154
+ for err in errors:
155
+ print(f" - {err}")
156
+ failed += 1
157
+
158
+ print("=" * 60)
159
+ print(f"Hasil: {passed} valid, {failed} error dari total {total} preset.")
160
+ print("=" * 60)
161
+
162
+ sys.exit(1 if failed > 0 else 0)
163
+
164
+
165
+ if __name__ == "__main__":
166
+ main()