From 30bad547429b810b318f6c9655f933afbbe6bd1f Mon Sep 17 00:00:00 2001 From: rm-dr <96270320+rm-dr@users.noreply.github.com> Date: Tue, 4 Nov 2025 14:15:00 -0800 Subject: [PATCH] Page meta from frontmatter --- Cargo.lock | 22 ++ Cargo.toml | 3 +- crates/service/service-webpage/Cargo.toml | 3 + .../service-webpage/src/components/md.rs | 97 +++++++- .../service/service-webpage/src/page/mod.rs | 41 +++- .../src/pages/{betalupi.rs => betalupi.md} | 39 +-- .../src/pages/{handouts.rs => handouts.md} | 40 +-- .../service-webpage/src/pages/index.rs | 3 +- .../service-webpage/src/pages/links.md | 38 ++- .../service-webpage/src/pages/links.rs | 35 --- .../service/service-webpage/src/pages/mod.rs | 27 ++- .../src/routes/htwah/typesetting.md | 229 ++++++++++++++++++ .../service/service-webpage/src/routes/mod.rs | 97 +++++--- 13 files changed, 511 insertions(+), 163 deletions(-) rename crates/service/service-webpage/src/pages/{betalupi.rs => betalupi.md} (59%) rename crates/service/service-webpage/src/pages/{handouts.rs => handouts.md} (93%) delete mode 100644 crates/service/service-webpage/src/pages/links.rs create mode 100644 crates/service/service-webpage/src/routes/htwah/typesetting.md diff --git a/Cargo.lock b/Cargo.lock index f42992a..f901873 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1985,6 +1985,19 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_yaml" +version = "0.9.34+deprecated" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" +dependencies = [ + "indexmap", + "itoa", + "ryu", + "serde", + "unsafe-libyaml", +] + [[package]] name = "service-webpage" version = "0.0.1" @@ -1993,6 +2006,7 @@ dependencies = [ "axum", "chrono", "emojis", + "lazy_static", "libservice", "lru", "macro-assets", @@ -2000,6 +2014,8 @@ dependencies = [ "markdown-it", "maud", "parking_lot", + "serde", + "serde_yaml", "strum", "tracing", ] @@ -2545,6 +2561,12 @@ version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" +[[package]] +name = "unsafe-libyaml" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" + [[package]] name = "untrusted" version = "0.9.0" diff --git a/Cargo.toml b/Cargo.toml index fd16423..02a261e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -112,6 +112,7 @@ anstyle = { version = "1.0.13" } serde = { version = "1.0.228", features = ["derive"] } serde_json = "1.0.145" toml = "0.9.8" +serde_yaml = "0.9" base64 = "0.22.1" # @@ -126,7 +127,7 @@ num = "0.4.3" chrono = "0.4.42" lru = "0.16.2" parking_lot = "0.12.5" - +lazy_static = "1.5.0" # # Macro utilities diff --git a/crates/service/service-webpage/Cargo.toml b/crates/service/service-webpage/Cargo.toml index 9d99516..aec794e 100644 --- a/crates/service/service-webpage/Cargo.toml +++ b/crates/service/service-webpage/Cargo.toml @@ -22,3 +22,6 @@ strum = { workspace = true } chrono = { workspace = true } parking_lot = { workspace = true } lru = { workspace = true } +lazy_static = { workspace = true } +serde_yaml = { workspace = true } +serde = { workspace = true } diff --git a/crates/service/service-webpage/src/components/md.rs b/crates/service/service-webpage/src/components/md.rs index d56a6df..c857059 100644 --- a/crates/service/service-webpage/src/components/md.rs +++ b/crates/service/service-webpage/src/components/md.rs @@ -1,33 +1,51 @@ +use lazy_static::lazy_static; +use markdown_it::parser::block::{BlockRule, BlockState}; +use markdown_it::parser::core::Root; use markdown_it::parser::inline::{InlineRule, InlineState}; -use markdown_it::{Node, NodeValue, Renderer}; +use markdown_it::{MarkdownIt, Node, NodeValue, Renderer}; use maud::{Markup, PreEscaped, Render}; use std::str::FromStr; use crate::components::fa::FAIcon; use crate::components::mangle::{MangledBetaEmail, MangledGoogleEmail}; +lazy_static! { + static ref MdParser: MarkdownIt = { + let mut md = markdown_it::MarkdownIt::new(); + markdown_it::plugins::cmark::add(&mut md); + markdown_it::plugins::html::add(&mut md); + md.inline.add_rule::(); + md.inline.add_rule::(); + md.inline.add_rule::(); + md.block.add_rule::().before_all(); + md + }; +} + pub struct Markdown<'a>(pub &'a str); impl Render for Markdown<'_> { fn render(&self) -> Markup { - // TODO: init once - let md = &mut markdown_it::MarkdownIt::new(); - markdown_it::plugins::cmark::add(md); - markdown_it::plugins::html::add(md); - md.inline.add_rule::(); - md.inline.add_rule::(); - - let md = md.parse(self.0); + let md = Self::parse(self.0); let html = md.render(); - return PreEscaped(html); } } +impl Markdown<'_> { + pub fn parse(md_str: &str) -> Node { + MdParser.parse(md_str) + } +} + // // MARK: extensions // +// +// MARK: emote +// + #[derive(Debug)] pub struct InlineEmote(String); @@ -66,6 +84,10 @@ impl InlineRule for InlineEmote { } } +// +// MARK: mdx +// + #[derive(Debug)] pub struct InlineMdx(String); @@ -245,3 +267,58 @@ fn mdx_include(mdx: &str, _node: &Node, fmt: &mut dyn Renderer) -> bool { return true; } + +// +// MARK: frontmatter +// + +#[derive(Debug)] +/// AST node for front-matter +pub struct FrontMatter { + pub content: String, +} + +impl NodeValue for FrontMatter { + fn render(&self, _node: &Node, _fmt: &mut dyn Renderer) {} +} + +impl BlockRule for FrontMatter { + fn run(state: &mut BlockState<'_, '_>) -> Option<(Node, usize)> { + // check the parent is the document Root + if !state.node.is::() { + return None; + } + + // check we are on the first line of the document + if state.line != 0 { + return None; + } + + // check line starts with opening dashes + let opening = state + .get_line(state.line) + .chars() + .take_while(|c| *c == '-') + .collect::(); + if !opening.starts_with("---") { + return None; + } + + // Search for the end of the block + let mut next_line = state.line; + loop { + next_line += 1; + if next_line >= state.line_max { + return None; + } + + let line = state.get_line(next_line); + if line.starts_with(&opening) { + break; + } + } + + let (content, _) = state.get_lines(state.line + 1, next_line, 0, true); + Some((Node::new(FrontMatter { content }), next_line + 1)) + } +} diff --git a/crates/service/service-webpage/src/page/mod.rs b/crates/service/service-webpage/src/page/mod.rs index 62990c4..0a42498 100644 --- a/crates/service/service-webpage/src/page/mod.rs +++ b/crates/service/service-webpage/src/page/mod.rs @@ -12,19 +12,24 @@ use axum::{ use chrono::{DateTime, Utc}; use libservice::ServiceConnectInfo; use lru::LruCache; -use maud::{Markup, Render, html}; +use maud::{Markup, PreEscaped, Render, html}; use parking_lot::Mutex; +use serde::Deserialize; use std::{collections::HashMap, num::NonZero, sync::Arc, time::Duration}; use tracing::{debug, trace}; -use crate::components::{md::Markdown, misc::Backlinks}; +use crate::components::{ + md::{FrontMatter, Markdown}, + misc::Backlinks, +}; -#[derive(Debug, Clone, Hash, PartialEq, Eq)] +#[derive(Debug, Clone, Hash, PartialEq, Eq, Deserialize)] pub struct PageMetadata { pub title: String, pub author: Option, pub description: Option, pub image: Option, + pub slug: Option, } impl Default for PageMetadata { @@ -34,6 +39,7 @@ impl Default for PageMetadata { author: None, description: None, image: None, + slug: None, } } } @@ -103,16 +109,37 @@ impl Page { (self.generate_html)(self) } - pub fn from_markdown(meta: PageMetadata, md: impl Into) -> Self { + pub fn from_markdown(md: impl Into, default_image: Option) -> Self { let md: String = md.into(); + let md = Markdown::parse(&md); + + let mut meta = md + .children + .get(0) + .map(|x| x.cast::()) + .flatten() + .map(|x| serde_yaml::from_str::(&x.content)) + .unwrap_or(Ok(Default::default())) + .unwrap_or(PageMetadata { + title: "Invalid frontmatter!".into(), + ..Default::default() + }); + + if meta.image.is_none() { + meta.image = default_image + } + + let html = PreEscaped(md.render()); - // TODO: define metadata and backlinks in markdown Page { meta, generate_html: Box::new(move |page| { html! { - (Backlinks(&[("/", "home")], &page.meta.title)) - (Markdown(&md)) + @if let Some(slug) = &page.meta.slug { + (Backlinks(&[("/", "home")], slug)) + } + + (html) } }), diff --git a/crates/service/service-webpage/src/pages/betalupi.rs b/crates/service/service-webpage/src/pages/betalupi.md similarity index 59% rename from crates/service/service-webpage/src/pages/betalupi.rs rename to crates/service/service-webpage/src/pages/betalupi.md index decbf2c..8e79ca8 100644 --- a/crates/service/service-webpage/src/pages/betalupi.rs +++ b/crates/service/service-webpage/src/pages/betalupi.md @@ -1,34 +1,10 @@ -use assetserver::Asset; -use maud::html; +--- +title: "What's a \"betalupi?\"" +author: "Mark" +slug: whats-a-betalupi +--- -use crate::{ - components::{md::Markdown, misc::Backlinks}, - page::{Page, PageMetadata}, - routes::assets::{Image_Betalupi, Image_Icon}, -}; - -pub fn page() -> Page { - Page { - meta: PageMetadata { - title: "What's a \"betalupi?\"".into(), - author: Some("Mark".into()), - description: None, - image: Some(Image_Icon::URL.into()), - }, - - generate_html: Box::new(|_page| { - html! { - (Backlinks(&[("/", "home")], "whats-a-betalupi")) - (Markdown(MD)) - img alt="betalupi map" class="image" src=(Image_Betalupi::URL) {} - } - }), - - ..Default::default() - } -} - -const MD: &str = r#"[es]: https://github.com/endless-sky/endless-sky +[es]: https://github.com/endless-sky/endless-sky [*Stellaris*]: https://www.paradoxinteractive.com/games/stellaris/about [Arabic]: https://en.wikipedia.org/wiki/List_of_Arabic_star_names [wiki-betalupi]: https://en.wikipedia.org/wiki/Beta_Lupi @@ -54,4 +30,5 @@ A snippet of the [_Endless Sky_][es] map is below. - Isn't owned by a scalper that's selling it for $300"
-"#; + +betalupi map diff --git a/crates/service/service-webpage/src/pages/handouts.rs b/crates/service/service-webpage/src/pages/handouts.md similarity index 93% rename from crates/service/service-webpage/src/pages/handouts.rs rename to crates/service/service-webpage/src/pages/handouts.md index b75d8cd..ea77a65 100644 --- a/crates/service/service-webpage/src/pages/handouts.rs +++ b/crates/service/service-webpage/src/pages/handouts.md @@ -1,33 +1,10 @@ -use assetserver::Asset; -use maud::html; +--- +title: Mark's Handouts +author: Mark +slug: handouts +--- -use crate::{ - components::{md::Markdown, misc::Backlinks}, - page::{Page, PageMetadata}, - routes::assets::Image_Icon, -}; - -pub fn page() -> Page { - Page { - meta: PageMetadata { - title: "Mark's Handouts".into(), - author: Some("Mark".into()), - description: None, - image: Some(Image_Icon::URL.into()), - }, - - generate_html: Box::new(|_page| { - html! { - (Backlinks(&[("/", "home")], "handouts")) - (Markdown(MD_A)) - } - }), - - ..Default::default() - } -} - -const MD_A: &str = r#"# Mark's Handouts +# Mark's Handouts [ORMC]: https://circles.math.ucla.edu/circles @@ -155,6 +132,8 @@ giving them something to do until we can start the lesson. +

+ ## Advanced @@ -250,5 +229,4 @@ they're ~14-18 years old. }); - -"#; +

diff --git a/crates/service/service-webpage/src/pages/index.rs b/crates/service/service-webpage/src/pages/index.rs index d2ef516..0397e9f 100644 --- a/crates/service/service-webpage/src/pages/index.rs +++ b/crates/service/service-webpage/src/pages/index.rs @@ -12,13 +12,14 @@ use crate::{ routes::assets::{Image_Cover, Image_Icon}, }; -pub fn page() -> Page { +pub fn index() -> Page { Page { meta: PageMetadata { title: "Betalupi: About".into(), author: Some("Mark".into()), description: Some("Description".into()), image: Some(Image_Icon::URL.into()), + slug: None, }, generate_html: Box::new(move |_page| { diff --git a/crates/service/service-webpage/src/pages/links.md b/crates/service/service-webpage/src/pages/links.md index 7943d67..0eada53 100644 --- a/crates/service/service-webpage/src/pages/links.md +++ b/crates/service/service-webpage/src/pages/links.md @@ -1,8 +1,15 @@ +--- +title: Links +author: Mark +slug: links +--- + + # Bookmarks This is a heavily opinionated bookmarks toolbar. -
+
## Podcasts @@ -13,12 +20,18 @@ This is a heavily opinionated bookmarks toolbar. - :star: [On the Metal](https://onthemetal.transistor.fm/): Quality stories from quality engineers. - [Security Cryptography Whatever](https://securitycryptographywhatever.com/): Modern cryptography, for those who understand the underlying theory. +

+ + ## Essays - [Real Programmers don't use Pascal](https://www.ee.torontomu.ca/~elf/hack/realmen.html) - [A Mathematician's Lament](/files/lockhart.pdf) - :star: [The Jargon File](http://www.catb.org/jargon/) +

+ + ## Textbooks - :star: [OpenLogic](https://openlogicproject.org/): The gold standard @@ -28,6 +41,9 @@ This is a heavily opinionated bookmarks toolbar. - [An Introduction to Mathematical Cryptography](https://link.springer.com/book/10.1007/978-0-387-77993-5) - [Stories about Maxima and Minima](https://archive.org/details/storiesaboutmaxi0000tikh) +

+ + ## Miscellanea - [Hackaday](https://hackaday.com/) @@ -41,7 +57,7 @@ This is a heavily opinionated bookmarks toolbar. - :star: [Spintronics](https://store.upperstory.com/collections/spintronics/products/spintronics-act-one): Mechanical circuits. Very clever toy. - :star: [Turing Tumble](https://store.upperstory.com/collections/turing-tumble-game/products/turing-tumble): Modern Dr. Nim -
+
## Tools @@ -65,6 +81,9 @@ This is a heavily opinionated bookmarks toolbar. - [delta](https://github.com/dandavison/delta): pretty pager for diffs - [dust](https://github.com/dandavison/delta): `du`, but better +

+ + ## Math Resources - [Quantum Quest](https://www.quantum-quest.org/) @@ -74,6 +93,9 @@ This is a heavily opinionated bookmarks toolbar. - [Euclidea](https://www.euclidea.xyz/) - [Problems.ru](https://problems.ru/) +

+ + ## OS Dev Resources - [OS Dev Wiki](https://wiki.osdev.org/Expanded_Main_Page) @@ -84,6 +106,9 @@ This is a heavily opinionated bookmarks toolbar. - [FDC Programming](http://www.brokenthorn.com/Resources/OSDev20.html) - [CS77 at Bristol College](http://www.c-jump.com/CIS77/CIS77syllabus.htm) +

+ + ## Misc Resources - [Learn OpenGL](https://learnopengl.com/) @@ -99,6 +124,10 @@ This is a heavily opinionated bookmarks toolbar. - [The Architecture of Open Source Applications](http://aosabook.org/en/index.html) - [wtfjs](https://github.com/denysdovhan/wtfjs): js [wat](https://www.destroyallsoftware.com/talks/wat)s + +

+ + ## Reference - [DevHints](https://devhints.io/) @@ -108,12 +137,15 @@ This is a heavily opinionated bookmarks toolbar. - [The Pinouts Book](https://pinouts.org/) - [Laws of UX](https://lawsofux.com/) +

+ + ## Rust - [Understanding Memory Ordering in Rust](https://emschwartz.me/understanding-memory-ordering-in-rust/) - [Unfair Rust Quiz](https://this.quiz.is.fckn.gay/): wtfjs, but in Rust. -
+
## Misc diff --git a/crates/service/service-webpage/src/pages/links.rs b/crates/service/service-webpage/src/pages/links.rs deleted file mode 100644 index 5790c57..0000000 --- a/crates/service/service-webpage/src/pages/links.rs +++ /dev/null @@ -1,35 +0,0 @@ -use assetserver::Asset; -use maud::html; - -use crate::{ - components::{md::Markdown, misc::Backlinks}, - page::{Page, PageMetadata}, - routes::assets::Image_Icon, -}; - -pub fn page() -> Page { - Page { - meta: PageMetadata { - title: "Links".into(), - author: Some("Mark".into()), - description: None, - image: Some(Image_Icon::URL.into()), - }, - - generate_html: Box::new(|_page| { - html! { - (Backlinks(&[("/", "home")], "links")) - (Markdown(include_str!("links.md"))) - } - }), - - ..Default::default() - } -} - -/* -Dead links: - -https://www.commitstrip.com/en/ -http://www.3dprintmath.com/ -*/ diff --git a/crates/service/service-webpage/src/pages/mod.rs b/crates/service/service-webpage/src/pages/mod.rs index e227dd0..c166ef7 100644 --- a/crates/service/service-webpage/src/pages/mod.rs +++ b/crates/service/service-webpage/src/pages/mod.rs @@ -1,4 +1,23 @@ -pub mod betalupi; -pub mod handouts; -pub mod index; -pub mod links; +mod index; +pub use index::index; + +use crate::page::Page; + +pub fn links() -> Page { + /* + Dead links: + + https://www.commitstrip.com/en/ + http://www.3dprintmath.com/ + */ + + Page::from_markdown(include_str!("links.md"), None) +} + +pub fn betalupi() -> Page { + Page::from_markdown(include_str!("betalupi.md"), None) +} + +pub fn handouts() -> Page { + Page::from_markdown(include_str!("handouts.md"), None) +} diff --git a/crates/service/service-webpage/src/routes/htwah/typesetting.md b/crates/service/service-webpage/src/routes/htwah/typesetting.md new file mode 100644 index 0000000..fd5c31f --- /dev/null +++ b/crates/service/service-webpage/src/routes/htwah/typesetting.md @@ -0,0 +1,229 @@ ++++ +title = "HtWaH: Typesetting" +template = "page.html" + +[extra] +show_title = false +back_links = [ + {target = "/htwah", text = "htwah"} +] ++++ + +## Table of Contents + +This page is part of my [how to write a handout](/htwah) series. + +- [Part 1: Typesetting](.) {{ color(c="green", t="**<-- you are here**") }} +- Part 2: Notation +- Part 3: Designing lessons +- Part 4: Designing puzzles +- Part 5: Leading a class + +
+ +## The Medium + +### Always teach in print + +A physical handout is three-dimensional. Every point inside it has a horizontal, vertical, and azimuthal position. This makes it easy to navigate, and thus easier to understand. + +Navigation is inseparable from memory---this fact has been known for millennia.[^1] +Lessons presented on a screen do not take advantage of this; lessons presented on paper do. + +### Never print on both sides + +Double-sided handouts break this spacial intuition. The act of turning a page upside-down severs its connection to physical space. Do not entangle your students in the pages of a double-sided worksheet---print only on one side. + +{{color(c="grey", t="This is not true of books, which are presented in a fundamentally different way.")}} + +Double-sided handouts also clutter the page with the opposite side's nodes. Even when using a pencil, work on the front of a page makes its way to the back. + +
+ +> Humans live and learn in the real world. \ +> Paper is a renewable resource. \ +> Edu-tech is bullshit. + +[^1]: See: method of loci + +
+ +## Spacing + +Writing is visual. It catches the eye before it has a chance to catch the brain.[^2] A good lesson takes advantage of this: it uses space to frame its content. + +Take care to organize the text you produce. Provide a clear visual boundary between separate ideas---unbroken waterfalls mathematical text are difficult to parse---and be sure to place visual boundaries only where there are logical ones. + +Use empty space generously, but never use it wastefully. Too much space is just as distracting as too little. + +### Leave space for work + +If you force your students to cram their work into the margin, +their understanding will have a similar quality. \ +Give them space to work and space to think. + +{{color(c="grey", t="This rule only applies to handouts. Lecture notes and textbooks do not need to worry about this, since students should work through them on a separate sheet of paper.")}} + +Every problem should be followed by a `\vfill`. \ +There are exceptions, but they are rare. + +If your handouts include solutions +{{color(c="grey", t="(they should),")}} +observe the following rule: each problem's typeset +solution should fit in the empty space after the problem. +This isn't perfectly accurate estimate of space +(your students will likely need more), but it is a very good minimum. + +{{color(c="grey", t="If your students will be drawing pictures, make each gap twice as bit as it needs to be.")}} + +In a handout that is typeset well, the layout of each page should not +change when solutions are shown. This helps you give students enough +room to solve each problem, and keeps page numbers consistent between +instructors' and students' handouts: + +{{ twopdf(left="/htwah/sols-a.pdf", right="/htwah/sols-b.pdf") }} + +
+ +### Break with intention + +A page should _never_ break in the middle of a definition, problem, or theorem. + +A page should _never_ break between the first mention of a concept and the problems used to introduce it. + +New sections should _always_ start on a new page. + +{{color(c="grey", t="Unlike the previous comments on spacing, the above rules apply to all formats. Whether you are writing a textbook, a handout, or lecture notes, do not allow your page breaks to break as student's train of thought.")}} + +These rules are easy to follow if you leave space for work: \ +Place your `\pagebreak`s well and let `\vfill` handle the rest. + +### Separation + +Problems, definitions, and theorems should come in clear blocks. +It should be easy to tell which element of a page each line of text +relates to[^3]. + +The [Open Logic Project](https://openlogicproject.org/) provides a +striking example of this. Definitions are boxed and titled---it is abundandly +clear that these are the definitions we will be using in the future. + +It is also a good idea to mark the first use of a term. \ +In the example below, this is done with italicized text. + +{{ onepdf(src="/htwah/definitions.pdf") }} + +Of course, not all texts need such aggressive grouping. I tend to avoid clear "boxing" in [my lessons](/handouts), opting for subtle grouping by whitespace instead. This is a matter of taste. + +{{color(c="grey", t="Regardless of implementation, visual boundaries should match conceptual boundaries. It should be easy to see where each logical element ends.")}} + +Finally, remember that too much separation is just as distracting as too little. +Balance is key. + +### Proximity + +Different elements should be far apart, and related elements should be close together[^4]. This rule is often violated by equations and diagrams. +Consider the two pages below: + +{{ twopdf(left="/htwah/spacing-a.pdf", right="/htwah/spacing-b.pdf") }} + +The circuit diagram on the left is clearly a part of the setup at the top of the page. It is not connected to Problem 14. + +Compare this to the equations under Examples 1.25 and 1.26 on the right. These elements do not have a clear owner, since they are just as far from the text above them as the text below. + +> The visual layout of a lesson must match its logical layout. \ +> Similar items are close, different items are far. \ +> Key information should be clearly visible and easy to find. + +[^2]: See "Paragraphs" in William Zinsser's _On Writing Well_. +[^3]: See: [the chunking principle](https://lawsofux.com/chunking/) +[^4]: See: [the law of proximity](https://lawsofux.com/law-of-proximity/) + +
+ +## Numbering + +### Pages + +Pages should be numbered unless the document only has one page. There are no exceptions to this rule. + +Numbering should be simple: use a single counter that is set to 1 on the first page of the document. Every page should have a number, even if that number isn't visible. This includes frontmatter and the table of contents. + +Different numbering systems (`i, ii, iii...` for a preface; `A31, A32, A33...` for appendices) have little value and only make navigation more difficult. + +{{color(c="grey", t="Roman numbering of frontmatter in large textbooks is acceptable, but anything more complicated is unnecessary.")}} + +### Versions + +The document itself should also be numbered. In most cases, a `\today` on the front page of the lesson is all you need. + +This helps synchronize the handout you _think_ the class has with the handout that the class _really_ has. + +Future instructors {{color(c="grey", t="(and future you)")}} will be thankful. + +### Items + +Propositions, definitions, and examples should all be numbered with the same counter. Problems should also use this counter, unless they are only listed at the end of each section {{color(c="grey", t="(as they often are in textbooks).")}} + +Do not use different counters for different objects. Theorem 1 should not follow Definition 2. This is the default behavior of LaTeX, and it is a serious mistake. + +Such a numbering system makes it difficult for readers to orient themselves. Say a student is solving Problem 6, which references Theorem 2. Where should she look? Is Theorem 2 on the previous page, or is it near the beginning of the lesson? It could even be on the _next_ page! + +The only choice she has is to look through the lesson page-by-page until she finds Theorem 2. Finding Theorem 3 will not help---it would tell her where Theorem 2 _isn't_, but it won't tell her where it _is_. + +With a single counter, this is not an issue. Readers are aware of their absolute position at every point in the lesson, and can easily find what they need. + +[Open Logic](https://openlogicproject.org/) again provides an example of quality typesetting. Notice how the numbering of Propositions, Definitions, and Examples on the page below is consecutive: + +{{ onepdf(src="/htwah/numbering.pdf") }} + +In long textbooks, prefixing numbers with the chapter index (e.g, the `2` in `2.32` above) is wise. In any other case, a single counter should be enough. + +
+ +> Good numbering is simple and consistent. \ +> Don't tell me to "look for the fourth theorem". \ +> Tell me _where it is_. + +
+ +## Margins + +### Always have a title + +Any document you produce should be identifiable by its first page. +Always have a title that tells the reader what they're looking at, +who made it, when, and why. + +If you're writing a document that is available in multiple variants (for example, a handout with solutions or an exam with multiple versions), the variant should be easily visible in the title. Note the box under the title in the handout on the left. + +{{ twopdf(left="/htwah/sols-a.pdf", right="/htwah/sols-b.pdf") }} + +This lets us detect errors quickly: we only need to look at the first page of a lesson to know if we printed the wrong variant, handed the students solutions, or graded an exam using the wrong answer key. + +### Never have a running header + +We already have a title, so a running header adds no value. \ +Let your students fill the margin. + +### Avoid footnotes + +Do not put important information in footnotes. \ +Find a way to state it directly. + +Footnotes are for trivia, historical notes, or extra reading. \ +They are for information your lesson could live without. + +This reasoning can be applied to parentheticals, hints, and other auxiliary text: +as a general rule, it should be avoided. + +Important information deserves a place in the main flow of the document, +and you should structure your prose so that it fits. Don't stick it on +as an afterthought. + +
+ +> If you don't need it, take it out. \ +> If you do, put it in. \ +> Always introduce yourself. +> Never do so twice. diff --git a/crates/service/service-webpage/src/routes/mod.rs b/crates/service/service-webpage/src/routes/mod.rs index ca2cfb7..cc36818 100644 --- a/crates/service/service-webpage/src/routes/mod.rs +++ b/crates/service/service-webpage/src/routes/mod.rs @@ -1,9 +1,14 @@ use assetserver::Asset; use axum::Router; -use maud::{DOCTYPE, PreEscaped, html}; +use maud::{DOCTYPE, Markup, PreEscaped, html}; use tracing::info; -use crate::{components::misc::FarLink, page::PageServer, pages, routes::assets::Styles_Main}; +use crate::{ + components::misc::FarLink, + page::{Page, PageServer}, + pages, + routes::assets::Styles_Main, +}; pub mod assets; @@ -11,52 +16,64 @@ pub(super) fn router() -> Router<()> { let (asset_prefix, asset_router) = assets::asset_router(); info!("Serving assets at {asset_prefix}"); - let server = PageServer::new(Box::new(|page| { - html! { - (DOCTYPE) - html { - head { - meta charset="UTF" {} - meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no" {} - meta content="text/html; charset=UTF-8" http-equiv="content-type" {} - meta property="og:type" content="website" {} + let server = build_server().into_router(); - link rel="stylesheet" href=(Styles_Main::URL) {} + Router::new().merge(server).nest(asset_prefix, asset_router) +} - (&page.meta) - title { (PreEscaped(page.meta.title.clone())) } - } +fn build_server() -> PageServer { + PageServer::new(Box::new(page_wrapper)) + .add_page("/", pages::index()) + .add_page("/links", pages::links()) + .add_page("/whats-a-betalupi", pages::betalupi()) + .add_page("/handouts", pages::handouts()) +} - body { - div class="wrapper" { - main { ( page.generate_html() ) } +fn page_wrapper(page: &Page) -> Markup { + html! { + (DOCTYPE) + html { + head { + meta charset="UTF" {} + meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no" {} + meta content="text/html; charset=UTF-8" http-equiv="content-type" {} + meta property="og:type" content="website" {} - footer { - hr class = "footline" {} - div class = "footContainer" { - p { - "This site was built by hand using " - (FarLink("https://rust-lang.org", "Rust")) - ", " - (FarLink("https://maud.lambda.xyz", "Maud")) - ", " - (FarLink("https://github.com/connorskees/grass", "Grass")) - ", and " - (FarLink("https://docs.rs/axum/latest/axum", "Axum")) - "." - } + link rel="stylesheet" href=(Styles_Main::URL) {} + + (&page.meta) + title { (PreEscaped(page.meta.title.clone())) } + } + + body { + div class="wrapper" { + main { ( page.generate_html() ) } + + footer { + hr class = "footline" {} + div class = "footContainer" { + p { + "This site was built by hand using " + (FarLink("https://rust-lang.org", "Rust")) + ", " + (FarLink("https://maud.lambda.xyz", "Maud")) + ", " + (FarLink("https://github.com/connorskees/grass", "Grass")) + ", and " + (FarLink("https://docs.rs/axum/latest/axum", "Axum")) + "." } } } } } } - })) - .add_page("/", pages::index::page()) - .add_page("/links", pages::links::page()) - .add_page("/whats-a-betalupi", pages::betalupi::page()) - .add_page("/handouts", pages::handouts::page()) - .into_router(); - - Router::new().merge(server).nest(asset_prefix, asset_router) + } +} + +#[test] +fn server_builds_without_panic() { + // Catches some runtime errors thrown by axum, + // e.g bad route nesting or routes not starting with "/" + let _server = build_server().into_router(); }