├── sample-usage.pdf ├── gallery └── SampleIndex.png ├── typst.toml ├── tests ├── MultiEntryTest.typ ├── DisplayTest.typ ├── RangeDelimiterTest.typ └── RangeTest.typ ├── .gitignore ├── LICENSE ├── README.md ├── in-dexter.typ └── sample-usage.typ /sample-usage.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RolfBremer/in-dexter/HEAD/sample-usage.pdf -------------------------------------------------------------------------------- /gallery/SampleIndex.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RolfBremer/in-dexter/HEAD/gallery/SampleIndex.png -------------------------------------------------------------------------------- /typst.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "in-dexter" 3 | version = "0.7.2" 4 | authors = ["JKRB ", "in-dexter Contributors"] 5 | license = "Apache-2.0" 6 | description = "Create an 'hand picked' Index" 7 | entrypoint = "in-dexter.typ" 8 | repository = "https://github.com/RolfBremer/in-dexter" 9 | keywords = [ "index", "metadata" ] 10 | categories = [ "components" ] 11 | exclude = [ "gallery", "tests", "sample-usage.*", ".git*" ] 12 | compiler = "0.12.0" 13 | -------------------------------------------------------------------------------- /tests/MultiEntryTest.typ: -------------------------------------------------------------------------------- 1 | #import "../in-dexter.typ": * 2 | #set page("a6", flipped: true, numbering: "1") 3 | 4 | _A Test with multiple identical entries on the same page. 5 | It tests, if the entries are combined._ 6 | 7 | 8 | #index[DoubleEntry] 9 | #index[DoubleEntry] 10 | #index[DoubleEntry] 11 | 12 | #pagebreak() 13 | #index-main[DoubleEntry] 14 | 15 | 16 | == Index 17 | 18 | #columns(2)[ 19 | #make-index( 20 | use-bang-grouping: true, 21 | use-page-counter: true, 22 | sort-order: upper, 23 | ) 24 | ] 25 | -------------------------------------------------------------------------------- /tests/DisplayTest.typ: -------------------------------------------------------------------------------- 1 | #import "../in-dexter.typ": * 2 | #set page("a6", flipped: true, numbering: "1") 3 | 4 | _A Test for the Display parameter. 5 | The order of marked entries specifies, which display is used in the reference on the index page._ 6 | 7 | In this sample, the first entry for Gaga hides the displays of the other entries for Gaga. 8 | 9 | #index[Gaga] 10 | #index(display: "GagaDisplay")[Gaga] 11 | #index(display: "GagaDisplay2")[Gaga] 12 | 13 | 14 | == Index 15 | 16 | #columns(2)[ 17 | #make-index( 18 | use-bang-grouping: true, 19 | use-page-counter: true, 20 | sort-order: upper, 21 | ) 22 | ] 23 | -------------------------------------------------------------------------------- /tests/RangeDelimiterTest.typ: -------------------------------------------------------------------------------- 1 | #import "../in-dexter.typ": * 2 | #set page("a6", flipped: true, numbering: "1") 3 | 4 | _A Test with alternate range-delimiter._ 5 | 6 | 7 | #index(index-type: indextype.Start)[Entry1] 8 | #pagebreak() 9 | #pagebreak() 10 | #index(index-type: indextype.End)[Entry1] 11 | 12 | #pagebreak() 13 | 14 | = Index 15 | 16 | #columns(2)[ 17 | #make-index( 18 | use-bang-grouping: true, 19 | use-page-counter: true, 20 | sort-order: upper, 21 | range-delimiter: [ to ] 22 | ) 23 | ] 24 | 25 | #columns(2)[ 26 | #make-index( 27 | use-bang-grouping: true, 28 | use-page-counter: true, 29 | sort-order: upper, 30 | range-delimiter: [ --- ] 31 | ) 32 | ] 33 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # You may want to customize this file depending on your Operating System 2 | # and the editor that you use. 3 | # 4 | # We recommend that you use a Global Gitignore for files that are not related 5 | # to the project. (https://help.github.com/articles/ignoring-files/#create-a-global-gitignore) 6 | # Author: Rolf Bremer 7 | # OS 8 | # 9 | # Ref: https://github.com/github/gitignore/blob/master/Global/macOS.gitignore 10 | # Ref: https://github.com/github/gitignore/blob/master/Global/Windows.gitignore 11 | # Ref: https://github.com/github/gitignore/blob/master/Global/Linux.gitignore 12 | .DS_STORE 13 | Thumbs.db 14 | 15 | # Editors 16 | # 17 | # Ref: https://github.com/github/gitignore/blob/master/Global 18 | # Ref: https://github.com/github/gitignore/blob/master/Global/JetBrains.gitignore 19 | # Ref: https://github.com/github/gitignore/blob/master/Global/VisualStudioCode.gitignore 20 | .idea 21 | .vs 22 | .chrome 23 | 24 | # .vscode/* 25 | # !.vscode/settings.json 26 | # !.vscode/tasks.json 27 | # !.vscode/launch.json 28 | # !.vscode/extensions.json 29 | 30 | # Dependencies 31 | node_modules 32 | 33 | # Compiled files 34 | bin 35 | obj 36 | dist 37 | out 38 | 39 | # Office temp files 40 | ~$* 41 | 42 | # DrawIO Backup files 43 | .$*.drawio.bkp 44 | 45 | # VSCode 46 | .vscode 47 | 48 | # Unwanted PDF Files of the project 49 | index.pdf 50 | -------------------------------------------------------------------------------- /tests/RangeTest.typ: -------------------------------------------------------------------------------- 1 | #import "../in-dexter.typ": * 2 | #set page("a6", flipped: true, numbering: "1") 3 | 4 | #show heading.where(level: 1): it => context{ 5 | pagebreak(weak: true) 6 | it 7 | } 8 | 9 | 10 | _This is a test document for in-dexters range#index[Range] references. 11 | It is used to demonstrate and test in-dexters range references._ 12 | 13 | // Define shortcuts for start and end 14 | #let index-start = index.with(index-type: indextype.Start) 15 | #let index-end = index.with(index-type: indextype.End) 16 | 17 | 18 | = This is a StartEntry for the "Sample"-Entry Sample 19 | 20 | #index-start[Sample] 21 | #lorem(10) 22 | 23 | 24 | = Another "Sample"-Entry, this time marked as main entry. 25 | 26 | #index-main[Sample] 27 | #lorem(10) 28 | 29 | 30 | = This is the EndEntry for the "Sample"-Entry. 31 | 32 | #index-end[Sample] 33 | #lorem(10) 34 | 35 | 36 | = A StartEntry for "Ipsum". 37 | 38 | #index-start[Ipsum] 39 | #lorem(10) 40 | #index[Test] 41 | 42 | 43 | = The EndEntry for "Ipsum". 44 | 45 | #index-end[Ipsum] 46 | #lorem(10) 47 | 48 | 49 | = Another Page. 50 | 51 | #lorem(10) 52 | 53 | 54 | = Another StartEntry for "Ipsum". 55 | 56 | #index-start[Ipsum] 57 | #lorem(10) 58 | 59 | 60 | = A normal entry for "Ipsum". 61 | 62 | #index[Ipsum] 63 | #lorem(10) 64 | 65 | 66 | = A normal entry for "Ipsum". 67 | 68 | #index[Ipsum] 69 | #lorem(10) 70 | 71 | 72 | = The EndEntry for "Ipsum". 73 | 74 | #index-end[Ipsum] 75 | #lorem(10) 76 | 77 | 78 | = A StartEntry for Ipsum without a corresponding EndEntry. 79 | 80 | #index-start[Ipsum] 81 | 82 | 83 | 84 | = Another page. 85 | 86 | #lorem(10) 87 | 88 | 89 | = A cardinal entry with display. 90 | 91 | #index(display: "BolloDisplay")[Bollo] 92 | #index(display: "IpsumDisplay")[Ipsum] 93 | 94 | 95 | == Index 96 | 97 | _Here we render the Index for the document:_ 98 | 99 | #columns(2)[ 100 | #make-index( 101 | use-bang-grouping: true, 102 | use-page-counter: true, 103 | sort-order: upper, 104 | ) 105 | ] 106 | 107 | 108 | == Index without Continuation Symbols 109 | 110 | _Here we render the Index for the document:_ 111 | 112 | #columns(2)[ 113 | #make-index( 114 | use-bang-grouping: true, 115 | use-page-counter: true, 116 | sort-order: upper, 117 | spc: none, 118 | mpc: none 119 | ) 120 | ] 121 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # in-dexter 2 | 3 | Automatically create a handcrafted index in [typst](https://typst.app/). 4 | This typst component allows the automatic creation of an Index page with entries 5 | that have been manually marked in the document by its authors. This, in times 6 | of advanced search functionality, seems somewhat outdated, but a handcrafted index 7 | like this allows the authors to point the reader to just the right location in the 8 | document. 9 | 10 | ⚠️ Typst is in beta and evolving, and this package evolves with it. As a result, no 11 | backward compatibility is guaranteed yet. Also, the package itself is under development 12 | and fine-tuning. 13 | 14 | ## Table of Contents 15 | 16 | * [Usage](#usage) 17 | * [Importing the Component](#importing-the-component) 18 | * [Remarks for new version](#remarks-for-new-version) 19 | * [Marking Entries](#marking-entries) 20 | * [Generating the index page](#generating-the-index-page) 21 | * [Brief Sample Document](#brief-sample-document) 22 | * [Full Sample Document](#full-sample-document) 23 | * [Changelog](#changelog) 24 | * [v0.7.2](#v072) 25 | * [v0.7.1](#v071) 26 | * [v0.7.0](#v070) 27 | * [v0.6.1](#v061) 28 | * [v0.6.0](#v060) 29 | * [v0.5.3](#v053) 30 | * [v0.5.2](#v052) 31 | * [v0.5.1](#v051) 32 | * [v0.5.0](#v050) 33 | * [v0.4.3](#v043) 34 | * [v0.4.2](#v042) 35 | * [v0.4.1](#v041) 36 | * [v0.4.0](#v040) 37 | * [v0.3.2](#v032) 38 | * [v0.3.1](#v031) 39 | * [v0.3.0](#v030) 40 | * [v0.2.0](#v020) 41 | * [v0.1.0](#v010) 42 | * [v0.0.6](#v006) 43 | * [v0.0.5](#v005) 44 | * [v0.0.4](#v004) 45 | * [v0.0.3](#v003) 46 | * [v0.0.2](#v002) 47 | 48 | ## Usage 49 | 50 | ## Importing the Component 51 | 52 | To use the index functionality, the component must be available. This 53 | can be achieved by importing the package `in-dexter` into the project: 54 | 55 | Add the following code to the head of the document file(s) 56 | that want to use the index: 57 | 58 | ```typ 59 | #import "@preview/in-dexter:0.7.2": * 60 | ``` 61 | 62 | Alternatively it can be loaded from the file, if you have it copied into your project. 63 | 64 | ```typ 65 | #import "in-dexter.typ": * 66 | ``` 67 | 68 | ## Remarks for new version 69 | 70 | In previous versions (before 0.0.6) of in-dexter, it was required to hide the index 71 | entries with a show rule. This is not required anymore. 72 | 73 | ## Marking Entries 74 | 75 | To mark a word to be included in the index, a simple function can be used. In the 76 | following sample code, the word "elit" is marked to be included into the index. 77 | 78 | ```typ 79 | = Sample Text 80 | Lorem ipsum dolor sit amet, consectetur adipiscing #index[elit], sed do eiusmod tempor 81 | incididunt ut labore et dolore. 82 | ``` 83 | 84 | Nested entries can be created - the following would create an entry `adipiscing` with sub entry 85 | `elit`. 86 | 87 | ```typ 88 | = Sample Text 89 | Lorem ipsum dolor sit amet, consectetur adipiscing elit#index("adipiscing", "elit"), sed do eiusmod 90 | tempor incididunt ut labore et dolore. 91 | ``` 92 | 93 | The marking, by default, is invisible in the resulting text, while the marked word 94 | will still be visible. With the marking in place, the index component knows about 95 | the word, as well as its location in the document. 96 | 97 | ## Generating the Index Page 98 | 99 | The index page can be generated by the following function: 100 | 101 | ```typ 102 | = Index 103 | #columns(3)[ 104 | #make-index(title: none) 105 | ] 106 | ``` 107 | 108 | This sample uses the optional title, outline, and use-page-counter parameters: 109 | 110 | ```typ 111 | #make-index(title: [Index], outlined: true, use-page-counter: true) 112 | ``` 113 | 114 | The `make-index()` function takes three optional arguments: `title`, `outlined`, and `use-page-counter`. 115 | 116 | * `title` adds a title (with `heading`) and 117 | * `outlined` is `false` by default and is passed to the heading function 118 | * `use-page-counter` is `false` by default. If set to `true` it will use `counter(page).display()` for the page 119 | number text in the index instead of the absolute page position (the absolute position is still 120 | used for the actual link target) 121 | 122 | If no title is given the heading should never appear in the layout. 123 | Note: The heading is (currently) not numbered. 124 | 125 | The first sample emits the index in three columns. 126 | Note: The actual appearance depends on your template or other settings of your document. 127 | 128 | You can find a preview image of the resulting page 129 | on [in-dexter´s GitHub repository](https://github.com/RolfBremer/in-dexter). 130 | 131 | You may have noticed that some page numbers are displayed as bold. These are index entries 132 | which are marked as "main" entries. Such entries are meant to be the most important for 133 | the given entry. They can be marked as follows: 134 | 135 | ```typ 136 | #index(fmt: strong, [Willkommen]) 137 | ``` 138 | 139 | or you can use the predefined semantically helper function 140 | 141 | ```typ 142 | #index-main[Willkommen] 143 | ``` 144 | 145 | ### Brief Sample Document 146 | 147 | This is a very brief sample to demonstrate how in-dexter can be used. The next chapter 148 | contains a more fleshed out sample. 149 | 150 | ```typ 151 | #import "@preview/in-dexter:0.7.2": * 152 | 153 | 154 | = My Sample Document with `in-dexter` 155 | 156 | In this document the usage of the `in-dexter` package is demonstrated to create 157 | a hand picked #index[Hand Picked] index. This sample #index-main[Sample] 158 | document #index[Document] is quite short, and so is its index. 159 | 160 | 161 | = Index 162 | 163 | This section contains the generated Index. 164 | 165 | #make-index() 166 | ``` 167 | 168 | ### Full Sample Document 169 | 170 | ```typ 171 | #import "@preview/in-dexter:0.7.2": * 172 | 173 | #let index-main(..args) = index(fmt: strong, ..args) 174 | 175 | // Document settings 176 | #set page("a5") 177 | #set text(font: ("Arial", "Trebuchet MS"), size: 12pt) 178 | 179 | 180 | = My Sample Document with `in-dexter` 181 | 182 | In this document #index[Document] the usage of the `in-dexter` package #index[Package] 183 | is demonstrated to create a hand picked index #index-main[Index]. This sample document 184 | is quite short, and so is its index. So to fill this sample with some real text, 185 | let´s elaborate on some aspects of a hand picked #index[Hand Picked] index. So, "hand 186 | picked" means, the entries #index[Entries] in the index are carefully chosen by the 187 | author(s) of the document to point the reader, who is interested in a specific topic 188 | within the documents domain #index[Domain], to the right spot #index[Spot]. That's, how 189 | it should be; and it is quite different to what is done in this sample text, where the 190 | objective #index-main[Objective] was to put many different index markers 191 | #index[Markers] into a small text, because a sample should be as brief as possible, 192 | while providing enough substance #index[Substance] to demo the subject 193 | #index[Subject]. The resulting index in this demo is somewhat pointless 194 | #index[Pointless], because all entries are pointing to few different pages 195 | #index[Pages], due to the fact that the demo text only has few pages #index[Page]. 196 | That is also the reason for what we chose the DIN A5 #index[DIN A5] format, and we 197 | also continue with some remarks #index[Remarks] on the next page. 198 | 199 | 200 | == Some more demo content without deeper meaning 201 | 202 | #lorem(50) #index[Lorem] 203 | 204 | #pagebreak() 205 | 206 | == Remarks 207 | 208 | Here are some more remarks #index-main[Remarks] to have some content on a second page, what 209 | is a precondition #index[Precondition] to demo that Index #index[Index] entries 210 | #index[Entries] may point to multiple pages. 211 | 212 | 213 | = Index 214 | 215 | This section #index[Section] contains the generated Index #index[Index], in a nice 216 | two-column-layout. 217 | 218 | #set text(size: 10pt) 219 | #columns(2)[ 220 | #make-index() 221 | ] 222 | ``` 223 | 224 | The following image shows a generated index page of another document, with additional 225 | formatting for headers applied. 226 | 227 | ![Sample for a generated index page.](gallery/SampleIndex.png) 228 | 229 | More usage samples are shown in the document `sample-usage.typ` on 230 | [in-dexter´s GitHub](https://github.com/RolfBremer/in-dexter). 231 | 232 | A more complex sample PDF is available there as well. 233 | 234 | 235 | 236 | ## Changelog 237 | 238 | ### v0.7.2 239 | 240 | * Add support for customization of the letter section title, the letter section body and their container. 241 | (see `make-index`, parameters `section-title`, `section-body`, `section-container` ). 242 | 243 | ### v0.7.1 244 | 245 | * Add support for further formatting of the resulting index to address, for example, the indentation 246 | of long entries. See `surround` parameter of `make-index`. The default for surround uses a par environment 247 | to beautify wrapped entries (`set par(first-line-indent: 0pt, spacing: 0.65em, hanging-indent: 1em)`). 248 | * Bump minimum required Typst version to 12.0. 249 | 250 | ### v0.7.0 251 | 252 | * Breaking: Change enum dict "indexTypes" to "indextype" to comply with the typst naming conventions. 253 | * Breaking: Change parameter "indexType" to "index-type" to comply with the typst naming conventions. 254 | 255 | ### v0.6.1 256 | 257 | * Configurable range delimiter. Defaults to em-dash now. 258 | 259 | ### v0.6.0 260 | 261 | * Support for Ranges and Continuations (range references, f. ff.). 262 | * Fix: Consolidate multiple references to the same page. 263 | 264 | ### v0.5.3 265 | 266 | * Fix error in typst.toml file. 267 | * Add a sample for raw display. 268 | 269 | ### v0.5.2 270 | 271 | * Fix a bug with bang notation. 272 | * Add compiler version to toml file. 273 | 274 | ### v0.5.1 275 | 276 | * Migrate deprecated locate to context. 277 | 278 | ### v0.5.0 279 | 280 | * Support page numbering formats (i.e. roman), when `use-page-counter` is set to 281 | true. Thanks to @ThePuzzlemaker! 282 | 283 | ### v0.4.3 284 | 285 | * Suppress extra space character emitted by the `index()` function. 286 | * Fix a bug where math formulas are not displayed. 287 | * Introduce `apply-casing` parameter to `index()` to suppress entry-casing for individual 288 | entries. 289 | 290 | ### v0.4.2 291 | 292 | * Improve internal method `as-text` to be more robust. 293 | * tidy up sample-usage.typ. 294 | 295 | ### v0.4.1 296 | 297 | * Bug fixed: Fix a bug where an index entry with same name as a group hides the group. 298 | * Fixed typos in the sample-usage document. 299 | 300 | ### v0.4.0 301 | 302 | * Support for a `display` parameter for entries. This allows the usage of complex 303 | content, like math expressions in the index. (based on suggestions by @lukasjuhrich) 304 | * Also support a tuple value for display and key parameters of the entry. 305 | * Improve internal robustness and fix some errors in the sample document. 306 | 307 | ### v0.3.2 308 | 309 | * Fix initial parsing and returning fist letter (thanks to @lukasjuhrich, #14) 310 | 311 | ### v0.3.1 312 | 313 | * Fix handling of trailing or multiple spaces and crlf in index entries. 314 | 315 | ### v0.3.0 316 | 317 | * Support multiple named indexes. Also allow the generation of 318 | combined index pages. 319 | * Support for LaTeX index group syntax (`#index("Group1!Group2@Entry"`). 320 | * Support for advanced case handling for the entries in the index. Note: The new default 321 | ist to ignore the casing for the sorting of the entries. The behavior can be changed by 322 | providing a `sort-order()` function to the `make-index` function. 323 | * The casing for the index entry can also be altered by providing a `entry-casing()` 324 | function to the `make-index` function. So it is possible that all entries have an 325 | uppercase first letter (which is also the new default!). 326 | 327 | ### v0.2.0 328 | 329 | * Allow index to respect unnumbered physical pages at the start of the 330 | document (Thanks to @jewelpit). See "Skipping physical pages" in the 331 | sample-usage document. 332 | 333 | ### v0.1.0 334 | 335 | * big refactor (by @epsilonhalbe). 336 | * changing "marker classes" to support direct format 337 | function `fmt: content -> content` e.g. `index(fmt: strong, [entry])`. 338 | * Implemented: 339 | * nested entries. 340 | * custom initials + custom sorting. 341 | 342 | ### v0.0.6 343 | 344 | * Change internal index marker to use metadata instead of figures. This 345 | allows a cleaner implementation and does not require a show rule to hide 346 | the marker-figure anymore. 347 | * This version requires Typst 0.8.0 due to the use of metadata(). 348 | * Consolidated the `PackageReadme.md` into a single `README.md`. 349 | 350 | ### v0.0.5 351 | 352 | * Address change in `figure.caption` in typst (commit: 976abdf ). 353 | 354 | ### v0.0.4 355 | 356 | * Add title and outline arguments to #make-index() by @sbatial in #4 357 | 358 | ### v0.0.3 359 | 360 | * Breaking: Renamed the main file from `index.typ` to `in-dexter.typ` to match package. 361 | * Added a Changelog to this README. 362 | * Introduced a brief and a full sample code to this README. 363 | * Added support for package manager in Typst. 364 | 365 | ### v.0.0.2 366 | 367 | * Moved version to GitHub. 368 | -------------------------------------------------------------------------------- /in-dexter.typ: -------------------------------------------------------------------------------- 1 | // Copyright 2023, 2024 Rolf Bremer, Jutta Klebe 2 | // Use of this code is governed by the License in the LICENSE.txt file. 3 | // For a 'how to use this package', see the accompanying .md, .pdf + .typ documents. 4 | 5 | #let indextype = ( 6 | Start: "Start", 7 | End: "End", 8 | Cardinal: "Cardinal", 9 | ) 10 | 11 | // Adds a new entry to the index 12 | // @param fmt: function: content -> content 13 | // @param index-type: indextype.Cardinal, indextype.Start, indextype.End. 14 | // @param initial: "letter" to sort entries under - otherwise first letter of entry is used, 15 | // useful for indexing umlauts or accented letters with their unaccented versions or 16 | // symbols under a common "Symbols" headline 17 | // @param index: Name of the index to add the entry to. Default is "Default". 18 | // @param display: If given, this will be the displayed content in the index page (instead of the key entry). 19 | // @param apply-casing: If set to auto (default) the parameter from make-index is used. 20 | // Otherwise it overwrites the setting. Set to false for entries starting with a formula! 21 | // @param ..entry, variable argument to nest index entries (left to right). Only the last, rightmost 22 | // entry is the key for the entry. The others are used for grouping. 23 | #let index( 24 | fmt: it => it, 25 | index-type: indextype.Cardinal, 26 | initial: none, 27 | index: "Default", 28 | display: auto, 29 | apply-casing: auto, 30 | ..entry, 31 | ) = ( 32 | context { 33 | let loc = here() 34 | [#metadata(( 35 | fmt: fmt, 36 | index-type: index-type, 37 | initial: initial, 38 | index-name: index, 39 | location: loc.position(), 40 | page-counter: counter(page).at(loc).at(0), 41 | page-counter-end: counter(page).at(loc).at(0), 42 | page-numbering: loc.page-numbering(), 43 | entry: entry, 44 | display: display, 45 | apply-casing: apply-casing, 46 | ))] 47 | } 48 | ) 49 | 50 | // Default function to semantically mark main entries in the index 51 | #let index-main = index.with(fmt: strong) 52 | 53 | #let as-text(input) = { 54 | let t = type(input) 55 | if input == none or input == auto { 56 | "" 57 | } else if t == str { 58 | input 59 | } else if t == label { 60 | repr(input) 61 | } else if t == int { 62 | str(input) 63 | } else if t == content { 64 | if input.has("text") { 65 | input.text 66 | } else if input.has("children") { 67 | if input.children.len() == 0 { 68 | "" 69 | } else { 70 | input.children.map(child => as-text(child)).join("") 71 | } 72 | } else if input.has("body") { 73 | as-text(input.body) 74 | } else { 75 | " " 76 | } 77 | } else { 78 | panic("Unexpected entry type " + t + " of " + repr(input)) 79 | } 80 | } 81 | 82 | // Internal function to set plain and nested entries 83 | #let make-entries( 84 | entries, 85 | page-link, 86 | reg-entry, 87 | use-bang-grouping, 88 | apply-casing, 89 | ) = { 90 | // Handling LaTeX nested entry syntax (only if it is the last entry) 91 | 92 | if use-bang-grouping and entries.len() == 1 { 93 | let entry = entries.at(0) 94 | if type(entry.key) == str and entry.key.len() > 0 { 95 | let disp = entry.display 96 | entries = entry.key.split("!").map(e => { 97 | let d = if type(disp) == str and disp.contains(regex("!\w+")) { 98 | e 99 | } else { 100 | disp 101 | } 102 | (display: d, key: e) 103 | }) 104 | 105 | if entries.last().key == "" { 106 | let emptyKeyIndex = entries.position(e => e.key == "") 107 | let bangCount = 0 108 | if emptyKeyIndex != none { 109 | bangCount = entries.len() - emptyKeyIndex 110 | } 111 | entries = entries.filter(e => e.key != "") 112 | entries.last() = (display: entries.last().display + "!" * bangCount, key: entries.last().key) 113 | } 114 | } 115 | } 116 | 117 | let (entry, ..rest) = entries 118 | 119 | if rest.len() > 0 { 120 | let nested-entries = reg-entry.at(entry.key, default: (:)) 121 | let ref = make-entries( 122 | rest, 123 | page-link, 124 | nested-entries.at("nested", default: (:)), 125 | use-bang-grouping, 126 | apply-casing, 127 | ) 128 | nested-entries.insert("nested", ref) 129 | reg-entry.insert(entry.key, nested-entries) 130 | } else { 131 | let key = if type(entry.key) == str { 132 | entry.key 133 | } else { 134 | as-text(entry.key) 135 | } 136 | if type(key) == content { 137 | panic("Entry must be string compatible. Consider specifying as display parameter.") 138 | } 139 | let current-entry = reg-entry.at( 140 | key, 141 | default: ( 142 | display: entry.display, 143 | pages: (), 144 | apply-casing: apply-casing, 145 | ), 146 | ) 147 | let pages = current-entry.at("pages", default: ()) 148 | let pageRanges = pages.map(p => { 149 | if p.index-type == indextype.Start and page-link.index-type == indextype.End { 150 | p.index-type = "Range" 151 | p.rangeEnd = page-link.page 152 | p.page-counter-end = page-link.page-counter 153 | } 154 | return p 155 | }) 156 | let searchFunc = p => { 157 | p.index-type == "Range" and p.rangeEnd == page-link.page or p.index-type == indextype.Cardinal and p.page == page-link.page and p.fmt == page-link.fmt 158 | } 159 | let foundRange = pageRanges.find(searchFunc) 160 | if foundRange == none { 161 | pageRanges.push(page-link) 162 | } 163 | current-entry.insert("pages", pageRanges) 164 | reg-entry.insert(key, current-entry) 165 | } 166 | reg-entry 167 | } 168 | 169 | // Internal function to collect plain and nested entries into the index 170 | #let references(indexes, use-bang-grouping, sort-order) = { 171 | let register = (:) 172 | let initials = (:) 173 | 174 | for indexed in query() { 175 | let ( 176 | fmt, 177 | index-type, 178 | initial, 179 | index-name, 180 | location, 181 | page-counter, 182 | page-counter-end, 183 | page-numbering, 184 | entry, 185 | display, 186 | apply-casing, 187 | ) = indexed.value 188 | 189 | if (indexes != auto) and (not indexes.contains(index-name)) { 190 | continue 191 | } 192 | 193 | // Handle tuple as (display, key) 194 | let entries = entry.pos().map(e => { 195 | if type(e) == content { 196 | e = as-text(e) 197 | } 198 | let disp = if display == auto { 199 | e 200 | } else { 201 | display 202 | } 203 | 204 | let k = none 205 | if type(e) == array { 206 | if e.len() == 2 { 207 | disp = e.at(0) 208 | k = e.at(1) 209 | } 210 | } else { 211 | k = as-text(e) 212 | } 213 | (display: disp, key: k) 214 | }) 215 | 216 | if entries.len() == 0 { 217 | panic("expected entry to have at least one entry to add to the index") 218 | } else { 219 | let initial-letter = if initial == none { 220 | let fe = sort-order(entries.first().at("key", default: "?")) 221 | let fe2 = if type(fe) == str { 222 | fe 223 | } else { 224 | as-text(fe) 225 | } 226 | if type(fe2) != str { 227 | panic("Content cannot be converted to string. Use `initial` or `display` parameter for the entry.") 228 | } 229 | let first-letter = fe2.first() 230 | initials.insert(first-letter, first-letter) 231 | first-letter 232 | } else { 233 | if (type(initial) == dictionary) { 234 | let letter = sort-order(initial.at("letter")) 235 | let sort-by = initial.at("sort-by", default: letter) 236 | initials.insert(sort-by, letter) 237 | letter 238 | } else if (type(initial) == str) { 239 | let first-letter = sort-order(initial.first()) 240 | initials.insert(first-letter, first-letter) 241 | first-letter 242 | } else { 243 | panic("Expected initial to be either a 'string' or '(letter: , sort-by: )'") 244 | } 245 | 246 | } 247 | let reg-entry = register.at(initial-letter, default: (:)) 248 | register.insert( 249 | initial-letter, 250 | make-entries( 251 | entries, 252 | ( 253 | page: location.page, 254 | rangeEnd: location.page, 255 | fmt: fmt, 256 | index-type: index-type, 257 | page-counter: page-counter, 258 | page-counter-end: page-counter-end, 259 | page-numbering: page-numbering, 260 | ), 261 | reg-entry, 262 | use-bang-grouping, 263 | apply-casing, 264 | ), 265 | ) 266 | } 267 | } 268 | (register: register, initials: initials) 269 | } 270 | 271 | 272 | // Internal function to format a page link 273 | #let render-link( 274 | use-page-counter, 275 | range-delimiter, 276 | spc, 277 | mpc, 278 | (page, rangeEnd, index-type, fmt, page-counter, page-counter-end, page-numbering), 279 | ) = { 280 | if page-numbering == none { 281 | page-numbering = "1" 282 | } 283 | let resPage = if use-page-counter { 284 | numbering(page-numbering, page-counter) 285 | } else { 286 | str(page) 287 | } 288 | let resEndPage = if use-page-counter { 289 | numbering(page-numbering, page-counter-end) 290 | } else { 291 | str(rangeEnd) 292 | } 293 | let t = if index-type == "Range" { 294 | if rangeEnd - page == 1 { 295 | if spc != none { 296 | resPage + spc 297 | } else { 298 | resPage + range-delimiter + resEndPage 299 | } 300 | } else if rangeEnd == page { 301 | resPage 302 | } else { 303 | resPage + range-delimiter + resEndPage 304 | } 305 | } else if index-type == indextype.Start { 306 | resPage + mpc 307 | } else { 308 | resPage 309 | } 310 | link((page: page, x: 0pt, y: 0pt), fmt[#t]) 311 | } 312 | 313 | 314 | // First letter casing 315 | #let first-letter-up(entry) = { 316 | if type(entry) == str and entry.len() > 0 { 317 | upper(entry.first()) + entry.clusters().slice(1).join() 318 | } 319 | } 320 | 321 | #let apply-entry-casing(display, entry-casing, apply-casing) = { 322 | let applied = state("applied", false) 323 | applied.update(false) 324 | show text: it => ( 325 | context { 326 | let t = it.text 327 | let transformed = entry-casing(t) 328 | if t == transformed or applied.get() == true { 329 | it 330 | } else { 331 | if apply-casing == false { 332 | it 333 | } else { 334 | let t2 = entry-casing(transformed) 335 | if t2 != transformed { 336 | panic("entry-casing not idempotent:" + repr(t) + "~>" + repr(transformed) + "~>" + repr(t2)) 337 | } 338 | applied.update(true) 339 | transformed 340 | } 341 | } 342 | } 343 | ) 344 | display 345 | } 346 | 347 | // Internal function to format a plain or nested entry 348 | #let render-entry(idx, entry, lvl, use-page-counter, sort-order, entry-casing, range-delimiter, spc, mpc) = { 349 | let pages = entry.at("pages", default: ()) 350 | let display = entry.at("display", default: idx) 351 | let render-function = render-link.with(use-page-counter, range-delimiter, spc, mpc) 352 | 353 | let rendered-pages = { 354 | let p = pages.map(render-function) 355 | box(width: lvl * 1em) 356 | apply-entry-casing( 357 | display, 358 | entry-casing, 359 | entry.at("apply-casing", default: auto), 360 | ) 361 | box(width: 1fr) 362 | p.join(", ") 363 | parbreak() 364 | } 365 | 366 | let sub-entries = entry.at("nested", default: (:)) 367 | 368 | let rendered-entries = if sub-entries.keys().len() > 0 [ 369 | #for key in sub-entries.keys().sorted(key: sort-order) [ 370 | #render-entry(key, sub-entries.at(key), lvl + 1, use-page-counter, sort-order, entry-casing, range-delimiter, spc, mpc) 371 | ] 372 | ] 373 | [ 374 | #rendered-pages 375 | #rendered-entries 376 | ] 377 | } 378 | 379 | 380 | // Inserts the index into the document 381 | // @param title (default: none) sets the title of the index to use. 382 | // @param outlined (default: false) if index is shown in outline (table of contents). 383 | // @param use-page-counter (default: false) use the value of the page counter for page number text. 384 | // @param use-bang-grouping (default: false) support the LaTeX bang grouping syntax. 385 | // @param sort-order (default: upper) a function to control how the entry is sorted. 386 | // @param entry-casing (default: first-letter-up) a function to control how the 387 | // entry key is displayed (when no display parameter is provided). 388 | // @param range-delimiter (default: [--]) the delimiter to use in ranges, like the 389 | // em-dash in "1--42". 390 | // @param spc (default: "f.") Symbol(s) to mark a Single-Page-Continuation. 391 | // If set to none, a numeric range is used instead, like "42-43". 392 | // @param mpc (default: "ff.") Symbol(s) to mark a Multi-Page-Continuation. 393 | // @param indexes (default: auto) optional name(s) of the index(es) to use. Auto uses all indexes. 394 | // @param surround (default: body) a function to surround the index body with additional content/settings. 395 | // @param section-container (default: section) a function to wrap the whole section in a container. 396 | // @param section-title (default: heading(level: 2, numbering: none, outlined: false, letter)) 397 | // @param section-body (default: body) a function to render the body of the section. 398 | #let make-index( 399 | title: none, 400 | outlined: false, 401 | use-page-counter: false, 402 | use-bang-grouping: false, 403 | sort-order: k => upper(k), 404 | entry-casing: k => first-letter-up(k), 405 | spc: "f.", 406 | mpc: "ff.", 407 | range-delimiter: [--], 408 | indexes: auto, 409 | surround: (body) => { 410 | set par(first-line-indent: 0pt, spacing: 0.65em, hanging-indent: 1em) 411 | body 412 | }, 413 | section-container: (section) => {section}, 414 | section-title: (letter, counter) => heading(level: 2, numbering: none, outlined: false, letter), 415 | section-body: (letter, counter, body) => {body} 416 | ) = ( 417 | context { 418 | surround({ 419 | let (register, initials) = references(indexes, use-bang-grouping, sort-order) 420 | 421 | if title != none { 422 | heading( 423 | outlined: outlined, 424 | numbering: none, 425 | title, 426 | ) 427 | } 428 | 429 | let lastPage = counter(page).get().last() 430 | let effective-mpc = if mpc == none { 431 | range-delimiter + str(lastPage) 432 | } else { 433 | mpc 434 | } 435 | 436 | let counter = 0 437 | for initial in initials.keys().sorted() { 438 | let letter = initials.at(initial) 439 | section-container({ 440 | section-title(letter, counter) 441 | section-body(letter, counter, { 442 | counter += 1 443 | let entry = register.at(letter) 444 | // sort and render entries 445 | for idx in entry.keys().sorted(key: sort-order) { 446 | render-entry(idx, entry.at(idx), 0, use-page-counter, sort-order, entry-casing, range-delimiter, spc, effective-mpc) 447 | } 448 | }) 449 | }) 450 | } 451 | }) 452 | } 453 | ) -------------------------------------------------------------------------------- /sample-usage.typ: -------------------------------------------------------------------------------- 1 | #import "./in-dexter.typ": * 2 | 3 | // This typst file demonstrates the usage of the in-dexter package. 4 | 5 | // Setup the document 6 | #set text(lang: "en", font: "Arial", size: 10pt) 7 | #set heading(numbering: "1.1") 8 | 9 | #set page( 10 | numbering: "1", 11 | footer: align(right)[#context { 12 | counter(page).display("1") 13 | }], 14 | ) 15 | 16 | #show heading: it => { 17 | let level = it.level 18 | let above = if level <= 3 { 2em } else if level <= 5 { 1.8em } else { 1.4em } 19 | set block(above: above) 20 | set text(blue) 21 | it 22 | } 23 | 24 | // Defining handy names for separate indexes to use with in-dexter in 25 | // this document. this is easier as having to type the index parameter 26 | // on each entry. 27 | #let index2 = index.with(index: "Secondary") 28 | #let index3 = index.with(index: "Tertiary") 29 | #let indexMath = index.with(index: "Math") 30 | 31 | // Front Matter 32 | #align(center)[ 33 | #text(size: 23pt)[in-dexter] 34 | #linebreak() #v(1em) 35 | #text(size: 16pt)[An index package for Typst] 36 | #linebreak() #v(.5em) 37 | #text(size: 12pt)[Version 0.7.2 (26.6.2025)] 38 | #linebreak() #v(.5em) 39 | #text(size: 10pt)[Rolf Bremer, Jutta Klebe] 40 | #linebreak() #v(.5em) 41 | #text(size: 10pt)[Contributors: \@epsilonhalbe, \@jewelpit, \@sbatial, \@lukasjuhrich, \@ThePuzzlemaker, \@hurzl, \@fungai2000, \@Iriamu19 ] 42 | #v(4em) 43 | ] 44 | 45 | 46 | // Table of Content 47 | #outline(indent: auto, title: [Content]) 48 | 49 | 50 | = Sample Document to Demonstrate the in-dexter package 51 | 52 | This document explains how to use the `ìn-dexter` package in typst. It contains several 53 | samples of how to use `in-dexter` to effectively index a document. Make sure to look up 54 | the typst code of this document to explore, what the package can do. 55 | 56 | Using the in-dexter package in a typst document consists of some simple steps: 57 | 58 | + Importing the package `in-dexter`. 59 | + Marking the words or phrases to include in an index. 60 | + Generating the index page(s) by calling the `make-index()` function. 61 | 62 | 63 | == Importing the Package 64 | 65 | The in-dexter package is currently available on GitHub in its home repository 66 | (https://github.com/RolfBremer/in-dexter). It is still in development and may have 67 | breaking changes #index[Breaking Changes] in its next iteration. 68 | #index[Iteration]#index[Development] 69 | 70 | ```typ 71 | #import "./in-dexter.typ": * 72 | ``` 73 | 74 | The package is also available via Typst's built-in Package Manager: 75 | 76 | ```typ 77 | #import "@preview/in-dexter:0.7.2": * 78 | ``` 79 | 80 | Note, that the version number of the typst package has to be adapted to get the wanted 81 | version. It may take some time for a new version to appear in the typst universe after it 82 | is available on GitHub. 83 | 84 | 85 | == Marking of Entries 86 | 87 | // This marks the start of the range for the Entry-Key "Entries" 88 | #index(index-type: indextype.Start)[Entries] 89 | 90 | // This marks the start of the range for the Entry-Key "Entry-Marker" 91 | #index(index-type: indextype.Start)[Entry-Marker] 92 | 93 | We have marked several words to be included in an index page. The markup for the entry 94 | stays invisible#index[Invisible]. Its location in the text gets recorded, and later it is 95 | shown as a page reference in the index page.#index([Index Page]) 96 | 97 | ```typ 98 | #index[The Entry Phrase] 99 | ``` 100 | 101 | or 102 | 103 | ```typ 104 | #index([The Entry Phrase]) 105 | ``` 106 | 107 | or 108 | 109 | ```typ 110 | #index("The Entry Phrase") 111 | ``` 112 | 113 | Entries marked this way are going to the "Default" Index. If only one index is needed, 114 | this is the only way needed to mark entries. In-dexter can support multiple Indexes 115 | #index[Multiple Indexes]. To specify the target index for a marking, the index must be 116 | addressed. 117 | 118 | ```typ 119 | #index(index: "Secondary")[The Entry Phrase] 120 | ``` 121 | 122 | This is the explicit addressing of the secondary index. 123 | It may be useful to define a function for the alternate index, to avoid the explicitness: 124 | 125 | 126 | ```typ 127 | #let index2 = index.with(index:"Secondary") 128 | 129 | #index2[One Entry Phrase] 130 | #index2[Another Entry Phrase] 131 | ``` 132 | 133 | 134 | === Nested entries 135 | 136 | Entries can be nested. The `index` function takes multiple arguments - one for each 137 | nesting level. 138 | 139 | ```typ 140 | #index("Sample", "medical", "tissue") 141 | #index("Sample", "musical", "piano") 142 | #index("Sample") 143 | ``` 144 | 145 | #index("Sample", "medical", "tissue") 146 | #index("Sample", "musical", "piano") 147 | #index("Sample") 148 | 149 | 150 | ==== LaTeX Style index grouping 151 | 152 | Alternatively or complementing to the grouping syntax above, the "bang style" syntax known 153 | from LaTeX can be used: 154 | 155 | // TODO: Make nice samples 156 | ```typ 157 | #index("Sample!medical!X-Ray") 158 | ``` 159 | 160 | #index("Sample!medical!X-Ray") 161 | 162 | They can even be combined: 163 | 164 | ```typ 165 | #index("CombiGroup", "Sample!musical!Chess!") 166 | ``` 167 | 168 | #index("CombiGroup", "Sample!musical!Chess!") 169 | 170 | Note that the last bang is not handled as a separator, but is part of the entry. To use 171 | the bang grouping syntax, the `make-index()` function must be called with the parameter 172 | `use-bang-grouping: true`: 173 | 174 | ```typ 175 | #make-index(use-bang-grouping: true) 176 | ``` 177 | 178 | 179 | === Entries with display 180 | 181 | These entries use an explicit display parameter. It is used to display the entry on the 182 | index page. It can contain rich content, like math expressions: 183 | 184 | ```typ 185 | #index(display: "Level3", "Aaa-set3!l2!l3") 186 | #indexMath(display: [$cal(T)_n$-set], "Aa-set") 187 | #indexMath(display: [$cal(T)^n$-set], "Aa-set4") 188 | ``` 189 | 190 | #index(display: "Level3", "Aaa-set3!l2!l3") 191 | #indexMath(display: [$cal(T)_n$-set], "Aa-set") 192 | #indexMath(display: [$cal(T)^n$-set], "Aa-set4") 193 | 194 | Note that display may be ignored, if entries with the same entry key are defined 195 | beforehand. The first occurrence of an entry defines the display of all other entries with 196 | that entry key. 197 | 198 | 199 | === Advanced entries 200 | 201 | Simple math expressions can be used as entry key, like the following sample, where we also 202 | provide an initial parameter to put sort the entry under "t" in the index: 203 | 204 | ```typ 205 | #indexMath(initial: "t")[$t$-tuple] 206 | ``` 207 | 208 | #indexMath(initial: "t")[$t$-tuple] 209 | 210 | but note, that more complex ones may not be convertible to a string by in-dexter. In such 211 | cases it is recommended to use the display parameter instead: 212 | 213 | ```typ 214 | #indexMath(initial: "t", display: [$cal(T)_n$-c], "Tnc") 215 | ``` 216 | 217 | #indexMath(initial: "t", display: [$cal(T)_n$-c], "Tnc") 218 | 219 | this will put the entry in the "t" section, and uses the key ("Tnc") as sort key within 220 | that 't' section. The entry is displayed as `$cal(T)_n$`. 221 | 222 | The following entry will place the entry in the "D" section, because we have not provided 223 | an explicit `initial` parameter, so the section is derived from the keys first letter 224 | ("DTN"). 225 | 226 | ```typ 227 | #indexMath(display: [d-$cal(T)_n$], "DTN") 228 | ``` 229 | 230 | #index(display: [d-$cal(T)_n$], "DTN") 231 | 232 | 233 | The index-function to mark entries also accepts a tuple value: 234 | 235 | ```typ 236 | #indexMath(([d-$rho_x$], "RTX")) 237 | ``` 238 | 239 | #indexMath(([d-$rho_x$], "RTX")) 240 | 241 | The first value of the tuple is interpreted as the `display`, the second as the `key` 242 | parameter. 243 | 244 | 245 | // More samples and tests 246 | #indexMath(([d-$phi_x^2*sum(d)$], "DPX")) 247 | 248 | #index(index-type: indextype.End)[Entry-Marker] 249 | 250 | 251 | ==== Suppressing the casing for formulas 252 | 253 | Sometimes, the entry-casing of the `make-index()` function should not apply to an entry. 254 | This is often the case for math formulas. The `index()` function therefore has a parameter 255 | `apply-casing`, that allows to suppress the application of the entry-casing function for 256 | this specific entry. 257 | 258 | ```typ 259 | #index(display: $(n, k)"-representable"$, "nkrepresentable", apply-casing: false) 260 | ``` 261 | 262 | #index(display: $(n, k)"-representable"$, "nkrepresentable", apply-casing: false) 263 | 264 | 265 | Note: If multiple entries have the same key, but different apply-casing flags, the first 266 | one wins. 267 | 268 | ```typ 269 | #index(display: $(x, p)"-double"$, "xprepresentable", apply-casing: false) 270 | #index(display: $(x, p)"-double"$, "xprepresentable", apply-casing: true) 271 | ``` 272 | 273 | #index(display: $(x, p)"-double"$, "xprepresentable", apply-casing: false) 274 | #index(display: $(x, p)"-double"$, "xprepresentable", apply-casing: true) 275 | 276 | 277 | ==== Symbols 278 | 279 | Symbols can be indexed to be sorted under `"Symbols"`, and be sorted at the top of the 280 | index like this 281 | 282 | ```typ 283 | #indexMath(initial: (letter: "Symbols", sort-by: "#"), [$(sigma)$]) 284 | ``` 285 | 286 | #indexMath(initial: (letter: "Symbols", sort-by: "#"), [$(sigma)$]) 287 | 288 | 289 | ==== Formatting Entries 290 | 291 | #index(fmt: strong, [Formatting Entries]) 292 | 293 | Entries can be formatted with arbitrary functions that map `content` to `content` 294 | 295 | ```typ 296 | #index(fmt: it => strong(it), [The Entry Phrase]) 297 | ``` 298 | 299 | or shorter 300 | 301 | ```typ 302 | #index(fmt: strong, [The Entry Phrase]) 303 | ``` 304 | 305 | For convenience in-dexter exposes `index-main` which formats the entry bold. It is 306 | semantically named to decouple the markup from the actual style. One can decide to have 307 | the main entries slanted or color formatted, which makes it clear that the style should 308 | not be part of the function name in markup. Naming markup functions according to their 309 | purpose (semantically) also eases the burden on the author, because she must not remember 310 | the currently valid styles for her intent. 311 | 312 | Another reason to use semantically markup functions is to have them defined in a central 313 | place. Changing the style becomes very easy this way. 314 | 315 | ```typ 316 | #index-main[The Entry Phrase] 317 | ``` 318 | 319 | It is predefined in in-dexter like this: 320 | 321 | ```typ 322 | #let index-main = index.with(fmt: strong) 323 | ``` 324 | 325 | Here we define another semantical index marker, which adds an "ff." to the page number. 326 | 327 | ```typ 328 | #let index-ff = index.with(fmt: it => [#it _ff._]) 329 | ``` 330 | 331 | #let index-ff = index.with(fmt: it => [#it _ff._]) 332 | 333 | 334 | ==== Wrapping long entries 335 | 336 | Sometimes, an index entry is too long to fit on a single line. In such cases, the 337 | `in-dexter` package will not apply the `hanging-indent` and `first-line-indent` parameters 338 | to the entry, so that the first line is indented, and the following lines are hanging 339 | indented. Instead, the entry will be displayed as a single line, which may lead to entries 340 | that are not properly aligned in the index page. 341 | 342 | This is an example of the hanging-indent#index("Hanging indent", "a longer line that will 343 | wrap around to a new line") problem in the index: 344 | 345 | ```typ 346 | #index("Hanging indent", "a longer line that will wrap around to a new line") 347 | ``` 348 | 349 | To solve this, the `make-index()` function has a parameter `surround`, which can be used 350 | to wrap the entries in a `par` environment. This will apply the `first-line-indent` and 351 | `hanging-indent` as well as the `spacing` parameters to the index page, so that long entries 352 | will be wrapped correctly. The following sample uses a `par` with `first-line-indent: 0pt` 353 | and `hanging-indent: 1em` to achieve this: 354 | 355 | ```typ 356 | #make-index( 357 | use-bang-grouping: true, 358 | use-page-counter: true, 359 | sort-order: upper, 360 | surround: (body) => { 361 | set par(first-line-indent: 0pt, spacing: 0.65em, hanging-indent: 1em) 362 | body 363 | }, 364 | ) 365 | ``` 366 | 367 | Note: The shown `surround` function is the default for the `make-index()` function. It can 368 | be customized to use different parameters for the `par` environment, or to use a different 369 | environment altogether. 370 | 371 | The parameter `body` in the `surround` function is the content of the resulting index. 372 | 373 | 374 | ==== Referencing Ranges and Continuations 375 | 376 | Up to this point, we used Cardinal Markers#index[Cardinal Markers] to mark the index 377 | entries. They are referred to with their single page number from the index page. But 378 | `in-dexter` also supports more advanced references to marked entries, like the following: 379 | 380 | - Ranges of Pages: 381 | - 42-46 382 | - 42-46, 52-59 383 | 384 | - Single Page Continuation (SPC): 385 | - 77f. 386 | - 77+ 387 | 388 | - Multi-Page Continuation (MPC): 389 | - 82ff. 390 | - 77- 391 | - 77++ 392 | 393 | The Continuation Symbols ("f.", "ff.") symbols can be customized via parameters `spc` and 394 | `mpc` to `make-index()`. 395 | 396 | This Sample uses "+" for _Single Page Continuation_ and "++" for _Multi Page Continuations_. 397 | ```typ 398 | #make-index( 399 | use-bang-grouping: true, 400 | use-page-counter: true, 401 | sort-order: upper, 402 | spc: "+", 403 | mpc: "++", 404 | ) 405 | ``` 406 | 407 | 408 | If `spc` is set to `none`, an explicit numeric range is used, like "42-43". 409 | If `mpc` is set to `none`, an explicit numeric range is used, like "42-49". 410 | 411 | Note that "f." and "ff." are the default symbols for `scp` and `mcp`. 412 | 413 | 414 | ===== Range of Pages 415 | 416 | To mark a Range of pages for an index entry, one can use the following marker: 417 | 418 | ```typ 419 | #index(index-type: indextype.Start)[Entry] 420 | 421 | // other content here 422 | 423 | #index(index-type: indextype.End)[Entry] 424 | ``` 425 | 426 | Of course, you can shorten this somewhat explicit marker with your own marker, like this: 427 | 428 | #let index-start = index.with(index-type: indextype.Start) 429 | #let index-end = index.with(index-type: indextype.End) 430 | 431 | 432 | Behavior: 433 | 434 | - If the markers are on the same resulting page, they are automatically combined to a 435 | Cardinal Marker#index[Cardinal Marker] in the generated index page. 436 | 437 | - If the End-Marker is on the next page following the Start-Marker, the Marker is handled 438 | as a Continuation Marker ("f."). If it uses the Continuation Symbol or the page numbers 439 | can be configured in a Parameter of `make-index()`. 440 | 441 | - If there is a Start-Marker, but no End-Marker, the Marker is handled as a Continuation 442 | Marker ("ff."). 443 | 444 | 445 | == The Index Page 446 | 447 | #index[Index Page] 448 | 449 | To actually create the index page, the `make-index()` function has to be called. Of course, 450 | it can be embedded into an appropriately formatted #index[Formatting] 451 | environment#index[Environment], like this: 452 | 453 | ```typ 454 | #columns(3)[ 455 | #make-index() 456 | ] 457 | ``` 458 | 459 | The `make-index()` function accepts an optional array of indexes to include into the index 460 | page. The default value, `auto`, takes all entries from all indexes. The following sample 461 | only uses the entries of the secondary and tertiary index. See sample output in 462 | @combinedIndex. 463 | 464 | ```typ 465 | #columns(3)[ 466 | #make-index(indexes: ("Secondary", "Tertiary")) 467 | ] 468 | ``` 469 | 470 | 471 | === Skipping physical pages 472 | 473 | If page number 1 is not the first physical page#index-main[physical page] of the document, 474 | the parameter ` use-page-counter` of the `make-index()` function can be set to `true`. 475 | Default is `false`. In-dexter uses the page counter instead of the physical page number 476 | then. 477 | 478 | 479 | === Customize Letter Section Headings 480 | 481 | It is possible to customize the letter section#index[Letter Section] headings of the index 482 | page. The `make-index()` function has a parameter `section-title`, which can be set 483 | to a function that takes the letter and the counter of the section as parameters. The 484 | function should return a `content` that is used as the section heading. See some samples 485 | in @sec:customize-letter-section-sample below. 486 | 487 | The default function for `section-title` is a level 2 heading: 488 | 489 | ```typst 490 | #heading(level: 2, numbering: none, outlined: false, letter) 491 | ``` 492 | 493 | but it can be easily customized to use a different style, like a block with a background 494 | color, a line, or even no heading at all. The following sample uses a block with 495 | a background color and a radius to create a rounded rectangle as section heading: 496 | 497 | ```typ 498 | #let my-section-title(letter, counter) = { 499 | set align(center + horizon) 500 | set text(weight: "bold") 501 | block(width: 100%, height: 1.5em, fill: blue.transparentize(50%), radius: 5pt)[ 502 | #letter 503 | ] 504 | } 505 | #columns(3)[ 506 | #make-index( 507 | use-bang-grouping: true, 508 | section-title: my-section-title 509 | ) 510 | ] 511 | ``` 512 | 513 | The `counter` parameter is the number of the section, starting with 0 for the first 514 | section. The `letter` parameter is the letter of the section, which is used 515 | to group the entries in the index page. 516 | 517 | 518 | === Customize Section Body 519 | 520 | The `make-index()` function also has a parameter `section-body`, which can be set to a 521 | function that takes the letter, the counter of the section, and the body of the section as 522 | parameters. The function should return a `content` that is used as the body of the section. 523 | This allows to customize the look of the section body, like adding an inset or a background 524 | color. The following sample uses an inset to add some space around the body of the section: 525 | 526 | ```typ 527 | #make-index( 528 | use-bang-grouping: true, 529 | section-title: my-section-title, 530 | section-body: (letter, counter, body) => { 531 | block(inset: (left:.5em, right: .5em), body) 532 | } 533 | ) 534 | ``` 535 | 536 | The `counter` parameter is the number of the section, starting with 0 for the first 537 | section. The `body` parameter is the content of the section, which is the list of entries 538 | for the section. The `letter` parameter is the letter of the section, which is used 539 | to group the entries in the index page. 540 | 541 | 542 | = Why Having an Index in Times of Search Functionality? 543 | #index(fmt: strong, [Searching vs. Index]) 544 | 545 | A _hand-picked_#index[Hand Picked] or _handcrafted_#index[Handcrafted] Index in times of 546 | search functionality seems a bit old-fashioned#index[Old-fashioned] at the first glance. 547 | But such an index allows the author to direct the reader, who is looking for a specific 548 | topic#index-main("Topic", "specific") (using index-main), to exactly the right places. 549 | 550 | Especially in larger documents#index[Large Documents] and books#index[Books] this becomes 551 | very useful, since search engines#index[Search Engines]#index3[Engines] may 552 | provide#index[Provide] too many locations of specific#index2[specific] words. The 553 | index#index[Index] is much more comprehensive,#index[Comprehensive] assuming that the 554 | author#index[Authors responsibility] has its content#index[Content] selected well. Authors 555 | know best where a certain topic#index("Topic", "certain") is explained#index[Explained] 556 | thoroughly#index[Thoroughly] or merely noteworthy #index[Noteworthy] mentioned (using the 557 | `index` function). 558 | 559 | Note, that this document is not necessarily a good example#index2[example] of the index. 560 | Here we just need to have as many index entries#index[Entries] as possible to 561 | demonstrate#index-ff([Demonstrate]) (using a custom made `index-ff` function) the 562 | functionality #index[Functionality] and have a properly#index[Properly] 563 | filled#index3[filled] index at the end. 564 | 565 | Even for symbols like `(ρ)`. 566 | #index([$(rho)$], initial: (letter: "Symbols", sort-by: "#"), apply-casing: false) 567 | Indexing should work for for any Unicode string like Cyrillic 568 | (Скороспелка#index(initial: (letter: "С", sort-by: "Ss"), "Скороспелка")) or German 569 | (Ölrückstoßabdämpfung).#index(initial: (letter: "Ö", sort-by: "Oo"), 570 | "Ölrückstoßabdämpfung") - though we need to add initials: 571 | 572 | `#index(initial: (letter: "С", sort-by: "Ss"), "Скороспелка")` 573 | 574 | or 575 | 576 | `#index(initial: (letter: "Ö", sort-by: "Oo"), "Ölrückstoßabdämpfung")`. 577 | 578 | 579 | #line(length: 100%, stroke: .1pt + gray) 580 | 581 | #pagebreak() 582 | 583 | #set page( 584 | numbering: "i", 585 | footer: align(right)[#context { 586 | counter(page).display("i") 587 | }], 588 | ) 589 | 590 | // This marks the end of the range for the Entry-Key "Entries" 591 | #index(index-type: indextype.End)[Entries] 592 | 593 | 594 | = Index pages 595 | 596 | In this chapter we emit several index pages for this document. We also switched page 597 | numbering to roman numbers#index[Roman Numbers], to demonstrate in-dexters ability to 598 | display them, if the option `use-page-counter` has been set to true. 599 | 600 | // Table of Content from here on 601 | #context (outline(title: none, target: selector(heading).after(here()))) 602 | 603 | 604 | == The Default Index page 605 | 606 | Here we generate the Index page in three columns. The default behavior (auto) is to use 607 | all indexes together. 608 | 609 | #index-main[Metadaten!Primäre] 610 | #index-main(display: "Joy")[Metadaten!Sekundäre!Fun] 611 | #index-main("Metadaten!Tertiäre") 612 | 613 | // A sample with a raw display text 614 | #index(display: `Aberration`, "Aberration") 615 | 616 | #columns(3)[ 617 | #make-index( 618 | use-bang-grouping: true, 619 | use-page-counter: true, 620 | sort-order: upper, 621 | range-delimiter: [--], 622 | ) 623 | ] 624 | 625 | #pagebreak() 626 | 627 | 628 | == Secondary Index 629 | 630 | Here we select explicitly only the secondary index. 631 | 632 | #columns(3)[ 633 | #make-index(indexes: "Secondary", use-bang-grouping: true, sort-order: upper) 634 | ] 635 | 636 | #line(length: 100%) 637 | 638 | 639 | == Tertiary Index 640 | 641 | Here we select explicitly only the tertiary index. 642 | 643 | #columns(3)[ 644 | #make-index( 645 | indexes: "Tertiary", 646 | use-bang-grouping: true, 647 | sort-order: upper, 648 | ) 649 | ] 650 | 651 | #line(length: 100%) 652 | 653 | 654 | == Combined Index 655 | 656 | Here we select explicitly secondary and tertiary indexes. 657 | 658 | #columns(3)[ 659 | #make-index( 660 | indexes: ("Secondary", "Tertiary"), 661 | use-bang-grouping: true, 662 | sort-order: upper, 663 | ) 664 | ] 665 | 666 | #line(length: 100%) 667 | 668 | 669 | == Combined Index - all lower case 670 | 671 | Here we select explicitly secondary#index[Secondary Index] and tertiary#index[Tertiary 672 | Index] indexes and format them all lower case. 673 | 674 | #columns(3)[ 675 | #make-index( 676 | indexes: ("Secondary", "Tertiary"), 677 | entry-casing: lower, 678 | use-bang-grouping: true, 679 | sort-order: upper, 680 | ) 681 | ] 682 | 683 | #line(length: 100%) 684 | #pagebreak() 685 | 686 | 687 | == Math Index 688 | 689 | Here we explicitly select only the Math index. 690 | 691 | #columns(3)[ 692 | #make-index( 693 | indexes: ("Math"), 694 | sort-order: upper, 695 | use-bang-grouping: true, 696 | ) 697 | ] 698 | 699 | #pagebreak() 700 | 701 | 702 | == Customize letter section 703 | 704 | You can customize the look of the letter section#index[Letter Section]/heading using a function: 705 | 706 | ```typst 707 | #let my-section-title(letter, counter) = { 708 | set align(center + horizon) 709 | set text(weight: "bold") 710 | block(width: 100%, height: 1.5em, fill: blue.transparentize(50%), radius: 5pt)[ 711 | #letter 712 | ] 713 | } 714 | 715 | #columns(3)[ 716 | #make-index( 717 | use-bang-grouping: true, 718 | section-title: my-section-title 719 | ) 720 | ] 721 | ``` 722 | 723 | #let my-section-title(letter, counter) = { 724 | set align(center + horizon) 725 | set text(weight: "bold") 726 | block(width: 100%, height: 1.5em, fill: blue.transparentize(50%), radius: 5pt, breakable: false)[ 727 | #letter 728 | ] 729 | } 730 | 731 | #columns(3)[ 732 | #make-index( 733 | use-bang-grouping: true, 734 | section-title: my-section-title, 735 | section-body: (letter, counter, body) => { 736 | block(inset: (left:.5em, right: .5em), body) 737 | } 738 | ) 739 | ] 740 | 741 | 742 | === Sample with a small gap as section divider: 743 | 744 | Here we have an index page without letter section headings, but with a small gap as 745 | section divider. Note that we skip the first section heading, if the counter is 0, so that 746 | the first section does not have a line above it: 747 | 748 | #columns(3)[ 749 | #make-index( 750 | use-bang-grouping: true, 751 | section-title: (letter, counter) => { 752 | if counter > 0 { v(1em) } 753 | } 754 | ) 755 | ] 756 | 757 | 758 | === Sample with a line as section divider: 759 | 760 | Here we have an index page without letter section headings, but with a line as section 761 | divider. Note that we skip the first section heading, if the counter is 0, so that 762 | the first section does not have a line above it: 763 | 764 | ```typ 765 | #columns(2)[ 766 | #make-index( 767 | use-bang-grouping: true, 768 | section-title: (letter, counter) => { 769 | if counter > 0 { line(length: 100%, stroke: .3pt + blue) } 770 | } 771 | ) 772 | ] 773 | ``` 774 | 775 | #columns(2)[ 776 | #make-index( 777 | use-bang-grouping: true, 778 | section-title: (letter, counter) => { 779 | if counter > 0 { line(length: 100%, stroke: .3pt + blue) } 780 | } 781 | ) 782 | ] 783 | 784 | 785 | === Sample without letter section headings: 786 | 787 | This is suitable for very small index pages. 788 | 789 | #columns(2)[ 790 | #make-index( 791 | use-bang-grouping: true, 792 | section-title: (letter, counter) => {} 793 | ) 794 | ] 795 | --------------------------------------------------------------------------------