Skip to content

Commit d0b2d01

Browse files
committed
Add todonotes 0.1.0
1 parent 78fedda commit d0b2d01

File tree

7 files changed

+320
-0
lines changed

7 files changed

+320
-0
lines changed
+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2024 Jens Tinggaard
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.
+83
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
# Todonotes
2+
3+
This package is inspired by the [drafting](https://typst.app/universe/package/drafting) package, whilst trying to mimic
4+
the [todonotes](https://ctan.org/pkg/todonotes) LaTeX package, with a todo outline,
5+
and placement on pages with inside / outside margins.
6+
7+
## Usage
8+
9+
Import the package
10+
11+
```typ
12+
#import "@preview/todonotes:0.1.0": todo, todo-outline
13+
```
14+
15+
The `todo` function takes the following parameters
16+
17+
- `side`: `auto` | `left` | `right` - The side of the page to display the note.
18+
Auto selects the side with the largest margin available, and right if they are equal. (**default**: `auto`)
19+
- `font-size`: `length` - Font size of the text in the todo (**default**: `9pt`)
20+
- `font-color`: `color` | `gradient` - Color of the font (**default**: `black`)
21+
- `color`: `color` | `gradient` - Color of the box and line (**default**: `orange`)
22+
- `body`: `content` - Body of the todo
23+
24+
The `todo-outline` function takes a single optional argument, specifying the title of the outline.
25+
As the name suggests it prints an outline of all the todos in the document, along with a box with the color of the todo.
26+
27+
### `.with` Macros
28+
29+
Create different todos for different authors / purposes.
30+
31+
```typ
32+
#set page("a6", flipped: true, margin: (x: 30mm))
33+
#let urgent = todo.with(color: red)
34+
#let todo-later = todo.with(color: green)
35+
```
36+
37+
We can then use them like this
38+
39+
```typ
40+
#lorem(20)
41+
#todo[This is the default todo]
42+
#lorem(10)
43+
#urgent[Remember to change this!]
44+
45+
#lorem(15)
46+
#todo-later[We should probably do X at some point...]
47+
48+
#lorem(20)
49+
#todo[side: left](I am manually placed on the left)
50+
#todo[color: blue, font-color: white](I am very customized)
51+
```
52+
53+
![Custom macros](assets/todo-macros.png)
54+
55+
### Uneven Margins
56+
57+
It even works with inside / outside margins.
58+
59+
```typ
60+
#set page(margin: (inside: 20mm, outside: 40mm))
61+
62+
#lorem(30)
63+
#todo[Wauw, so much space! I am just soooooo long, I can't even fit on two lines!]
64+
#lorem(3)
65+
#todo[side: right, font-size: 7pt](I am on the right side, good thing my font is so small...)
66+
```
67+
68+
![Inside / outside margins](assets/inside-outside-margin.png)
69+
70+
### Outline
71+
72+
We can also create an outline of the document, optionally supplying custom title
73+
74+
```typ
75+
#todo-outline(title: "Todos")
76+
```
77+
78+
![Todo outline](assets/todo-outline.png)
79+
80+
## Compiler Warnings
81+
82+
Please note, that the compiler will give a warning if 4 or more notes overlap,
83+
due to the dynamic placement of the notes, combined with Typst's iterative typesetting.
Loading
Loading
Loading
+205
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,205 @@
1+
#import "@preview/t4t:0.3.2": get
2+
3+
#let note-descent = state("note-descent", (:))
4+
5+
#let todo-outline(title: "List of Todos") = context {
6+
show outline.entry.where(
7+
level: 1
8+
): it => {
9+
// box with color, followed by content
10+
[#it.body.children.first() #it.body.children.last()]
11+
box(width: 1fr, repeat[.])
12+
it.page
13+
}
14+
// only show if there actually are any todos
15+
if (counter(figure.where(kind: "todo")).final().first() > 0) {
16+
page(outline(target: figure.where(kind: "todo"), title: title))
17+
}
18+
}
19+
20+
#let _get-current-descent(descents-dict, page-number: auto) = {
21+
if page-number == auto {
22+
page-number = descents-dict.keys().at(-1, default: "0")
23+
} else {
24+
page-number = str(page-number)
25+
}
26+
(page-number, descents-dict.at(page-number, default: (left: 0pt, right: 0pt)))
27+
}
28+
29+
#let _update-descent(side, dy, anchor-y, note-rect, page-number) = {
30+
let height = measure(note-rect).height
31+
// let dy = measure(v(dy + height)).height + anchor-y // needed if any values or not just pt.
32+
let dy = dy + height + anchor-y
33+
note-descent.update(old => {
34+
let (cnt, props) = _get-current-descent(old, page-number: page-number)
35+
props.insert(side, dy)
36+
old.insert(cnt, props)
37+
old
38+
})
39+
}
40+
41+
// invisible figure, s.t. we can reference it in the outline
42+
#let _todo-outline-entry(color, body) = hide(
43+
box(
44+
height: 0pt,
45+
width: 0pt,
46+
figure(
47+
none,
48+
kind: "todo",
49+
// colored box in outline
50+
supplement: box(fill: color, height: 11pt, width: 11pt, stroke: black + .5pt),
51+
caption: get.text(body),
52+
outlined: true,
53+
)
54+
)
55+
)
56+
57+
#let _get-margin(side: auto, page-num) = {
58+
let w = if page.flipped {
59+
page.height
60+
} else {
61+
page.width
62+
}
63+
let default-margin = 2.5 / 21 * w
64+
let defaults = (
65+
"left": default-margin,
66+
"right": default-margin
67+
)
68+
// no margin is specified at all
69+
if (page.margin == auto) {
70+
return defaults
71+
}
72+
73+
if ("right" in page.margin.keys() and page.margin.right != auto) {
74+
defaults.at("right") = page.margin.right
75+
}
76+
if ("left" in page.margin.keys() and page.margin.left != auto) {
77+
defaults.at("left") = page.margin.left
78+
}
79+
if ("inside" in page.margin.keys() and page.margin.inside != auto) {
80+
if (calc.odd(page-num)) {
81+
defaults.at("left") = page.margin.inside
82+
} else {
83+
defaults.at("right") = page.margin.inside
84+
}
85+
}
86+
if ("outside" in page.margin.keys() and page.margin.outside != auto) {
87+
if (calc.even(page-num)) {
88+
defaults.at("left") = page.margin.outside
89+
} else {
90+
defaults.at("right") = page.margin.outside
91+
}
92+
}
93+
return defaults
94+
}
95+
96+
#let _todo-right(body, dy, anchor-x, anchor-y, font-size, font-color, color) = {
97+
let w = if page.flipped {
98+
page.height
99+
} else {
100+
page.width
101+
}
102+
let m = _get-margin(side: right, here().page()).right
103+
let dist-to-margin = w - anchor-x - m
104+
105+
let path-pts = (
106+
(0pt, -.2em),
107+
(0pt, 2pt),
108+
(dist-to-margin - 2pt, 2pt),
109+
(dist-to-margin - 2pt, dy + 2pt),
110+
(dist-to-margin + 2pt, dy + 2pt),
111+
)
112+
113+
dy += 1pt // todo-spacing
114+
let note-rect = rect(
115+
stroke: .5pt,
116+
fill: color,
117+
width: m - 10pt, // todo-margin
118+
inset: 4pt,
119+
radius: 4pt,
120+
text(size: font-size,
121+
fill: font-color,
122+
body)
123+
)
124+
// Boxing prevents forced paragraph breaks
125+
box[
126+
#place(path(stroke: 1pt + color, ..path-pts))
127+
#place(dx: dist-to-margin + 2pt, dy: dy - 10pt, note-rect) // lift todo a bit
128+
#_todo-outline-entry(color, body)
129+
]
130+
_update-descent("right", dy, anchor-y, note-rect, here().page())
131+
}
132+
133+
#let _todo-left(body, dy, anchor-x, anchor-y, font-size, font-color, color) = {
134+
let w = page.width
135+
let m = _get-margin(side: left, here().page()).left
136+
let dist-to-margin = - anchor-x
137+
138+
let path-pts = (
139+
(0pt, -.2em),
140+
(0pt, 2pt),
141+
(dist-to-margin + m - 8pt, 2pt),
142+
(dist-to-margin + m - 8pt, 2pt),
143+
(dist-to-margin + m - 8pt, dy + 2pt),
144+
(dist-to-margin + m - 10pt - 2pt, dy + 2pt), // todo-margin
145+
)
146+
147+
dy += 1pt // todo-spacing
148+
let note-rect = rect(
149+
stroke: .5pt,
150+
fill: color,
151+
width: m - 10pt, // todo-margin
152+
inset: 4pt,
153+
radius: 4pt,
154+
text(size: font-size, body)
155+
)
156+
157+
let foo = measure(note-rect)
158+
// Boxing prevents forced paragraph breaks
159+
box[
160+
#place(path(stroke: color, ..path-pts))
161+
#place(dx: dist-to-margin - 2pt, dy: dy - 10pt, note-rect) // lift todo a bit
162+
#_todo-outline-entry(color, body)
163+
]
164+
_update-descent("left", dy, anchor-y, note-rect, here().page())
165+
}
166+
167+
#let todo(side: auto, font-size: 9pt, font-color: black, color: orange, body) = [
168+
#h(0pt) // ensure here().position() accounts for indented paragraphs
169+
#context {
170+
let pos = here().position()
171+
let margin = _get-margin(side: side, pos.page)
172+
// shadow argument
173+
let side = if (side == auto) {
174+
let left = margin.at("left")
175+
let right = margin.at("right")
176+
if (left > right) {
177+
"left"
178+
} else {
179+
"right"
180+
}
181+
} else {
182+
repr(side)
183+
}
184+
let margin-func = if (side == "right") {
185+
_todo-right
186+
} else {
187+
_todo-left
188+
}
189+
let (anchor-x, anchor-y) = (pos.x - 5pt, pos.y)
190+
191+
let (cur-page, descents) = _get-current-descent(note-descent.get(), page-number: pos.page)
192+
let cur-descent = descents.at(side)
193+
let dy = calc.max(0pt, cur-descent - pos.y)
194+
195+
margin-func(
196+
body,
197+
dy,
198+
anchor-x,
199+
anchor-y,
200+
font-size,
201+
font-color,
202+
color
203+
)
204+
}
205+
]
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
[package]
2+
name = "todonotes"
3+
version = "0.1.0"
4+
entrypoint = "lib.typ"
5+
authors = ["Jens Tinggaard <@Tinggaard>"]
6+
license = "MIT"
7+
description = "A simple margin note package inspired by the LaTeX todonotes package."
8+
repository = "https://github.com/Tinggaard/todonotes"
9+
keywords = ["Todo", "note", "Margin"]
10+
compiler = "0.11.0"
11+
exclude = ["assets/"]

0 commit comments

Comments
 (0)