Compare commits
No commits in common. "master" and "v0.0.1" have entirely different histories.
14 changed files with 59 additions and 472 deletions
90
Cargo.lock
generated
90
Cargo.lock
generated
|
|
@ -554,29 +554,6 @@ 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"
|
||||||
|
|
@ -1173,25 +1150,6 @@ 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"
|
||||||
|
|
@ -1222,30 +1180,6 @@ 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"
|
||||||
|
|
@ -1407,9 +1341,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "log"
|
name = "log"
|
||||||
version = "0.4.29"
|
version = "0.4.27"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
|
checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "memchr"
|
name = "memchr"
|
||||||
|
|
@ -1656,15 +1590,6 @@ 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"
|
||||||
|
|
@ -1848,9 +1773,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "regex"
|
name = "regex"
|
||||||
version = "1.12.3"
|
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 = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276"
|
checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"aho-corasick",
|
"aho-corasick",
|
||||||
"memchr",
|
"memchr",
|
||||||
|
|
@ -1860,9 +1785,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "regex-automata"
|
name = "regex-automata"
|
||||||
version = "0.4.14"
|
version = "0.4.9"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f"
|
checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"aho-corasick",
|
"aho-corasick",
|
||||||
"memchr",
|
"memchr",
|
||||||
|
|
@ -2413,9 +2338,6 @@ 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,10 +6,7 @@ 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
3
build.rs
|
|
@ -1,3 +0,0 @@
|
||||||
fn main() {
|
|
||||||
println!("cargo:rerun-if-changed=prepends/");
|
|
||||||
}
|
|
||||||
8
common.typ
Normal file
8
common.typ
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
#let image(source, width: "400px") = {
|
||||||
|
html.elem("img", attrs: (
|
||||||
|
src: "/static/articles/" + source,
|
||||||
|
alt: source,
|
||||||
|
width: str(width),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -1,17 +0,0 @@
|
||||||
// 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")
|
|
||||||
|
|
@ -1,22 +0,0 @@
|
||||||
#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
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
@ -1,19 +0,0 @@
|
||||||
// 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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,15 +0,0 @@
|
||||||
// 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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,15 +0,0 @@
|
||||||
// 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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
[toolchain]
|
|
||||||
channel = "stable"
|
|
||||||
|
|
||||||
10
shell.nix
Normal file
10
shell.nix
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
{ pkgs ? import <nixpkgs> {} }:
|
||||||
|
|
||||||
|
pkgs.mkShell {
|
||||||
|
buildInputs = with pkgs; [
|
||||||
|
cargo
|
||||||
|
rustc
|
||||||
|
rust-analyzer
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
217
src/lib.rs
217
src/lib.rs
|
|
@ -1,87 +1,21 @@
|
||||||
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::syntax::Source;
|
use typst_as_lib::{typst_kit_options::TypstKitFontOptions, TypstEngine};
|
||||||
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 plugin_block = concat_plugin_sources(plugins)?;
|
let prepend_content = if let Some(prepend_file) = prepend {
|
||||||
|
|
||||||
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}",
|
||||||
|
|
@ -92,26 +26,19 @@ 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 index_source = fs::read_to_string(&template_file).map_err(|e| {
|
let mut template: EcoString = EcoString::new();
|
||||||
|
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(full_source_str.clone())
|
.main_file(template.to_string())
|
||||||
.search_fonts_with(
|
.search_fonts_with(
|
||||||
TypstKitFontOptions::default()
|
TypstKitFontOptions::default()
|
||||||
.include_system_fonts(false)
|
.include_system_fonts(false)
|
||||||
|
|
@ -123,27 +50,13 @@ pub fn compile_article(
|
||||||
let mut doc: HtmlDocument = engine
|
let mut doc: HtmlDocument = engine
|
||||||
.compile()
|
.compile()
|
||||||
.output
|
.output
|
||||||
.map_err(|e| format_typst_compile_error(e, &full_source_str, index_byte_start, &index_source))?;
|
.map_err(|e| format!("typst compile failed: {e}"))?;
|
||||||
|
|
||||||
let mut outline = EcoString::new();
|
let mut outline = EcoString::new();
|
||||||
let mut curr_level = 1u32;
|
let mut curr_level = 1;
|
||||||
let mut ul_depth = 0u32;
|
parse_outline(&mut doc.root, &mut outline, &mut curr_level);
|
||||||
let mut title_h2_pending = !include_title;
|
for i in (1..curr_level).rev() {
|
||||||
let mut first_outline_heading = true;
|
outline.push_str(" ".repeat(i as usize - 1).as_str());
|
||||||
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| {
|
||||||
|
|
@ -193,51 +106,11 @@ pub fn compile_article(
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn heading_level_from_tag(tag: &str) -> Option<u32> {
|
fn parse_outline(elem: &mut HtmlElement, outline: &mut EcoString, curr_level: &mut u32) {
|
||||||
match tag {
|
if matches!(
|
||||||
"<h2>" => Some(2),
|
elem.tag.to_string().as_str(),
|
||||||
"<h3>" => Some(3),
|
"<h2>" | "<h3>" | "<h4>" | "<h5>" | "<h6>"
|
||||||
"<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 {
|
||||||
|
|
@ -263,26 +136,21 @@ fn parse_outline(
|
||||||
.trim_matches('-')
|
.trim_matches('-')
|
||||||
.replace("--", "-");
|
.replace("--", "-");
|
||||||
|
|
||||||
if *first_outline_heading {
|
let level: u32 = elem.tag.to_string().chars().nth(2).unwrap() as u32 - '0' as u32;
|
||||||
*first_outline_heading = false;
|
|
||||||
*curr_level = level.saturating_sub(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
while level > *curr_level {
|
while level > *curr_level {
|
||||||
outline.push_str(" ".repeat(*ul_depth as usize).as_str());
|
|
||||||
outline.push_str("<ul>\n");
|
|
||||||
*ul_depth += 1;
|
|
||||||
*curr_level += 1;
|
*curr_level += 1;
|
||||||
|
outline.push_str(" ".repeat(*curr_level as usize - 2).as_str());
|
||||||
|
outline.push_str("<ul>\n");
|
||||||
}
|
}
|
||||||
while level < *curr_level && *ul_depth > 0 {
|
while level < *curr_level {
|
||||||
*curr_level -= 1;
|
outline.push_str(" ".repeat(*curr_level as usize - 2).as_str());
|
||||||
*ul_depth -= 1;
|
|
||||||
outline.push_str(" ".repeat(*ul_depth as usize).as_str());
|
|
||||||
outline.push_str("</ul>\n");
|
outline.push_str("</ul>\n");
|
||||||
|
*curr_level -= 1;
|
||||||
}
|
}
|
||||||
*curr_level = level;
|
*curr_level = level;
|
||||||
|
|
||||||
outline.push_str(" ".repeat(*ul_depth as usize).as_str());
|
outline.push_str(" ".repeat(*curr_level as usize - 1).as_str());
|
||||||
outline.push_str(
|
outline.push_str(
|
||||||
format!(
|
format!(
|
||||||
"<li><a href=\"#{}\">{}</a></li>\n",
|
"<li><a href=\"#{}\">{}</a></li>\n",
|
||||||
|
|
@ -297,39 +165,8 @@ fn parse_outline(
|
||||||
|
|
||||||
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) => {
|
HtmlNode::Element(e) => parse_outline(e, outline, curr_level),
|
||||||
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,8 +2,7 @@ use std::env;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
use typssg::{compile_all, compile_article};
|
use typssg::compile_article;
|
||||||
use log::{info, error};
|
|
||||||
|
|
||||||
#[derive(Parser)]
|
#[derive(Parser)]
|
||||||
struct Args {
|
struct Args {
|
||||||
|
|
@ -12,45 +11,18 @@ 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) => info!("Starting in working directory: {}", path.display()),
|
Ok(path) => println!("Current working directory: {}", path.display()),
|
||||||
Err(e) => error!("Error getting current directory: {}", e),
|
Err(e) => eprintln!("Error getting current directory: {}", e),
|
||||||
}
|
}
|
||||||
|
|
||||||
let args = Args::parse();
|
let args = Args::parse();
|
||||||
|
|
||||||
let result = if args.recursive {
|
if let Err(e) = compile_article(&args.dir, &args.prepend) {
|
||||||
compile_all(
|
eprintln!("{e}");
|
||||||
&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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,65 +0,0 @@
|
||||||
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