diff --git a/packages/preview/jilid/0.2.0/LICENSE b/packages/preview/jilid/0.2.0/LICENSE new file mode 100644 index 0000000000..617facd13a --- /dev/null +++ b/packages/preview/jilid/0.2.0/LICENSE @@ -0,0 +1,44 @@ +MIT License + +Copyright (c) 2026 Tri Rianto Utomo + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +--- + +The files in the `template/` directory are licensed under MIT No Attribution: + +MIT No Attribution + +Copyright (c) 2026 Tri Rianto Utomo + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/packages/preview/jilid/0.2.0/README.md b/packages/preview/jilid/0.2.0/README.md new file mode 100644 index 0000000000..366be3ea4f --- /dev/null +++ b/packages/preview/jilid/0.2.0/README.md @@ -0,0 +1,590 @@ +# jilid + +A Typst template for Indonesian university reports. + +See the [example.pdf](docs/example.pdf) file to see how it looks. + +## Usage + +You can use this template in the Typst web app by clicking "Start from +template" on the dashboard and searching for `jilid`. + +Alternatively, you can use the CLI to kick this project off using the +command + +```sh +typst init @preview/jilid +``` + +Typst will create a new directory with all the files needed to get you +started. + +The template initializes your project with a sample call to the `jilid` +function in a show rule. If you want to change an existing project to use this template, add a show rule like this at the top of your file: + +```typ +#import "@preview/jilid:0.2.0": jilid, frontmatter, appendix + +#show: jilid.with( + title: [Judul Laporan], + kind: [Laporan Kerja Praktik], + course: "Nama Mata Kuliah", + lecturers: (name: "Nama Dosen, S.Kom., M.Kom.", id: "10000000000000000"), + students: ( + (name: "Nama Mahasiswa", id: "1000000001"), + ), + program: "Teknik Informatika", + faculty: "Teknik", + university: "Universitas Negeri", + year: "2026", + bibliography: bibliography("refs.bib", style: "apa"), + // logo: image("logo.png"), + // cover-details: (([Mitra], [Nama Mitra]),), + // typography: (font-family: "Times New Roman"), + // margin: "print", + // numbering: (position: "top"), +) + +#frontmatter(title: [Kata Pengantar])[ ... ] + += Pendahuluan +... + +#appendix(title: [Data Pengujian])[ ... ] +``` + +## Fonts + +The body font is Typst's bundled Libertinus Serif. Most campus guidelines ask for Times New Roman. To use it, install it or upload the font files to your web app project, then set: + +```typ +#show: jilid.with(typography: (font-family: "Times New Roman")) +``` + +## Packages used + +- [zebraw](https://typst.app/universe/package/zebraw): code blocks with + line numbers (turn off with `code: (zebraw: false)`). Re-exported, so + `#import "@preview/jilid:0.2.0": zebraw` gives you its full API. + +## Document structure + +jilid displays your document in this order: + +1. Cover, when `title` is set +2. Front matter, from `#frontmatter(title: [..])[..]` +3. Daftar Isi, then Daftar Tabel, Gambar, Kode and Lampiran when the + document has any +4. Chapters, from `= Heading` +5. Bibliography, from the `bibliography` option +6. Appendices, from `#appendix(title: [..])[..]` + +## Configuration + +This template exports the `jilid` function with the named arguments +below. It also takes a single positional argument: the body of your +document, which `#show: jilid.with(..)` passes for you. + +Option groups such as `cover` or `footer` take a dictionary, and only the +keys you give change, e.g. `footer: (left: [Laporan Akhir])`. + +### Document information + +- `title`\ + type: [content] or [str]\ + default: `""`\ + description: Document title, on the cover and in the PDF metadata. + +- `kind`\ + type: [content] or `none`\ + default: `none`\ + description: Document kind on the cover, e.g. `[Laporan Praktikum]`. + +- `subtitle`\ + type: [content] or [str]\ + default: `""`\ + description: Second title line on the cover. + +- `cover-details`\ + type: [array]\ + default: `()`\ + description: Extra `(label, value)` rows under the title, e.g. `(([Mitra Kolaborator:], [Nama Mitra]),)`. + +- `course`\ + type: [content] or [str]\ + default: `""`\ + description: Course name, after "Mata Kuliah :". + +- `lecturers`\ + type: [dictionary] or [array]\ + default: `()`\ + description: One `(name: .., id: ..)` dictionary or an array of them. `id` is optional. + +- `students`\ + type: [dictionary] or [array]\ + default: `()`\ + description: One `(name: .., id: ..)` dictionary or an array of them. `id` is optional, and `name` may be content. + +- `program`, `department`, `faculty`\ + type: [content] or [str]\ + default: `""`\ + description: Printed with their label, e.g. `faculty: "Teknik"` gives "FAKULTAS TEKNIK". A value that already starts with the label, such as `"Fakultas Teknik"`, is not prefixed twice. + +- `university`, `year`\ + type: [content] or [str]\ + default: `""`\ + description: Bottom of the cover. + +- `logo`\ + type: [content] or `none`\ + default: `none`\ + description: The logo on the cover, e.g. `image("logo.png")`. + +- `bibliography`\ + type: [content] or `none`\ + default: `none`\ + description: The result of a call to the [bibliography function][bibliography-fn], e.g. `bibliography("refs.bib", style: "apa")`. + +- `lang`\ + type: [str]\ + default: `"id"`\ + description: `"id"` or `"en"`. Other languages go through `labels`. + +- `paper`\ + type: [str]\ + default: `"a4"`\ + description: A [paper size string]. + +- `margin`\ + type: [str], [length] or [dictionary]\ + default: `"digital"`\ + description: `"print"` has 4 cm on the left and 3 cm elsewhere, for bound copies. `"digital"` has 1 inch all round. Any [page margin] value also works. + +- `include-cover`\ + type: [bool] or [auto]\ + default: `auto`\ + description: `auto` shows the cover when `title` is set. + +### `cover` + +- `top`\ + type: [length]\ + default: `2cm`\ + description: Space above the title. + +- `logo-width`\ + type: [length]\ + default: `8cm`\ + description: Logo width, unless the image sets its own. + +- `kind-pos`\ + type: [str]\ + default: `"bottom"`\ + description: `"top"` puts `kind` above the title, `"bottom"` below it. + +- `gap`\ + type: [length]\ + default: `0.2cm`\ + description: Space between the course, lecturer and student blocks. + +- `gap-institution`\ + type: [length]\ + default: `1cm`\ + description: Space kept above the university block. + +- `gap-logo`\ + type: [length]\ + default: `0.5cm`\ + description: Space kept above and below the logo. + +- `student-columns`\ + type: [int] or [auto]\ + default: `auto`\ + description: Columns of the student list. `auto` uses the fewest columns up to 3 that fit the page. + +- `student-id-pos`\ + type: [str]\ + default: `"right"`\ + description: `"right"` puts the student id beside the name, `"below"` under it. + +- `title`, `kind`, `subtitle`, `details`, `label`, `course`, `lecturer-name`, `student-name`, `id`, `institution`\ + type: [dictionary]\ + default: see below\ + description: Text style of each cover text. A style takes any [`text`][text] argument, such as `size`, `weight`, `style`, `fill` or `tracking`, plus `upper` and `underline`. + + - `title`: `(size: 18pt, weight: "bold", upper: true)` + - `kind`: `(size: 14pt, weight: "bold", upper: true)` + - `subtitle`: `(size: 18pt, weight: "bold", upper: true)` + - `details`: `(size: 14pt, weight: "bold")`, the `cover-details` rows + - `label`: `(:)`, "Mata Kuliah :", "Dosen Pengampu :", "Disusun oleh :" + - `course`: `(weight: "bold")` + - `lecturer-name`: `(:)` + - `student-name`: `(:)` + - `id`: `(:)`, the "NIP ..." and "NIM ..." lines + - `institution`: `(weight: "bold", upper: true)`, university, faculty, department, program and year + +- `institution-order`\ + type: [array]\ + default: `("university", "faculty", "department", "program", "year")`\ + description: Institution lines from top to bottom. Leave a key out to hide its line. + +- `institution-render`\ + type: [function] or [auto]\ + default: `auto`\ + description: Draws the institution block yourself. The function gets one dictionary with `university`, `faculty`, `department`, `program`, `year` and `lines`, the filled lines in `institution-order`. for example, `institution-render: it => strong(it.lines.join(" · "))`. + +- `render`\ + type: [function] or [auto]\ + default: `auto`\ + description: Draws the whole cover yourself. The function gets one dictionary with `title`, `kind`, `subtitle`, `details`, `course`, `lecturers`, `students`, `logo`, `university`, `faculty`, `department`, `program` (the last three already prefixed, e.g. "FAKULTAS Teknik"), `year` and `labels`. The styles and layout options above no longer apply. + +If the cover runs onto a second page, lower `logo-width`, the `details` +size or the `gap-*` options, or set `student-columns`. + +for example: + +```typ +#show: jilid.with( + kind: [Laporan], + cover: ( + kind: (style: "italic"), + student-name: (style: "normal", upper: true), + ), + labels: (students: [Oleh]), +) +``` + +### `typography` + +- `font-family`\ + type: [str], [array] or [auto]\ + default: `auto`\ + description: Body font, or a list of fonts to try in order. `auto` uses Typst's bundled Libertinus Serif. + +- `font-size`\ + type: [length]\ + default: `12pt`\ + description: Body text size. + +- `caption-size`\ + type: [length]\ + default: `10pt`\ + description: Figure and table caption size. + +- `caption-gap`\ + type: [length]\ + default: `1em`\ + description: Space between a figure and its caption. + +- `table-size`\ + type: [length]\ + default: `10pt`\ + description: Text size inside tables. + +- `url`\ + type: [dictionary]\ + default: `(font: auto, size: 0.85em, fill: blue.darken(20%), underline: true)`\ + description: Text style of web links. `font: auto` uses the code font. for example, `url: (font: "Libertinus Serif", size: 1em)` writes links in the body font. + +- `caption`\ + type: [function] or [auto]\ + default: `auto`\ + description: Draws each caption yourself. The function gets one dictionary with `supplement` ("Gambar"), `number` ("3.1", "L1.1" in appendices), `body` and `kind`. `caption-size` still applies. for example, `caption: it => strong[#it.supplement #it.number. #it.body]`. + +### `paragraph` + +- `justify`\ + type: [bool]\ + default: `true`\ + description: Justify paragraphs. + +- `indent`\ + type: [length]\ + default: `0.63cm`\ + description: First-line indent. + +- `leading`\ + type: [length]\ + default: `0.575em`\ + description: Space between lines. + +- `spacing`\ + type: [length]\ + default: `1.15em`\ + description: Space between paragraphs. + +- `list-indent`\ + type: [length]\ + default: `0cm`\ + description: Space before numbered and bullet markers. + +- `marker-width`\ + type: [length]\ + default: `0.75cm`\ + description: Width of the marker column. List text starts after it. + +### `numbering` + +- `front`\ + type: [str]\ + default: `"i"`\ + description: Page number style before the first chapter. + +- `back`\ + type: [str]\ + default: `"body"`\ + description: Appendix page numbers. `"body"` continues the chapter page numbers, `"front"` continues the front matter ones. + +- `position`\ + type: [str]\ + default: `"bottom"`\ + description: `"top"` puts chapter and appendix page numbers at the top right, except on pages that open a chapter. Front matter stays at the bottom. + +- `chapter`\ + type: [str]\ + default: `"I"`\ + description: Chapter number style. `"I"` gives BAB I, `"1"` gives BAB 1. + +- `appendix`\ + type: [str]\ + default: `"1"`\ + description: Appendix number style. `"1"` gives Lampiran 1, `"A"` gives Lampiran A. + +- `heading`\ + type: [str]\ + default: `"1.1."`\ + description: Section number style inside a chapter. + +- `appendix-prefix`\ + type: [bool]\ + default: `true`\ + description: Put "Lampiran 1." before each appendix title. + +### `headings` + +- `h1`\ + type: [dictionary]\ + default: `(size: 12pt, above: 24pt, below: 18pt, pagebreak: true, uppercase: true)`\ + description: Chapter and front matter titles. `pagebreak` starts each chapter on a new page. `uppercase` writes the title in capitals, on the page and in DAFTAR ISI. + +- `h2`\ + type: [dictionary]\ + default: `(size: 12pt, above: 24pt, below: 18pt, indent: 0cm)`\ + description: Level-2 headings. + +- `h3`\ + type: [dictionary]\ + default: `(size: 12pt, above: 14pt, below: 18pt, indent: 0cm)`\ + description: Level-3 headings. + +- `h4`\ + type: [dictionary]\ + default: `(size: 12pt, above: 12pt, below: 18pt, indent: 0cm)`\ + description: Level-4 headings and deeper. + +- `appendix`\ + type: [dictionary]\ + default: `(uppercase: false)`\ + description: Numbered appendix titles, e.g. "Lampiran 1. Hasil Wawancara". + +### `outlines` + +- `toc`\ + type: [bool]\ + default: `true`\ + description: Show DAFTAR ISI. + +- `depth`\ + type: [int]\ + default: `3`\ + description: Heading levels in DAFTAR ISI. + +- `tables`, `figures`, `codes`\ + type: [bool]\ + default: `true`\ + description: Show DAFTAR TABEL, GAMBAR and KODE when the document has any. + +- `appendices`\ + type: [bool]\ + default: `true`\ + description: Show DAFTAR LAMPIRAN when there are appendices. + +- `toc-appendices`\ + type: [bool]\ + default: `false`\ + description: List each appendix in DAFTAR ISI too. `false` lists only the LAMPIRAN title, as most campus guidelines do. + +- `h1`\ + type: [dictionary]\ + default: `(weight: "bold")`\ + description: Text style of chapter rows in DAFTAR ISI, like the cover styles. + +- `leader`\ + type: [str], [content] or `none`\ + default: `"."`\ + description: Text repeated between an entry and its page number. `none` removes it. + +- `align-titles`\ + type: [str] or `none`\ + default: `"each"`\ + description: Where titles start in Daftar Gambar, Tabel, Kode and Lampiran. `"each"` starts every title in a list at the same place, after the widest number such as "Gambar 2.10". `"shared"` uses one place for all of these lists. `none` puts each title right after its number. + +- `toc-indent`\ + type: [str], [auto] or [length]\ + default: `"title"`\ + description: Where titles start in DAFTAR ISI. `"title"` lines up the chapter titles and starts each row below chapter level under the title of the level above, so "1.1" sits under "PENDAHULUAN". A length such as `1cm` also lines up the chapter titles and moves each lower level by that length, and `0cm` puts those rows at the left. `auto` puts each title right after its number and uses the Typst default for the rows below. + +### `footer` + +- `enabled`\ + type: [bool]\ + default: `true`\ + description: Show a footer on every page except the cover. + +- `left`\ + type: [content] or `none`\ + default: `none`\ + description: Footer text, above the page number. + +- `show-page-number`\ + type: [bool]\ + default: `true`\ + description: Show the page number. + +- `page-number-align`\ + type: [alignment]\ + default: `center`\ + description: Page number alignment. + +- `text`\ + type: [dictionary]\ + default: `(size: 9pt, weight: "bold")`\ + description: Text style of `left`, like the cover styles. + +- `render`\ + type: [function] or [auto]\ + default: `auto`\ + description: Draws the footer yourself. The function gets one dictionary with `number` (the page number, or `none` where it is not shown), `left` (the current footer text) and `part` (`"front"`, `"main"` or `"back"`). The other footer options, except `enabled` and `show-page-number`, no longer apply. + +for example, "Halaman 3" on the right: + +```typ +footer: ( + left: [Laporan Praktikum], + render: it => grid( + columns: (1fr, auto), + emph(it.left), if it.number != none [Halaman #it.number], + ), +) +``` + +### `code` + +- `zebraw`\ + type: [bool] or [dictionary]\ + default: `true`\ + description: Code blocks with line numbers via [zebraw](https://typst.app/universe/package/zebraw). `false` gives a plain shaded block. A dictionary passes options to zebraw, e.g. `(lang: false)`. + +- `fill`\ + type: [color]\ + default: `luma(240)`\ + description: Code block background. + +- `font`\ + type: [str] or [auto]\ + default: `auto`\ + description: Code font. `auto` keeps Typst's bundled monospace font. + +- `size`\ + type: [length]\ + default: `10pt`\ + description: Code text size. + +### `labels` + +type: [dictionary]\ +default: `(:)`\ +description: Every word jilid puts on the page. `lang` picks the defaults, +and `labels` replaces single words with your own [content] or [str]. + +for example: + +```typ +labels: ( + students: [Disusun oleh : \ Kelompok 3], // cover, with the group below + student-id: none, // cover: the student number without "NIM" + figure: "Gbr.", // captions and refs: "Gbr. 2.1" + toc: [ISI], // "DAFTAR ISI" +) +``` + +Keys, with their `id` and `en` defaults: + +- `course`, `lecturer`, `students`: Mata Kuliah :, Dosen Pengampu :, Disusun oleh : / Course :, Lecturer :, Prepared by : +- `student-id`, `lecturer-id`: NIM, NIP / NIM, NIP +- `program`, `faculty`, `department`: PROGRAM STUDI, FAKULTAS, JURUSAN / STUDY PROGRAM OF, FACULTY OF, DEPARTMENT OF +- `toc`, `lof`, `lot`, `loc`, `appendix-list`: DAFTAR ISI, GAMBAR, TABEL, KODE, LAMPIRAN / TABLE OF CONTENTS, LIST OF FIGURES, TABLES, CODES, APPENDICES +- `bibliography`, `appendices`, `appendix`, `chapter`: DAFTAR PUSTAKA, LAMPIRAN-LAMPIRAN, Lampiran, BAB / BIBLIOGRAPHY, APPENDICES, Appendix, CHAPTER +- `appendix-short`: L / A, before figure numbers in appendices, e.g. "Gambar L1.2" +- `figure`, `table`, `code`, `equation`, `section`: Gambar, Tabel, Kode, Persamaan, Bagian / Figure, Table, Code, Equation, Section +- `page`: halaman / page + +For a language other than `id` or `en`, set `lang` and provide every key; +the error message lists the missing ones. + +## Functions + +- `frontmatter(title: none, label: none)[..]`\ + description: A front matter page, such as Kata Pengantar or Abstrak. jilid puts it before the table of contents and shows the title like a chapter title, without a number. Use `==` for headings inside. With `label: `, `@abstrak` gives "Abstrak (halaman ii)". + +- `appendix(title: none, label: none)[..]`\ + description: One appendix, such as Lampiran 1. Kuesioner. jilid puts it after the bibliography and numbers it by the order you write them. Use `==` for headings inside. With `label: `, `@kuesioner` gives "Lampiran 1". + +- `signature(role: none, name: none, id: none, id-label: "NIP", space: 2cm, underline-name: false, alignment: center)`\ + description: One signature block, with a role, space to sign, a name and an ID. jilid keeps the block on one page. + +- `signatures(..signature, header: none, columns: 2, gutter: 1cm)`\ + description: Signature blocks in rows, under a header that spans the full width, such as the place, the date and "Mengetahui,". Names in a row are level. If the last row has fewer signatures, jilid puts it in the center. + +- `set-footer-text(content)`\ + description: Changes the footer text from this page on. If you give `none`, jilid shows the `footer.left` text again. + +- `zebraw`\ + description: The zebraw package, for highlighted lines and comments in code blocks. + +## Migrating from 0.1 + +- `appendices[]` becomes one `appendix(title: [..])[..]` per appendix. +- `frontmatter[= Title ..]` becomes `frontmatter(title: [Title])[..]`. +- `frontmatter` no longer takes `numbering`, `start-page` or `outlined`. +- `margin` is `"digital"` by default. For the old margins, set `margin: "print"`. +- Lecturer names are plain by default. For the old look, set `cover: (lecturer-name: (weight: "bold", style: "italic", underline: true))`. +- DAFTAR ISI lines up titles and starts sub-chapter rows under the chapter title. For the old look, set `outlines: (toc-indent: auto)`. + +## Contributing + +Bug reports and requests for campus rules jilid does not support yet are +welcome as [GitHub issues](https://github.com/shuretokki/jilid/issues). See +[CONTRIBUTING.md](https://github.com/shuretokki/jilid/blob/v0.2.0/CONTRIBUTING.md) +for development. + +## License + +MIT, see [LICENSE](LICENSE). The files in `template/`, which become your +own document, are MIT-0: use them without attribution. + +[alignment]: https://typst.app/docs/reference/layout/alignment/ +[array]: https://typst.app/docs/reference/foundations/array/ +[auto]: https://typst.app/docs/reference/foundations/auto/ +[bibliography-fn]: https://typst.app/docs/reference/model/bibliography/ +[bool]: https://typst.app/docs/reference/foundations/bool/ +[color]: https://typst.app/docs/reference/visualize/color/ +[content]: https://typst.app/docs/reference/foundations/content/ +[dictionary]: https://typst.app/docs/reference/foundations/dictionary/ +[function]: https://typst.app/docs/reference/foundations/function/ +[int]: https://typst.app/docs/reference/foundations/int/ +[length]: https://typst.app/docs/reference/layout/length/ +[page margin]: https://typst.app/docs/reference/layout/page/#parameters-margin +[paper size string]: https://typst.app/docs/reference/layout/page/#parameters-paper +[str]: https://typst.app/docs/reference/foundations/str/ +[text]: https://typst.app/docs/reference/text/text/ diff --git a/packages/preview/jilid/0.2.0/docs/example.pdf b/packages/preview/jilid/0.2.0/docs/example.pdf new file mode 100644 index 0000000000..4f55e238d5 Binary files /dev/null and b/packages/preview/jilid/0.2.0/docs/example.pdf differ diff --git a/packages/preview/jilid/0.2.0/lib.typ b/packages/preview/jilid/0.2.0/lib.typ new file mode 100644 index 0000000000..7d383cb735 --- /dev/null +++ b/packages/preview/jilid/0.2.0/lib.typ @@ -0,0 +1,4 @@ +#import "src/document.typ": jilid +#import "src/matter.typ": appendices, appendix, frontmatter +#import "src/components.typ": set-footer-text, signature, signatures +#import "@preview/zebraw:0.6.3": zebraw diff --git a/packages/preview/jilid/0.2.0/src/components.typ b/packages/preview/jilid/0.2.0/src/components.typ new file mode 100644 index 0000000000..f2c30a372a --- /dev/null +++ b/packages/preview/jilid/0.2.0/src/components.typ @@ -0,0 +1,137 @@ +#import "state.typ": footer-text + +#import "utils.typ": filled + +/// A signature block, with a role, space to sign, a name and an ID. +/// +/// jilid keeps the block on one page. +/// The name is bold. If there is no ID, jilid keeps an empty line in its place, so names in a row stay level. +/// To put signatures side by side, give them to `signatures`. +/// +/// = Example +/// +/// ``` +/// #signature( +/// role: [Dosen Pembimbing], +/// name: "Nama Dosen", +/// id: "10000000000000000", +/// ) +/// ``` +/// +/// - role (content, str, none): The role above the space, such as Dosen Pembimbing. +/// - name (content, str, none): The name under the space. +/// - id (content, str, none): The ID under the name. +/// - id-label (content, str, none): The word before the ID, such as NIP or NIM. +/// - space (length): The height of the space to sign in. +/// - underline-name (bool): If `true`, jilid underlines the name. +/// - alignment (alignment): The horizontal alignment of the block. +/// -> content +#let signature( + /// The role above the space, such as Dosen Pembimbing. + role: none, + /// The name under the space. + name: none, + /// The ID under the name. + id: none, + /// The word before the ID, such as NIP or NIM. + id-label: "NIP", + /// The height of the space to sign in. + space: 2cm, + /// If `true`, jilid underlines the name. + underline-name: false, + /// The horizontal alignment of the block. + alignment: center, +) = block(width: 100%, breakable: false, { + set align(alignment) + set par(first-line-indent: 0pt, justify: false) + if filled(role) { role } + v(space) + let lines = () + if filled(name) { + lines.push(text(weight: "bold", if underline-name { underline(name) } else { + name + })) + } + // If there is no ID, keep an empty line so the name stays level with its neighbors. + lines.push( + if not filled(id) { hide[0] } else if filled( + id-label, + ) [#id-label #id] else [#id], + ) + lines.join(linebreak()) +}) + +/// Signature blocks in rows, under a header that spans the full width. +/// +/// Each row has `columns` signatures, and the names in a row are level. +/// If the last row has fewer signatures, jilid puts it in the center. +/// jilid keeps the header on the same page as the first row. +/// +/// = Example +/// +/// ``` +/// #signatures( +/// header: [Kota, 1 Januari 2026 \ Mengetahui,], +/// signature(role: [Dosen], name: "Nama Dosen"), +/// signature(role: [Mahasiswa], name: "Nama Mahasiswa", id-label: "NIM"), +/// ) +/// ``` +/// +/// - header (content, none): The text above the first row, such as the place, the date and "Mengetahui,". +/// - columns (int): The number of signatures in each row. +/// - gutter (length): The space between rows. +/// - items (content): The `signature` blocks, in order. +/// -> content +#let signatures( + /// The text above the first row, such as the place, the date and "Mengetahui,". + header: none, + /// The number of signatures in each row. + columns: 2, + /// The space between rows. + gutter: 1cm, + /// The `signature` blocks, in order. + ..items, +) = { + assert( + items.named().len() == 0, + message: "jilid: unknown argument(s) for `signatures`: " + + items.named().keys().map(k => "`" + k + "`").join(", ") + + ".", + ) + assert( + type(columns) == int and columns >= 1, + message: "jilid: `signatures(columns: ..)` must be a positive integer.", + ) + let rows = items + .pos() + .chunks(columns) + .map(row => grid(columns: (1fr,) * row.len(), align: bottom, ..row)) + if filled(header) { + // Keep the header on the same page as the first row. + let first = if rows.len() > 0 { rows.remove(0) } + rows.insert(0, block(breakable: false, { + set par(first-line-indent: 0pt, justify: false) + align(center, header) + v(0.5em) + first + })) + } + stack(spacing: gutter, ..rows) +} + +/// Changes the footer text from this page on. +/// +/// If you give `none`, jilid shows the `footer.left` text again. +/// +/// = Example +/// +/// ``` +/// #set-footer-text[Bab II Tinjauan Pustaka] +/// ``` +/// +/// - body (content, str, none): The new footer text. +/// -> content +#let set-footer-text( + /// The new footer text. + body, +) = footer-text.update(body) diff --git a/packages/preview/jilid/0.2.0/src/config.typ b/packages/preview/jilid/0.2.0/src/config.typ new file mode 100644 index 0000000000..8053ea9972 --- /dev/null +++ b/packages/preview/jilid/0.2.0/src/config.typ @@ -0,0 +1,276 @@ +#let defaults = ( + cover: ( + top: 2cm, + logo-width: 8cm, + // "top" puts `kind` above the title. + // "bottom" puts it below the title. + kind-pos: "bottom", + // The space between the course, lecturer and student blocks. + gap: 0.2cm, + // The space above the institution block. + gap-institution: 1cm, + // The space above and below the logo. + gap-logo: 0.5cm, + // The number of student columns. + // auto uses the fewest columns, up to 3, that fit on the page. + student-columns: auto, + // "right" puts the student ID beside the name. + // "below" puts it under the name. + student-id-pos: "right", + // The text styles of the cover texts below. + // Each style takes any `text` argument, plus `upper` and `underline`. + title: (size: 18pt, weight: "bold", upper: true), + kind: (size: 14pt, weight: "bold", upper: true), + subtitle: (size: 18pt, weight: "bold", upper: true), + details: (size: 14pt, weight: "bold"), + label: (:), + course: (weight: "bold"), + lecturer-name: (:), + student-name: (:), + // The "NIP ..." and "NIM ..." lines. + id: (:), + // The university, faculty, department, program and year. + institution: (weight: "bold", upper: true), + // The institution lines, from top to bottom. + // If you leave a key out, jilid hides its line. + institution-order: ( + "university", + "faculty", + "department", + "program", + "year", + ), + // A function that draws the institution block from its lines. + institution-render: auto, + // A function that draws the whole cover from its data. + render: auto, + ), + footer: ( + enabled: true, + left: none, + show-page-number: true, + page-number-align: center, + // The text style of `left`. + text: (size: 9pt, weight: "bold"), + // A function that draws the footer from the page number and the text. + render: auto, + ), + typography: ( + font-family: auto, + font-size: 12pt, + caption-size: 10pt, + caption-gap: 1em, + table-size: 10pt, + // The text style of web links. + // `font: auto` uses the code font. + url: (font: auto, size: 0.85em, fill: blue.darken(20%), underline: true), + // A function that draws each caption from its parts. + caption: auto, + ), + paragraph: ( + justify: true, + indent: 0.63cm, + leading: 0.575em, + spacing: 1.15em, + // The space before numbered and bullet markers. + list-indent: 0cm, + // The width of the marker column. + // The list text starts after it. + marker-width: 0.75cm, + ), + numbering: ( + // The page number style before the first chapter, such as "i". + front: "i", + // "body" continues the chapter page numbers in the appendices. + // "front" continues the front matter page numbers. + back: "body", + // "bottom" puts page numbers in the footer. + // "top" puts chapter and appendix page numbers at the top right. + // A page that opens a chapter keeps its number at the bottom. + position: "bottom", + // The chapter number style. + // "I" gives BAB I, and "1" gives BAB 1. + chapter: "I", + // The appendix number style. + // "1" gives Lampiran 1, and "A" gives Lampiran A. + appendix: "1", + // The section number style inside a chapter, such as "1.1.". + heading: "1.1.", + // Put "Lampiran 1." before each appendix title. + appendix-prefix: true, + ), + outlines: ( + depth: 3, + toc: true, + figures: true, + tables: true, + codes: true, + // Show DAFTAR LAMPIRAN if the document has appendices. + appendices: true, + // List each appendix in DAFTAR ISI too. + // If false, DAFTAR ISI lists only the LAMPIRAN title. + toc-appendices: false, + // The text style of chapter rows in DAFTAR ISI. + h1: (weight: "bold"), + // The text between an entry and its page number, repeated to fill the line. + // none removes it. + leader: ".", + // Where titles start in Daftar Gambar, Tabel, Kode and Lampiran. + // "each" starts every title in a list at the same place, after the widest number such as "Gambar 2.10". + // "shared" uses one place for all of these lists. + // none puts each title right after its number. + align-titles: "each", + // Where titles start in DAFTAR ISI. + // "title" lines up the chapter titles, and a row below chapter level starts under the title of the level above. + // A length, such as 1cm, also lines up the chapter titles and moves each lower level by that length. + // 0cm puts the rows below chapter level at the left. + // auto puts each title right after its number and uses the Typst default for the rows below. + toc-indent: "title", + ), + headings: ( + // Chapter titles and front matter titles. + h1: ( + size: 12pt, + above: 24pt, + below: 18pt, + pagebreak: true, + uppercase: true, + ), + h2: (size: 12pt, above: 24pt, below: 18pt, indent: 0cm), + h3: (size: 12pt, above: 14pt, below: 18pt, indent: 0cm), + h4: (size: 12pt, above: 12pt, below: 18pt, indent: 0cm), + // Numbered appendix titles, on their page and in the lists. + // The LAMPIRAN-LAMPIRAN title follows `h1`. + appendix: (uppercase: false), + ), + code: ( + fill: luma(240), + font: auto, + size: 10pt, + zebraw: true, + ), +) + +// Accept only the values in the list. +// Any other value fails. +#let one-of(..values) = ( + check: v => v in values.pos(), + message: "must be one of " + values.pos().map(repr).join(", "), +) +#let hook = ( + check: v => v == auto or type(v) == function, + message: "must be auto or a function, e.g. `it => [...]`", +) + +// The values that each option accepts. +#let rules = ( + "cover.kind-pos": one-of("top", "bottom"), + "cover.student-id-pos": one-of("right", "below"), + "cover.student-columns": ( + check: v => v == auto or (type(v) == int and v >= 1), + message: "must be auto or a whole number of 1 or more, e.g. `2`", + ), + "cover.render": hook, + "cover.institution-render": hook, + "cover.institution-order": ( + check: v => ( + type(v) == array + and v.all(k => ( + k in ("university", "faculty", "department", "program", "year") + )) + ), + message: "must be a list of \"university\", \"faculty\", \"department\", \"program\" and \"year\", e.g. `(\"university\", \"program\", \"year\")`", + ), + "footer.render": hook, + "typography.caption": hook, + "numbering.back": one-of("body", "front"), + "numbering.position": one-of("bottom", "top"), + "outlines.align-titles": one-of("each", "shared", none), + "outlines.toc-indent": ( + check: v => v in ("title", auto) or type(v) in (length, relative), + message: "must be \"title\", auto or a length, e.g. `1cm`", + ), + "code.zebraw": ( + check: v => type(v) in (bool, dictionary), + message: "must be true, false, or a dictionary of zebraw options, e.g. `(lang: false)`", + ), +) + +// Options that take any `text` argument, plus `upper` and `underline`. +// jilid does not check their keys, and a user key replaces only that key. +#let text-styles = ( + "cover.title", + "cover.kind", + "cover.subtitle", + "cover.details", + "cover.label", + "cover.course", + "cover.lecturer-name", + "cover.student-name", + "cover.id", + "cover.institution", + "footer.text", + "typography.url", + "outlines.h1", +) + +// The margins for `margin: "print"` and `margin: "digital"`. +#let margin-presets = ( + print: (top: 3cm, bottom: 3cm, left: 4cm, right: 3cm), + digital: 1in, +) + +// Put the options of the user over the defaults, in nested groups too. +// `path` is the option name that errors show. +#let merge(base, user, path: "") = { + let out = base + for (key, value) in user { + let name = if path == "" { key } else { path + "." + key } + assert( + key in base, + message: "jilid: unknown option `" + + name + + "`. Valid options here: " + + base.keys().map(k => "`" + k + "`").join(", ") + + ".", + ) + let default = base.at(key) + if type(default) == dictionary { + let example = if name in text-styles { "size" } else { + default.keys().first() + } + assert( + type(value) == dictionary, + message: "jilid: option `" + + name + + "` expects a dictionary, e.g. `" + + name + + ": (" + + example + + ": ...)`.", + ) + out.insert( + key, + if name in text-styles { default + value } else { + merge(default, value, path: name) + }, + ) + } else { + if name in rules { + let rule = rules.at(name) + assert( + (rule.check)(value), + message: "jilid: option `" + + name + + "` " + + rule.message + + ", not " + + repr(value) + + ".", + ) + } + out.insert(key, value) + } + } + out +} diff --git a/packages/preview/jilid/0.2.0/src/document.typ b/packages/preview/jilid/0.2.0/src/document.typ new file mode 100644 index 0000000000..6a6499369e --- /dev/null +++ b/packages/preview/jilid/0.2.0/src/document.typ @@ -0,0 +1,211 @@ +#import "config.typ": defaults, margin-presets, merge +#import "validate.typ": check-args, people +#import "labels.typ": resolve as resolve-labels +#import "rules/text.typ": para-rules, text-rules +#import "rules/headings.typ": heading-rules +#import "rules/figures.typ": figure-rules +#import "rules/refs.typ": ref-rules +#import "pages/cover.typ": cover-page +#import "pages/footer.typ": page-footer, page-header +#import "matter.typ": flow +#import "utils.typ": plain + +/// Formats a report, proposal or thesis for an Indonesian university. +/// +/// Use it as a show rule at the top of the file. +/// jilid makes the cover, the front matter, the lists of contents, figures and tables, the chapters, the bibliography and the appendices. +/// +/// = Example +/// +/// ``` +/// #show: jilid.with( +/// title: [Judul Laporan], +/// students: (name: "Nama Mahasiswa", id: "1000000001"), +/// university: "Universitas Negeri", +/// year: "2026", +/// ) +/// +/// = Pendahuluan +/// ``` +/// +/// = Options +/// +/// Each option group takes a dictionary with only the keys that you change, such as `footer: (left: [Laporan Akhir])`. +/// These are the keys of each group: +/// +/// / cover: `top`, `logo-width`, `kind-pos`, `gap`, `gap-institution`, `gap-logo`, `student-columns`, `student-id-pos`, `title`, `kind`, `subtitle`, `details`, `label`, `course`, `lecturer-name`, `student-name`, `id`, `institution`, `institution-order`, `institution-render`, `render` +/// / footer: `enabled`, `left`, `show-page-number`, `page-number-align`, `text`, `render` +/// / typography: `font-family`, `font-size`, `caption-size`, `caption-gap`, `table-size`, `url`, `caption` +/// / paragraph: `justify`, `indent`, `leading`, `spacing`, `list-indent`, `marker-width` +/// / numbering: `front`, `back`, `position`, `chapter`, `appendix`, `heading`, `appendix-prefix` +/// / outlines: `depth`, `toc`, `figures`, `tables`, `codes`, `appendices`, `toc-appendices`, `h1`, `leader`, `align-titles`, `toc-indent` +/// / headings: `h1`, `h2`, `h3`, `h4`, `appendix` +/// / code: `fill`, `font`, `size`, `zebraw` +/// +/// The README gives the type, the default value and a description for each key. +/// +/// - title (content, str): The document title. If it is not empty, jilid makes a cover. +/// - kind (content, str, none): The document type, such as Laporan Praktikum or Skripsi. +/// - subtitle (content, str): A second title line under the title. +/// - cover-details (array): Extra (label, value) rows under the title, such as `(([Mitra], [Nama Mitra]),)`. +/// - course (content, str): The course name. +/// - lecturers (dictionary, array): One or more lecturers as `(name: .., id: ..)`. +/// - students (dictionary, array): One or more students as `(name: .., id: ..)`. +/// - program (content, str): The study program. +/// - department (content, str): The department. +/// - faculty (content, str): The faculty. +/// - university (content, str): The university. +/// - year (content, str): The year on the cover. +/// - logo (content, none): The logo, such as `image("logo.png")`. +/// - bibliography (content, none): The result of `bibliography(..)`. jilid puts it after the last chapter. +/// - lang (str): The text language. `"id"` and `"en"` are built in. +/// - paper (str): The paper size, such as `"a4"`. +/// - margin (str, length, dictionary): `"digital"` gives 1 inch on all sides. `"print"` gives 4 cm on the left and 3 cm on the other sides. Any page margin value also works. +/// - include-cover (bool, auto): If `auto`, jilid makes a cover when `title` is not empty. +/// - cover (dictionary): The cover layout and text styles. +/// - footer (dictionary): The footer text and page number. +/// - typography (dictionary): The fonts and text sizes. +/// - paragraph (dictionary): The paragraph and list layout. +/// - numbering (dictionary): The number styles for pages, chapters, sections and appendices. +/// - outlines (dictionary): The lists of contents, figures, tables, code and appendices. +/// - headings (dictionary): The heading sizes and space. +/// - code (dictionary): The code block style. +/// - labels (dictionary): Replacements for the words that jilid prints, such as `(figure: "Gbr.")`. +/// - body (content): The document. The show rule gives it to jilid. +/// -> content +#let jilid( + /// The document title. If it is not empty, jilid makes a cover. + title: "", + /// The document type, such as Laporan Praktikum or Skripsi. + kind: none, + /// A second title line under the title. + subtitle: "", + /// Extra (label, value) rows under the title, such as `(([Mitra], [Nama Mitra]),)`. + cover-details: (), + /// The course name. + course: "", + /// One or more lecturers as `(name: .., id: ..)`. + lecturers: (), + /// One or more students as `(name: .., id: ..)`. + students: (), + /// The study program. + program: "", + /// The department. + department: "", + /// The faculty. + faculty: "", + /// The university. + university: "", + /// The year on the cover. + year: "", + /// The logo, such as `image("logo.png")`. + logo: none, + /// The result of `bibliography(..)`. jilid puts it after the last chapter. + bibliography: none, + /// The text language. `"id"` and `"en"` are built in. + lang: "id", + /// The paper size, such as `"a4"`. + paper: "a4", + /// `"digital"`, `"print"` or any page margin value. + margin: "digital", + /// If `auto`, jilid makes a cover when `title` is not empty. + include-cover: auto, + /// The cover layout and text styles. + cover: (:), + /// The footer text and page number. + footer: (:), + /// The fonts and text sizes. + typography: (:), + /// The paragraph and list layout. + paragraph: (:), + /// The number styles for pages, chapters, sections and appendices. + numbering: (:), + /// The lists of contents, figures, tables, code and appendices. + outlines: (:), + /// The heading sizes and space. + headings: (:), + /// The code block style. + code: (:), + /// Replacements for the words that jilid prints, such as `(figure: "Gbr.")`. + labels: (:), + /// The document. The show rule gives it to jilid. + body, +) = { + let cfg = merge(defaults, ( + cover: cover, + footer: footer, + typography: typography, + paragraph: paragraph, + numbering: numbering, + outlines: outlines, + headings: headings, + code: code, + )) + check-args( + logo: logo, + bibliography: bibliography, + cover-details: cover-details, + margin: margin, + ) + let students = people(students, "students") + let lecturers = people(lecturers, "lecturers") + + let cfg = ( + cfg + + ( + lang: lang, + t: resolve-labels(labels, lang), + info: ( + title: title, + kind: kind, + subtitle: subtitle, + cover-details: cover-details, + course: course, + lecturers: lecturers, + students: students, + program: program, + department: department, + faculty: faculty, + university: university, + year: year, + logo: logo, + ), + ) + ) + + // Turn the student names into plain text for the PDF metadata. + set document(title: title, author: students.map(s => plain(s.name))) + + set page(numbering: none) + set page(header: page-header(cfg), footer: page-footer(cfg)) + set page( + paper: paper, + margin: if type(margin) == str { margin-presets.at(margin) } else { + margin + }, + ) + + show: text-rules.with(cfg) + show: heading-rules.with(cfg) + show: figure-rules.with(cfg) + show: ref-rules.with(cfg) + + show: it => { + let with-cover = if include-cover == auto { title not in ("", none) } else { + include-cover + } + if with-cover { cover-page(cfg) } + flow(cfg, it) + } + + show: para-rules.with(cfg) + + body + + if bibliography != none { + pagebreak(weak: true) + heading(level: 1, numbering: none)[#cfg.t.bibliography] + set std.bibliography(title: none) + bibliography + } +} diff --git a/packages/preview/jilid/0.2.0/src/labels.typ b/packages/preview/jilid/0.2.0/src/labels.typ new file mode 100644 index 0000000000..6c334e37bd --- /dev/null +++ b/packages/preview/jilid/0.2.0/src/labels.typ @@ -0,0 +1,67 @@ +#let strings = ( + course: (id: "Mata Kuliah :", en: "Course :"), + lecturer: (id: "Dosen Pengampu :", en: "Lecturer :"), + students: (id: [Disusun oleh :], en: "Prepared by :"), + student-id: (id: "NIM", en: "NIM"), + lecturer-id: (id: "NIP", en: "NIP"), + program: (id: "PROGRAM STUDI", en: "STUDY PROGRAM OF"), + faculty: (id: "FAKULTAS", en: "FACULTY OF"), + department: (id: "JURUSAN", en: "DEPARTMENT OF"), + toc: (id: [DAFTAR ISI], en: [TABLE OF CONTENTS]), + lof: (id: [DAFTAR GAMBAR], en: [LIST OF FIGURES]), + lot: (id: [DAFTAR TABEL], en: [LIST OF TABLES]), + loc: (id: [DAFTAR KODE], en: [LIST OF CODES]), + appendix-list: (id: [DAFTAR LAMPIRAN], en: [LIST OF APPENDICES]), + bibliography: (id: [DAFTAR PUSTAKA], en: [BIBLIOGRAPHY]), + appendices: (id: [LAMPIRAN-LAMPIRAN], en: [APPENDICES]), + appendix: (id: "Lampiran", en: "Appendix"), + appendix-short: (id: "L", en: "A"), + chapter: (id: "BAB", en: "CHAPTER"), + figure: (id: "Gambar", en: "Figure"), + table: (id: "Tabel", en: "Table"), + code: (id: "Kode", en: "Code"), + equation: (id: "Persamaan", en: "Equation"), + section: (id: "Bagian", en: "Section"), + page: (id: "halaman", en: "page"), +) + +// Pick every word for `lang`. +// Then put the words of the user over them. +#let resolve(overrides, lang) = { + let out = (:) + let missing = () + for (key, entry) in strings { + if lang in entry { out.insert(key, entry.at(lang)) } else { + missing.push(key) + } + } + for (key, value) in overrides { + assert( + key in strings, + message: "jilid: unknown `labels` key `" + + key + + "`. Valid keys: " + + strings.keys().map(k => "`" + k + "`").join(", ") + + ".", + ) + assert( + type(value) != dictionary, + message: "jilid: `labels." + + key + + "` takes the text itself, e.g. `labels: (" + + key + + ": [..])`, not a dictionary of languages.", + ) + out.insert(key, value) + } + missing = missing.filter(key => key not in overrides) + assert( + missing.len() == 0, + message: "jilid: language `" + + lang + + "` is not built in (built in: `id`, `en`). Provide it through `labels` for: " + + missing.map(k => "`" + k + "`").join(", ") + + ".", + ) + out +} diff --git a/packages/preview/jilid/0.2.0/src/matter.typ b/packages/preview/jilid/0.2.0/src/matter.typ new file mode 100644 index 0000000000..a8dd577682 --- /dev/null +++ b/packages/preview/jilid/0.2.0/src/matter.typ @@ -0,0 +1,235 @@ +#import "state.typ": page-label, section, sections +#import "rules/text.typ": para-rules +#import "pages/outlines.typ": figure-entry-rules, outlines + +// Fail on a level-1 heading that is written with `=` in `fn`. +#let check-body(body, fn) = { + let children = if body.has("children") { body.children } else { (body,) } + for c in children { + if ( + c.func() == heading + and c.at("level", default: auto) in (auto, 1) + and c.at("depth", default: 1) == 1 + ) { + panic( + "jilid: use `title:` instead of `=` in `" + + fn + + "`, e.g. `#" + + fn + + "(title: [Judul])[..]`.", + ) + } + } +} + +// Fail on an argument that jilid 0.2 removed. +#let check-removed(args, fn) = { + let named = args.named().keys() + if named.len() > 0 { + panic( + "jilid: `" + + fn + + "` takes only `title:` since 0.2, remove " + + named.map(k => "`" + k + "`").join(", ") + + ". See \"Migrating from 0.1\" in the README.", + ) + } +} + +/// A front matter page, such as Kata Pengantar or Abstrak. +/// +/// Front matter is the pages before the table of contents. +/// jilid puts each `frontmatter` page there, in the order you write them. +/// You can write them anywhere in the file. +/// For a heading inside the page, use `==` or deeper. +/// If you write a `=` heading inside, jilid stops with an error. +/// +/// = Example +/// +/// ``` +/// #frontmatter(title: [Kata Pengantar])[ +/// Puji syukur ... +/// ] +/// ``` +/// +/// ``` +/// #frontmatter(title: [Abstrak], label: )[ +/// Abstrak ... +/// ] +/// ``` +/// +/// - title (content, str, none): The page title. jilid shows it like a chapter title, centered and without a number. +/// - label (label, none): A label that you can refer to with `@`, such as ``. jilid shows the reference as the title and its page, such as "Abstrak (halaman ii)". It needs a `title`. +/// - removed (arguments): Arguments that jilid 0.2 removed: `numbering`, `start-page` and `outlined`. If you give one of them, jilid stops with an error that tells you what changed. +/// - body (content): The text of the page. +/// -> content +#let frontmatter( + /// The page title. jilid shows it like a chapter title, centered and without a number. + title: none, + /// A label that you can refer to with `@`, such as ``. It needs a `title`. + label: none, + /// Arguments that jilid 0.2 removed. If you give one, jilid stops with an error. + ..removed, + /// The text of the page. + body, +) = { + check-removed(removed, "frontmatter") + check-body(body, "frontmatter") + assert( + label == none or type(label) == std.label, + message: "jilid: `frontmatter(label: ..)` must be a label, e.g. `label: `.", + ) + assert( + label == none or title != none, + message: "jilid: `frontmatter(label: ..)` needs a `title`.", + ) + [#metadata(( + kind: sections.front, + title: title, + label: label, + body: body, + )) ] +} + +/// An appendix page, such as Lampiran 1. +/// +/// jilid puts every appendix after the bibliography, in the order you write them. +/// You can write them anywhere in the file. +/// jilid numbers each appendix by its place in that order. +/// Figures, tables and equations inside get the appendix number, such as Gambar L1.2. +/// For a heading inside the appendix, use `==` or deeper. +/// If you write a `=` heading inside, jilid stops with an error. +/// +/// = Example +/// +/// ``` +/// #appendix(title: [Kuesioner], label: )[ +/// Daftar pertanyaan ... +/// ] +/// +/// Lihat @kuesioner. +/// ``` +/// +/// - title (content, str, none): The appendix title. jilid shows it after the number, such as "Lampiran 1. Kuesioner". +/// - label (label, none): A label that you can refer to with `@`, such as ``. +/// - body (content): The text of the appendix. +/// -> content +#let appendix( + /// The appendix title. jilid shows it after the number. + title: none, + /// A label that you can refer to with `@`, such as ``. + label: none, + /// The text of the appendix. + body, +) = { + assert( + label == none or type(label) == std.label, + message: "jilid: `appendix(label: ..)` must be a label, e.g. `label: `.", + ) + check-body(body, "appendix") + [#metadata(( + kind: sections.back, + title: title, + label: label, + body: body, + )) ] +} + +// jilid 0.2 renamed this function to `appendix`. +#let appendices(..args) = panic( + "jilid: `appendices` is `appendix(title: [..])[..]` since 0.2, one call per appendix. See \"Migrating from 0.1\" in the README.", +) + +#let blocks-of(kind) = query().filter(m => m.value.kind == kind) + +#let render-frontmatter(cfg, m) = { + let v = m.value + let title = if v.title != none { heading(level: 1, numbering: none, v.title) } + if v.label != none { title = [#title#v.label] } + let body = { + set figure(outlined: false) + set heading(numbering: none) + v.body + } + para-rules(cfg, title + body) +} + +// Number figures, tables and equations with the appendix number, such as "L1.2". +#let render-appendix(cfg, m, n) = { + let v = m.value + let title = heading(level: 1, if v.title == none [] else { v.title }) + let prefix = [#cfg.t.appendix-short#numbering(cfg.numbering.appendix, n).] + let body = { + set figure(outlined: false, numbering: x => [#prefix#x]) + set math.equation(numbering: x => [(#prefix#x)]) + set heading(numbering: none) + v.body + } + para-rules(cfg, if v.label != none [#title#v.label#body] else [#title#body]) +} + +// Everything after the cover, in this order: +// the front matter, the lists, `body` and the appendices. +#let flow(cfg, body) = { + section.update(sections.front) + counter(page).update(1) + + show: figure-entry-rules.with(cfg) + + [#metadata(none) ] + context { + for m in blocks-of(sections.front) { + render-frontmatter(cfg, m) + pagebreak(weak: true) + } + } + + outlines(cfg) + + [#metadata(none) ] + pagebreak(weak: true) + section.update(sections.main) + counter(page).update(1) + counter(heading).update(0) + [#metadata(none) ] + body + + context { + let back = blocks-of(sections.back) + if back.len() > 0 { + pagebreak(weak: true) + + if cfg.numbering.back == "front" { + let end-marker = query().first() + let val = counter(page).at(end-marker.location()).first() + counter(page).update(val + 1) + } + + section.update(sections.back) + [#metadata(none) ] + heading(level: 1, numbering: none, outlined: true)[#cfg.t.appendices] + [#metadata(none) ] + counter(heading).update(0) + + for (i, m) in back.enumerate() { render-appendix(cfg, m, i + 1) } + } + } + + // Save the first page of each part: the front matter, the chapters and the appendices. + // For each part, jilid saves the page number as a whole number and as the footer shows it, such as 4 and "iv". + // To read the result, run `typst eval 'query()' --in doc.typ`. + context { + let start(lbl) = { + let q = query(lbl) + if q.len() > 0 { + let loc = q.first().location() + (idx: counter(page).at(loc).first(), display: page-label(cfg, loc)) + } + } + [#metadata(( + front: start(), + body: start(), + back: start(), + )) ] + } +} diff --git a/packages/preview/jilid/0.2.0/src/pages/cover.typ b/packages/preview/jilid/0.2.0/src/pages/cover.typ new file mode 100644 index 0000000000..f59d40bf4a --- /dev/null +++ b/packages/preview/jilid/0.2.0/src/pages/cover.typ @@ -0,0 +1,163 @@ +#import "../state.typ": section, sections +#import "../utils.typ": filled, prefixed, styled + +#let default-cover(c, it) = { + let t = it.labels + + let id-line(label, person) = { + let id = person.at("id", default: none) + if not filled(id) [] else if not filled(label) { + styled(c.id, [#id]) + } else { styled(c.id, [#label #id]) } + } + + let title-text = styled(c.title, it.title) + let kind-text = styled(c.kind, it.kind) + + let logo = if it.logo != none { + set image(width: c.logo-width) + it.logo + } + + let lecturers = if it.lecturers.len() > 0 [ + #styled(c.label, t.lecturer)\ + #for l in it.lecturers [ + #styled(c.lecturer-name, l.name)\ + #if filled(l.at("id", default: none)) [#id-line(t.lecturer-id, l)\ ] + ] + #v(c.gap) + ] + + // A grid that jilid fills column by column. + // With id-pos "right", the name and the ID sit side by side. + // With id-pos "below", the ID sits under the name in one cell. + let students(cols) = if it.students.len() == 0 { none } else { + let list = it.students + let beside = c.student-id-pos == "right" + let rows = calc.ceil(list.len() / cols) + let cells = range(rows * cols).map(j => { + let i = calc.rem(j, cols) * rows + calc.quo(j, cols) + if i >= list.len() { if beside { ([], []) } else { [] } } else { + let s = list.at(i) + let name = styled(c.student-name, s.name) + if beside { (name, id-line(t.student-id, s)) } else [ + #name + #if filled(s.at("id", default: none)) [\ #id-line(t.student-id, s)] + ] + } + }) + + context stack( + spacing: par.leading, + styled(c.label, t.students), + align(center, grid( + columns: (auto,) * (if beside { 2 * cols } else { cols }), + // Make a gap between the name and the ID of a student. + column-gutter: if beside { + range(2 * cols - 1).map(g => if calc.odd(g) { 1.5em } else { 0pt }) + } else { 1em }, + row-gutter: if beside { par.leading } else { 1em }, + align: if beside { (left, right) * cols } else { center }, + inset: (x: 4pt), + ..if beside { cells.flatten() } else { cells }, + )), + ) + } + + let lines = c.institution-order.map(k => it.at(k)).filter(filled) + let institution = if c.institution-render == auto { + styled(c.institution, lines.join(linebreak())) + } else { + (c.institution-render)(( + university: it.university, + faculty: it.faculty, + department: it.department, + program: it.program, + year: it.year, + lines: lines, + )) + } + + let body(cols) = align(center)[ + #v(c.top) + + #if c.kind-pos == "top" [ + #if filled(it.kind) { kind-text + v(0.1cm) } + #title-text + ] else [ + #title-text + #if filled(it.kind) { v(0.1cm) + kind-text } + ] + + #if filled(it.subtitle) [ + #v(0.5cm) + #styled(c.subtitle, it.subtitle) + ] + + #for (label, value) in it.details [ + #v(0.5cm) + #styled(c.details, [#label \ #value]) + ] + + #v(1fr) + #if logo != none { v(c.gap-logo) + logo + v(c.gap-logo) } + #v(1fr) + + #if filled(it.course) [ + #styled(c.label, t.course)\ + #styled(c.course, it.course) + #v(c.gap) + ] + + #lecturers + #students(cols) + + #v(1fr) + #v(c.gap-institution) + + #institution + ] + + layout(size => { + let height(cols) = measure(block(width: size.width, body(cols))).height + + // auto: use the fewest columns, up to 3, that fit. + // If none fit, use the shortest cover. + let cols = c.student-columns + if cols == auto { + let options = (1, 2, 3) + cols = options.find(n => height(n) <= size.height) + if cols == none { cols = options.sorted(key: height).first() } + } + body(cols) + }) +} + +#let cover-page(cfg) = { + let info = cfg.info + let t = cfg.t + let unit(label, value) = if filled(value) { prefixed(label, value) } + let render = if cfg.cover.render == auto { + default-cover.with(cfg.cover) + } else { cfg.cover.render } + + page(numbering: none, footer: none)[ + #section.update(sections.cover) + #render(( + title: info.title, + kind: info.kind, + subtitle: info.subtitle, + details: info.cover-details, + course: info.course, + lecturers: info.lecturers, + students: info.students, + logo: info.logo, + university: info.university, + faculty: unit(t.faculty, info.faculty), + department: unit(t.department, info.department), + program: unit(t.program, info.program), + year: info.year, + labels: t, + )) + ] +} diff --git a/packages/preview/jilid/0.2.0/src/pages/footer.typ b/packages/preview/jilid/0.2.0/src/pages/footer.typ new file mode 100644 index 0000000000..bb8e19916b --- /dev/null +++ b/packages/preview/jilid/0.2.0/src/pages/footer.typ @@ -0,0 +1,56 @@ +#import "../state.typ": footer-text, page-format, section, sections +#import "../utils.typ": styled + +// If `numbering.position` is "top", chapter and appendix pages show the number at the top right. +// A page that opens a chapter keeps it at the bottom. +#let number-on-top(cfg, sec) = ( + cfg.numbering.position == "top" + and sec in (sections.main, sections.back) + and not query(heading.where(level: 1)).any(h => ( + h.location().page() == here().page() + )) +) + +#let page-number(cfg, sec) = text(size: cfg.typography.font-size, numbering( + page-format(cfg, sec), + counter(page).get().first(), +)) + +// Show the page number at the top right if `number-on-top` allows it. +#let page-header(cfg) = context { + let sec = section.get() + if cfg.footer.show-page-number and number-on-top(cfg, sec) { + align(right, page-number(cfg, sec)) + } +} + +// The built-in footer: the text above the page number. +// It gets the same data as a `footer.render` hook. +#let default-footer(f, it) = [ + #if it.left != none [ + #align(left, styled(f.text, it.left)) + #v(0.2em) + ] + #if it.number != none [ + #align(f.page-number-align, it.number) + ] +] + +#let page-footer(cfg) = context { + let f = cfg.footer + let sec = section.get() + if sec == sections.cover or not f.enabled { + return none + } + set par(first-line-indent: 0pt) + + let left-text = footer-text.get() + let render = if f.render == auto { default-footer.with(f) } else { f.render } + render(( + number: if f.show-page-number and not number-on-top(cfg, sec) { + page-number(cfg, sec) + }, + left: if left-text == none { f.left } else { left-text }, + part: sec, + )) +} diff --git a/packages/preview/jilid/0.2.0/src/pages/outlines.typ b/packages/preview/jilid/0.2.0/src/pages/outlines.typ new file mode 100644 index 0000000000..a8b55ad2fc --- /dev/null +++ b/packages/preview/jilid/0.2.0/src/pages/outlines.typ @@ -0,0 +1,238 @@ +#import "../state.typ": page-label, section, sections +#import "../utils.typ": h1-number, h1-title, styled +#import "../rules/figures.typ": figure-number + +// The label that marks where the appendix headings start. +// `matter.flow` puts it after the appendices title. +#let appendices-start = + +// A list row: `prefix` and `title`, the leader and the page number, linked to `loc`. +// With a `width`, the prefix goes in a column of that width. +// Without a `width`, `sep` joins the prefix and the title. +#let dotted-row(cfg, loc, prefix, title, width: none, sep: [ ]) = { + let leader = if cfg.outlines.leader == none { h(1fr) } else { + box(width: 1fr, inset: (x: 2pt), repeat(cfg.outlines.leader)) + } + let page = context page-label(cfg, loc) + link(loc, if prefix == none or width == none { + grid( + columns: (1fr, auto), + column-gutter: 0.5em, + align: (left, bottom), + [#if prefix != none [#prefix#sep]#title #leader], page, + ) + } else { + grid( + columns: (width, 1fr, auto), + column-gutter: 0.5em, + align: (left, left, bottom), + prefix, [#title #leader], page, + ) + }) +} + +// The number before a title in the lists, such as "BAB II" or "Lampiran 1.". +// Appendices get it if `numbering.appendix-prefix` is true. +#let entry-prefix(cfg, sec, n) = { + let number = h1-number(cfg, sec, n) + if sec == sections.main { number } else if ( + sec == sections.back and cfg.numbering.appendix-prefix + ) [#number.] +} + +#let heading-prefix(cfg, h) = if h.numbering != none { + let loc = h.location() + entry-prefix(cfg, section.at(loc), counter(heading).at(loc).first()) +} + +// The figure number in the lists, such as "Gambar 2.1". +#let figure-prefix(f) = { + let loc = f.location() + let n = counter(figure.where(kind: f.kind)).at(loc).first() + [#f.supplement #figure-number(loc, n)] +} + +#let widest(items) = calc.max( + 0pt, + ..items.filter(x => x != none).map(x => measure(x).width), +) + +// The width of the number column in the list `name`. +// `name` is "toc", "back" or a figure kind. +// DAFTAR ISI gets a column unless `toc-indent` is auto. +// With "shared", the figure, table, code and appendix lists share one width. +// none puts the number right before the title. +#let number-width(cfg, name) = { + let mode = if name != "toc" { cfg.outlines.align-titles } else if ( + cfg.outlines.toc-indent != auto + ) { "each" } + if mode == none { return none } + let numbered = query(heading.where(level: 1)).filter(h => ( + h.numbering != none and h.outlined + )) + let in-back(h) = section.at(h.location()) == sections.back + let rows(name) = if name == "toc" { + numbered + .filter(h => cfg.outlines.toc-appendices or not in-back(h)) + .map(h => styled(cfg.outlines.h1, heading-prefix(cfg, h))) + } else if name == "back" { + numbered.filter(in-back).map(h => heading-prefix(cfg, h)) + } else { + query(figure.where(kind: name)) + .filter(f => f.caption != none and f.at("outlined", default: true)) + .map(figure-prefix) + } + let names = if mode == "shared" { ("back", image, table, raw) } else { + (name,) + } + widest(names.map(rows).flatten()) +} + +// Figure rows in every outline, also outlines that the user places: +// "Gambar 2.1 Caption .... 12". +#let figure-entry-rules(cfg, body) = { + show outline.entry: it => { + if ( + it.element != none + and it.element.func() == figure + and it.element.caption != none + ) { + context dotted-row( + cfg, + it.element.location(), + figure-prefix(it.element), + it.element.caption.body, + width: number-width(cfg, it.element.kind), + sep: [ #h(0.5em) ], + ) + } else { + it + } + } + + body +} + +// DAFTAR ISI. +// If `outlines.toc-appendices` is false, it lists only the LAMPIRAN title. +// DAFTAR LAMPIRAN then lists the appendices. +#let table-of-contents(cfg) = { + // The headings that DAFTAR ISI lists. + let target() = { + let has-appendices = query(appendices-start).len() > 0 + if has-appendices and not cfg.outlines.toc-appendices { + selector(heading).before(appendices-start) + } else { heading } + } + // The widest number at each heading level in DAFTAR ISI, such as "BAB VIII" and "1.10.". + let level-widths() = { + let hs = query(target()).filter(h => ( + h.outlined and h.numbering != none and h.level <= cfg.outlines.depth + )) + range(1, cfg.outlines.depth + 1).map(level => widest( + hs + .filter(h => h.level == level) + .map(h => if level == 1 { + styled(cfg.outlines.h1, heading-prefix(cfg, h)) + } else { + numbering(h.numbering, ..counter(heading).at(h.location())) + }), + )) + } + + // Use the same leader in rows below chapter level. + set outline.entry(fill: if cfg.outlines.leader != none { + repeat(gap: 0.15em, cfg.outlines.leader) + }) + show outline.entry.where(level: 1): it => { + let loc = it.element.location() + v(0.5em, weak: true) + context { + let appendix = ( + it.element.numbering != none and section.at(loc) == sections.back + ) + let title = h1-title(cfg, it.element.body, appendix: appendix) + styled(cfg.outlines.h1, dotted-row( + cfg, + loc, + heading-prefix(cfg, it.element), + title, + width: number-width(cfg, "toc"), + )) + } + } + // Without `toc-indent: auto`, jilid draws the rows below chapter level. + // "title" starts each row under the title of the level above. + // A length moves each level by that length. + show outline.entry: it => { + let indent = cfg.outlines.toc-indent + if it.level < 2 or indent == auto { return it } + v(0.5em, weak: true) + context { + let widths = level-widths() + let x = if indent == "title" { + widths + .slice(0, it.level - 1) + .map(w => if w > 0pt { w + 0.5em } else { 0pt }) + .sum() + } else { (it.level - 1) * indent } + pad(left: x, dotted-row( + cfg, + it.element.location(), + it.prefix(), + it.element.body, + width: widths.at(it.level - 1), + )) + } + } + + if cfg.outlines.toc { + heading(level: 1, numbering: none)[#cfg.t.toc] + context outline( + title: none, + depth: cfg.outlines.depth, + target: target(), + ) + } +} + +// DAFTAR TABEL, GAMBAR and KODE. +#let figure-list(enabled, kind, title) = context { + if enabled and query(figure.where(kind: kind)).len() > 0 { + pagebreak(weak: true) + heading(level: 1, numbering: none)[#title] + outline( + title: none, + target: figure.where(kind: kind), + ) + } +} + +// DAFTAR LAMPIRAN. +#let appendix-list(cfg) = context { + if not cfg.outlines.appendices or query(appendices-start).len() == 0 { + return + } + let target = heading.where(level: 1).after(appendices-start) + if query(target).len() == 0 { return } + + pagebreak(weak: true) + heading(level: 1, numbering: none)[#cfg.t.appendix-list] + show outline.entry: it => context dotted-row( + cfg, + it.element.location(), + heading-prefix(cfg, it.element), + h1-title(cfg, it.element.body, appendix: it.element.numbering != none), + width: number-width(cfg, "back"), + ) + outline(title: none, target: target) +} + +#let outlines(cfg) = { + set par(leading: 0.65em) + table-of-contents(cfg) + figure-list(cfg.outlines.tables, table, cfg.t.lot) + figure-list(cfg.outlines.figures, image, cfg.t.lof) + figure-list(cfg.outlines.codes, raw, cfg.t.loc) + appendix-list(cfg) +} diff --git a/packages/preview/jilid/0.2.0/src/rules/figures.typ b/packages/preview/jilid/0.2.0/src/rules/figures.typ new file mode 100644 index 0000000000..2565a2346c --- /dev/null +++ b/packages/preview/jilid/0.2.0/src/rules/figures.typ @@ -0,0 +1,80 @@ +#import "@preview/zebraw:0.6.3": zebraw +#import "../state.typ": section, sections + +// The number of the `n`-th item at `loc`. +// It gives "2.3" in a chapter and "3" in the front matter. +// `render-appendix` numbers the items in appendices. +#let figure-number(loc, n) = { + let chapter = counter(heading).at(loc).first() + if section.at(loc) == sections.main [#chapter.#n] else [#n] +} + +// Rules for figures, captions, equations and code blocks. +#let figure-rules(cfg, body) = { + set figure(gap: cfg.typography.caption-gap, placement: none) + + let default-caption(it) = [*#it.supplement #it.number:* #it.body] + let render-caption = if cfg.typography.caption == auto { + default-caption + } else { cfg.typography.caption } + show figure.caption: it => text( + size: cfg.typography.caption-size, + render-caption(( + supplement: it.supplement, + number: context it.counter.display(it.numbering), + body: it.body, + kind: it.kind, + )), + ) + + let number(n) = context figure-number(here(), n) + + set figure(numbering: number) + + set math.equation( + numbering: n => [(#number(n))], + supplement: cfg.t.equation, + block: true, + ) + show math.equation.where(block: true): set block(above: 1.5em, below: 2.5em) + + show figure.where(kind: image): set figure(supplement: cfg.t.figure) + show figure.where(kind: table): set figure(supplement: cfg.t.table) + show figure.where(kind: raw): set figure(supplement: cfg.t.code) + + show figure.where(kind: table): it => { + set figure.caption(position: top) + set align(center) + it + } + show figure.where(kind: raw): it => { + set figure.caption(position: top) + set align(left) + it + } + show figure.where(kind: image): it => { + set align(center) + it + } + + let z = cfg.code.zebraw + show: it => if z == false { + show raw.where(block: true): r => block( + fill: cfg.code.fill, + inset: 8pt, + radius: 3pt, + width: 100%, + r, + ) + it + } else { + let options = if type(z) == dictionary { z } else { (:) } + zebraw(background-color: cfg.code.fill, ..options, it) + } + let code-font = if cfg.code.font == auto { (:) } else { + (font: cfg.code.font) + } + show raw: set text(..code-font, size: cfg.code.size) + + body +} diff --git a/packages/preview/jilid/0.2.0/src/rules/headings.typ b/packages/preview/jilid/0.2.0/src/rules/headings.typ new file mode 100644 index 0000000000..5927f7a42f --- /dev/null +++ b/packages/preview/jilid/0.2.0/src/rules/headings.typ @@ -0,0 +1,67 @@ +#import "../state.typ": section, sections +#import "../utils.typ": h1-number, h1-title + +// The heading look in each part: +// - Front matter: centered and never numbered. +// - Chapters: "BAB I" above the title, centered. +// - Appendices: "Lampiran 1. Title", on the left. +// Each chapter restarts the figure, table, code and equation counters. +#let heading-rules(cfg, body) = { + let hs = cfg.headings + set heading(numbering: cfg.numbering.heading, supplement: cfg.t.section) + + show heading: it => { + set text(weight: "bold") + set par(first-line-indent: 0pt) + + let number = if it.numbering != none { + counter(heading).display(it.numbering) + } + + if it.level == 1 { + counter(figure.where(kind: image)).update(0) + counter(figure.where(kind: table)).update(0) + counter(figure.where(kind: raw)).update(0) + counter(math.equation).update(0) + + context { + let sec = section.get() + let numbered = it.numbering != none and sec != sections.front + let appendix = sec == sections.back and numbered + let title = h1-title(cfg, it.body, appendix: appendix) + + // "BAB I \ TITLE", "Lampiran 1. Title" or only the title. + let n = h1-number(cfg, sec, counter(heading).get().first()) + let label = if not numbered { + title + } else if appendix and cfg.numbering.appendix-prefix { + [#n. #text(weight: "regular", title)] + } else if appendix { + title + } else { + [#n \ #title] + } + + // The first appendix shares its page with the LAMPIRAN title. + let first-appendix = appendix and counter(heading).get().first() == 1 + if hs.h1.pagebreak and not first-appendix { pagebreak(weak: true) } + + align(if appendix { left } else { center })[ + #v(hs.h1.above) + #text(size: hs.h1.size, weight: "bold")[#label] + #v(hs.h1.below) + ] + } + } else { + // Levels deeper than 4 use `h4`. + let style = hs.at("h" + str(calc.min(it.level, 4))) + v(style.above, weak: true) + text(size: style.size)[ + #h(style.indent)#if number != none [#number.trim() ]#it.body + ] + v(style.below, weak: true) + } + } + + body +} diff --git a/packages/preview/jilid/0.2.0/src/rules/refs.typ b/packages/preview/jilid/0.2.0/src/rules/refs.typ new file mode 100644 index 0000000000..16af29c8f7 --- /dev/null +++ b/packages/preview/jilid/0.2.0/src/rules/refs.typ @@ -0,0 +1,38 @@ +#import "../state.typ": page-label, section +#import "../utils.typ": h1-number + +// References to headings: +// - An unnumbered heading gives its title and page, such as "Kata Pengantar (halaman iv)". +// - A chapter gives "BAB II". +// - An appendix gives "Lampiran 1". +// Lower levels keep the Typst default, such as "Bagian 1.2". +#let ref-rules(cfg, body) = { + show ref: it => { + let el = it.element + if el == none or el.func() != heading { return it } + let loc = el.location() + + if el.numbering == none { + return context { + let name = if it.supplement not in (none, auto) { it.supplement } else { + el.body + } + link(loc)[#name (#cfg.t.page #page-label(cfg, loc))] + } + } + + if el.level == 1 and it.supplement == auto { + return context { + let n = h1-number( + cfg, + section.at(loc), + counter(heading).at(loc).first(), + ) + if n == none { it } else { link(loc, n) } + } + } + it + } + + body +} diff --git a/packages/preview/jilid/0.2.0/src/rules/text.typ b/packages/preview/jilid/0.2.0/src/rules/text.typ new file mode 100644 index 0000000000..7a77c17585 --- /dev/null +++ b/packages/preview/jilid/0.2.0/src/rules/text.typ @@ -0,0 +1,58 @@ +#import "../utils.typ": styled + +// Rules for the font, the language, links and table cells. +#let text-rules(cfg, body) = { + let font = cfg.typography.font-family + set text( + ..if font != auto { (font: font) }, + size: cfg.typography.font-size, + lang: cfg.lang, + ) + + show link: it => { + if type(it.dest) == str { + let style = cfg.typography.url + if style.at("font", default: auto) == auto { + style.font = if cfg.code.font == auto { "DejaVu Sans Mono" } else { + cfg.code.font + } + } + styled(style, it) + } else { + it + } + } + + show table.cell: it => { + set text(size: cfg.typography.table-size) + set par(justify: false, leading: 0.5em, first-line-indent: 0pt) + it + } + + body +} + +// Rules for the paragraph and list layout. +// The front matter and the appendices get them too. +#let para-rules(cfg, body) = { + let p = cfg.paragraph + set par( + justify: p.justify, + first-line-indent: (amount: p.indent, all: true), + leading: p.leading, + spacing: p.spacing, + ) + let w = p.marker-width + set enum( + indent: p.list-indent, + body-indent: 0pt, + number-align: start + top, + numbering: (..n) => box(width: w, align(left, numbering("1.", ..n))), + ) + set list( + indent: p.list-indent, + body-indent: 0pt, + marker: ([•], [‣], [–]).map(m => box(width: w, align(left, m))), + ) + body +} diff --git a/packages/preview/jilid/0.2.0/src/state.typ b/packages/preview/jilid/0.2.0/src/state.typ new file mode 100644 index 0000000000..2b983f041a --- /dev/null +++ b/packages/preview/jilid/0.2.0/src/state.typ @@ -0,0 +1,27 @@ +// The state that the rules, pages and footer read. + +// The parts of a document. +// A typo such as `sections.mian` fails. +#let sections = (cover: "cover", front: "front", main: "main", back: "back") +#let section = state("jilid-section", sections.front) + +// The text from `set-footer-text`. +// none uses `footer.left`. +#let footer-text = state("jilid-footer-text", none) + +#let page-format(cfg, sec) = { + if ( + sec == sections.main + or (sec == sections.back and cfg.numbering.back == "body") + ) { + "1" + } else { + cfg.numbering.front + } +} + +// The page number at `loc` as text, as the footer shows it, such as "iv". +#let page-label(cfg, loc) = numbering( + page-format(cfg, section.at(loc)), + counter(page).at(loc).first(), +) diff --git a/packages/preview/jilid/0.2.0/src/utils.typ b/packages/preview/jilid/0.2.0/src/utils.typ new file mode 100644 index 0000000000..f430f180bb --- /dev/null +++ b/packages/preview/jilid/0.2.0/src/utils.typ @@ -0,0 +1,51 @@ +#import "state.typ": sections + +#let filled(x) = x not in (none, "") + +// Turn a string or simple content into plain text. +// jilid uses it to compare [TEXT] with "text". +#let plain(it) = if type(it) == str { it } else if it.has("text") { + it.text +} else if it.has("children") { + it.children.map(plain).sum(default: "") +} else if it.has("body") { plain(it.body) } else { "" } + +// Put the label before the value. +// If the value already starts with the label, skip the label. +#let prefixed(label, value) = { + let l = upper(plain(label)) + let v = upper(plain(value)) + if v == l or v.starts-with(l + " ") { value } else [#label #value] +} + +// Write a level-1 title in capitals if `headings.h1.uppercase` is true. +// Numbered appendix titles follow `headings.appendix.uppercase` instead. +#let h1-title(cfg, body, appendix: false) = { + let up = if appendix { cfg.headings.appendix.uppercase } else { + cfg.headings.h1.uppercase + } + if up { upper(body) } else { body } +} + +// Give "BAB II" for a chapter or "Lampiran 1" for an appendix. +// `n` is the number of the heading inside the part `sec`. +#let h1-number(cfg, sec, n) = { + let (word, style) = if sec == sections.main { + (cfg.t.chapter, cfg.numbering.chapter) + } else if sec == sections.back { + (cfg.t.appendix, cfg.numbering.appendix) + } else { return none } + [#word #numbering(style, n)] +} + +// Write `body` in a text style. +// A style takes any `text` argument, plus `upper` and `underline`. +#let styled(style, body) = { + if body == none { return none } + let args = style + let up = args.remove("upper", default: false) + let line = args.remove("underline", default: false) + if up { body = upper(body) } + if line { body = underline(body) } + text(..args, body) +} diff --git a/packages/preview/jilid/0.2.0/src/validate.typ b/packages/preview/jilid/0.2.0/src/validate.typ new file mode 100644 index 0000000000..72b0636200 --- /dev/null +++ b/packages/preview/jilid/0.2.0/src/validate.typ @@ -0,0 +1,64 @@ +#import "config.typ": margin-presets + +// Turn one person or a list of people into a list. +// `option` is the argument name that errors show. +#let people(value, option) = { + let list = if type(value) == dictionary { (value,) } else { value } + let example = option + ": ((name: \"Nama\", id: \"1001\"),)" + assert( + type(list) == array, + message: "jilid: `" + + option + + "` must be a list of people, e.g. `" + + example + + "`.", + ) + for p in list { + assert( + type(p) == dictionary and "name" in p, + message: "jilid: each entry of `" + + option + + "` must be `(name: .., id: ..)`, e.g. `" + + example + + "`. Got " + + repr(p) + + ".", + ) + } + list +} + +#let content-arg(value, option, example) = assert( + value == none or type(value) == content, + message: "jilid: `" + + option + + "` must be content, e.g. `" + + example + + "`, not a path string.", +) + +// Check the arguments outside the option groups. +// `config.rules` checks the option groups. +#let check-args( + logo: none, + bibliography: none, + cover-details: (), + margin: none, +) = { + content-arg(logo, "logo", "logo: image(\"logo.png\")") + content-arg( + bibliography, + "bibliography", + "bibliography: bibliography(\"refs.bib\")", + ) + for row in cover-details { + assert( + type(row) == array and row.len() == 2, + message: "jilid: each `cover-details` entry must be a (label, value) pair, e.g. `(([Mitra Kolaborator], [Nama Mitra]),)`. Note the trailing comma for a single entry.", + ) + } + assert( + type(margin) != str or margin in margin-presets, + message: "jilid: `margin` must be \"print\", \"digital\" or a `page.margin` value such as `2.5cm` or `(x: 2cm, y: 3cm)`.", + ) +} diff --git a/packages/preview/jilid/0.2.0/template/main.typ b/packages/preview/jilid/0.2.0/template/main.typ new file mode 100644 index 0000000000..fa3d62695b --- /dev/null +++ b/packages/preview/jilid/0.2.0/template/main.typ @@ -0,0 +1,160 @@ +#import "@preview/jilid:0.2.0": ( + appendix, frontmatter, jilid, signature, signatures, +) + +#show: jilid.with( + title: [Judul Laporan], + kind: [Laporan Kerja Praktik], + course: "Nama Mata Kuliah", + lecturers: (name: "Nama Dosen, S.Kom., M.Kom.", id: "10000000000000000"), + students: ( + (name: "Nama Mahasiswa", id: "1000000001"), + ), + program: "Teknik Informatika", + faculty: "Teknik", + university: "Universitas Negeri", + year: "2026", + bibliography: bibliography("refs.bib", style: "apa"), + // Hapus tanda // di depan baris yang ingin dipakai. + // logo: image("logo.png"), + // cover-details: (([Mitra], [Nama Mitra]),), + // typography: (font-family: "Times New Roman"), + // margin: "print", + // numbering: (position: "top"), +) + +// Halaman depan muncul sebelum daftar isi, sesuai urutan penulisannya. +#frontmatter(title: [Lembar Pengesahan])[ + #v(1cm) + #signatures( + header: [Kota, 1 Januari 2026 \ Mengetahui,], + signature( + role: [Dosen Pembimbing], + name: "Nama Dosen, S.Kom., M.Kom.", + id: "10000000000000000", + ), + signature( + role: [Mahasiswa], + name: "Nama Mahasiswa", + id-label: "NIM", + id: "1000000001", + ), + ) +] + +// Dengan `label`, halaman ini bisa dirujuk dengan @abstrak. +#frontmatter(title: [Abstrak], label: )[ + Tulis ringkasan laporan dalam satu paragraf: masalah, metode, hasil, dan + kesimpulan. Abstrak biasanya berisi 150 sampai 250 kata. + + *Kata kunci:* kata kunci satu, kata kunci dua, kata kunci tiga +] + +#frontmatter(title: [Kata Pengantar])[ + Tulis ucapan syukur dan terima kasih kepada pihak yang membantu penyusunan + laporan ini. + + #align(right)[Kota, 1 Januari 2026 \ Penulis] +] + +// Bab ditulis dengan `=`, subbab dengan `==` dan `===`. += Pendahuluan + +== Latar Belakang + +Jelaskan masalah yang mendorong laporan ini dan alasan masalah itu penting. +Ringkasan laporan ada pada @abstrak. + +== Rumusan Masalah + ++ Tulis pertanyaan pertama yang ingin dijawab. ++ Tulis pertanyaan kedua yang ingin dijawab. + +== Tujuan + +Tulis tujuan yang menjawab setiap rumusan masalah. + +== Manfaat + +Tulis manfaat laporan ini bagi pembaca, instansi, atau penulis. + += Tinjauan Pustaka + +Rangkum teori dan penelitian sebelumnya yang menjadi dasar laporan. Sitasi +ditulis dengan `@`, misalnya @einstein1905. + +Rumus diberi nomor otomatis, seperti @persamaan-energi. + +$ E = m c^2 $ + += Metodologi + +Jelaskan langkah kerja, alat, dan data yang dipakai. + +// Diagram alur dibuat dengan paket fletcher: https://typst.app/universe/package/fletcher +// Paket ini ikut terhapus jika gambar ini dihapus. +#figure( + { + import "@preview/fletcher:0.5.8": diagram, edge, node + diagram( + node-stroke: 0.6pt, + spacing: 1.2em, + node((0, 0), [Studi Literatur]), + edge("-|>"), + node((1, 0), [Pengumpulan Data]), + edge("-|>"), + node((2, 0), [Analisis]), + edge("-|>"), + node((3, 0), [Kesimpulan]), + ) + }, + caption: [Diagram alur penelitian], +) + +Alur penelitian ada pada @gambar-alur. + += Hasil dan Pembahasan + +Sajikan hasil dan jelaskan artinya. Tabel diberi judul di atasnya, seperti +@tabel-hasil. + +#figure( + table( + columns: 3, + [*No*], [*Pengujian*], [*Hasil*], + [1], [Pengujian pertama], [Berhasil], + [2], [Pengujian kedua], [Berhasil], + ), + caption: [Hasil pengujian], +) + +#figure( + ```python + def halo(nama): + return f"Halo, {nama}!" + ```, + caption: [Contoh kode program], +) + += Penutup + +== Kesimpulan + +Tulis jawaban singkat untuk setiap rumusan masalah pada @bab-pendahuluan. + +== Saran + +Tulis saran untuk penelitian atau pekerjaan berikutnya. Data lengkap ada pada +@lampiran-data. + +// Lampiran muncul setelah daftar pustaka dan diberi nomor sesuai urutannya. +#appendix(title: [Data Pengujian], label: )[ + Lampirkan data mentah, kuesioner, atau dokumen pendukung. +] + +#appendix(title: [Dokumentasi Kegiatan])[ + #figure( + rect(width: 6cm, height: 3cm, fill: luma(230)), + caption: [Foto kegiatan], + ) +] diff --git a/packages/preview/jilid/0.2.0/template/refs.bib b/packages/preview/jilid/0.2.0/template/refs.bib new file mode 100644 index 0000000000..a67eb2500e --- /dev/null +++ b/packages/preview/jilid/0.2.0/template/refs.bib @@ -0,0 +1,14 @@ +@article{einstein1905, + title = {Zur Elektrodynamik bewegter K\"{o}rper}, + volume = {322}, + ISSN = {1521-3889}, + url = {http://dx.doi.org/10.1002/andp.19053221004}, + DOI = {10.1002/andp.19053221004}, + number = {10}, + journal = {Annalen der Physik}, + publisher = {Wiley}, + author = {Einstein, A.}, + year = {1905}, + month = Jan, + pages = {891–921} +} \ No newline at end of file diff --git a/packages/preview/jilid/0.2.0/thumbnail.png b/packages/preview/jilid/0.2.0/thumbnail.png new file mode 100644 index 0000000000..aee18f2700 Binary files /dev/null and b/packages/preview/jilid/0.2.0/thumbnail.png differ diff --git a/packages/preview/jilid/0.2.0/typst.toml b/packages/preview/jilid/0.2.0/typst.toml new file mode 100644 index 0000000000..ea8714daca --- /dev/null +++ b/packages/preview/jilid/0.2.0/typst.toml @@ -0,0 +1,21 @@ +[package] +name = "jilid" +version = "0.2.0" +entrypoint = "lib.typ" +authors = ["Tri Rianto Utomo <@shuretokki>"] +license = "MIT AND MIT-0" +description = "Report, proposal, and thesis template for Indonesian universities." +repository = "https://github.com/shuretokki/jilid" +keywords = ["Indonesia", "laporan", "proposal", "skripsi", "report", "thesis", "university"] +categories = ["report", "thesis"] +disciplines = ["education"] +compiler = "0.15.0" +exclude = ["docs/*"] + +[template] +path = "template" +entrypoint = "main.typ" +thumbnail = "thumbnail.png" + +[tool.tytanic] +default.ppi = 50