diff --git a/packages/preview/sheetstorm/0.3.3/LICENSE b/packages/preview/sheetstorm/0.3.3/LICENSE new file mode 100644 index 0000000000..aa433f2388 --- /dev/null +++ b/packages/preview/sheetstorm/0.3.3/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Rasmus Buurman + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/packages/preview/sheetstorm/0.3.3/README.md b/packages/preview/sheetstorm/0.3.3/README.md new file mode 100644 index 0000000000..5738525fe9 --- /dev/null +++ b/packages/preview/sheetstorm/0.3.3/README.md @@ -0,0 +1,43 @@ +# sheetstorm +A Typst template for university exercise sheets. + +## Quick Start + +### Template CLI +```sh +typst init @preview/sheetstorm +``` + +### Manual +```typst +#import "@preview/sheetstorm:0.3.3" + +#show: sheetstorm.setup.with( + course: smallcaps[A very interesting course 101], + title: "Assignment 42", + authors: ( + (name: "John Doe", id: 123456), + (name: "Erika Mustermann", id: 654321), + ), + + info-box-enabled: true, +) +``` + +## Preview +![Preview of the sheetstorm template](./thumbnail.png) + +There are more [examples](./examples). + +## Development +For local development, install the package to the `@local` namespace. + +This is very easy with a tool like [typship](https://github.com/sjfhsjfh/typship): +```sh +typship install local +``` + +Then, you can use it in a Typst file: +```typst +#import "@local/sheetstorm:0.3.3" +``` diff --git a/packages/preview/sheetstorm/0.3.3/examples/german.typ b/packages/preview/sheetstorm/0.3.3/examples/german.typ new file mode 100644 index 0000000000..08e929a329 --- /dev/null +++ b/packages/preview/sheetstorm/0.3.3/examples/german.typ @@ -0,0 +1,18 @@ +#import "@preview/sheetstorm:0.3.3" as sheetstorm: task + +#set text(lang: "de") + +#show: sheetstorm.setup.with( + title: "Deutsches Beispiel", + authors: "Max Mustermann", +) + +#task(points: 42)[ + Es ist sehr einfach, das Template für deutschsprachige Dokumente zu benutzen. + Es muss lediglich die Sprache umgestellt werden: + ```typst +#set text(lang: "de") + ``` +] + +#task(lorem(1000)) diff --git a/packages/preview/sheetstorm/0.3.3/examples/minimal.typ b/packages/preview/sheetstorm/0.3.3/examples/minimal.typ new file mode 100644 index 0000000000..1ec1e68c43 --- /dev/null +++ b/packages/preview/sheetstorm/0.3.3/examples/minimal.typ @@ -0,0 +1,12 @@ +#import "@preview/sheetstorm:0.3.3" as sheetstorm: task + +#show: sheetstorm.setup.with( + title: "Minimal Example", + authors: "John Doe", +) + +#task[ + This is how the template looks with no/minimal configuration. +] + +#task(lorem(1000)) diff --git a/packages/preview/sheetstorm/0.3.3/examples/task-config.typ b/packages/preview/sheetstorm/0.3.3/examples/task-config.typ new file mode 100644 index 0000000000..54e99cb467 --- /dev/null +++ b/packages/preview/sheetstorm/0.3.3/examples/task-config.typ @@ -0,0 +1,31 @@ +#import "@preview/sheetstorm:0.3.3" as sheetstorm: task + +#show: sheetstorm.setup.with( + title: "Task Configuration Example", + authors: "John Doe", + initial-task-number: 3, +) + +#let my-custom-numbering-pattern(depth) = { + if depth == 1 { "i)" } + else if depth == 2 { "1." } + else { "(a)" } +} + +// You can customize the task command like so: +#let task = task.with( + counter-show: false, + subtask-numbering: true, + subtask-numbering-pattern: my-custom-numbering-pattern, +) + +#task(name: "Unnumbered Task")[ + Now, the task numbers are disabled for the whole document. + + Also, we have our custom numbering pattern enabled by default: + + Hi + + Hey + + Ho +] + +#task(counter-show: true)[Unless you explicitely enable the counter.] diff --git a/packages/preview/sheetstorm/0.3.3/src/header.typ b/packages/preview/sheetstorm/0.3.3/src/header.typ new file mode 100644 index 0000000000..d220e7ead0 --- /dev/null +++ b/packages/preview/sheetstorm/0.3.3/src/header.typ @@ -0,0 +1,60 @@ +#import "i18n.typ" +#import "util.typ": is-some + +/// Helper function that takes an array of content and puts it together as a block +#let header-section(xs) = box( + for i in xs.filter(is-some).intersperse(linebreak()) { i } +) + +/// Create the contents of the header +#let header-content( + course: none, + title: none, + authors: none, + tutor: none, + + date: datetime.today(), + date-format: none, + + show-title-on-first-page: false, + + extra-left: none, + extra-center: none, + extra-right: none, +) = { + let header = grid( + columns: (1fr, 3fr, 1fr), + align: (left, center, right), + rows: (auto, auto), + row-gutter: 0.5em, + + // left + header-section(( + if date != none { + context date.display(if date-format != none { date-format } else { i18n.default-date() }) + }, + if date-format != none { datetime.today().display(date-format) }, + if tutor != none [Tutor: #tutor], + extra-left, + )), + + // center + header-section(( + course, + if show-title-on-first-page { title } else { + context { + let n = counter(page).get().first() + if n != 1 { title } + } + }, + extra-center, + )), + + // right + header-section(authors + (extra-right,)), + + grid.hline(), + ) + + return pad(top: 0.8cm, bottom: 1cm, header) +} diff --git a/packages/preview/sheetstorm/0.3.3/src/i18n.typ b/packages/preview/sheetstorm/0.3.3/src/i18n.typ new file mode 100644 index 0000000000..1a337959ca --- /dev/null +++ b/packages/preview/sheetstorm/0.3.3/src/i18n.typ @@ -0,0 +1,14 @@ +#let default-date() = { + if text.lang == "de" { "[day].[month].[year]" } + else { "[day] [month repr:long] [year]" } +} + +#let task() = { + if text.lang == "de" { "Aufgabe" } + else { "Task" } +} + +#let points() = { + if text.lang == "de" { "Punkte" } + else { "Points" } +} diff --git a/packages/preview/sheetstorm/0.3.3/src/lib.typ b/packages/preview/sheetstorm/0.3.3/src/lib.typ new file mode 100644 index 0000000000..246107dd45 --- /dev/null +++ b/packages/preview/sheetstorm/0.3.3/src/lib.typ @@ -0,0 +1,3 @@ +#import "setup.typ": setup +#import "task.typ": task +#import "widgets.typ" as widgets diff --git a/packages/preview/sheetstorm/0.3.3/src/numbering.typ b/packages/preview/sheetstorm/0.3.3/src/numbering.typ new file mode 100644 index 0000000000..1fb46e9ea3 --- /dev/null +++ b/packages/preview/sheetstorm/0.3.3/src/numbering.typ @@ -0,0 +1,19 @@ +#let default-numbering-pattern(depth) = { + if calc.rem(depth, 3) == 1 { "1." } + else if calc.rem(depth, 3) == 2 { "a." } + else { "i." } +} + +#let subtask-numbering-pattern(depth) = { + if depth == 1 { "(a)" } + else if depth > 1 and calc.rem(depth, 2) == 0 { "1." } + else { "i." } +} + +#let apply-numbering-pattern( + numbering-pattern: default-numbering-pattern, + ..nums, +) = { + let nums = nums.pos() + numbering(numbering-pattern(nums.len()), nums.last()) +} diff --git a/packages/preview/sheetstorm/0.3.3/src/setup.typ b/packages/preview/sheetstorm/0.3.3/src/setup.typ new file mode 100644 index 0000000000..ad1313a6d1 --- /dev/null +++ b/packages/preview/sheetstorm/0.3.3/src/setup.typ @@ -0,0 +1,226 @@ +#import "header.typ": header-content +#import "widgets.typ" +#import "numbering.typ": apply-numbering-pattern, default-numbering-pattern +#import "i18n.typ" +#import "util.typ": is-some + +/// The setup function for the template +/// +/// This is the main "entrypoint" for the template. +/// Apply this function with a show everything rule to use it: +/// ```typst +/// #show: sheetstorm.setup.with( +/// title: "A cool title", +/// page-numbering: "1", +/// ) +/// ``` +/// +/// Here you can set many options to customize the template settings. +/// For general page settings, prefer to set it using this function if available. +#let setup( + course: none, + authors: none, + tutor: none, + + title: none, + title-default-styling: true, + title-size: 1.6em, + + margin-left: 1.7cm, + margin-right: 1.7cm, + margin-bottom: 1.7cm, + margin-above-header: 0cm, + margin-below-header: 0cm, + + paper: "a4", + page-numbering: "1 / 1", + + header-date: datetime.today(), + header-date-format: none, + header-show-title-on-first-page: false, + header-extra-left: none, + header-extra-center: none, + header-extra-right: none, + + initial-task-number: 1, + + widget-order-reversed: false, + widget-column-gap: 4em, + widget-row-gap: 1em, + widget-spacing-above: 0em, + widget-spacing-below: 1em, + + score-box-enabled: false, + score-box-tasks: none, + score-box-show-points: true, + score-box-bonus-counts-for-sum: false, + score-box-bonus-show-star: true, + score-box-inset: 0.7em, + score-box-cell-width: 4.5em, + + info-box-enabled: false, + info-box-show-ids: true, + info-box-show-emails: true, + info-box-inset: 0.7em, + info-box-gutter: 1em, + + doc, +) = { + let author-names + let author-ids + let author-emails + let has-ids = false + let has-emails = false + + if authors != none { + if type(authors) != array { authors = (authors,) } + + author-names = authors.map(a => + if type(a) == dictionary and "name" in a [ #a.name ] + else if a != none [ #a ] + ) + + author-ids = authors.map(a => if type(a) == dictionary and "id" in a [ #a.id ]) + author-emails = authors.map(a => if type(a) == dictionary and "email" in a [ #a.email ]) + + if author-ids != none { has-ids = author-ids.filter(is-some).len() > 0 } + if author-emails != none { has-emails = author-emails.filter(is-some).len() > 0 } + } + + let header = header-content( + course: course, + title: title, + authors: author-names, + tutor: tutor, + date: header-date, + date-format: header-date-format, + show-title-on-first-page: header-show-title-on-first-page, + extra-left: header-extra-left, + extra-center: header-extra-center, + extra-right: header-extra-right, + ) + + context { + // + // SETTINGS + // + + let header-height = measure(width: page.width - margin-left - margin-right, header).height + + set page( + paper: paper, + numbering: page-numbering, + margin: ( + top: header-height + margin-above-header, + bottom: margin-bottom, + left: margin-left, + right: margin-right, + ), + header: header, + header-ascent: 0pt, + ) + + set par( + first-line-indent: 1em, + justify: true, + ) + + set enum( + tight: false, + full: true, + numbering: apply-numbering-pattern, + ) + + show link: underline + + // + // TASK COUNTER + // + + let task-counter = counter("sheetstorm-task") + task-counter.update(initial-task-number - 1) + + // + // SPACING BELOW HEADER + // + v(margin-below-header) + + // + // WIDGETS + // (info box & score box) + // + + let info-box-enabled = info-box-enabled and author-names != none + + let widget-number = (score-box-enabled, info-box-enabled).map(x => if x { 1 } else { 0 }).sum() + + let info-box = if info-box-enabled { widgets.info-box( + author-names, + student-ids: if info-box-show-ids and has-ids { author-ids }, + emails: if info-box-show-emails and has-emails { author-emails }, + inset: info-box-inset, + gutter: info-box-gutter, + )} + + let score-box = if score-box-enabled { widgets.score-box( + tasks: score-box-tasks, + show-points: score-box-show-points, + bonus-counts-for-sum: score-box-bonus-counts-for-sum, + bonus-show-star: score-box-bonus-show-star, + inset: score-box-inset, + cell-width: score-box-cell-width, + )} + + if score-box-enabled or info-box-enabled { + let display-widgets = if not widget-order-reversed { + (info-box, score-box) + } else { + (score-box, info-box) + }.filter(is-some) + + v(widget-spacing-above) + + layout(size => { + let (columns, alignment) = { + if widget-number == 1 { + (1, center + horizon) + } else if widget-number == 2 { + let a = measure(info-box).width + let b = measure(score-box).width + if a + b > size.width { + (1, center + horizon) + } else { + (2, (left + horizon, right + horizon)) + } + } + } + + align(center, grid( + columns: columns, + align: alignment, + column-gutter: widget-column-gap, + row-gutter: widget-row-gap, + ..display-widgets + )) + }) + + v(widget-spacing-below) + } + + // + // TITLE + // + + if title != none { + let styled-title = if title-default-styling { underline[*#title*] } else [#title] + align(center, text(title-size, styled-title)) + } + + // + // REST OF THE DOCUMENT + // + + doc + } + +} diff --git a/packages/preview/sheetstorm/0.3.3/src/task.typ b/packages/preview/sheetstorm/0.3.3/src/task.typ new file mode 100644 index 0000000000..692f77f695 --- /dev/null +++ b/packages/preview/sheetstorm/0.3.3/src/task.typ @@ -0,0 +1,88 @@ +#import "numbering.typ": apply-numbering-pattern, subtask-numbering-pattern +#import "i18n.typ" + +/// A task block +/// +/// Use this function to create a section for each task. +/// It supports customized task numbers, points and bonus tasks. +/// ```typst +/// #task(name: "Pythagorean theorem")[ +/// _What is the Pythagorean theorem?_ +/// +/// $ a^2 + b^2 = c^2 $ +/// ] +/// ``` +#let task( + name: none, + task-string: none, + counter-show: true, + counter-reset: none, + subtask-numbering: false, + subtask-numbering-pattern: subtask-numbering-pattern, + points: none, + points-show: true, + points-string: none, + bonus: false, + bonus-show-star: true, + hidden: false, + space-above: auto, + space-below: 2em, + content, +) = { + let task-count = counter("sheetstorm-task") + if counter-reset == none { task-count.step() } else { task-count.update(counter-reset) } + + let points-enabled = false + let current-points + let display-points + + if type(points) == int { + points-enabled = true + current-points = points + display-points = [#points] + + // multiple points specified, e.g. `points: (1, 3, 1)`, gets rendered as "1 + 3 + 1" + } else if type(points) == array and points.map(p => type(p) == int).reduce((a, b) => a and b) { + points-enabled = true + current-points = points.sum() + display-points = points.map(str).intersperse(" + ").sum() + } + + state("sheetstorm-points").update(if points-enabled { current-points }) + state("sheetstorm-bonus").update(bonus) + state("sheetstorm-hidden-task").update(hidden) + + task-string = if task-string == none { context i18n.task() } else { task-string } + points-string = if points-string == none { context i18n.points() } else { points-string } + + let title = { + task-string + if counter-show { + if task-string != "" [ ] + context task-count.display() + } + if name != none [: #emph(name)] + if bonus and bonus-show-star [\*] + } + + block(width: 100%, above: space-above, below: space-below)[ + #set enum( + full: true, + numbering: if subtask-numbering { + apply-numbering-pattern.with(numbering-pattern: subtask-numbering-pattern) + } else { + apply-numbering-pattern + } + ) + + #block({ + show heading: box + [= #title ] + if points-enabled and points-show { + h(1fr) + [(#display-points #points-string)] + } + }) + #content + ] +} diff --git a/packages/preview/sheetstorm/0.3.3/src/util.typ b/packages/preview/sheetstorm/0.3.3/src/util.typ new file mode 100644 index 0000000000..9541ac15a9 --- /dev/null +++ b/packages/preview/sheetstorm/0.3.3/src/util.typ @@ -0,0 +1,3 @@ +#let is-some(x) = x != none + +#let to-content(x) = [#x] diff --git a/packages/preview/sheetstorm/0.3.3/src/widgets.typ b/packages/preview/sheetstorm/0.3.3/src/widgets.typ new file mode 100644 index 0000000000..f17d297e69 --- /dev/null +++ b/packages/preview/sheetstorm/0.3.3/src/widgets.typ @@ -0,0 +1,87 @@ +#import "util.typ": is-some, to-content + +/// Score Box widget +/// +/// This function creates an empty table for each task where the scores can be filled in. +/// By default, it reads the number of tasks from the `task` counter, +/// but you can set the task values manually. +#let score-box( + tasks: none, + show-points: true, + bonus-show-star: true, + bonus-counts-for-sum: false, + fill-space: false, + cell-width: 4.5em, + inset: 0.7em, +) = context { + let empty = v(1em) + + let display-tasks + let display-points + + if tasks == none { + let task-query = query() + let task-counter = counter("sheetstorm-task") + let hidden-task-state = state("sheetstorm-hidden-task") + let points-state = state("sheetstorm-points") + let bonus-state = state("sheetstorm-bonus") + + let task-query = task-query.filter(t => not hidden-task-state.at(t.location())) + + let task-list = task-query.map(t => task-counter.at(t.location()).first()) + let point-list = task-query.map(t => points-state.at(t.location())) + let bonus-task-list = task-query.map(t => bonus-state.at(t.location())) + + display-tasks = task-list.zip(bonus-task-list, exact: true).map(((t, b)) => + if b and bonus-show-star [*#t\**] else [*#t*] + ) + + let counting-points = point-list + .zip(bonus-task-list, exact: true) + .map(((p, b)) => if not b or bonus-counts-for-sum { p }) + .filter(is-some) + + let points-sum = if counting-points.len() > 0 { counting-points.sum() } + + display-points = (point-list + (points-sum,)).map(p => + if show-points and p != none [\/ #p] else { empty } + ) + } else { + display-tasks = tasks.map(to-content) + display-points = tasks.map(_ => empty) + } + + table( + columns: if fill-space { + display-tasks.map(_ => 1fr) + (1.3fr,) + } else { + display-tasks.map(_ => cell-width) + (1.3 * cell-width,) + }, + inset: inset, + align: (_, row) => if row == 1 { right } else { center }, + table.header(..(display-tasks + ([$sum$],))), + ..display-points + ) +} + +/// Info Box widget +/// +/// This function creates a box with information about the authors of the document. +/// You need to provide the names of the authors and optionally student IDs and/or email addresses. +#let info-box( + names, + student-ids: none, + emails: none, + inset: 0.7em, + gutter: 1em, +) = { + let info = (student-ids, emails).filter(is-some) + let entries = names.zip(..info).flatten() + + box(stroke: black, inset: inset, grid( + columns: info.len() + 1, + gutter: gutter, + align: left, + ..entries, + )) +} diff --git a/packages/preview/sheetstorm/0.3.3/template/main.typ b/packages/preview/sheetstorm/0.3.3/template/main.typ new file mode 100644 index 0000000000..6d01545051 --- /dev/null +++ b/packages/preview/sheetstorm/0.3.3/template/main.typ @@ -0,0 +1,55 @@ +#import "@preview/sheetstorm:0.3.3" as sheetstorm: task + +#show: sheetstorm.setup.with( + course: smallcaps[A very interesting course 101], + title: "Assignment 42", + authors: ( + (name: "John Doe", id: 123456), + (name: "Erika Mustermann", id: 654321), + ), + + info-box-enabled: true, + score-box-enabled: true, + + // Here you can customize the layout of the page, the header, the widgets. + // Look at the parameters of the `setup` function. +) + +#task(name: "Introduction")[ + This is #link("https://github.com/rabuu/sheetstorm")[`sheetstorm`], + a template library that provides a sane default layout for university assignment submissions with the option of customizability. + + Here you would write down your solutions for the first task: + #lorem(100) +] + +#task(name: "Subtasks", subtask-numbering: true, points: (1, 2))[ + + _What is the color of a banana?_ + + A banana is *yellow*. + + + _Solve the following equations for $x$._ + + $x^2 = 4 ==> x = plus.minus 2$ + + $x = integral_0^1 x^2 ==> x = [1/3 x^3]_0^1 = 1/3$ +] + +#task(points: 11)[ + Another task but without a name. + + Then you can do some cool math. You could, for example, try to proof that: + $ forall n gt.eq 0: sum_(i=0)^n i = (n dot (n+1))/2 $ + + _Proof._ It is easy to see that the statement is true for the number $0$: + $sum_(i=0)^0 i = 0 = (0 dot 1)/2$ + Let's assume that the statement is true for some $n$. It follows: + $ sum_(i=0)^(n+1) i + &= sum_(i=0)^n i + (n+1) + = (n dot (n+1)) / 2 + (n + 1) + = (n^2 + n) / 2 + (2n + 2)/2 \ + &= (n^2 + 3n + 2) / 2 + = ((n+1) dot (n+2)) / 2 $ + + Therefore, the statement is proven using the principle of induction. #h(1fr)$square$ +] + +#task(points: 1, bonus: true, lorem(300)) diff --git a/packages/preview/sheetstorm/0.3.3/thumbnail.png b/packages/preview/sheetstorm/0.3.3/thumbnail.png new file mode 100644 index 0000000000..6cb31e580a Binary files /dev/null and b/packages/preview/sheetstorm/0.3.3/thumbnail.png differ diff --git a/packages/preview/sheetstorm/0.3.3/typst.toml b/packages/preview/sheetstorm/0.3.3/typst.toml new file mode 100644 index 0000000000..d879fab02a --- /dev/null +++ b/packages/preview/sheetstorm/0.3.3/typst.toml @@ -0,0 +1,22 @@ +[package] +name = "sheetstorm" +version = "0.3.3" +compiler = "0.13.1" +entrypoint = "src/lib.typ" + +authors = [ "Rasmus Buurman " ] + +repository = "https://github.com/rabuu/sheetstorm" +license = "MIT" + +description = "A template for university exercise sheets." + +categories = ["layout", "report"] +keywords = ["handin", "assignment"] + +exclude = ["/examples/*"] + +[template] +path = "template" +entrypoint = "main.typ" +thumbnail = "thumbnail.png"