├── .gitignore ├── LICENSE ├── README.md ├── assets ├── evince.mov └── screenshot.png ├── examples ├── example-notes.typ └── simple.typ ├── scripts └── note.fish └── template-notebook.typ /.gitignore: -------------------------------------------------------------------------------- 1 | *.pdf 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Andreas Kröpelin 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Typst notebook 2 | 3 | ![license](https://img.shields.io/github/license/andreasKroepelin/typst-notebook?style=flat-square) 4 | 5 | This is a small template to write a notebook using [Typst](https://typst.app). 6 | 7 | ## Getting started 8 | Put the file `template-notebook.typ` in the directory where you want to store 9 | your notebook. 10 | 11 | Then, create a `notebook.typ` file similar to the following: 12 | ```typ 13 | #import "template-notebook.typ": notebook 14 | 15 | // not necessary, but I think the font works good for a notebook 16 | #set text(font: "Inria Sans") 17 | 18 | #show: notebook.with( 19 | title: [My cool notebook], 20 | author: [My name], 21 | tags: ( 22 | tag-1: orange, 23 | tag-2: aqua, 24 | ) 25 | ) 26 | 27 | = Note 1 28 | 29 | Something about @tag-1. 30 | #lorem(10) 31 | 32 | @tag-1 @tag-2 33 | 34 | 35 | = Note 2 36 | 37 | Refer to @note-1. 38 | #lorem(10) 39 | 40 | @tag-2 41 | 42 | 43 | = Note 3 44 | 45 | TODO do something 46 | 47 | DONE something else 48 | 49 | TODO do another thing 50 | ``` 51 | 52 | This produces the following document: 53 | ![screenshot](assets/screenshot.png) 54 | 55 | 56 | ## Features 57 | 58 | ### The document itself 59 | Using this template creates a document with a single ever-growing page. 60 | This is achieved by using `#set page(height: auto)`. 61 | The width of that page can be configured using the `width` argument in the 62 | template function: 63 | ```typ 64 | #show: notebook.with( 65 | // ... 66 | width: 80em, 67 | ) 68 | ``` 69 | 70 | ### Creating notes 71 | By using a level-one heading (`= Heading`), you create a new note. 72 | It is automatically assigned a label based on its title text that is printed next 73 | to the title for convenience, so that you know what to refer to. 74 | 75 | ### Keeping track of TODOs 76 | Whenever you put `TODO` somewhere in your notes, it is recognised, printed in 77 | red, and the containing note is added to a list of TODOs at the top of the 78 | document. 79 | When there are more than one TODOs in one note, a counter next to the title 80 | informs you about that fact. 81 | The title in the TODO list is a link to that note. 82 | 83 | ### Referring to other notes 84 | As explained above, each note automatically gets its label. 85 | The name of the label can be found next to the title in the document. 86 | By using Typst's reference syntax, you can link to that note (e.g. `@note-1` for 87 | a note that was created with `= Note 1`). 88 | 89 | ### Using tags 90 | At the top of your code, when you apply the template function, you can give a list 91 | of tags in the form of a Typst dictionary. 92 | The keys are the names of the tags and the values are the colors they are supposed 93 | to have. 94 | The name of the tag is printed in white per default. 95 | If, however, you want to have a tag with a very light color, that can become an 96 | issue. 97 | In that case, you can use a more complex syntax: 98 | ```typ 99 | tags: ( 100 | tag-1: red, 101 | tag-2: (color: silver, text-color: black), 102 | ) 103 | ``` 104 | For `tag-1: red`, we could therefore also write 105 | `tag-1: (color: red, text-color: white)`. 106 | 107 | You can refer to a tag in your notes again using the refernce syntax. 108 | For example, `@tag-1` creates a link to an overview of `tag-1` at the top of the 109 | document. 110 | In this overview, you can find all notes that mention this tag and by clicking 111 | any of their titles in this list you can jump to that note. 112 | 113 | ### Entry overview 114 | Also at the top of the document, you can find an automatic table of contents 115 | with all then entries in your notebook. 116 | 117 | ## Considerations for choosing the PDF viewer 118 | Some PDF viewers like `evince` on Linux have a _preview_ feature that comes in 119 | really handy here. 120 | For example, if you look at the TODO overview in the example doument above and 121 | want to know what kind of TODOs you have for Note 3 without jumping there, you 122 | can just hover the mouse pointer over the title: 123 | 124 | https://github.com/andreasKroepelin/typst-notebook/assets/42342396/6738663c-ca16-40e1-aaeb-ebe006955878 125 | 126 | 127 | ## Shell support 128 | In `scripts/note.fish`, you can find a utility function for the fish shell to 129 | create new notes more easily. 130 | If you copy this file to `~/.config/fish/functions/`, you can call it as 131 | `note "Some title"` in fish and it creates a new file `some-title.typ` with content 132 | ```typ 133 | = Some title 134 | ``` 135 | and adds 136 | ```typ 137 | #include "some-title.typ" 138 | ``` 139 | to `notebook.typ`. 140 | 141 | If you omit the title argument (i.e. you just call `note`), it uses the current 142 | date in the form `2023-jun-01` as the title. 143 | 144 | Contributions porting this script to other shells are welcome. 145 | 146 | -------------------------------------------------------------------------------- /assets/evince.mov: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andreasKroepelin/typst-notebook/138db9f468afd002bbb6fbd95a47c5821a055364/assets/evince.mov -------------------------------------------------------------------------------- /assets/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andreasKroepelin/typst-notebook/138db9f468afd002bbb6fbd95a47c5821a055364/assets/screenshot.png -------------------------------------------------------------------------------- /examples/example-notes.typ: -------------------------------------------------------------------------------- 1 | #import "../template-notebook.typ": notebook 2 | 3 | #set text(font: ("Inria Sans", "OpenMoji")) 4 | 5 | #show: notebook.with( 6 | title: [My cool notebook], 7 | author: [My name], 8 | tags: ( 9 | work: orange, 10 | family: aqua, 11 | ) 12 | ) 13 | 14 | = My first entry 15 | This is a very important note about @work. 16 | You can find more details in @fermat-s-1st-theorem. 17 | 18 | @work 19 | 20 | = Birthday party #emoji.party 21 | TODO Don't forget to buy a present for grandma! #emoji.cake 22 | 23 | TODO Write card. 24 | 25 | DONE Organise cake. 26 | 27 | @family 28 | 29 | = Fermat's 1st theorem $a^p ident a med (mod p)$ 30 | This extends @my-first-entry by an interesting note about number theory. 31 | 32 | TODO Read more about primes. 33 | -------------------------------------------------------------------------------- /examples/simple.typ: -------------------------------------------------------------------------------- 1 | #import "../template-notebook.typ": notebook 2 | 3 | #set text(font: "Inria Sans") 4 | 5 | #show: notebook.with( 6 | title: [My cool notebook], 7 | author: [My name], 8 | tags: ( 9 | tag-1: orange, 10 | tag-2: aqua, 11 | ) 12 | ) 13 | 14 | = Note 1 15 | 16 | Something about @tag-1. 17 | #lorem(10) 18 | 19 | @tag-1 @tag-2 20 | 21 | 22 | = Note 2 23 | 24 | Refer to @note-1. 25 | #lorem(10) 26 | 27 | @tag-2 28 | 29 | 30 | = Note 3 31 | 32 | TODO do something 33 | 34 | DONE something else 35 | 36 | TODO do another thing 37 | -------------------------------------------------------------------------------- /scripts/note.fish: -------------------------------------------------------------------------------- 1 | function note 2 | if set -q argv[1] 3 | set -f title $argv[1] 4 | else 5 | set -f title (date "+%Y-%b-%d" | string lower) 6 | end 7 | 8 | set -f filename (echo $title | string lower | string replace " " "-") 9 | 10 | echo -e "\n#include \"$filename.typ\"" >> notebook.typ 11 | echo "= $title" > "$filename.typ" 12 | end 13 | -------------------------------------------------------------------------------- /template-notebook.typ: -------------------------------------------------------------------------------- 1 | #let notebook( 2 | tags: (:), 3 | title: [Notebook], 4 | author: none, 5 | page-width: 50em, 6 | body 7 | ) = { 8 | set page(width: page-width, height: auto, margin: 3em) 9 | 10 | for tag in tags.keys() { 11 | let curr-value = tags.at(tag) 12 | if type(curr-value) == "color" { 13 | tags.insert(tag, (color: curr-value, text-color: white)) 14 | } 15 | } 16 | let named-tags = tags.pairs().map( ((name, colors)) => (name: name, ..colors) ) 17 | 18 | let tag-occurrences = state("tag-occurences", (:)) 19 | let heading-positions = state("heading-positions", ()) 20 | let todos = state("todos", ()) 21 | 22 | let tag-box(tag) = box( 23 | width: auto, height: auto, 24 | fill: tag.color, stroke: none, 25 | inset: 1mm, baseline: 1mm, radius: 1mm, 26 | text(fill: tag.text-color, tag.name) 27 | ) 28 | 29 | let sections-with-count(locs, display-count: (_ => [])) = { 30 | let locs-and-titles = locs.map( l => { 31 | let title = query(heading.where(level: 1).before(l), l).last() 32 | (l, title) 33 | }) 34 | 35 | let deduped = () 36 | for (l, title) in locs-and-titles { 37 | if deduped.len() == 0 { 38 | deduped.push((loc: l, title: title, count: 1)) 39 | continue 40 | } 41 | 42 | let last = deduped.pop() 43 | if last.title != title { 44 | deduped.push(last) 45 | deduped.push((loc: l, title: title, count: 1)) 46 | } else { 47 | last.count += 1 48 | deduped.push(last) 49 | } 50 | } 51 | 52 | deduped.map( it => { 53 | link(it.loc, sym.arrow.tr + it.title.body + display-count(it.count)) 54 | }).join(h(1em)) 55 | } 56 | 57 | show ref: it => { 58 | 59 | let refed-tag = named-tags.find(tag => it.target == label(tag.name)) 60 | if refed-tag == none { 61 | underline({ sym.arrow.tr; it }) 62 | } else { 63 | let display = refed-tag 64 | display.name = it 65 | tag-box(display) 66 | 67 | locate( loc => { 68 | tag-occurrences.update( to => { 69 | if refed-tag.name in to { 70 | to.at(refed-tag.name).push(loc) 71 | } else { 72 | to.insert(refed-tag.name, (loc,)) 73 | } 74 | to 75 | }) 76 | }) 77 | } 78 | } 79 | 80 | block(width: 100%, height: 5em, align(center + horizon)[ 81 | #text(size: 1.5em, strong(title)) 82 | #linebreak() 83 | #author 84 | ]) 85 | 86 | [= Open TODOs] 87 | locate( loc => { 88 | sections-with-count( 89 | todos.final(loc), 90 | display-count: c => text(fill: red, [ (#c)]) 91 | ) 92 | }) 93 | 94 | [= Entries] 95 | locate( loc => { 96 | let all-headings = heading-positions.final(loc) 97 | grid( 98 | columns: (1fr, 1fr, 1fr), gutter: 1em, 99 | ..all-headings.map( h => link(h.last(), sym.arrow.tr + h.first()) ) 100 | ) 101 | }) 102 | 103 | [= Tags] 104 | { 105 | let grid-children = () 106 | for tag-name in tags.keys().sorted() { 107 | let tag = tags.at(tag-name) 108 | let named-tag = (name: tag-name, ..tag) 109 | let def = box(baseline: 1mm)[ 110 | #figure(kind: "tag", supplement: tag-name, numbering: (..n) => [], tag-box(named-tag)) 111 | #label(tag-name) 112 | ] 113 | let details = locate( loc => { 114 | let all-tag-occurrences = tag-occurrences.final(loc) 115 | if tag-name in all-tag-occurrences { 116 | sections-with-count(all-tag-occurrences.at(tag-name)) 117 | } 118 | }) 119 | grid-children.push(def) 120 | grid-children.push(details) 121 | } 122 | 123 | grid( 124 | columns: (auto, 1fr), 125 | gutter: 1em, 126 | ..grid-children 127 | ) 128 | } 129 | 130 | show heading: it => { 131 | let content-text(c) = if type(c) == "string" { 132 | c 133 | } else if c.has("text") { 134 | content-text(c.text) 135 | } else if c.has("children") { 136 | c.children.map(content-text).join(" ") 137 | } else { 138 | "" 139 | } 140 | 141 | let label-name = content-text(it.body).codepoints().filter( cp => { 142 | cp.match(regex("[[:alnum:]]| ")) != none 143 | }).map( cp => { 144 | lower(if cp == " " { "-" } else { cp }) 145 | }).fold( (), (cps, cp) => { 146 | if cp == "-" and cps.len() > 0 and cps.last() == "-" { 147 | cps 148 | } else { 149 | cps + (cp,) 150 | } 151 | }).join().trim("-") 152 | 153 | locate( loc => { 154 | heading-positions.update( hp => hp + ( (it.body, loc), ) ) 155 | }) 156 | block[ 157 | #it.body 158 | #text(size: .5em, raw(block: false, lang: "typ", "@" + label-name)) 159 | #box[ 160 | #figure(kind: "entry", supplement: it.body, numbering: (..n) => [], []) 161 | #label(label-name) 162 | ] 163 | ] 164 | } 165 | 166 | show link: it => { 167 | if type(it.dest) == "string" { 168 | text(fill: blue, sym.triangle.stroked.tr + [ ] + it) 169 | } else { 170 | it 171 | } 172 | } 173 | 174 | show "TODO": it => { 175 | locate( loc => { 176 | todos.update(ts => ts + (loc,)) 177 | }) 178 | text(fill: red, size: 1em, weight: "bold", smallcaps(it)) 179 | } 180 | show "DONE": it => text(fill: green, size: 1em, weight: "bold", smallcaps(it)) 181 | 182 | line(length: 100%, stroke: .2em + gray) 183 | 184 | body 185 | } 186 | --------------------------------------------------------------------------------