diff --git a/Cargo.lock b/Cargo.lock
index 1e3034e..c6be5de 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -554,6 +554,29 @@ dependencies = [
"syn",
]
+[[package]]
+name = "env_filter"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "32e90c2accc4b07a8456ea0debdc2e7587bdd890680d71173a15d4ae604f6eef"
+dependencies = [
+ "log",
+ "regex",
+]
+
+[[package]]
+name = "env_logger"
+version = "0.11.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0621c04f2196ac3f488dd583365b9c09be011a4ab8b9f37248ffcc8f6198b56a"
+dependencies = [
+ "anstream",
+ "anstyle",
+ "env_filter",
+ "jiff",
+ "log",
+]
+
[[package]]
name = "equivalent"
version = "1.0.2"
@@ -1150,6 +1173,25 @@ version = "0.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09e54e57b4c48b40f7aec75635392b12b3421fa26fe8b4332e63138ed278459c"
+[[package]]
+name = "include_dir"
+version = "0.7.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "923d117408f1e49d914f1a379a309cffe4f18c05cf4e3d12e613a15fc81bd0dd"
+dependencies = [
+ "include_dir_macros",
+]
+
+[[package]]
+name = "include_dir_macros"
+version = "0.7.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7cab85a7ed0bd5f0e76d93846e0147172bed2e2d3f859bcc33a8d9699cad1a75"
+dependencies = [
+ "proc-macro2",
+ "quote",
+]
+
[[package]]
name = "indexmap"
version = "2.14.0"
@@ -1180,6 +1222,30 @@ version = "1.0.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c"
+[[package]]
+name = "jiff"
+version = "0.2.24"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f00b5dbd620d61dfdcb6007c9c1f6054ebd75319f163d886a9055cec1155073d"
+dependencies = [
+ "jiff-static",
+ "log",
+ "portable-atomic",
+ "portable-atomic-util",
+ "serde_core",
+]
+
+[[package]]
+name = "jiff-static"
+version = "0.2.24"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e000de030ff8022ea1da3f466fbb0f3a809f5e51ed31f6dd931c35181ad8e6d7"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
[[package]]
name = "js-sys"
version = "0.3.77"
@@ -1341,9 +1407,9 @@ dependencies = [
[[package]]
name = "log"
-version = "0.4.27"
+version = "0.4.29"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94"
+checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
[[package]]
name = "memchr"
@@ -1590,6 +1656,15 @@ version = "1.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483"
+[[package]]
+name = "portable-atomic-util"
+version = "0.2.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c2a106d1259c23fac8e543272398ae0e3c0b8d33c88ed73d0cc71b0f1d902618"
+dependencies = [
+ "portable-atomic",
+]
+
[[package]]
name = "postcard"
version = "1.1.1"
@@ -1773,9 +1848,9 @@ dependencies = [
[[package]]
name = "regex"
-version = "1.11.1"
+version = "1.12.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191"
+checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276"
dependencies = [
"aho-corasick",
"memchr",
@@ -1785,9 +1860,9 @@ dependencies = [
[[package]]
name = "regex-automata"
-version = "0.4.9"
+version = "0.4.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908"
+checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f"
dependencies = [
"aho-corasick",
"memchr",
@@ -2338,6 +2413,9 @@ name = "typssg"
version = "0.1.0"
dependencies = [
"clap",
+ "env_logger",
+ "include_dir",
+ "log",
"typst",
"typst-as-lib",
"typst-html",
diff --git a/Cargo.toml b/Cargo.toml
index fd7f150..28eeeb5 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -6,7 +6,10 @@ edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
+include_dir = "0.7.4"
clap = { version = "4.6.1", features = ["derive"] }
+env_logger = "0.11.10"
+log = "0.4.29"
typst = "0.14.2"
typst-as-lib = { version = "0.15.4", features = ["typst-html", "typst-kit-fonts", "typst-kit-embed-fonts"] }
typst-html = "0.14.2"
diff --git a/build.rs b/build.rs
new file mode 100644
index 0000000..e5458ff
--- /dev/null
+++ b/build.rs
@@ -0,0 +1,3 @@
+fn main() {
+ println!("cargo:rerun-if-changed=prepends/");
+}
diff --git a/common.typ b/common.typ
deleted file mode 100644
index eabfd0a..0000000
--- a/common.typ
+++ /dev/null
@@ -1,8 +0,0 @@
-#let image(source, width: "400px") = {
- html.elem("img", attrs: (
- src: "/static/articles/" + source,
- alt: source,
- width: str(width),
- ))
-}
-
diff --git a/prepends/bibliography.typ b/prepends/bibliography.typ
new file mode 100644
index 0000000..f72b036
--- /dev/null
+++ b/prepends/bibliography.typ
@@ -0,0 +1,17 @@
+// By default, bibliography titles render with a level 1 heading. Typssg
+// rather assumes that lvl 1 headings are just for page titles, and that
+// section titles are denoted with lvl 2 headings.
+#let _bibliography = bibliography
+#let refs(content) = {
+ heading(level: 2, "References")
+ _bibliography(bytes(content.text), style: "ieee", title: none)
+}
+
+// Override default bibliography so that users don't accidentally use it
+#let bibliography = (path, ..args) => {
+ text[This project has enabled the bibliography extension which provides the `#refs()` funciton for convenient in-file bibliographies. If you _did_ intend to use the default bibliography function, call it instead with `#_bibliography().`]
+}
+
+// Have in-text citations appear as just numbers, instead of the IEEE default
+// style of [#] with brackets.
+#set cite(style: "vancouver-superscript")
diff --git a/prepends/card.typ b/prepends/card.typ
new file mode 100644
index 0000000..25cc3e0
--- /dev/null
+++ b/prepends/card.typ
@@ -0,0 +1,22 @@
+#let card(
+ caption: "",
+ media: (),
+ score: 0,
+ defense: "",
+ offense: "",
+) = {
+ text[= #caption (Card)
+
+ #html.elem("div", attrs: (class: "card"))[
+ #media.at(0)
+ ]
+
+ score: #score
+
+ == Defense
+ #defense
+
+ == Offense
+ #offense
+ ]
+}
diff --git a/prepends/figure.typ b/prepends/figure.typ
new file mode 100644
index 0000000..73ec4c8
--- /dev/null
+++ b/prepends/figure.typ
@@ -0,0 +1,19 @@
+// Given a figure with an image, this show rule will wrap the image in a link
+// that goes to the full version of the pic. For example, image("bird.jpg")
+// will get wrapped in a #link("bird_full.jpg"). This, of course, assumes
+// bird_full.jpg also exists in the directory.
+#show figure: it => {
+ if it.body.func() == image {
+ let src = it.body.source
+ let dot-pos = src.rev().position(".")
+ let full-src = if dot-pos != none {
+ src.slice(0, src.len() - dot-pos - 1) + "_full" + src.slice(src.len() - dot-pos - 1)
+ } else {
+ src + "_full"
+ }
+ show image: img => link(full-src, img)
+ it
+ } else {
+ it
+ }
+}
diff --git a/prepends/link.typ b/prepends/link.typ
new file mode 100644
index 0000000..ad107f9
--- /dev/null
+++ b/prepends/link.typ
@@ -0,0 +1,15 @@
+// Adds an "internal" class to links that stay within the application,
+// allowing them to be styled differently from external links.
+#show link: it => {
+ let dest = it.dest
+ if type(dest) == str {
+ let is-internal = dest.starts-with("/") or dest.starts-with(".")
+ let attrs = (href: dest)
+ if is-internal {
+ attrs = (href: dest, class: "internal")
+ }
+ html.elem("a", attrs: attrs, it.body)
+ } else {
+ it
+ }
+}
diff --git a/prepends/quote.typ b/prepends/quote.typ
new file mode 100644
index 0000000..bba122d
--- /dev/null
+++ b/prepends/quote.typ
@@ -0,0 +1,15 @@
+// Typst version 14.0.2 outputs a block quote as a
followed by
+// a
for the attribution. This makes it difficult to target the
+// attribution for styling. This snippet instead uses a
and
+// a wrapped in a block.
+#show quote.where(block: true): it => {
+ let inner = html.elem("blockquote", it.body)
+ if it.attribution != none {
+ html.elem("figure", {
+ inner
+ html.elem("figcaption", it.attribution)
+ })
+ } else {
+ inner
+ }
+}
diff --git a/rust-toolchain.toml b/rust-toolchain.toml
new file mode 100644
index 0000000..9cab269
--- /dev/null
+++ b/rust-toolchain.toml
@@ -0,0 +1,3 @@
+[toolchain]
+channel = "stable"
+
diff --git a/shell.nix b/shell.nix
deleted file mode 100644
index afd09a7..0000000
--- a/shell.nix
+++ /dev/null
@@ -1,10 +0,0 @@
-{ pkgs ? import {} }:
-
-pkgs.mkShell {
- buildInputs = with pkgs; [
- cargo
- rustc
- rust-analyzer
- ];
-}
-
diff --git a/src/lib.rs b/src/lib.rs
index a03e23d..34bc866 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -1,21 +1,87 @@
+mod plugin;
+
+use std::fmt::Write;
use std::fs;
use std::path::PathBuf;
+pub use plugin::{concat_plugin_sources, embedded_prepend_source, list_embedded_plugin_ids};
+
use typst::ecow::EcoString;
-use typst_as_lib::{typst_kit_options::TypstKitFontOptions, TypstEngine};
+use typst::syntax::Source;
+use typst_as_lib::{typst_kit_options::TypstKitFontOptions, TypstAsLibError, TypstEngine};
use typst_html::{HtmlAttr, HtmlDocument, HtmlElement, HtmlNode};
+use log::info;
+
+fn format_typst_compile_error(
+ err: TypstAsLibError,
+ full_source: &str,
+ index_byte_start: usize,
+ index_source: &str,
+) -> std::io::Error {
+ let report = match err {
+ TypstAsLibError::TypstSource(diagnostics) if !diagnostics.is_empty() => {
+ let combined = Source::detached(full_source);
+ let index_only = Source::detached(index_source);
+ let index_end = index_byte_start.saturating_add(index_source.len());
+ let mut out = String::from("Typst compile failed:\n");
+ for d in diagnostics.iter() {
+ let msg = d.message.as_str();
+ if let Some(range) = combined.range(d.span) {
+ let byte = range.start;
+ if byte >= index_byte_start && byte < index_end {
+ let rel = byte - index_byte_start;
+ if let Some((line, col)) = index_only.lines().byte_to_line_column(rel) {
+ let _ = writeln!(
+ &mut out,
+ " index.typ:{}:{}: {}",
+ line + 1,
+ col + 1,
+ msg
+ );
+ } else {
+ let _ = writeln!(&mut out, " {msg}");
+ }
+ } else if let Some((line, col)) = combined.lines().byte_to_line_column(byte) {
+ let _ = writeln!(
+ &mut out,
+ " (preamble) line {}:{}: {}",
+ line + 1,
+ col + 1,
+ msg
+ );
+ } else {
+ let _ = writeln!(&mut out, " {msg}");
+ }
+ } else {
+ let _ = writeln!(&mut out, " {msg}");
+ }
+ for hint in d.hints.iter() {
+ let _ = writeln!(&mut out, " hint: {}", hint.as_str());
+ }
+ }
+ out
+ }
+ other => format!("typst compile failed: {other}"),
+ };
+ std::io::Error::new(std::io::ErrorKind::Other, report)
+}
pub fn compile_article(
article_dir: &PathBuf,
prepend: &Option,
+ plugins: &[impl AsRef],
+ include_title: bool,
) -> Result<(), Box> {
+ info!("compiling {} ...", article_dir.display());
let template_file = article_dir.join("index.typ");
let output = article_dir.join("index.html");
let outline_file = article_dir.join("outline.html");
- let prepend_content = if let Some(prepend_file) = prepend {
+ let plugin_block = concat_plugin_sources(plugins)?;
+
+ let user_prepend = if let Some(prepend_file) = prepend {
fs::read_to_string(&prepend_file).map_err(|e| {
format!(
"could not read prepend file {}: {e}",
@@ -26,19 +92,26 @@ pub fn compile_article(
fs::read_to_string(article_dir.join("prepend.typ")).unwrap_or_default()
};
+ let index_source = fs::read_to_string(&template_file).map_err(|e| {
+ format!(
+ "could not read template {}: {e}",
+ template_file.display()
+ )
+ })?;
+
let mut template: EcoString = EcoString::new();
- template.push_str(&prepend_content);
- template.push_str(
- &fs::read_to_string(&template_file).map_err(|e| {
- format!(
- "could not read template {}: {e}",
- template_file.display()
- )
- })?,
- );
+ template.push_str(&plugin_block);
+ if !plugin_block.is_empty() && !user_prepend.is_empty() {
+ template.push('\n');
+ }
+ template.push_str(&user_prepend);
+ let index_byte_start = template.len();
+ template.push_str(&index_source);
+
+ let full_source_str = template.to_string();
let engine = TypstEngine::builder()
- .main_file(template.to_string())
+ .main_file(full_source_str.clone())
.search_fonts_with(
TypstKitFontOptions::default()
.include_system_fonts(false)
@@ -50,13 +123,27 @@ pub fn compile_article(
let mut doc: HtmlDocument = engine
.compile()
.output
- .map_err(|e| format!("typst compile failed: {e}"))?;
+ .map_err(|e| format_typst_compile_error(e, &full_source_str, index_byte_start, &index_source))?;
let mut outline = EcoString::new();
- let mut curr_level = 1;
- parse_outline(&mut doc.root, &mut outline, &mut curr_level);
- for i in (1..curr_level).rev() {
- outline.push_str(" ".repeat(i as usize - 1).as_str());
+ let mut curr_level = 1u32;
+ let mut ul_depth = 0u32;
+ let mut title_h2_pending = !include_title;
+ let mut first_outline_heading = true;
+ parse_outline(
+ &mut doc.root,
+ &mut outline,
+ &mut curr_level,
+ &mut ul_depth,
+ include_title,
+ &mut title_h2_pending,
+ &mut first_outline_heading,
+ );
+ // `ul_depth` counts open `
` tags; it can diverge from `curr_level - 1` when the first
+ // outline heading uses lazy depth (skip title). Always drain by `ul_depth`, not `curr_level`.
+ while ul_depth > 0 {
+ ul_depth -= 1;
+ outline.push_str(" ".repeat(ul_depth as usize).as_str());
outline.push_str("