Compare commits
13 commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7bc66067e6 | |||
| 4e61e4acb3 | |||
|
|
f13433fc4b | ||
|
|
e96b613f43 | ||
|
|
03d3a31d71 | ||
| 4fc4922256 | |||
| d1a96e1362 | |||
|
|
a6e30123a5 | ||
|
|
4b2918853b | ||
|
|
68ed807e95 | ||
|
|
aa7b1d0a63 | ||
| 04ebd38691 | |||
| c798e387bd |
14 changed files with 472 additions and 59 deletions
90
Cargo.lock
generated
90
Cargo.lock
generated
|
|
@ -554,6 +554,29 @@ dependencies = [
|
||||||
"syn",
|
"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]]
|
[[package]]
|
||||||
name = "equivalent"
|
name = "equivalent"
|
||||||
version = "1.0.2"
|
version = "1.0.2"
|
||||||
|
|
@ -1150,6 +1173,25 @@ version = "0.14.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "09e54e57b4c48b40f7aec75635392b12b3421fa26fe8b4332e63138ed278459c"
|
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]]
|
[[package]]
|
||||||
name = "indexmap"
|
name = "indexmap"
|
||||||
version = "2.14.0"
|
version = "2.14.0"
|
||||||
|
|
@ -1180,6 +1222,30 @@ version = "1.0.15"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c"
|
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]]
|
[[package]]
|
||||||
name = "js-sys"
|
name = "js-sys"
|
||||||
version = "0.3.77"
|
version = "0.3.77"
|
||||||
|
|
@ -1341,9 +1407,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "log"
|
name = "log"
|
||||||
version = "0.4.27"
|
version = "0.4.29"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94"
|
checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "memchr"
|
name = "memchr"
|
||||||
|
|
@ -1590,6 +1656,15 @@ version = "1.11.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483"
|
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]]
|
[[package]]
|
||||||
name = "postcard"
|
name = "postcard"
|
||||||
version = "1.1.1"
|
version = "1.1.1"
|
||||||
|
|
@ -1773,9 +1848,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "regex"
|
name = "regex"
|
||||||
version = "1.11.1"
|
version = "1.12.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191"
|
checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"aho-corasick",
|
"aho-corasick",
|
||||||
"memchr",
|
"memchr",
|
||||||
|
|
@ -1785,9 +1860,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "regex-automata"
|
name = "regex-automata"
|
||||||
version = "0.4.9"
|
version = "0.4.14"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908"
|
checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"aho-corasick",
|
"aho-corasick",
|
||||||
"memchr",
|
"memchr",
|
||||||
|
|
@ -2338,6 +2413,9 @@ name = "typssg"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"clap",
|
"clap",
|
||||||
|
"env_logger",
|
||||||
|
"include_dir",
|
||||||
|
"log",
|
||||||
"typst",
|
"typst",
|
||||||
"typst-as-lib",
|
"typst-as-lib",
|
||||||
"typst-html",
|
"typst-html",
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,10 @@ edition = "2021"
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
include_dir = "0.7.4"
|
||||||
clap = { version = "4.6.1", features = ["derive"] }
|
clap = { version = "4.6.1", features = ["derive"] }
|
||||||
|
env_logger = "0.11.10"
|
||||||
|
log = "0.4.29"
|
||||||
typst = "0.14.2"
|
typst = "0.14.2"
|
||||||
typst-as-lib = { version = "0.15.4", features = ["typst-html", "typst-kit-fonts", "typst-kit-embed-fonts"] }
|
typst-as-lib = { version = "0.15.4", features = ["typst-html", "typst-kit-fonts", "typst-kit-embed-fonts"] }
|
||||||
typst-html = "0.14.2"
|
typst-html = "0.14.2"
|
||||||
|
|
|
||||||
3
build.rs
Normal file
3
build.rs
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
fn main() {
|
||||||
|
println!("cargo:rerun-if-changed=prepends/");
|
||||||
|
}
|
||||||
|
|
@ -1,8 +0,0 @@
|
||||||
#let image(source, width: "400px") = {
|
|
||||||
html.elem("img", attrs: (
|
|
||||||
src: "/static/articles/" + source,
|
|
||||||
alt: source,
|
|
||||||
width: str(width),
|
|
||||||
))
|
|
||||||
}
|
|
||||||
|
|
||||||
17
prepends/bibliography.typ
Normal file
17
prepends/bibliography.typ
Normal file
|
|
@ -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")
|
||||||
22
prepends/card.typ
Normal file
22
prepends/card.typ
Normal file
|
|
@ -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
|
||||||
|
]
|
||||||
|
}
|
||||||
19
prepends/figure.typ
Normal file
19
prepends/figure.typ
Normal file
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
15
prepends/link.typ
Normal file
15
prepends/link.typ
Normal file
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
15
prepends/quote.typ
Normal file
15
prepends/quote.typ
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
// Typst version 14.0.2 outputs a block quote as a <blockquote> followed by
|
||||||
|
// a <p> for the attribution. This makes it difficult to target the
|
||||||
|
// attribution for styling. This snippet instead uses a <blockquote> and
|
||||||
|
// a <figcaption> wrapped in a <figure> 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
|
||||||
|
}
|
||||||
|
}
|
||||||
3
rust-toolchain.toml
Normal file
3
rust-toolchain.toml
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
[toolchain]
|
||||||
|
channel = "stable"
|
||||||
|
|
||||||
10
shell.nix
10
shell.nix
|
|
@ -1,10 +0,0 @@
|
||||||
{ pkgs ? import <nixpkgs> {} }:
|
|
||||||
|
|
||||||
pkgs.mkShell {
|
|
||||||
buildInputs = with pkgs; [
|
|
||||||
cargo
|
|
||||||
rustc
|
|
||||||
rust-analyzer
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
217
src/lib.rs
217
src/lib.rs
|
|
@ -1,21 +1,87 @@
|
||||||
|
mod plugin;
|
||||||
|
|
||||||
|
use std::fmt::Write;
|
||||||
use std::fs;
|
use std::fs;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
pub use plugin::{concat_plugin_sources, embedded_prepend_source, list_embedded_plugin_ids};
|
||||||
|
|
||||||
use typst::ecow::EcoString;
|
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 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(
|
pub fn compile_article(
|
||||||
article_dir: &PathBuf,
|
article_dir: &PathBuf,
|
||||||
prepend: &Option<PathBuf>,
|
prepend: &Option<PathBuf>,
|
||||||
|
plugins: &[impl AsRef<str>],
|
||||||
|
include_title: bool,
|
||||||
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
||||||
|
info!("compiling {} ...", article_dir.display());
|
||||||
|
|
||||||
let template_file = article_dir.join("index.typ");
|
let template_file = article_dir.join("index.typ");
|
||||||
let output = article_dir.join("index.html");
|
let output = article_dir.join("index.html");
|
||||||
let outline_file = article_dir.join("outline.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| {
|
fs::read_to_string(&prepend_file).map_err(|e| {
|
||||||
format!(
|
format!(
|
||||||
"could not read prepend file {}: {e}",
|
"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()
|
fs::read_to_string(article_dir.join("prepend.typ")).unwrap_or_default()
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut template: EcoString = EcoString::new();
|
let index_source = fs::read_to_string(&template_file).map_err(|e| {
|
||||||
template.push_str(&prepend_content);
|
|
||||||
template.push_str(
|
|
||||||
&fs::read_to_string(&template_file).map_err(|e| {
|
|
||||||
format!(
|
format!(
|
||||||
"could not read template {}: {e}",
|
"could not read template {}: {e}",
|
||||||
template_file.display()
|
template_file.display()
|
||||||
)
|
)
|
||||||
})?,
|
})?;
|
||||||
);
|
|
||||||
|
let mut template: EcoString = EcoString::new();
|
||||||
|
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()
|
let engine = TypstEngine::builder()
|
||||||
.main_file(template.to_string())
|
.main_file(full_source_str.clone())
|
||||||
.search_fonts_with(
|
.search_fonts_with(
|
||||||
TypstKitFontOptions::default()
|
TypstKitFontOptions::default()
|
||||||
.include_system_fonts(false)
|
.include_system_fonts(false)
|
||||||
|
|
@ -50,13 +123,27 @@ pub fn compile_article(
|
||||||
let mut doc: HtmlDocument = engine
|
let mut doc: HtmlDocument = engine
|
||||||
.compile()
|
.compile()
|
||||||
.output
|
.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 outline = EcoString::new();
|
||||||
let mut curr_level = 1;
|
let mut curr_level = 1u32;
|
||||||
parse_outline(&mut doc.root, &mut outline, &mut curr_level);
|
let mut ul_depth = 0u32;
|
||||||
for i in (1..curr_level).rev() {
|
let mut title_h2_pending = !include_title;
|
||||||
outline.push_str(" ".repeat(i as usize - 1).as_str());
|
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 `<ul>` 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("</ul>\n");
|
outline.push_str("</ul>\n");
|
||||||
}
|
}
|
||||||
fs::write(&outline_file, outline.as_bytes()).map_err(|e| {
|
fs::write(&outline_file, outline.as_bytes()).map_err(|e| {
|
||||||
|
|
@ -106,11 +193,51 @@ pub fn compile_article(
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_outline(elem: &mut HtmlElement, outline: &mut EcoString, curr_level: &mut u32) {
|
fn heading_level_from_tag(tag: &str) -> Option<u32> {
|
||||||
if matches!(
|
match tag {
|
||||||
elem.tag.to_string().as_str(),
|
"<h2>" => Some(2),
|
||||||
"<h2>" | "<h3>" | "<h4>" | "<h5>" | "<h6>"
|
"<h3>" => Some(3),
|
||||||
) {
|
"<h4>" => Some(4),
|
||||||
|
"<h5>" => Some(5),
|
||||||
|
"<h6>" => Some(6),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_outline(
|
||||||
|
elem: &mut HtmlElement,
|
||||||
|
outline: &mut EcoString,
|
||||||
|
curr_level: &mut u32,
|
||||||
|
ul_depth: &mut u32,
|
||||||
|
include_title: bool,
|
||||||
|
title_h2_pending: &mut bool,
|
||||||
|
first_outline_heading: &mut bool,
|
||||||
|
) {
|
||||||
|
let tag = elem.tag.to_string();
|
||||||
|
let tag_ref = tag.as_str();
|
||||||
|
|
||||||
|
if let Some(level) = heading_level_from_tag(tag_ref) {
|
||||||
|
if !include_title && *title_h2_pending {
|
||||||
|
*title_h2_pending = false;
|
||||||
|
if tag_ref == "<h2>" {
|
||||||
|
for child in elem.children.make_mut().iter_mut() {
|
||||||
|
if let HtmlNode::Element(e) = child {
|
||||||
|
parse_outline(
|
||||||
|
e,
|
||||||
|
outline,
|
||||||
|
curr_level,
|
||||||
|
ul_depth,
|
||||||
|
include_title,
|
||||||
|
title_h2_pending,
|
||||||
|
first_outline_heading,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*title_h2_pending = false;
|
||||||
|
|
||||||
let mut header_text = EcoString::new();
|
let mut header_text = EcoString::new();
|
||||||
|
|
||||||
for child in &elem.children {
|
for child in &elem.children {
|
||||||
|
|
@ -136,21 +263,26 @@ fn parse_outline(elem: &mut HtmlElement, outline: &mut EcoString, curr_level: &m
|
||||||
.trim_matches('-')
|
.trim_matches('-')
|
||||||
.replace("--", "-");
|
.replace("--", "-");
|
||||||
|
|
||||||
let level: u32 = elem.tag.to_string().chars().nth(2).unwrap() as u32 - '0' as u32;
|
if *first_outline_heading {
|
||||||
|
*first_outline_heading = false;
|
||||||
|
*curr_level = level.saturating_sub(1);
|
||||||
|
}
|
||||||
|
|
||||||
while level > *curr_level {
|
while level > *curr_level {
|
||||||
*curr_level += 1;
|
outline.push_str(" ".repeat(*ul_depth as usize).as_str());
|
||||||
outline.push_str(" ".repeat(*curr_level as usize - 2).as_str());
|
|
||||||
outline.push_str("<ul>\n");
|
outline.push_str("<ul>\n");
|
||||||
|
*ul_depth += 1;
|
||||||
|
*curr_level += 1;
|
||||||
}
|
}
|
||||||
while level < *curr_level {
|
while level < *curr_level && *ul_depth > 0 {
|
||||||
outline.push_str(" ".repeat(*curr_level as usize - 2).as_str());
|
|
||||||
outline.push_str("</ul>\n");
|
|
||||||
*curr_level -= 1;
|
*curr_level -= 1;
|
||||||
|
*ul_depth -= 1;
|
||||||
|
outline.push_str(" ".repeat(*ul_depth as usize).as_str());
|
||||||
|
outline.push_str("</ul>\n");
|
||||||
}
|
}
|
||||||
*curr_level = level;
|
*curr_level = level;
|
||||||
|
|
||||||
outline.push_str(" ".repeat(*curr_level as usize - 1).as_str());
|
outline.push_str(" ".repeat(*ul_depth as usize).as_str());
|
||||||
outline.push_str(
|
outline.push_str(
|
||||||
format!(
|
format!(
|
||||||
"<li><a href=\"#{}\">{}</a></li>\n",
|
"<li><a href=\"#{}\">{}</a></li>\n",
|
||||||
|
|
@ -165,8 +297,39 @@ fn parse_outline(elem: &mut HtmlElement, outline: &mut EcoString, curr_level: &m
|
||||||
|
|
||||||
for child in elem.children.make_mut().iter_mut() {
|
for child in elem.children.make_mut().iter_mut() {
|
||||||
match child {
|
match child {
|
||||||
HtmlNode::Element(e) => parse_outline(e, outline, curr_level),
|
HtmlNode::Element(e) => {
|
||||||
|
parse_outline(
|
||||||
|
e,
|
||||||
|
outline,
|
||||||
|
curr_level,
|
||||||
|
ul_depth,
|
||||||
|
include_title,
|
||||||
|
title_h2_pending,
|
||||||
|
first_outline_heading,
|
||||||
|
);
|
||||||
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn compile_all(
|
||||||
|
root_dir: &PathBuf,
|
||||||
|
prepend: &Option<PathBuf>,
|
||||||
|
plugins: &[impl AsRef<str>],
|
||||||
|
include_title_in_outline: bool,
|
||||||
|
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
||||||
|
for entry in fs::read_dir(root_dir)? {
|
||||||
|
let entry = entry?;
|
||||||
|
let path = entry.path();
|
||||||
|
|
||||||
|
if path.is_dir() {
|
||||||
|
compile_all(&path, prepend, plugins, include_title_in_outline)?;
|
||||||
|
} else if path.file_name().is_some_and(|n| n == "index.typ") {
|
||||||
|
let dir = path.parent().unwrap().to_path_buf();
|
||||||
|
compile_article(&dir, prepend, plugins, include_title_in_outline)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
|
||||||
38
src/main.rs
38
src/main.rs
|
|
@ -2,7 +2,8 @@ use std::env;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
use typssg::compile_article;
|
use typssg::{compile_all, compile_article};
|
||||||
|
use log::{info, error};
|
||||||
|
|
||||||
#[derive(Parser)]
|
#[derive(Parser)]
|
||||||
struct Args {
|
struct Args {
|
||||||
|
|
@ -11,18 +12,45 @@ struct Args {
|
||||||
|
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
prepend: Option<PathBuf>,
|
prepend: Option<PathBuf>,
|
||||||
|
|
||||||
|
#[arg(long, value_delimiter = ',')]
|
||||||
|
plugin: Vec<String>,
|
||||||
|
|
||||||
|
#[arg(short)]
|
||||||
|
recursive: bool,
|
||||||
|
|
||||||
|
#[arg(long)]
|
||||||
|
include_title_in_outline: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
|
env_logger::init();
|
||||||
|
|
||||||
match env::current_dir() {
|
match env::current_dir() {
|
||||||
Ok(path) => println!("Current working directory: {}", path.display()),
|
Ok(path) => info!("Starting in working directory: {}", path.display()),
|
||||||
Err(e) => eprintln!("Error getting current directory: {}", e),
|
Err(e) => error!("Error getting current directory: {}", e),
|
||||||
}
|
}
|
||||||
|
|
||||||
let args = Args::parse();
|
let args = Args::parse();
|
||||||
|
|
||||||
if let Err(e) = compile_article(&args.dir, &args.prepend) {
|
let result = if args.recursive {
|
||||||
eprintln!("{e}");
|
compile_all(
|
||||||
|
&args.dir,
|
||||||
|
&args.prepend,
|
||||||
|
&args.plugin,
|
||||||
|
args.include_title_in_outline,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
compile_article(
|
||||||
|
&args.dir,
|
||||||
|
&args.prepend,
|
||||||
|
&args.plugin,
|
||||||
|
args.include_title_in_outline,
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Err(e) = result {
|
||||||
|
error!("{e}");
|
||||||
std::process::exit(1);
|
std::process::exit(1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
65
src/plugin.rs
Normal file
65
src/plugin.rs
Normal file
|
|
@ -0,0 +1,65 @@
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::sync::OnceLock;
|
||||||
|
|
||||||
|
use include_dir::{include_dir, Dir};
|
||||||
|
|
||||||
|
static PREPENDS_DIR: Dir<'static> = include_dir!("prepends");
|
||||||
|
|
||||||
|
fn prepends_table() -> &'static HashMap<String, String> {
|
||||||
|
static TABLE: OnceLock<HashMap<String, String>> = OnceLock::new();
|
||||||
|
TABLE.get_or_init(|| {
|
||||||
|
let mut m = HashMap::new();
|
||||||
|
for file in PREPENDS_DIR.files() {
|
||||||
|
let Some(path_str) = file.path().to_str() else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
if !path_str.ends_with(".typ") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let id = path_str[..path_str.len() - 4].replace('\\', "/");
|
||||||
|
let text = file
|
||||||
|
.contents_utf8()
|
||||||
|
.unwrap_or_else(|| panic!("prepends/{path_str} is not valid UTF-8"))
|
||||||
|
.to_string();
|
||||||
|
if m.insert(id.clone(), text).is_some() {
|
||||||
|
panic!("duplicate prepend plugin id after normalize: {id}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
m
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn list_embedded_plugin_ids() -> Vec<String> {
|
||||||
|
let mut v: Vec<String> = prepends_table().keys().cloned().collect();
|
||||||
|
v.sort();
|
||||||
|
v
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn embedded_prepend_source(id: &str) -> Result<String, String> {
|
||||||
|
let id = id.trim().replace('\\', "/");
|
||||||
|
if id.is_empty() {
|
||||||
|
return Err("empty plugin id".into());
|
||||||
|
}
|
||||||
|
prepends_table()
|
||||||
|
.get(&id)
|
||||||
|
.cloned()
|
||||||
|
.ok_or_else(|| {
|
||||||
|
let known = list_embedded_plugin_ids().join(", ");
|
||||||
|
if known.is_empty() {
|
||||||
|
format!("unknown plugin '{id}' (no embedded prepends in this build)")
|
||||||
|
} else {
|
||||||
|
format!("unknown plugin '{id}' (embedded: {known})")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn concat_plugin_sources(plugin_ids: &[impl AsRef<str>]) -> Result<String, String> {
|
||||||
|
let mut out = String::new();
|
||||||
|
for (i, id) in plugin_ids.iter().enumerate() {
|
||||||
|
if i > 0 {
|
||||||
|
out.push('\n');
|
||||||
|
}
|
||||||
|
out.push_str(&embedded_prepend_source(id.as_ref())?);
|
||||||
|
}
|
||||||
|
Ok(out)
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue