Page meta from frontmatter
This commit is contained in:
22
Cargo.lock
generated
22
Cargo.lock
generated
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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::<InlineEmote>();
|
||||
md.inline.add_rule::<InlineEmote>();
|
||||
md.inline.add_rule::<InlineMdx>();
|
||||
md.block.add_rule::<FrontMatter>().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::<InlineEmote>();
|
||||
md.inline.add_rule::<InlineMdx>();
|
||||
|
||||
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::<Root>() {
|
||||
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::<String>();
|
||||
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))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<String>,
|
||||
pub description: Option<String>,
|
||||
pub image: Option<String>,
|
||||
pub slug: Option<String>,
|
||||
}
|
||||
|
||||
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<String>) -> Self {
|
||||
pub fn from_markdown(md: impl Into<String>, default_image: Option<String>) -> Self {
|
||||
let md: String = md.into();
|
||||
let md = Markdown::parse(&md);
|
||||
|
||||
let mut meta = md
|
||||
.children
|
||||
.get(0)
|
||||
.map(|x| x.cast::<FrontMatter>())
|
||||
.flatten()
|
||||
.map(|x| serde_yaml::from_str::<PageMetadata>(&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)
|
||||
}
|
||||
}),
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
<br/>
|
||||
"#;
|
||||
|
||||
<img alt="betalupi map" class="image" src="/assets/img/betalupi.png"></img>
|
||||
@@ -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.
|
||||
</script>
|
||||
|
||||
|
||||
<br></br>
|
||||
|
||||
|
||||
## Advanced
|
||||
|
||||
@@ -250,5 +229,4 @@ they're ~14-18 years old.
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
"#;
|
||||
<br></br>
|
||||
@@ -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| {
|
||||
|
||||
@@ -1,8 +1,15 @@
|
||||
---
|
||||
title: Links
|
||||
author: Mark
|
||||
slug: links
|
||||
---
|
||||
|
||||
|
||||
# Bookmarks
|
||||
|
||||
This is a heavily opinionated bookmarks toolbar.
|
||||
|
||||
<hr style="margin-top: 8rem; margin-bottom: 8rem"></hr>
|
||||
<hr style="margin-top: 4rem; margin-bottom: 4rem"></hr>
|
||||
|
||||
## 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.
|
||||
|
||||
<br></br>
|
||||
|
||||
|
||||
## 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/)
|
||||
|
||||
<br></br>
|
||||
|
||||
|
||||
## 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)
|
||||
|
||||
<br></br>
|
||||
|
||||
|
||||
## 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
|
||||
|
||||
<hr style="margin-top: 8rem; margin-bottom: 8rem"></hr>
|
||||
<hr style="margin-top: 4rem; margin-bottom: 4rem"></hr>
|
||||
|
||||
## 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
|
||||
|
||||
<br></br>
|
||||
|
||||
|
||||
## 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/)
|
||||
|
||||
<br></br>
|
||||
|
||||
|
||||
## 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)
|
||||
|
||||
<br></br>
|
||||
|
||||
|
||||
## 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
|
||||
|
||||
|
||||
<br></br>
|
||||
|
||||
|
||||
## 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/)
|
||||
|
||||
<br></br>
|
||||
|
||||
|
||||
## 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.
|
||||
|
||||
|
||||
<hr style="margin-top: 8rem; margin-bottom: 8rem"></hr>
|
||||
<hr style="margin-top: 4rem; margin-bottom: 4rem"></hr>
|
||||
|
||||
## Misc
|
||||
|
||||
|
||||
@@ -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/
|
||||
*/
|
||||
@@ -1,4 +1,30 @@
|
||||
pub mod betalupi;
|
||||
pub mod handouts;
|
||||
pub mod index;
|
||||
pub mod links;
|
||||
mod index;
|
||||
use assetserver::Asset;
|
||||
pub use index::index;
|
||||
|
||||
use crate::{page::Page, routes::assets::Image_Icon};
|
||||
|
||||
pub fn links() -> Page {
|
||||
/*
|
||||
Dead links:
|
||||
|
||||
https://www.commitstrip.com/en/
|
||||
http://www.3dprintmath.com/
|
||||
*/
|
||||
|
||||
Page::from_markdown(include_str!("links.md"), Some(Image_Icon::URL.to_string()))
|
||||
}
|
||||
|
||||
pub fn betalupi() -> Page {
|
||||
Page::from_markdown(
|
||||
include_str!("betalupi.md"),
|
||||
Some(Image_Icon::URL.to_string()),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn handouts() -> Page {
|
||||
Page::from_markdown(
|
||||
include_str!("handouts.md"),
|
||||
Some(Image_Icon::URL.to_string()),
|
||||
)
|
||||
}
|
||||
|
||||
229
crates/service/service-webpage/src/routes/htwah/typesetting.md
Normal file
229
crates/service/service-webpage/src/routes/htwah/typesetting.md
Normal file
@@ -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
|
||||
|
||||
<hr style="margin-top: 5rem; margin-bottom: 5rem"/>
|
||||
|
||||
## 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.
|
||||
|
||||
<br/>
|
||||
|
||||
> Humans live and learn in the real world. \
|
||||
> Paper is a renewable resource. \
|
||||
> Edu-tech is bullshit.
|
||||
|
||||
[^1]: See: method of loci
|
||||
|
||||
<hr style="margin-top: 5rem; margin-bottom: 5rem"/>
|
||||
|
||||
## 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") }}
|
||||
|
||||
<br>
|
||||
|
||||
### 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/)
|
||||
|
||||
<hr style="margin-top: 5rem; margin-bottom: 5rem"/>
|
||||
|
||||
## 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.
|
||||
|
||||
<br>
|
||||
|
||||
> Good numbering is simple and consistent. \
|
||||
> Don't tell me to "look for the fourth theorem". \
|
||||
> Tell me _where it is_.
|
||||
|
||||
<hr style="margin-top: 5rem; margin-bottom: 5rem"/>
|
||||
|
||||
## 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.
|
||||
|
||||
<br>
|
||||
|
||||
> If you don't need it, take it out. \
|
||||
> If you do, put it in. \
|
||||
> Always introduce yourself.
|
||||
> Never do so twice.
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user