From c973f878c3d89eeaa3201320ea24311f2e27fc9c Mon Sep 17 00:00:00 2001 From: A_A <21040751+Otto-AA@users.noreply.github.com> Date: Fri, 17 Oct 2025 21:32:28 +0200 Subject: [PATCH] dashy-todo:0.1.3 --- packages/preview/dashy-todo/0.1.3/LICENSE | 16 + packages/preview/dashy-todo/0.1.3/README.md | 50 + packages/preview/dashy-todo/0.1.3/example.svg | 1738 +++++++++++++++++ packages/preview/dashy-todo/0.1.3/example.typ | 20 + .../dashy-todo/0.1.3/lib/box-placement.typ | 47 + .../dashy-todo/0.1.3/lib/side-margin.typ | 101 + .../preview/dashy-todo/0.1.3/lib/todo.typ | 195 ++ packages/preview/dashy-todo/0.1.3/typst.toml | 11 + 8 files changed, 2178 insertions(+) create mode 100644 packages/preview/dashy-todo/0.1.3/LICENSE create mode 100644 packages/preview/dashy-todo/0.1.3/README.md create mode 100644 packages/preview/dashy-todo/0.1.3/example.svg create mode 100644 packages/preview/dashy-todo/0.1.3/example.typ create mode 100644 packages/preview/dashy-todo/0.1.3/lib/box-placement.typ create mode 100644 packages/preview/dashy-todo/0.1.3/lib/side-margin.typ create mode 100644 packages/preview/dashy-todo/0.1.3/lib/todo.typ create mode 100644 packages/preview/dashy-todo/0.1.3/typst.toml diff --git a/packages/preview/dashy-todo/0.1.3/LICENSE b/packages/preview/dashy-todo/0.1.3/LICENSE new file mode 100644 index 0000000000..5b7f6866df --- /dev/null +++ b/packages/preview/dashy-todo/0.1.3/LICENSE @@ -0,0 +1,16 @@ +MIT No Attribution + +Copyright 2024-2025 Otto-AA + +Permission is hereby granted, free of charge, to any person obtaining a copy of this +software and associated documentation files (the "Software"), to deal in the Software +without restriction, including without limitation the rights to use, copy, modify, +merge, publish, distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, +INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file diff --git a/packages/preview/dashy-todo/0.1.3/README.md b/packages/preview/dashy-todo/0.1.3/README.md new file mode 100644 index 0000000000..0b54dbd3ac --- /dev/null +++ b/packages/preview/dashy-todo/0.1.3/README.md @@ -0,0 +1,50 @@ +# Dashy TODO + +Create simple TODO comments, which are displayed at the sides of the page. + +I suggest to also take a look at the [drafting package](https://typst.app/universe/package/drafting), which is a more feature-complete annotation library, but also suitable for simple TODO comments. + +![Screenshot](example.svg) + +## Usage + +The package provides a `todo(message, position: auto | left | right)` method. Call it anywhere you need a todo message. + +```typst +#import "@preview/dashy-todo:0.1.3": todo + +// It automatically goes to the closer side (left or right) +A todo on the left #todo[On the left]. + +// You can specify a position if you want to (auto, left, right or "inline") +#todo(position: right)[Also right] + +// You can add arbitrary content +#todo[We need to fix the $lim_(x -> oo)$ equation. See #link("https://example.com")[example.com]] + +// And you can create an outline for the TODOs +#outline(title: "TODOs", target: figure.where(kind: "todo")) +``` + +## Styling + +You can change the border color with any [stroke](https://typst.app/docs/reference/visualize/stroke/) type: + +``` +#todo(stroke: blue)[This is in blue!] + +#let blue-todo = todo.with(stroke: blue) +#blue-todo[This is also in blue!] +``` + +You can modify the text by wrapping it, e.g.: + +``` +#let small-todo = (..args) => text(size: 0.6em)[#todo(..args)] + +#small-todo[This will be in fine print] +``` + +## Contributing + +See https://github.com/Otto-AA/dashy-todo/blob/main/CONTRIBUTING.md. diff --git a/packages/preview/dashy-todo/0.1.3/example.svg b/packages/preview/dashy-todo/0.1.3/example.svg new file mode 100644 index 0000000000..143c31ef7a --- /dev/null +++ b/packages/preview/dashy-todo/0.1.3/example.svg @@ -0,0 +1,1738 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/preview/dashy-todo/0.1.3/example.typ b/packages/preview/dashy-todo/0.1.3/example.typ new file mode 100644 index 0000000000..5f3d216df8 --- /dev/null +++ b/packages/preview/dashy-todo/0.1.3/example.typ @@ -0,0 +1,20 @@ +#import "/lib/todo.typ": todo + +#set page(width: 16cm, height: 12cm, margin: (left: 20%, right: 20%), fill: white) +#show link: underline + += Dashy TODOs + +#let blue-todo = todo.with(stroke: blue) + +TODOs are automatically positioned on the closer#todo[On the right] side to the method call.#blue-todo[On the left] If it doesn't fit, you can manually override it. #todo(position: "inline")[You can also place them in an inline box.] + +You can add custom content.#todo(position: right, stroke: (paint: green, thickness: 1pt, dash: "densely-dashed"))[We need to fix the $lim_(x -> oo)$ equation. See #link("https://example.com")[example.com]] + +#let small-todo = (..args) => text(size: 0.6em)[#todo(..args)] + +We can also control the text #small-todo[This will be in fine print]size if we wrap it in a `text` call. + +And finally, you can create a table of all your TODOs: + +#outline(title: "TODOs", target: figure.where(kind: "todo")) diff --git a/packages/preview/dashy-todo/0.1.3/lib/box-placement.typ b/packages/preview/dashy-todo/0.1.3/lib/box-placement.typ new file mode 100644 index 0000000000..296d2e9821 --- /dev/null +++ b/packages/preview/dashy-todo/0.1.3/lib/box-placement.typ @@ -0,0 +1,47 @@ +// Given a set of box heights and the preferred positions for the boxes this function calculates the position for each box +#let calculate-box-positions(boxes) = { + let total-height = boxes.map(box => box.height).sum() + + let positions = () + // If the total height of the boxes is bigger than the page height, we don't have room to re-arrange the boxes based on their preferred position. Just start at the top and place them underneath each other + if total-height >= page.height { + let accumulated-height = 0mm + for box in boxes { + let top = accumulated-height + let bottom = top + box.height + positions.push((top: top, bottom: bottom)) + accumulated-height = bottom + } + } + // If there is still room available, try to move as many boxes to their preferred position as possible + else { + // `slack` is the amount of free space we still have available to move around the boxes + let slack = page.height - total-height + // Start filling the page at the bottom with the last box and work upwards + let last-box-pos = page.height + for box in boxes.rev() { + let preferred-pos = box.preferred-pos.to-absolute() // We cannot compare pt to em, so we need to make the position absolute first + // `lowest-possible-pos` is the lowest position we can give the box to make it barely fit the available space + let lowest-possible-pos = last-box-pos - box.height + // `highest-possible-pos` is the highest possible postion we can give the box so the remaining boxes still barely fit on the remaining page (all slack would be used up in this case) + let highest-possible-pos = lowest-possible-pos - slack + let box-pos + // If the preferred position can be achieved, just give it to the box + if preferred-pos >= highest-possible-pos and preferred-pos <= lowest-possible-pos { + box-pos = preferred-pos + } + // If putting the box at its preferred position is impossible, move it down as much as possible to give the other boxes as much space as possible to move to their preferred positions + else { + box-pos = lowest-possible-pos + } + let top = box-pos + let bottom = top + box.height + positions.push((top: top, bottom: bottom)) + last-box-pos = box-pos + // Track how much of the slack we've used up + slack -= lowest-possible-pos - box-pos + } + positions = positions.rev() + } + positions +} \ No newline at end of file diff --git a/packages/preview/dashy-todo/0.1.3/lib/side-margin.typ b/packages/preview/dashy-todo/0.1.3/lib/side-margin.typ new file mode 100644 index 0000000000..2e39fc59dd --- /dev/null +++ b/packages/preview/dashy-todo/0.1.3/lib/side-margin.typ @@ -0,0 +1,101 @@ +#let rel-to-abs = (rel, size) => rel.length + rel.ratio * size + +// must run within a context +#let resolve-page-binding() = { + assert( + not (page.binding == auto and text.dir == auto), + message: "Could not infer page margins. Please use `#set page(binding: left)` or `#set page(binding: right)` or `#set text(dir: left)` or `#set text(dir: right)`.", + ) + + if page.binding == left or (page.binding == auto and text.dir == ltr) { + left + } else { + right + } +} + +// must run within a context +#let get-right-page-margin() = { + if "inside" in page.margin.keys() or "outside" in page.margin.keys() { + if calc.even(here().page()) { + if resolve-page-binding() == left { + page.margin.inside + } else { + page.margin.outside + } + } else { + if resolve-page-binding() == left { + page.margin.outside + } else { + page.margin.inside + } + } + } else { + page.margin.right + } +} + +// must run within a context +#let get-left-page-margin() = { + if "inside" in page.margin.keys() or "outside" in page.margin.keys() { + if calc.even(here().page()) { + if resolve-page-binding() == left { + page.margin.outside + } else { + page.margin.inside + } + } else { + if resolve-page-binding() == left { + page.margin.inside + } else { + page.margin.outside + } + } + } else { + page.margin.left + } +} + +#let calc-side-margin(side) = { + // https://typst.app/docs/reference/layout/page/#parameters-margin + let auto-margin = calc.min(page.width, page.height) * 2.5 / 21 + + if page.margin == auto { + auto-margin + } else if type(page.margin) == relative { + rel-to-abs(page.margin, page.width) + } else { + if side == left and get-left-page-margin() == auto or side == right and get-right-page-margin() == auto { + auto-margin + } else { + if side == left { + rel-to-abs(get-left-page-margin(), page.width) + } else { + rel-to-abs(get-right-page-margin(), page.width) + } + } + } +} + +// must run within a context +#let calculate-page-margin-box(side) = { + assert(side in (left, right)) + + let margin = calc-side-margin(side) + + if side == left { + ( + "x": 0pt, + "y": 0pt, + "width": margin, + "height": page.height, + ) + } else { + ( + "x": page.width - margin, + "y": 0pt, + "width": margin, + "height": page.height, + ) + } +} diff --git a/packages/preview/dashy-todo/0.1.3/lib/todo.typ b/packages/preview/dashy-todo/0.1.3/lib/todo.typ new file mode 100644 index 0000000000..f1dad0f6f5 --- /dev/null +++ b/packages/preview/dashy-todo/0.1.3/lib/todo.typ @@ -0,0 +1,195 @@ +#import "box-placement.typ": calculate-box-positions +#import "side-margin.typ": calculate-page-margin-box + +// set to true for additional debugging output +#let debug = false +#let debug-only(body) = { if debug { body } } + +#let to-string(content) = { + if type(content) == str { + content + } else if content.has("text") { + content.text + } else if content.has("children") { + content.children.map(to-string).join("") + } else if content.has("body") { + to-string(content.body) + } else if content == [ ] { + " " + } +} + +#let outline-entry(body) = { + // invisible figure, s.t. we can reference it in the outline + // probably depends on https://github.com/typst/typst/issues/147 for a cleaner solution + hide( + box( + height: 0pt, + width: 0pt, + figure( + none, + kind: "todo", + supplement: [TODO], + caption: to-string(body), + outlined: true, + ), + ), + ) +} + +#let inline-todo(body, stroke) = { + box(stroke: stroke, width: 100%, inset: 4pt)[#body] + outline-entry(body) +} + +#let side-todo(body, position, stroke) = { + box(context { + let text-position = here().position() + + let side = position + if position == auto { + if text-position.x > page.width / 2 { + side = right + } else { + side = left + } + } + + let page-margin-box = calculate-page-margin-box(side) + let shift-y = .5em + let outer-box-inset = 4pt + + // Create the todo box. It will later be measured to determine it's location + let todo-box = box(inset: outer-box-inset, width: page-margin-box.width)[ + #box(stroke: stroke, width: 100%)[ + #box(body, inset: 0.2em) + #outline-entry(body) + ] + ] + + // Create a state that accumulates all the todo box sizes of the current page + // There are two states per page. One for the boxes on the left, one for the boxes on the right + let boxes = state("dashy-todo-page-" + str(here().page()) + "-" + repr(side) + "-boxes", ()) + + let box-size = measure(todo-box) + + // Register the box in the state and determine the index of the box within the state. This index will later be used to retrieve the final position for the box. + let box-index = boxes.get().len() + boxes.update(boxes => { + boxes.push(( + height: box-size.height, // Height of the box in pt, including margins + preferred-pos: text-position.y - shift-y, // Where is the ideal y-position for the box? + )) + return boxes + }) + + let boxes = boxes.final() + + if boxes.len() > box-index { + // The typst compiler will skip state updates during initial layout, leaving the list empty, leading to boxes.len() < box-index. To avoid compiler errors, we'll wait for the states to be updated before placing the boxes + let box-positions = calculate-box-positions(boxes) + + // This place will shift the coordinate system so (0, 0) will address the top left corner of the page + place(dx: -text-position.x, dy: -text-position.y)[ + // Retrieve the calculated position for the current box & render it + #let box-pos = box-positions.at(box-index) + #let box-dx = if text.dir == ltr or text.dir == auto { page-margin-box.x } else { + page-margin-box.x + page-margin-box.width + } + #place(dx: box-dx, dy: box-pos.top)[#todo-box] + + // for debugging output, set debug = true at the top of this file + #let debug-dx-margin = if text.dir == ltr or text.dir == auto { page-margin-box.x } else { + page-margin-box.x + page-margin-box.width + } + #debug-only(place( + dx: debug-dx-margin, + dy: page-margin-box.y, + rect( + width: page-margin-box.width, + height: page-margin-box.height, + stroke: (thickness: 1pt, paint: gray, dash: "dashed"), + ), + )) + #let debug-line-dx = if text.dir == ltr or text.dir == auto { 0pt } else { page.width } + #debug-only(place(dx: debug-line-dx, dy: box-pos.top, line(start: (0pt, 0pt), end: (page.width, 0pt), stroke: ( + thickness: 1pt, + paint: blue, + dash: "dotted", + )))) + #debug-only(place(dx: debug-line-dx, dy: box-pos.bottom, line( + end: (page.width, 0pt), + stroke: ( + thickness: 1pt, + paint: blue, + dash: "dotted", + ), + ))) + #let debug-circle-dx = if text.dir == ltr or text.dir == auto { -4pt } else { 4pt } + #debug-only(place( + dx: text-position.x + debug-circle-dx, + dy: text-position.y - 4pt, + circle(radius: 4pt, stroke: (thickness: 0.5pt, paint: blue)), + )) + + // Draw the tick mark within the text + #place(dx: text-position.x, dy: text-position.y)[#line( + length: shift-y, + angle: 90deg, + stroke: stroke, + )] + + // Horizontally connect the tick mark to the position of the box + #let distance-tick-to-box-x = if side == left { + text-position.x - page-margin-box.width + outer-box-inset + } else { + page-margin-box.x - text-position.x + outer-box-inset + } + #let tick-to-box-line-dx = if text.dir == ltr or text.dir == auto { + if side == left { text-position.x - distance-tick-to-box-x } else { text-position.x } + } else { + if side == left { text-position.x } else { text-position.x + distance-tick-to-box-x } + } + + #place(dx: tick-to-box-line-dx, dy: text-position.y + shift-y, line( + length: distance-tick-to-box-x, + stroke: stroke, + )) + + // If the box is above or below the line, connect it vertically + #let box-border-x = if side == left { + page-margin-box.x + page-margin-box.width - outer-box-inset + } else { + page-margin-box.x + outer-box-inset + } + #if text-position.y + shift-y.to-absolute() < box-pos.top + outer-box-inset { + place(dx: box-border-x)[#line( + start: (0pt, text-position.y + shift-y), + end: (0pt, box-pos.top + outer-box-inset), + stroke: stroke, + )] + } else if text-position.y + shift-y.to-absolute() > box-pos.bottom - outer-box-inset { + place(dx: box-border-x)[#line( + start: (0pt, text-position.y + shift-y), + end: (0pt, box-pos.bottom - outer-box-inset), + stroke: stroke, + )] + } + ] + } + }) +} + +#let todo(body, position: auto, stroke: orange) = { + assert( + position in (auto, left, right, "inline"), + message: "Can only position todo either inline, or on the left or right side.", + ) + // do not influence line numbering + set par.line(numbering: none) + if position == "inline" { + inline-todo(body, stroke) + } else { + side-todo(body, position, stroke) + } +} diff --git a/packages/preview/dashy-todo/0.1.3/typst.toml b/packages/preview/dashy-todo/0.1.3/typst.toml new file mode 100644 index 0000000000..bdc799baf0 --- /dev/null +++ b/packages/preview/dashy-todo/0.1.3/typst.toml @@ -0,0 +1,11 @@ +[package] +name = "dashy-todo" +version = "0.1.3" +entrypoint = "lib/todo.typ" +authors = ["Otto-AA"] +license = "MIT-0" +description = "A method to display TODOs at the side of the page." +repository = "https://github.com/Otto-AA/dashy-todo" +keywords = ["todo"] +categories = ["utility"] +exclude = ["*.svg*", "example.typ"]