Page meta from frontmatter
This commit is contained in:
22
Cargo.lock
generated
22
Cargo.lock
generated
@@ -1985,6 +1985,19 @@ dependencies = [
|
|||||||
"serde",
|
"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]]
|
[[package]]
|
||||||
name = "service-webpage"
|
name = "service-webpage"
|
||||||
version = "0.0.1"
|
version = "0.0.1"
|
||||||
@@ -1993,6 +2006,7 @@ dependencies = [
|
|||||||
"axum",
|
"axum",
|
||||||
"chrono",
|
"chrono",
|
||||||
"emojis",
|
"emojis",
|
||||||
|
"lazy_static",
|
||||||
"libservice",
|
"libservice",
|
||||||
"lru",
|
"lru",
|
||||||
"macro-assets",
|
"macro-assets",
|
||||||
@@ -2000,6 +2014,8 @@ dependencies = [
|
|||||||
"markdown-it",
|
"markdown-it",
|
||||||
"maud",
|
"maud",
|
||||||
"parking_lot",
|
"parking_lot",
|
||||||
|
"serde",
|
||||||
|
"serde_yaml",
|
||||||
"strum",
|
"strum",
|
||||||
"tracing",
|
"tracing",
|
||||||
]
|
]
|
||||||
@@ -2545,6 +2561,12 @@ version = "0.2.6"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
|
checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "unsafe-libyaml"
|
||||||
|
version = "0.2.11"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "untrusted"
|
name = "untrusted"
|
||||||
version = "0.9.0"
|
version = "0.9.0"
|
||||||
|
|||||||
@@ -112,6 +112,7 @@ anstyle = { version = "1.0.13" }
|
|||||||
serde = { version = "1.0.228", features = ["derive"] }
|
serde = { version = "1.0.228", features = ["derive"] }
|
||||||
serde_json = "1.0.145"
|
serde_json = "1.0.145"
|
||||||
toml = "0.9.8"
|
toml = "0.9.8"
|
||||||
|
serde_yaml = "0.9"
|
||||||
base64 = "0.22.1"
|
base64 = "0.22.1"
|
||||||
|
|
||||||
#
|
#
|
||||||
@@ -126,7 +127,7 @@ num = "0.4.3"
|
|||||||
chrono = "0.4.42"
|
chrono = "0.4.42"
|
||||||
lru = "0.16.2"
|
lru = "0.16.2"
|
||||||
parking_lot = "0.12.5"
|
parking_lot = "0.12.5"
|
||||||
|
lazy_static = "1.5.0"
|
||||||
|
|
||||||
#
|
#
|
||||||
# Macro utilities
|
# Macro utilities
|
||||||
|
|||||||
@@ -22,3 +22,6 @@ strum = { workspace = true }
|
|||||||
chrono = { workspace = true }
|
chrono = { workspace = true }
|
||||||
parking_lot = { workspace = true }
|
parking_lot = { workspace = true }
|
||||||
lru = { 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::parser::inline::{InlineRule, InlineState};
|
||||||
use markdown_it::{Node, NodeValue, Renderer};
|
use markdown_it::{MarkdownIt, Node, NodeValue, Renderer};
|
||||||
use maud::{Markup, PreEscaped, Render};
|
use maud::{Markup, PreEscaped, Render};
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
|
|
||||||
use crate::components::fa::FAIcon;
|
use crate::components::fa::FAIcon;
|
||||||
use crate::components::mangle::{MangledBetaEmail, MangledGoogleEmail};
|
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);
|
pub struct Markdown<'a>(pub &'a str);
|
||||||
|
|
||||||
impl Render for Markdown<'_> {
|
impl Render for Markdown<'_> {
|
||||||
fn render(&self) -> Markup {
|
fn render(&self) -> Markup {
|
||||||
// TODO: init once
|
let md = Self::parse(self.0);
|
||||||
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 html = md.render();
|
let html = md.render();
|
||||||
|
|
||||||
return PreEscaped(html);
|
return PreEscaped(html);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Markdown<'_> {
|
||||||
|
pub fn parse(md_str: &str) -> Node {
|
||||||
|
MdParser.parse(md_str)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
//
|
//
|
||||||
// MARK: extensions
|
// MARK: extensions
|
||||||
//
|
//
|
||||||
|
|
||||||
|
//
|
||||||
|
// MARK: emote
|
||||||
|
//
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct InlineEmote(String);
|
pub struct InlineEmote(String);
|
||||||
|
|
||||||
@@ -66,6 +84,10 @@ impl InlineRule for InlineEmote {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// MARK: mdx
|
||||||
|
//
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct InlineMdx(String);
|
pub struct InlineMdx(String);
|
||||||
|
|
||||||
@@ -245,3 +267,58 @@ fn mdx_include(mdx: &str, _node: &Node, fmt: &mut dyn Renderer) -> bool {
|
|||||||
|
|
||||||
return true;
|
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 chrono::{DateTime, Utc};
|
||||||
use libservice::ServiceConnectInfo;
|
use libservice::ServiceConnectInfo;
|
||||||
use lru::LruCache;
|
use lru::LruCache;
|
||||||
use maud::{Markup, Render, html};
|
use maud::{Markup, PreEscaped, Render, html};
|
||||||
use parking_lot::Mutex;
|
use parking_lot::Mutex;
|
||||||
|
use serde::Deserialize;
|
||||||
use std::{collections::HashMap, num::NonZero, sync::Arc, time::Duration};
|
use std::{collections::HashMap, num::NonZero, sync::Arc, time::Duration};
|
||||||
use tracing::{debug, trace};
|
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 struct PageMetadata {
|
||||||
pub title: String,
|
pub title: String,
|
||||||
pub author: Option<String>,
|
pub author: Option<String>,
|
||||||
pub description: Option<String>,
|
pub description: Option<String>,
|
||||||
pub image: Option<String>,
|
pub image: Option<String>,
|
||||||
|
pub slug: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for PageMetadata {
|
impl Default for PageMetadata {
|
||||||
@@ -34,6 +39,7 @@ impl Default for PageMetadata {
|
|||||||
author: None,
|
author: None,
|
||||||
description: None,
|
description: None,
|
||||||
image: None,
|
image: None,
|
||||||
|
slug: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -103,16 +109,37 @@ impl Page {
|
|||||||
(self.generate_html)(self)
|
(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: 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 {
|
Page {
|
||||||
meta,
|
meta,
|
||||||
generate_html: Box::new(move |page| {
|
generate_html: Box::new(move |page| {
|
||||||
html! {
|
html! {
|
||||||
(Backlinks(&[("/", "home")], &page.meta.title))
|
@if let Some(slug) = &page.meta.slug {
|
||||||
(Markdown(&md))
|
(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::{
|
[es]: https://github.com/endless-sky/endless-sky
|
||||||
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
|
|
||||||
[*Stellaris*]: https://www.paradoxinteractive.com/games/stellaris/about
|
[*Stellaris*]: https://www.paradoxinteractive.com/games/stellaris/about
|
||||||
[Arabic]: https://en.wikipedia.org/wiki/List_of_Arabic_star_names
|
[Arabic]: https://en.wikipedia.org/wiki/List_of_Arabic_star_names
|
||||||
[wiki-betalupi]: https://en.wikipedia.org/wiki/Beta_Lupi
|
[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"
|
- Isn't owned by a scalper that's selling it for $300"
|
||||||
|
|
||||||
<br/>
|
<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::{
|
# Mark's Handouts
|
||||||
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
|
|
||||||
|
|
||||||
[ORMC]: https://circles.math.ucla.edu/circles
|
[ORMC]: https://circles.math.ucla.edu/circles
|
||||||
|
|
||||||
@@ -155,6 +132,8 @@ giving them something to do until we can start the lesson.
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
||||||
|
<br></br>
|
||||||
|
|
||||||
|
|
||||||
## Advanced
|
## Advanced
|
||||||
|
|
||||||
@@ -250,5 +229,4 @@ they're ~14-18 years old.
|
|||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<br></br>
|
||||||
"#;
|
|
||||||
@@ -12,13 +12,14 @@ use crate::{
|
|||||||
routes::assets::{Image_Cover, Image_Icon},
|
routes::assets::{Image_Cover, Image_Icon},
|
||||||
};
|
};
|
||||||
|
|
||||||
pub fn page() -> Page {
|
pub fn index() -> Page {
|
||||||
Page {
|
Page {
|
||||||
meta: PageMetadata {
|
meta: PageMetadata {
|
||||||
title: "Betalupi: About".into(),
|
title: "Betalupi: About".into(),
|
||||||
author: Some("Mark".into()),
|
author: Some("Mark".into()),
|
||||||
description: Some("Description".into()),
|
description: Some("Description".into()),
|
||||||
image: Some(Image_Icon::URL.into()),
|
image: Some(Image_Icon::URL.into()),
|
||||||
|
slug: None,
|
||||||
},
|
},
|
||||||
|
|
||||||
generate_html: Box::new(move |_page| {
|
generate_html: Box::new(move |_page| {
|
||||||
|
|||||||
@@ -1,8 +1,15 @@
|
|||||||
|
---
|
||||||
|
title: Links
|
||||||
|
author: Mark
|
||||||
|
slug: links
|
||||||
|
---
|
||||||
|
|
||||||
|
|
||||||
# Bookmarks
|
# Bookmarks
|
||||||
|
|
||||||
This is a heavily opinionated bookmarks toolbar.
|
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
|
## 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.
|
- :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.
|
- [Security Cryptography Whatever](https://securitycryptographywhatever.com/): Modern cryptography, for those who understand the underlying theory.
|
||||||
|
|
||||||
|
<br></br>
|
||||||
|
|
||||||
|
|
||||||
## Essays
|
## Essays
|
||||||
|
|
||||||
- [Real Programmers don't use Pascal](https://www.ee.torontomu.ca/~elf/hack/realmen.html)
|
- [Real Programmers don't use Pascal](https://www.ee.torontomu.ca/~elf/hack/realmen.html)
|
||||||
- [A Mathematician's Lament](/files/lockhart.pdf)
|
- [A Mathematician's Lament](/files/lockhart.pdf)
|
||||||
- :star: [The Jargon File](http://www.catb.org/jargon/)
|
- :star: [The Jargon File](http://www.catb.org/jargon/)
|
||||||
|
|
||||||
|
<br></br>
|
||||||
|
|
||||||
|
|
||||||
## Textbooks
|
## Textbooks
|
||||||
|
|
||||||
- :star: [OpenLogic](https://openlogicproject.org/): The gold standard
|
- :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)
|
- [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)
|
- [Stories about Maxima and Minima](https://archive.org/details/storiesaboutmaxi0000tikh)
|
||||||
|
|
||||||
|
<br></br>
|
||||||
|
|
||||||
|
|
||||||
## Miscellanea
|
## Miscellanea
|
||||||
|
|
||||||
- [Hackaday](https://hackaday.com/)
|
- [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: [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
|
- :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
|
## Tools
|
||||||
|
|
||||||
@@ -65,6 +81,9 @@ This is a heavily opinionated bookmarks toolbar.
|
|||||||
- [delta](https://github.com/dandavison/delta): pretty pager for diffs
|
- [delta](https://github.com/dandavison/delta): pretty pager for diffs
|
||||||
- [dust](https://github.com/dandavison/delta): `du`, but better
|
- [dust](https://github.com/dandavison/delta): `du`, but better
|
||||||
|
|
||||||
|
<br></br>
|
||||||
|
|
||||||
|
|
||||||
## Math Resources
|
## Math Resources
|
||||||
|
|
||||||
- [Quantum Quest](https://www.quantum-quest.org/)
|
- [Quantum Quest](https://www.quantum-quest.org/)
|
||||||
@@ -74,6 +93,9 @@ This is a heavily opinionated bookmarks toolbar.
|
|||||||
- [Euclidea](https://www.euclidea.xyz/)
|
- [Euclidea](https://www.euclidea.xyz/)
|
||||||
- [Problems.ru](https://problems.ru/)
|
- [Problems.ru](https://problems.ru/)
|
||||||
|
|
||||||
|
<br></br>
|
||||||
|
|
||||||
|
|
||||||
## OS Dev Resources
|
## OS Dev Resources
|
||||||
|
|
||||||
- [OS Dev Wiki](https://wiki.osdev.org/Expanded_Main_Page)
|
- [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)
|
- [FDC Programming](http://www.brokenthorn.com/Resources/OSDev20.html)
|
||||||
- [CS77 at Bristol College](http://www.c-jump.com/CIS77/CIS77syllabus.htm)
|
- [CS77 at Bristol College](http://www.c-jump.com/CIS77/CIS77syllabus.htm)
|
||||||
|
|
||||||
|
<br></br>
|
||||||
|
|
||||||
|
|
||||||
## Misc Resources
|
## Misc Resources
|
||||||
|
|
||||||
- [Learn OpenGL](https://learnopengl.com/)
|
- [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)
|
- [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
|
- [wtfjs](https://github.com/denysdovhan/wtfjs): js [wat](https://www.destroyallsoftware.com/talks/wat)s
|
||||||
|
|
||||||
|
|
||||||
|
<br></br>
|
||||||
|
|
||||||
|
|
||||||
## Reference
|
## Reference
|
||||||
|
|
||||||
- [DevHints](https://devhints.io/)
|
- [DevHints](https://devhints.io/)
|
||||||
@@ -108,12 +137,15 @@ This is a heavily opinionated bookmarks toolbar.
|
|||||||
- [The Pinouts Book](https://pinouts.org/)
|
- [The Pinouts Book](https://pinouts.org/)
|
||||||
- [Laws of UX](https://lawsofux.com/)
|
- [Laws of UX](https://lawsofux.com/)
|
||||||
|
|
||||||
|
<br></br>
|
||||||
|
|
||||||
|
|
||||||
## Rust
|
## Rust
|
||||||
- [Understanding Memory Ordering in Rust](https://emschwartz.me/understanding-memory-ordering-in-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.
|
- [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
|
## 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,23 @@
|
|||||||
pub mod betalupi;
|
mod index;
|
||||||
pub mod handouts;
|
pub use index::index;
|
||||||
pub mod index;
|
|
||||||
pub mod links;
|
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)
|
||||||
|
}
|
||||||
|
|||||||
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 assetserver::Asset;
|
||||||
use axum::Router;
|
use axum::Router;
|
||||||
use maud::{DOCTYPE, PreEscaped, html};
|
use maud::{DOCTYPE, Markup, PreEscaped, html};
|
||||||
use tracing::info;
|
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;
|
pub mod assets;
|
||||||
|
|
||||||
@@ -11,7 +16,20 @@ pub(super) fn router() -> Router<()> {
|
|||||||
let (asset_prefix, asset_router) = assets::asset_router();
|
let (asset_prefix, asset_router) = assets::asset_router();
|
||||||
info!("Serving assets at {asset_prefix}");
|
info!("Serving assets at {asset_prefix}");
|
||||||
|
|
||||||
let server = PageServer::new(Box::new(|page| {
|
let server = build_server().into_router();
|
||||||
|
|
||||||
|
Router::new().merge(server).nest(asset_prefix, asset_router)
|
||||||
|
}
|
||||||
|
|
||||||
|
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())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn page_wrapper(page: &Page) -> Markup {
|
||||||
html! {
|
html! {
|
||||||
(DOCTYPE)
|
(DOCTYPE)
|
||||||
html {
|
html {
|
||||||
@@ -51,12 +69,11 @@ pub(super) fn router() -> Router<()> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}))
|
}
|
||||||
.add_page("/", pages::index::page())
|
|
||||||
.add_page("/links", pages::links::page())
|
#[test]
|
||||||
.add_page("/whats-a-betalupi", pages::betalupi::page())
|
fn server_builds_without_panic() {
|
||||||
.add_page("/handouts", pages::handouts::page())
|
// Catches some runtime errors thrown by axum,
|
||||||
.into_router();
|
// e.g bad route nesting or routes not starting with "/"
|
||||||
|
let _server = build_server().into_router();
|
||||||
Router::new().merge(server).nest(asset_prefix, asset_router)
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user