laporan-generator 2.7.0 → 2.8.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 +15 -5
- package/.agents/skills/laporan-generator/resources/cover.md +18 -14
- package/.agents/skills/laporan-generator/resources/docx.lua +65 -0
- package/.agents/skills/laporan-generator/resources/laporan +1 -1
- package/.agents/skills/laporan-generator/resources/laporan.ps1 +1 -1
- package/.agents/skills/laporan-generator/resources/metadata.yml +13 -16
- package/.agents/skills/laporan-generator/resources/scripts/test_scripts.py +97 -0
- package/.agents/skills/laporan-generator/resources/template.typ +3 -0
- package/.agents/skills/laporan-generator/scripts/test_scripts.py +97 -0
- package/README.md +6 -1
- package/bin/laporan-generator.js +1 -1
- package/cover.md +18 -14
- package/docs/metadata-schema.md +2 -0
- package/docx.lua +65 -0
- package/laporan +1 -1
- package/laporan.ps1 +1 -1
- package/lib/init.js +26 -3
- package/lib/mcp.js +11 -1
- package/metadata.yml +13 -16
- package/package.json +1 -1
- package/scripts/test_scripts.py +97 -0
- package/template.typ +3 -0
|
@@ -58,15 +58,25 @@ Identify the document type and target university:
|
|
|
58
58
|
- `unpad-skripsi`: Universitas Padjadjaran.
|
|
59
59
|
- `standard`: General University (2.5cm / 2cm).
|
|
60
60
|
|
|
61
|
-
### Stage 2:
|
|
62
|
-
1.
|
|
63
|
-
-
|
|
64
|
-
|
|
61
|
+
### Stage 2: Document Structure Confirmation & Scaffolding
|
|
62
|
+
1. **Confirm Document Structure**:
|
|
63
|
+
- The universal default structure is minimal: **Cover -> Kata Pengantar -> Daftar Isi -> Batang Tubuh (BAB I..n) -> Daftar Pustaka**.
|
|
64
|
+
- **MANDATORY INQUIRY**: Ask the user before scaffolding if they want to activate optional components:
|
|
65
|
+
- Abstrak / Abstract (`abstract_id` / `abstract_en`)
|
|
66
|
+
- Lembar Pengesahan (`approval`)
|
|
67
|
+
- Daftar Gambar (`daftar_gambar: true`)
|
|
68
|
+
- Daftar Tabel (`daftar_tabel: true`)
|
|
69
|
+
- Lampiran
|
|
70
|
+
2. **Initialize the workspace**:
|
|
71
|
+
- Call MCP tool `laporan_init` (e.g. `laporan_init({ preset, title, daftar_gambar, daftar_tabel })`) or run `npx laporan-generator init`.
|
|
72
|
+
3. **Configure `metadata.yml`**:
|
|
65
73
|
- `title`: Clear, concise, capitalized (rendered as inverted pyramid).
|
|
66
74
|
- `author`: Author array with `name` and `nim` (Student ID).
|
|
67
75
|
- `preset`: Selected university preset (e.g. `skripsi-4433`, `itb-ta`).
|
|
76
|
+
- `daftar_gambar: false`, `daftar_tabel: false` (set to `true` only if opted-in).
|
|
77
|
+
- `abstract_id`: Kept empty or commented out unless user requested Abstrak.
|
|
68
78
|
- `institution`, `faculty`, `department`, `year`.
|
|
69
|
-
|
|
79
|
+
4. **Configure `cover.md`**: Set formal title, institutional affiliations, and kata pengantar.
|
|
70
80
|
|
|
71
81
|
### Stage 3: Rigorous Academic Writing Standard
|
|
72
82
|
When authoring content into `chapters/bab*.md`, strictly enforce **Indonesian Standard Academic Tone**:
|
|
@@ -22,21 +22,25 @@ Penulis menyadari bahwa laporan ini masih jauh dari sempurna. Oleh karena itu, k
|
|
|
22
22
|
depth: 3,
|
|
23
23
|
)
|
|
24
24
|
#context {
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
25
|
+
if opt-daftar-gambar {
|
|
26
|
+
let imgs = query(figure.where(kind: image))
|
|
27
|
+
if imgs.len() > 0 {
|
|
28
|
+
pagebreak()
|
|
29
|
+
outline(
|
|
30
|
+
title: align(center)[#text(size: 14pt, weight: "bold")[DAFTAR GAMBAR]],
|
|
31
|
+
target: figure.where(kind: image),
|
|
32
|
+
)
|
|
33
|
+
}
|
|
32
34
|
}
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
35
|
+
if opt-daftar-tabel {
|
|
36
|
+
let tbls = query(figure.where(kind: table))
|
|
37
|
+
if tbls.len() > 0 {
|
|
38
|
+
pagebreak()
|
|
39
|
+
outline(
|
|
40
|
+
title: align(center)[#text(size: 14pt, weight: "bold")[DAFTAR TABEL]],
|
|
41
|
+
target: figure.where(kind: table),
|
|
42
|
+
)
|
|
43
|
+
}
|
|
40
44
|
}
|
|
41
45
|
}
|
|
42
46
|
#pagebreak()
|
|
@@ -138,9 +138,24 @@ local function meta_str(meta, key)
|
|
|
138
138
|
if v == nil then
|
|
139
139
|
return ""
|
|
140
140
|
end
|
|
141
|
+
if type(v) == "boolean" then
|
|
142
|
+
return tostring(v)
|
|
143
|
+
end
|
|
141
144
|
return pandoc.utils.stringify(v)
|
|
142
145
|
end
|
|
143
146
|
|
|
147
|
+
local function meta_bool(meta, key)
|
|
148
|
+
local v = meta[key]
|
|
149
|
+
if v == nil then
|
|
150
|
+
return false
|
|
151
|
+
end
|
|
152
|
+
if type(v) == "boolean" then
|
|
153
|
+
return v
|
|
154
|
+
end
|
|
155
|
+
local s = meta_str(meta, key):lower():gsub("%s+", "")
|
|
156
|
+
return s == "true" or s == "1" or s == "yes"
|
|
157
|
+
end
|
|
158
|
+
|
|
144
159
|
local function format_chapter_number(n)
|
|
145
160
|
if chapter_num_fmt == "arabic" then
|
|
146
161
|
return tostring(n)
|
|
@@ -280,6 +295,44 @@ local function daftar_isi()
|
|
|
280
295
|
return out
|
|
281
296
|
end
|
|
282
297
|
|
|
298
|
+
local function daftar_gambar()
|
|
299
|
+
local out = pandoc.List{}
|
|
300
|
+
out[#out + 1] = pagebreak()
|
|
301
|
+
out[#out + 1] = pandoc.RawBlock(
|
|
302
|
+
"openxml",
|
|
303
|
+
'<w:p><w:pPr><w:pStyle w:val="TOCHeading"/><w:jc w:val="center"/><w:outlineLvl w:val="9"/></w:pPr>' ..
|
|
304
|
+
'<w:r><w:rPr><w:b/><w:sz w:val="28"/><w:szCs w:val="28"/></w:rPr><w:t>DAFTAR GAMBAR</w:t></w:r></w:p>'
|
|
305
|
+
)
|
|
306
|
+
out[#out + 1] = pandoc.RawBlock(
|
|
307
|
+
"openxml",
|
|
308
|
+
'<w:p><w:pPr><w:pStyle w:val="TOC1"/></w:pPr>' ..
|
|
309
|
+
'<w:r><w:fldChar w:fldCharType="begin" w:dirty="true"/></w:r>' ..
|
|
310
|
+
'<w:r><w:instrText xml:space="preserve"> TOC \\c "Gambar" \\h \\z </w:instrText></w:r>' ..
|
|
311
|
+
'<w:r><w:fldChar w:fldCharType="separate"/></w:r>' ..
|
|
312
|
+
'<w:r><w:fldChar w:fldCharType="end"/></w:r></w:p>'
|
|
313
|
+
)
|
|
314
|
+
return out
|
|
315
|
+
end
|
|
316
|
+
|
|
317
|
+
local function daftar_tabel()
|
|
318
|
+
local out = pandoc.List{}
|
|
319
|
+
out[#out + 1] = pagebreak()
|
|
320
|
+
out[#out + 1] = pandoc.RawBlock(
|
|
321
|
+
"openxml",
|
|
322
|
+
'<w:p><w:pPr><w:pStyle w:val="TOCHeading"/><w:jc w:val="center"/><w:outlineLvl w:val="9"/></w:pPr>' ..
|
|
323
|
+
'<w:r><w:rPr><w:b/><w:sz w:val="28"/><w:szCs w:val="28"/></w:rPr><w:t>DAFTAR TABEL</w:t></w:r></w:p>'
|
|
324
|
+
)
|
|
325
|
+
out[#out + 1] = pandoc.RawBlock(
|
|
326
|
+
"openxml",
|
|
327
|
+
'<w:p><w:pPr><w:pStyle w:val="TOC1"/></w:pPr>' ..
|
|
328
|
+
'<w:r><w:fldChar w:fldCharType="begin" w:dirty="true"/></w:r>' ..
|
|
329
|
+
'<w:r><w:instrText xml:space="preserve"> TOC \\c "Tabel" \\h \\z </w:instrText></w:r>' ..
|
|
330
|
+
'<w:r><w:fldChar w:fldCharType="separate"/></w:r>' ..
|
|
331
|
+
'<w:r><w:fldChar w:fldCharType="end"/></w:r></w:p>'
|
|
332
|
+
)
|
|
333
|
+
return out
|
|
334
|
+
end
|
|
335
|
+
|
|
283
336
|
function Pandoc(doc)
|
|
284
337
|
if FORMAT ~= "docx" then
|
|
285
338
|
return doc
|
|
@@ -292,6 +345,8 @@ function Pandoc(doc)
|
|
|
292
345
|
local faculty = meta_str(meta, "faculty")
|
|
293
346
|
local institution = meta_str(meta, "institution")
|
|
294
347
|
local year = meta_str(meta, "year")
|
|
348
|
+
local show_dg = (meta_bool(meta, "daftar_gambar") or meta_bool(meta, "list_of_figures"))
|
|
349
|
+
local show_dt = (meta_bool(meta, "daftar_tabel") or meta_bool(meta, "list_of_tables"))
|
|
295
350
|
|
|
296
351
|
local blocks = pandoc.List{}
|
|
297
352
|
|
|
@@ -401,6 +456,16 @@ function Pandoc(doc)
|
|
|
401
456
|
for _, b in ipairs(daftar_isi()) do
|
|
402
457
|
body[#body + 1] = b
|
|
403
458
|
end
|
|
459
|
+
if show_dg then
|
|
460
|
+
for _, b in ipairs(daftar_gambar()) do
|
|
461
|
+
body[#body + 1] = b
|
|
462
|
+
end
|
|
463
|
+
end
|
|
464
|
+
if show_dt then
|
|
465
|
+
for _, b in ipairs(daftar_tabel()) do
|
|
466
|
+
body[#body + 1] = b
|
|
467
|
+
end
|
|
468
|
+
end
|
|
404
469
|
inserted = true
|
|
405
470
|
end
|
|
406
471
|
body[#body + 1] = blk
|
|
@@ -27,7 +27,7 @@ export PYTHONUTF8=1
|
|
|
27
27
|
show_banner() {
|
|
28
28
|
echo -e "${CYAN}${BOLD}"
|
|
29
29
|
echo " ========================================================"
|
|
30
|
-
echo " LAPORAN GENERATOR CLI v2.
|
|
30
|
+
echo " LAPORAN GENERATOR CLI v2.8.0 "
|
|
31
31
|
echo " Otomatisasi Dokumen Akademik (Typst + DOCX Engine) "
|
|
32
32
|
echo " ========================================================"
|
|
33
33
|
echo -e "${NC}"
|
|
@@ -40,7 +40,7 @@ function Get-PythonCommand {
|
|
|
40
40
|
|
|
41
41
|
function Show-Banner {
|
|
42
42
|
Write-Host " ========================================================" -ForegroundColor Cyan
|
|
43
|
-
Write-Host " LAPORAN GENERATOR CLI v2.
|
|
43
|
+
Write-Host " LAPORAN GENERATOR CLI v2.8.0 " -ForegroundColor Cyan
|
|
44
44
|
Write-Host " Otomatisasi Dokumen Akademik (Typst + DOCX Engine) " -ForegroundColor Cyan
|
|
45
45
|
Write-Host " ========================================================" -ForegroundColor Cyan
|
|
46
46
|
Write-Host ""
|
|
@@ -15,21 +15,18 @@ institution: "Universitas Example"
|
|
|
15
15
|
faculty: "Program Studi Informatika"
|
|
16
16
|
year: "2026/2027"
|
|
17
17
|
date: "Agustus 2026"
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
abstract_en: |
|
|
27
|
-
|
|
28
|
-
keywords_en:
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
- Typst
|
|
32
|
-
- OpenXML
|
|
33
|
-
- Software Engineering
|
|
18
|
+
daftar_gambar: false
|
|
19
|
+
daftar_tabel: false
|
|
20
|
+
# Komponen halaman depan opsional (aktifkan jika diperlukan):
|
|
21
|
+
# abstract_id: |
|
|
22
|
+
# Ringkasan atau abstrak laporan dalam Bahasa Indonesia...
|
|
23
|
+
# keywords_id:
|
|
24
|
+
# - Kata Kunci 1
|
|
25
|
+
# - Kata Kunci 2
|
|
26
|
+
# abstract_en: |
|
|
27
|
+
# Summary or abstract of the report in English...
|
|
28
|
+
# keywords_en:
|
|
29
|
+
# - Keyword 1
|
|
30
|
+
# - Keyword 2
|
|
34
31
|
preset: "standard"
|
|
35
32
|
...
|
|
@@ -184,6 +184,103 @@ class TestReportDoctor(unittest.TestCase):
|
|
|
184
184
|
self.assertIn("citations", data)
|
|
185
185
|
|
|
186
186
|
|
|
187
|
+
class TestFrontMatterOptIn(unittest.TestCase):
|
|
188
|
+
def test_default_metadata_structure(self):
|
|
189
|
+
meta_path = os.path.join(get_project_root(), "metadata.yml")
|
|
190
|
+
with open(meta_path, "r", encoding="utf-8") as f:
|
|
191
|
+
content = f.read()
|
|
192
|
+
# Verify abstract is commented or empty by default
|
|
193
|
+
self.assertNotIn("\nabstract_id: |", content, "abstract_id should not be active by default in metadata.yml")
|
|
194
|
+
self.assertNotIn("\nabstract_en: |", content, "abstract_en should not be active by default in metadata.yml")
|
|
195
|
+
# Verify opt-in flags for daftar_gambar & daftar_tabel are explicitly false or commented
|
|
196
|
+
has_dg = "daftar_gambar: false" in content
|
|
197
|
+
has_dt = "daftar_tabel: false" in content
|
|
198
|
+
self.assertTrue(has_dg, "metadata.yml must explicitly specify daftar_gambar: false by default")
|
|
199
|
+
self.assertTrue(has_dt, "metadata.yml must explicitly specify daftar_tabel: false by default")
|
|
200
|
+
|
|
201
|
+
def test_cover_md_gated_outlines(self):
|
|
202
|
+
cover_path = os.path.join(get_project_root(), "cover.md")
|
|
203
|
+
with open(cover_path, "r", encoding="utf-8") as f:
|
|
204
|
+
content = f.read()
|
|
205
|
+
# Unconditional outline queries must not exist
|
|
206
|
+
self.assertNotIn("let imgs = query(figure.where(kind: image))\n if imgs.len() > 0", content,
|
|
207
|
+
"cover.md must not unconditionally emit DAFTAR GAMBAR outline")
|
|
208
|
+
self.assertNotIn("let tbls = query(figure.where(kind: table))\n if tbls.len() > 0", content,
|
|
209
|
+
"cover.md must not unconditionally emit DAFTAR TABEL outline")
|
|
210
|
+
# Must be gated by opt-in check
|
|
211
|
+
self.assertIn("opt-daftar-gambar", content, "cover.md must gate DAFTAR GAMBAR with opt-daftar-gambar")
|
|
212
|
+
self.assertIn("opt-daftar-tabel", content, "cover.md must gate DAFTAR TABEL with opt-daftar-tabel")
|
|
213
|
+
|
|
214
|
+
def test_lua_meta_bool_coercion(self):
|
|
215
|
+
"""Verify docx.lua handles YAML boolean scalars and string equivalents safely."""
|
|
216
|
+
lua_code = """
|
|
217
|
+
local pandoc = {
|
|
218
|
+
utils = {
|
|
219
|
+
stringify = function(x)
|
|
220
|
+
if type(x) ~= 'string' and type(x) ~= 'table' then
|
|
221
|
+
error('pandoc.utils.stringify expects table or string, got ' .. type(x))
|
|
222
|
+
end
|
|
223
|
+
return tostring(x)
|
|
224
|
+
end
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
local function meta_str(meta, key)
|
|
228
|
+
local v = meta[key]
|
|
229
|
+
if v == nil then return "" end
|
|
230
|
+
if type(v) == "boolean" then return tostring(v) end
|
|
231
|
+
return pandoc.utils.stringify(v)
|
|
232
|
+
end
|
|
233
|
+
local function meta_bool(meta, key)
|
|
234
|
+
local v = meta[key]
|
|
235
|
+
if v == nil then return false end
|
|
236
|
+
if type(v) == "boolean" then return v end
|
|
237
|
+
local s = meta_str(meta, key):lower():gsub("%s+", "")
|
|
238
|
+
return s == "true" or s == "1" or s == "yes"
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
local meta = {
|
|
242
|
+
dg_bool_false = false,
|
|
243
|
+
dg_bool_true = true,
|
|
244
|
+
dg_str_false = "false",
|
|
245
|
+
dg_str_true = "true",
|
|
246
|
+
dg_nil = nil
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
assert(meta_str(meta, "dg_bool_false") == "false")
|
|
250
|
+
assert(meta_bool(meta, "dg_bool_false") == false)
|
|
251
|
+
assert(meta_str(meta, "dg_bool_true") == "true")
|
|
252
|
+
assert(meta_bool(meta, "dg_bool_true") == true)
|
|
253
|
+
assert(meta_bool(meta, "dg_str_false") == false)
|
|
254
|
+
assert(meta_bool(meta, "dg_str_true") == true)
|
|
255
|
+
assert(meta_bool(meta, "dg_nil") == false)
|
|
256
|
+
print("LUA_OK")
|
|
257
|
+
"""
|
|
258
|
+
proc = subprocess.run(["lua", "-e", lua_code], capture_output=True, text=True)
|
|
259
|
+
self.assertEqual(proc.returncode, 0, f"Lua helper test failed: {proc.stderr}")
|
|
260
|
+
self.assertIn("LUA_OK", proc.stdout)
|
|
261
|
+
|
|
262
|
+
def test_init_project_boolean_coercion(self):
|
|
263
|
+
"""Verify lib/init.js does not treat string 'false' as truthy."""
|
|
264
|
+
import tempfile
|
|
265
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
266
|
+
node_code = f"""
|
|
267
|
+
const path = require('path');
|
|
268
|
+
const {{ initProject }} = require('{os.path.join(ROOT_DIR, "lib/init.js")}');
|
|
269
|
+
initProject({{ targetDir: '{tmpdir}/test_f', daftar_gambar: 'false', daftar_tabel: false, silent: true }});
|
|
270
|
+
initProject({{ targetDir: '{tmpdir}/test_t', daftar_gambar: 'true', daftar_tabel: true, silent: true }});
|
|
271
|
+
"""
|
|
272
|
+
proc = subprocess.run(["node", "-e", node_code], capture_output=True, text=True)
|
|
273
|
+
self.assertEqual(proc.returncode, 0, f"Node initProject failed: {proc.stderr}")
|
|
274
|
+
with open(os.path.join(tmpdir, "test_f", "metadata.yml"), "r", encoding="utf-8") as f:
|
|
275
|
+
meta_f = f.read()
|
|
276
|
+
with open(os.path.join(tmpdir, "test_t", "metadata.yml"), "r", encoding="utf-8") as f:
|
|
277
|
+
meta_t = f.read()
|
|
278
|
+
self.assertIn("daftar_gambar: false", meta_f)
|
|
279
|
+
self.assertIn("daftar_tabel: false", meta_f)
|
|
280
|
+
self.assertIn("daftar_gambar: true", meta_t)
|
|
281
|
+
self.assertIn("daftar_tabel: true", meta_t)
|
|
282
|
+
|
|
283
|
+
|
|
187
284
|
if __name__ == "__main__":
|
|
188
285
|
unittest.main(verbosity=2)
|
|
189
286
|
|
|
@@ -391,4 +391,7 @@ $endif$
|
|
|
391
391
|
#pagebreak()
|
|
392
392
|
$endif$
|
|
393
393
|
|
|
394
|
+
#let opt-daftar-gambar = $if(daftar_gambar)$$daftar_gambar$$else$$if(list_of_figures)$$list_of_figures$$else$false$endif$$endif$
|
|
395
|
+
#let opt-daftar-tabel = $if(daftar_tabel)$$daftar_tabel$$else$$if(list_of_tables)$$list_of_tables$$else$false$endif$$endif$
|
|
396
|
+
|
|
394
397
|
$body$
|
|
@@ -184,6 +184,103 @@ class TestReportDoctor(unittest.TestCase):
|
|
|
184
184
|
self.assertIn("citations", data)
|
|
185
185
|
|
|
186
186
|
|
|
187
|
+
class TestFrontMatterOptIn(unittest.TestCase):
|
|
188
|
+
def test_default_metadata_structure(self):
|
|
189
|
+
meta_path = os.path.join(get_project_root(), "metadata.yml")
|
|
190
|
+
with open(meta_path, "r", encoding="utf-8") as f:
|
|
191
|
+
content = f.read()
|
|
192
|
+
# Verify abstract is commented or empty by default
|
|
193
|
+
self.assertNotIn("\nabstract_id: |", content, "abstract_id should not be active by default in metadata.yml")
|
|
194
|
+
self.assertNotIn("\nabstract_en: |", content, "abstract_en should not be active by default in metadata.yml")
|
|
195
|
+
# Verify opt-in flags for daftar_gambar & daftar_tabel are explicitly false or commented
|
|
196
|
+
has_dg = "daftar_gambar: false" in content
|
|
197
|
+
has_dt = "daftar_tabel: false" in content
|
|
198
|
+
self.assertTrue(has_dg, "metadata.yml must explicitly specify daftar_gambar: false by default")
|
|
199
|
+
self.assertTrue(has_dt, "metadata.yml must explicitly specify daftar_tabel: false by default")
|
|
200
|
+
|
|
201
|
+
def test_cover_md_gated_outlines(self):
|
|
202
|
+
cover_path = os.path.join(get_project_root(), "cover.md")
|
|
203
|
+
with open(cover_path, "r", encoding="utf-8") as f:
|
|
204
|
+
content = f.read()
|
|
205
|
+
# Unconditional outline queries must not exist
|
|
206
|
+
self.assertNotIn("let imgs = query(figure.where(kind: image))\n if imgs.len() > 0", content,
|
|
207
|
+
"cover.md must not unconditionally emit DAFTAR GAMBAR outline")
|
|
208
|
+
self.assertNotIn("let tbls = query(figure.where(kind: table))\n if tbls.len() > 0", content,
|
|
209
|
+
"cover.md must not unconditionally emit DAFTAR TABEL outline")
|
|
210
|
+
# Must be gated by opt-in check
|
|
211
|
+
self.assertIn("opt-daftar-gambar", content, "cover.md must gate DAFTAR GAMBAR with opt-daftar-gambar")
|
|
212
|
+
self.assertIn("opt-daftar-tabel", content, "cover.md must gate DAFTAR TABEL with opt-daftar-tabel")
|
|
213
|
+
|
|
214
|
+
def test_lua_meta_bool_coercion(self):
|
|
215
|
+
"""Verify docx.lua handles YAML boolean scalars and string equivalents safely."""
|
|
216
|
+
lua_code = """
|
|
217
|
+
local pandoc = {
|
|
218
|
+
utils = {
|
|
219
|
+
stringify = function(x)
|
|
220
|
+
if type(x) ~= 'string' and type(x) ~= 'table' then
|
|
221
|
+
error('pandoc.utils.stringify expects table or string, got ' .. type(x))
|
|
222
|
+
end
|
|
223
|
+
return tostring(x)
|
|
224
|
+
end
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
local function meta_str(meta, key)
|
|
228
|
+
local v = meta[key]
|
|
229
|
+
if v == nil then return "" end
|
|
230
|
+
if type(v) == "boolean" then return tostring(v) end
|
|
231
|
+
return pandoc.utils.stringify(v)
|
|
232
|
+
end
|
|
233
|
+
local function meta_bool(meta, key)
|
|
234
|
+
local v = meta[key]
|
|
235
|
+
if v == nil then return false end
|
|
236
|
+
if type(v) == "boolean" then return v end
|
|
237
|
+
local s = meta_str(meta, key):lower():gsub("%s+", "")
|
|
238
|
+
return s == "true" or s == "1" or s == "yes"
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
local meta = {
|
|
242
|
+
dg_bool_false = false,
|
|
243
|
+
dg_bool_true = true,
|
|
244
|
+
dg_str_false = "false",
|
|
245
|
+
dg_str_true = "true",
|
|
246
|
+
dg_nil = nil
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
assert(meta_str(meta, "dg_bool_false") == "false")
|
|
250
|
+
assert(meta_bool(meta, "dg_bool_false") == false)
|
|
251
|
+
assert(meta_str(meta, "dg_bool_true") == "true")
|
|
252
|
+
assert(meta_bool(meta, "dg_bool_true") == true)
|
|
253
|
+
assert(meta_bool(meta, "dg_str_false") == false)
|
|
254
|
+
assert(meta_bool(meta, "dg_str_true") == true)
|
|
255
|
+
assert(meta_bool(meta, "dg_nil") == false)
|
|
256
|
+
print("LUA_OK")
|
|
257
|
+
"""
|
|
258
|
+
proc = subprocess.run(["lua", "-e", lua_code], capture_output=True, text=True)
|
|
259
|
+
self.assertEqual(proc.returncode, 0, f"Lua helper test failed: {proc.stderr}")
|
|
260
|
+
self.assertIn("LUA_OK", proc.stdout)
|
|
261
|
+
|
|
262
|
+
def test_init_project_boolean_coercion(self):
|
|
263
|
+
"""Verify lib/init.js does not treat string 'false' as truthy."""
|
|
264
|
+
import tempfile
|
|
265
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
266
|
+
node_code = f"""
|
|
267
|
+
const path = require('path');
|
|
268
|
+
const {{ initProject }} = require('{os.path.join(ROOT_DIR, "lib/init.js")}');
|
|
269
|
+
initProject({{ targetDir: '{tmpdir}/test_f', daftar_gambar: 'false', daftar_tabel: false, silent: true }});
|
|
270
|
+
initProject({{ targetDir: '{tmpdir}/test_t', daftar_gambar: 'true', daftar_tabel: true, silent: true }});
|
|
271
|
+
"""
|
|
272
|
+
proc = subprocess.run(["node", "-e", node_code], capture_output=True, text=True)
|
|
273
|
+
self.assertEqual(proc.returncode, 0, f"Node initProject failed: {proc.stderr}")
|
|
274
|
+
with open(os.path.join(tmpdir, "test_f", "metadata.yml"), "r", encoding="utf-8") as f:
|
|
275
|
+
meta_f = f.read()
|
|
276
|
+
with open(os.path.join(tmpdir, "test_t", "metadata.yml"), "r", encoding="utf-8") as f:
|
|
277
|
+
meta_t = f.read()
|
|
278
|
+
self.assertIn("daftar_gambar: false", meta_f)
|
|
279
|
+
self.assertIn("daftar_tabel: false", meta_f)
|
|
280
|
+
self.assertIn("daftar_gambar: true", meta_t)
|
|
281
|
+
self.assertIn("daftar_tabel: true", meta_t)
|
|
282
|
+
|
|
283
|
+
|
|
187
284
|
if __name__ == "__main__":
|
|
188
285
|
unittest.main(verbosity=2)
|
|
189
286
|
|
package/README.md
CHANGED
|
@@ -113,7 +113,7 @@ npx laporan-generator sync-hosts
|
|
|
113
113
|
|
|
114
114
|
## Dukungan Model Context Protocol (MCP)
|
|
115
115
|
|
|
116
|
-
Laporan Generator v2.
|
|
116
|
+
Laporan Generator v2.8.0 dilengkapi dengan **Native MCP Server** resmi yang berjalan melalui `stdio` berbasis protokol JSON-RPC 2.0 (spesifikasi 2024-11-05). Server ini memungkinkan AI Agent otonom (seperti Google Antigravity, Claude Desktop, Claude Code, dan OpenCode) untuk berinteraksi langsung dengan dokumen akademik melalui tools deterministik.
|
|
117
117
|
|
|
118
118
|
### Konfigurasi Otomatis (Direkomendasikan)
|
|
119
119
|
Cukup jalankan satu perintah berikut untuk mendaftarkan skill sekaligus konfigurasi MCP Server ke seluruh agent di mesin Anda:
|
|
@@ -293,6 +293,11 @@ title: "Analisis dan Implementasi Sistem Deteksi Intrusi Jaringan Berbasis eBPF"
|
|
|
293
293
|
document_type: "Skripsi"
|
|
294
294
|
margin_preset: "itb-ta"
|
|
295
295
|
|
|
296
|
+
# Kontrol Struktur Halaman Depan (Opt-In)
|
|
297
|
+
# Default struktur bersih: Cover -> Kata Pengantar -> Daftar Isi -> Batang Tubuh -> Daftar Pustaka
|
|
298
|
+
daftar_gambar: false # Ubah true jika dokumen memuat gambar dan membutuhkan DAFTAR GAMBAR
|
|
299
|
+
daftar_tabel: false # Ubah true jika dokumen memuat tabel dan membutuhkan DAFTAR TABEL
|
|
300
|
+
|
|
296
301
|
author:
|
|
297
302
|
- name: "Mu'adz Hudzaifah"
|
|
298
303
|
nim: "13522001"
|
package/bin/laporan-generator.js
CHANGED
|
@@ -25,7 +25,7 @@ function showHelp() {
|
|
|
25
25
|
========================================================
|
|
26
26
|
LAPORAN GENERATOR CLI (Multi-Host AI Engine)
|
|
27
27
|
========================================================
|
|
28
|
-
Versi: 2.
|
|
28
|
+
Versi: 2.8.0
|
|
29
29
|
|
|
30
30
|
Penggunaan:
|
|
31
31
|
npx laporan-generator <perintah> [opsi]
|
package/cover.md
CHANGED
|
@@ -22,21 +22,25 @@ Penulis menyadari bahwa laporan ini masih jauh dari sempurna. Oleh karena itu, k
|
|
|
22
22
|
depth: 3,
|
|
23
23
|
)
|
|
24
24
|
#context {
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
25
|
+
if opt-daftar-gambar {
|
|
26
|
+
let imgs = query(figure.where(kind: image))
|
|
27
|
+
if imgs.len() > 0 {
|
|
28
|
+
pagebreak()
|
|
29
|
+
outline(
|
|
30
|
+
title: align(center)[#text(size: 14pt, weight: "bold")[DAFTAR GAMBAR]],
|
|
31
|
+
target: figure.where(kind: image),
|
|
32
|
+
)
|
|
33
|
+
}
|
|
32
34
|
}
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
35
|
+
if opt-daftar-tabel {
|
|
36
|
+
let tbls = query(figure.where(kind: table))
|
|
37
|
+
if tbls.len() > 0 {
|
|
38
|
+
pagebreak()
|
|
39
|
+
outline(
|
|
40
|
+
title: align(center)[#text(size: 14pt, weight: "bold")[DAFTAR TABEL]],
|
|
41
|
+
target: figure.where(kind: table),
|
|
42
|
+
)
|
|
43
|
+
}
|
|
40
44
|
}
|
|
41
45
|
}
|
|
42
46
|
#pagebreak()
|
package/docs/metadata-schema.md
CHANGED
|
@@ -49,6 +49,8 @@ abstract: |
|
|
|
49
49
|
| `faculty` | String | **Ya** | Nama Fakultas, Departemen, atau Program Studi. |
|
|
50
50
|
| `year` | String | **Ya** | Tahun Ajaran (contoh: `2025/2026`). |
|
|
51
51
|
| `date` | String | Opsional | Bulan dan Tahun pembuatan (contoh: `Juli 2026`). |
|
|
52
|
+
| `daftar_gambar` | Boolean | Opsional | Aktifkan halaman DAFTAR GAMBAR (`true`/`false`, default: `false`). |
|
|
53
|
+
| `daftar_tabel` | Boolean | Opsional | Aktifkan halaman DAFTAR TABEL (`true`/`false`, default: `false`). |
|
|
52
54
|
| `abstract` | Multiline String | Opsional | Ringkasan/Abstrak laporan dalam Bahasa Indonesia. |
|
|
53
55
|
|
|
54
56
|
---
|
package/docx.lua
CHANGED
|
@@ -138,9 +138,24 @@ local function meta_str(meta, key)
|
|
|
138
138
|
if v == nil then
|
|
139
139
|
return ""
|
|
140
140
|
end
|
|
141
|
+
if type(v) == "boolean" then
|
|
142
|
+
return tostring(v)
|
|
143
|
+
end
|
|
141
144
|
return pandoc.utils.stringify(v)
|
|
142
145
|
end
|
|
143
146
|
|
|
147
|
+
local function meta_bool(meta, key)
|
|
148
|
+
local v = meta[key]
|
|
149
|
+
if v == nil then
|
|
150
|
+
return false
|
|
151
|
+
end
|
|
152
|
+
if type(v) == "boolean" then
|
|
153
|
+
return v
|
|
154
|
+
end
|
|
155
|
+
local s = meta_str(meta, key):lower():gsub("%s+", "")
|
|
156
|
+
return s == "true" or s == "1" or s == "yes"
|
|
157
|
+
end
|
|
158
|
+
|
|
144
159
|
local function format_chapter_number(n)
|
|
145
160
|
if chapter_num_fmt == "arabic" then
|
|
146
161
|
return tostring(n)
|
|
@@ -280,6 +295,44 @@ local function daftar_isi()
|
|
|
280
295
|
return out
|
|
281
296
|
end
|
|
282
297
|
|
|
298
|
+
local function daftar_gambar()
|
|
299
|
+
local out = pandoc.List{}
|
|
300
|
+
out[#out + 1] = pagebreak()
|
|
301
|
+
out[#out + 1] = pandoc.RawBlock(
|
|
302
|
+
"openxml",
|
|
303
|
+
'<w:p><w:pPr><w:pStyle w:val="TOCHeading"/><w:jc w:val="center"/><w:outlineLvl w:val="9"/></w:pPr>' ..
|
|
304
|
+
'<w:r><w:rPr><w:b/><w:sz w:val="28"/><w:szCs w:val="28"/></w:rPr><w:t>DAFTAR GAMBAR</w:t></w:r></w:p>'
|
|
305
|
+
)
|
|
306
|
+
out[#out + 1] = pandoc.RawBlock(
|
|
307
|
+
"openxml",
|
|
308
|
+
'<w:p><w:pPr><w:pStyle w:val="TOC1"/></w:pPr>' ..
|
|
309
|
+
'<w:r><w:fldChar w:fldCharType="begin" w:dirty="true"/></w:r>' ..
|
|
310
|
+
'<w:r><w:instrText xml:space="preserve"> TOC \\c "Gambar" \\h \\z </w:instrText></w:r>' ..
|
|
311
|
+
'<w:r><w:fldChar w:fldCharType="separate"/></w:r>' ..
|
|
312
|
+
'<w:r><w:fldChar w:fldCharType="end"/></w:r></w:p>'
|
|
313
|
+
)
|
|
314
|
+
return out
|
|
315
|
+
end
|
|
316
|
+
|
|
317
|
+
local function daftar_tabel()
|
|
318
|
+
local out = pandoc.List{}
|
|
319
|
+
out[#out + 1] = pagebreak()
|
|
320
|
+
out[#out + 1] = pandoc.RawBlock(
|
|
321
|
+
"openxml",
|
|
322
|
+
'<w:p><w:pPr><w:pStyle w:val="TOCHeading"/><w:jc w:val="center"/><w:outlineLvl w:val="9"/></w:pPr>' ..
|
|
323
|
+
'<w:r><w:rPr><w:b/><w:sz w:val="28"/><w:szCs w:val="28"/></w:rPr><w:t>DAFTAR TABEL</w:t></w:r></w:p>'
|
|
324
|
+
)
|
|
325
|
+
out[#out + 1] = pandoc.RawBlock(
|
|
326
|
+
"openxml",
|
|
327
|
+
'<w:p><w:pPr><w:pStyle w:val="TOC1"/></w:pPr>' ..
|
|
328
|
+
'<w:r><w:fldChar w:fldCharType="begin" w:dirty="true"/></w:r>' ..
|
|
329
|
+
'<w:r><w:instrText xml:space="preserve"> TOC \\c "Tabel" \\h \\z </w:instrText></w:r>' ..
|
|
330
|
+
'<w:r><w:fldChar w:fldCharType="separate"/></w:r>' ..
|
|
331
|
+
'<w:r><w:fldChar w:fldCharType="end"/></w:r></w:p>'
|
|
332
|
+
)
|
|
333
|
+
return out
|
|
334
|
+
end
|
|
335
|
+
|
|
283
336
|
function Pandoc(doc)
|
|
284
337
|
if FORMAT ~= "docx" then
|
|
285
338
|
return doc
|
|
@@ -292,6 +345,8 @@ function Pandoc(doc)
|
|
|
292
345
|
local faculty = meta_str(meta, "faculty")
|
|
293
346
|
local institution = meta_str(meta, "institution")
|
|
294
347
|
local year = meta_str(meta, "year")
|
|
348
|
+
local show_dg = (meta_bool(meta, "daftar_gambar") or meta_bool(meta, "list_of_figures"))
|
|
349
|
+
local show_dt = (meta_bool(meta, "daftar_tabel") or meta_bool(meta, "list_of_tables"))
|
|
295
350
|
|
|
296
351
|
local blocks = pandoc.List{}
|
|
297
352
|
|
|
@@ -401,6 +456,16 @@ function Pandoc(doc)
|
|
|
401
456
|
for _, b in ipairs(daftar_isi()) do
|
|
402
457
|
body[#body + 1] = b
|
|
403
458
|
end
|
|
459
|
+
if show_dg then
|
|
460
|
+
for _, b in ipairs(daftar_gambar()) do
|
|
461
|
+
body[#body + 1] = b
|
|
462
|
+
end
|
|
463
|
+
end
|
|
464
|
+
if show_dt then
|
|
465
|
+
for _, b in ipairs(daftar_tabel()) do
|
|
466
|
+
body[#body + 1] = b
|
|
467
|
+
end
|
|
468
|
+
end
|
|
404
469
|
inserted = true
|
|
405
470
|
end
|
|
406
471
|
body[#body + 1] = blk
|
package/laporan
CHANGED
|
@@ -27,7 +27,7 @@ export PYTHONUTF8=1
|
|
|
27
27
|
show_banner() {
|
|
28
28
|
echo -e "${CYAN}${BOLD}"
|
|
29
29
|
echo " ========================================================"
|
|
30
|
-
echo " LAPORAN GENERATOR CLI v2.
|
|
30
|
+
echo " LAPORAN GENERATOR CLI v2.8.0 "
|
|
31
31
|
echo " Otomatisasi Dokumen Akademik (Typst + DOCX Engine) "
|
|
32
32
|
echo " ========================================================"
|
|
33
33
|
echo -e "${NC}"
|
package/laporan.ps1
CHANGED
|
@@ -40,7 +40,7 @@ function Get-PythonCommand {
|
|
|
40
40
|
|
|
41
41
|
function Show-Banner {
|
|
42
42
|
Write-Host " ========================================================" -ForegroundColor Cyan
|
|
43
|
-
Write-Host " LAPORAN GENERATOR CLI v2.
|
|
43
|
+
Write-Host " LAPORAN GENERATOR CLI v2.8.0 " -ForegroundColor Cyan
|
|
44
44
|
Write-Host " Otomatisasi Dokumen Akademik (Typst + DOCX Engine) " -ForegroundColor Cyan
|
|
45
45
|
Write-Host " ========================================================" -ForegroundColor Cyan
|
|
46
46
|
Write-Host ""
|
package/lib/init.js
CHANGED
|
@@ -23,6 +23,7 @@ function copyRecursiveSync(src, dest) {
|
|
|
23
23
|
function initProject(options = {}) {
|
|
24
24
|
const targetDir = options.targetDir ? path.resolve(options.targetDir) : process.cwd();
|
|
25
25
|
const silent = !!options.silent;
|
|
26
|
+
const isTruthy = (val) => val === true || val === 'true';
|
|
26
27
|
|
|
27
28
|
if (!fs.existsSync(targetDir)) {
|
|
28
29
|
fs.mkdirSync(targetDir, { recursive: true });
|
|
@@ -103,6 +104,8 @@ faculty: "${options.faculty || 'Fakultas Teknik'}"
|
|
|
103
104
|
department: "${options.department || 'Program Studi Informatika'}"
|
|
104
105
|
year: "${options.year || '2026'}"
|
|
105
106
|
preset: "${options.preset || 'skripsi-4433'}"
|
|
107
|
+
daftar_gambar: ${isTruthy(options.daftar_gambar) ? 'true' : 'false'}
|
|
108
|
+
daftar_tabel: ${isTruthy(options.daftar_tabel) ? 'true' : 'false'}
|
|
106
109
|
...
|
|
107
110
|
`;
|
|
108
111
|
fs.writeFileSync(metaPath, defaultMeta, 'utf8');
|
|
@@ -124,7 +127,7 @@ preset: "${options.preset || 'skripsi-4433'}"
|
|
|
124
127
|
}
|
|
125
128
|
|
|
126
129
|
// Update metadata.yml if custom options were provided
|
|
127
|
-
if (fs.existsSync(metaPath) && (options.title || options.preset || options.authorName || options.institution)) {
|
|
130
|
+
if (fs.existsSync(metaPath) && (options.title || options.preset || options.authorName || options.institution || options.daftar_gambar !== undefined || options.daftar_tabel !== undefined)) {
|
|
128
131
|
try {
|
|
129
132
|
let metaContent = fs.readFileSync(metaPath, 'utf8');
|
|
130
133
|
if (options.title) {
|
|
@@ -151,8 +154,26 @@ preset: "${options.preset || 'skripsi-4433'}"
|
|
|
151
154
|
metaContent = metaContent.replace(/nim:\s*".*?"/, `nim: "${options.authorId}"`);
|
|
152
155
|
}
|
|
153
156
|
}
|
|
157
|
+
if (options.daftar_gambar !== undefined) {
|
|
158
|
+
const val = isTruthy(options.daftar_gambar) ? 'true' : 'false';
|
|
159
|
+
if (/^daftar_gambar:\s*(true|false)/m.test(metaContent)) {
|
|
160
|
+
metaContent = metaContent.replace(/^daftar_gambar:\s*(true|false)/m, `daftar_gambar: ${val}`);
|
|
161
|
+
} else {
|
|
162
|
+
metaContent = metaContent.replace(/preset:\s*".*?"/m, `daftar_gambar: ${val}\n$&`);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
if (options.daftar_tabel !== undefined) {
|
|
166
|
+
const val = isTruthy(options.daftar_tabel) ? 'true' : 'false';
|
|
167
|
+
if (/^daftar_tabel:\s*(true|false)/m.test(metaContent)) {
|
|
168
|
+
metaContent = metaContent.replace(/^daftar_tabel:\s*(true|false)/m, `daftar_tabel: ${val}`);
|
|
169
|
+
} else {
|
|
170
|
+
metaContent = metaContent.replace(/preset:\s*".*?"/m, `daftar_tabel: ${val}\n$&`);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
154
173
|
fs.writeFileSync(metaPath, metaContent, 'utf8');
|
|
155
|
-
} catch (
|
|
174
|
+
} catch (err) {
|
|
175
|
+
console.warn(`[init] Failed to customize metadata.yml: ${err.message}`);
|
|
176
|
+
}
|
|
156
177
|
}
|
|
157
178
|
|
|
158
179
|
// Ensure scripts are executable on Unix
|
|
@@ -163,7 +184,9 @@ preset: "${options.preset || 'skripsi-4433'}"
|
|
|
163
184
|
if (fs.existsSync(laporanBin)) fs.chmodSync(laporanBin, '755');
|
|
164
185
|
if (fs.existsSync(buildSh)) fs.chmodSync(buildSh, '755');
|
|
165
186
|
}
|
|
166
|
-
} catch (
|
|
187
|
+
} catch (err) {
|
|
188
|
+
console.warn(`[init] Failed to set executable permissions: ${err.message}`);
|
|
189
|
+
}
|
|
167
190
|
|
|
168
191
|
return {
|
|
169
192
|
success: true,
|
package/lib/mcp.js
CHANGED
|
@@ -7,7 +7,7 @@ const https = require("https");
|
|
|
7
7
|
const { spawnSync } = require("child_process");
|
|
8
8
|
const { initProject } = require("./init");
|
|
9
9
|
|
|
10
|
-
const VERSION = "2.
|
|
10
|
+
const VERSION = "2.8.0";
|
|
11
11
|
const PACKAGE_ROOT = path.join(__dirname, "..");
|
|
12
12
|
|
|
13
13
|
function getPythonCommand() {
|
|
@@ -92,6 +92,14 @@ const TOOLS = [
|
|
|
92
92
|
type: "string",
|
|
93
93
|
description: "Academic year (e.g. 2026)."
|
|
94
94
|
},
|
|
95
|
+
daftar_gambar: {
|
|
96
|
+
type: "boolean",
|
|
97
|
+
description: "Whether to include Daftar Gambar (List of Figures). Default is false."
|
|
98
|
+
},
|
|
99
|
+
daftar_tabel: {
|
|
100
|
+
type: "boolean",
|
|
101
|
+
description: "Whether to include Daftar Tabel (List of Tables). Default is false."
|
|
102
|
+
},
|
|
95
103
|
force: {
|
|
96
104
|
type: "boolean",
|
|
97
105
|
description: "Whether to overwrite existing files if already present."
|
|
@@ -211,6 +219,8 @@ async function handleToolCall(name, args = {}) {
|
|
|
211
219
|
faculty: args.faculty,
|
|
212
220
|
department: args.department,
|
|
213
221
|
year: args.year,
|
|
222
|
+
daftar_gambar: args.daftar_gambar,
|
|
223
|
+
daftar_tabel: args.daftar_tabel,
|
|
214
224
|
force: args.force,
|
|
215
225
|
silent: true
|
|
216
226
|
});
|
package/metadata.yml
CHANGED
|
@@ -15,21 +15,18 @@ institution: "Universitas Example"
|
|
|
15
15
|
faculty: "Program Studi Informatika"
|
|
16
16
|
year: "2026/2027"
|
|
17
17
|
date: "Agustus 2026"
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
abstract_en: |
|
|
27
|
-
|
|
28
|
-
keywords_en:
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
- Typst
|
|
32
|
-
- OpenXML
|
|
33
|
-
- Software Engineering
|
|
18
|
+
daftar_gambar: false
|
|
19
|
+
daftar_tabel: false
|
|
20
|
+
# Komponen halaman depan opsional (aktifkan jika diperlukan):
|
|
21
|
+
# abstract_id: |
|
|
22
|
+
# Ringkasan atau abstrak laporan dalam Bahasa Indonesia...
|
|
23
|
+
# keywords_id:
|
|
24
|
+
# - Kata Kunci 1
|
|
25
|
+
# - Kata Kunci 2
|
|
26
|
+
# abstract_en: |
|
|
27
|
+
# Summary or abstract of the report in English...
|
|
28
|
+
# keywords_en:
|
|
29
|
+
# - Keyword 1
|
|
30
|
+
# - Keyword 2
|
|
34
31
|
preset: "standard"
|
|
35
32
|
...
|
package/package.json
CHANGED
package/scripts/test_scripts.py
CHANGED
|
@@ -184,6 +184,103 @@ class TestReportDoctor(unittest.TestCase):
|
|
|
184
184
|
self.assertIn("citations", data)
|
|
185
185
|
|
|
186
186
|
|
|
187
|
+
class TestFrontMatterOptIn(unittest.TestCase):
|
|
188
|
+
def test_default_metadata_structure(self):
|
|
189
|
+
meta_path = os.path.join(get_project_root(), "metadata.yml")
|
|
190
|
+
with open(meta_path, "r", encoding="utf-8") as f:
|
|
191
|
+
content = f.read()
|
|
192
|
+
# Verify abstract is commented or empty by default
|
|
193
|
+
self.assertNotIn("\nabstract_id: |", content, "abstract_id should not be active by default in metadata.yml")
|
|
194
|
+
self.assertNotIn("\nabstract_en: |", content, "abstract_en should not be active by default in metadata.yml")
|
|
195
|
+
# Verify opt-in flags for daftar_gambar & daftar_tabel are explicitly false or commented
|
|
196
|
+
has_dg = "daftar_gambar: false" in content
|
|
197
|
+
has_dt = "daftar_tabel: false" in content
|
|
198
|
+
self.assertTrue(has_dg, "metadata.yml must explicitly specify daftar_gambar: false by default")
|
|
199
|
+
self.assertTrue(has_dt, "metadata.yml must explicitly specify daftar_tabel: false by default")
|
|
200
|
+
|
|
201
|
+
def test_cover_md_gated_outlines(self):
|
|
202
|
+
cover_path = os.path.join(get_project_root(), "cover.md")
|
|
203
|
+
with open(cover_path, "r", encoding="utf-8") as f:
|
|
204
|
+
content = f.read()
|
|
205
|
+
# Unconditional outline queries must not exist
|
|
206
|
+
self.assertNotIn("let imgs = query(figure.where(kind: image))\n if imgs.len() > 0", content,
|
|
207
|
+
"cover.md must not unconditionally emit DAFTAR GAMBAR outline")
|
|
208
|
+
self.assertNotIn("let tbls = query(figure.where(kind: table))\n if tbls.len() > 0", content,
|
|
209
|
+
"cover.md must not unconditionally emit DAFTAR TABEL outline")
|
|
210
|
+
# Must be gated by opt-in check
|
|
211
|
+
self.assertIn("opt-daftar-gambar", content, "cover.md must gate DAFTAR GAMBAR with opt-daftar-gambar")
|
|
212
|
+
self.assertIn("opt-daftar-tabel", content, "cover.md must gate DAFTAR TABEL with opt-daftar-tabel")
|
|
213
|
+
|
|
214
|
+
def test_lua_meta_bool_coercion(self):
|
|
215
|
+
"""Verify docx.lua handles YAML boolean scalars and string equivalents safely."""
|
|
216
|
+
lua_code = """
|
|
217
|
+
local pandoc = {
|
|
218
|
+
utils = {
|
|
219
|
+
stringify = function(x)
|
|
220
|
+
if type(x) ~= 'string' and type(x) ~= 'table' then
|
|
221
|
+
error('pandoc.utils.stringify expects table or string, got ' .. type(x))
|
|
222
|
+
end
|
|
223
|
+
return tostring(x)
|
|
224
|
+
end
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
local function meta_str(meta, key)
|
|
228
|
+
local v = meta[key]
|
|
229
|
+
if v == nil then return "" end
|
|
230
|
+
if type(v) == "boolean" then return tostring(v) end
|
|
231
|
+
return pandoc.utils.stringify(v)
|
|
232
|
+
end
|
|
233
|
+
local function meta_bool(meta, key)
|
|
234
|
+
local v = meta[key]
|
|
235
|
+
if v == nil then return false end
|
|
236
|
+
if type(v) == "boolean" then return v end
|
|
237
|
+
local s = meta_str(meta, key):lower():gsub("%s+", "")
|
|
238
|
+
return s == "true" or s == "1" or s == "yes"
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
local meta = {
|
|
242
|
+
dg_bool_false = false,
|
|
243
|
+
dg_bool_true = true,
|
|
244
|
+
dg_str_false = "false",
|
|
245
|
+
dg_str_true = "true",
|
|
246
|
+
dg_nil = nil
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
assert(meta_str(meta, "dg_bool_false") == "false")
|
|
250
|
+
assert(meta_bool(meta, "dg_bool_false") == false)
|
|
251
|
+
assert(meta_str(meta, "dg_bool_true") == "true")
|
|
252
|
+
assert(meta_bool(meta, "dg_bool_true") == true)
|
|
253
|
+
assert(meta_bool(meta, "dg_str_false") == false)
|
|
254
|
+
assert(meta_bool(meta, "dg_str_true") == true)
|
|
255
|
+
assert(meta_bool(meta, "dg_nil") == false)
|
|
256
|
+
print("LUA_OK")
|
|
257
|
+
"""
|
|
258
|
+
proc = subprocess.run(["lua", "-e", lua_code], capture_output=True, text=True)
|
|
259
|
+
self.assertEqual(proc.returncode, 0, f"Lua helper test failed: {proc.stderr}")
|
|
260
|
+
self.assertIn("LUA_OK", proc.stdout)
|
|
261
|
+
|
|
262
|
+
def test_init_project_boolean_coercion(self):
|
|
263
|
+
"""Verify lib/init.js does not treat string 'false' as truthy."""
|
|
264
|
+
import tempfile
|
|
265
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
266
|
+
node_code = f"""
|
|
267
|
+
const path = require('path');
|
|
268
|
+
const {{ initProject }} = require('{os.path.join(ROOT_DIR, "lib/init.js")}');
|
|
269
|
+
initProject({{ targetDir: '{tmpdir}/test_f', daftar_gambar: 'false', daftar_tabel: false, silent: true }});
|
|
270
|
+
initProject({{ targetDir: '{tmpdir}/test_t', daftar_gambar: 'true', daftar_tabel: true, silent: true }});
|
|
271
|
+
"""
|
|
272
|
+
proc = subprocess.run(["node", "-e", node_code], capture_output=True, text=True)
|
|
273
|
+
self.assertEqual(proc.returncode, 0, f"Node initProject failed: {proc.stderr}")
|
|
274
|
+
with open(os.path.join(tmpdir, "test_f", "metadata.yml"), "r", encoding="utf-8") as f:
|
|
275
|
+
meta_f = f.read()
|
|
276
|
+
with open(os.path.join(tmpdir, "test_t", "metadata.yml"), "r", encoding="utf-8") as f:
|
|
277
|
+
meta_t = f.read()
|
|
278
|
+
self.assertIn("daftar_gambar: false", meta_f)
|
|
279
|
+
self.assertIn("daftar_tabel: false", meta_f)
|
|
280
|
+
self.assertIn("daftar_gambar: true", meta_t)
|
|
281
|
+
self.assertIn("daftar_tabel: true", meta_t)
|
|
282
|
+
|
|
283
|
+
|
|
187
284
|
if __name__ == "__main__":
|
|
188
285
|
unittest.main(verbosity=2)
|
|
189
286
|
|
package/template.typ
CHANGED
|
@@ -391,4 +391,7 @@ $endif$
|
|
|
391
391
|
#pagebreak()
|
|
392
392
|
$endif$
|
|
393
393
|
|
|
394
|
+
#let opt-daftar-gambar = $if(daftar_gambar)$$daftar_gambar$$else$$if(list_of_figures)$$list_of_figures$$else$false$endif$$endif$
|
|
395
|
+
#let opt-daftar-tabel = $if(daftar_tabel)$$daftar_tabel$$else$$if(list_of_tables)$$list_of_tables$$else$false$endif$$endif$
|
|
396
|
+
|
|
394
397
|
$body$
|