thumbnails!

This commit is contained in:
lachrymaLF 2025-04-04 14:23:45 -04:00
parent 939abec73f
commit 4397a7a17c
7 changed files with 230 additions and 70 deletions

96
Cargo.lock generated
View file

@ -83,6 +83,8 @@ dependencies = [
"pulldown-cmark",
"rayon",
"regex",
"serde",
"toml",
"uuid",
]
@ -123,6 +125,12 @@ version = "1.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719"
[[package]]
name = "equivalent"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
[[package]]
name = "getopts"
version = "0.2.21"
@ -150,6 +158,12 @@ version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2"
[[package]]
name = "hashbrown"
version = "0.15.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289"
[[package]]
name = "html-escape"
version = "0.2.13"
@ -182,6 +196,16 @@ dependencies = [
"cc",
]
[[package]]
name = "indexmap"
version = "2.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3954d50fe15b02142bf25d3b8bdadb634ec3948f103d04ffe3031bc8fe9d7058"
dependencies = [
"equivalent",
"hashbrown",
]
[[package]]
name = "js-sys"
version = "0.3.77"
@ -317,6 +341,35 @@ version = "1.0.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eded382c5f5f786b989652c49544c4877d9f015cc22e145a5ea8ea66c2921cd2"
[[package]]
name = "serde"
version = "1.0.219"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.219"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "serde_spanned"
version = "0.6.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "87607cb1398ed59d48732e575a4c28a7a8ebf2454b964fe3f224f2afc07909e1"
dependencies = [
"serde",
]
[[package]]
name = "shlex"
version = "1.3.0"
@ -334,6 +387,40 @@ dependencies = [
"unicode-ident",
]
[[package]]
name = "toml"
version = "0.8.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cd87a5cdd6ffab733b2f74bc4fd7ee5fff6634124999ac278c35fc78c6120148"
dependencies = [
"serde",
"serde_spanned",
"toml_datetime",
"toml_edit",
]
[[package]]
name = "toml_datetime"
version = "0.6.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41"
dependencies = [
"serde",
]
[[package]]
name = "toml_edit"
version = "0.22.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "17b4795ff5edd201c7cd6dca065ae59972ce77d1b80fa0a84d94950ece7d1474"
dependencies = [
"indexmap",
"serde",
"serde_spanned",
"toml_datetime",
"winnow",
]
[[package]]
name = "unicase"
version = "2.8.1"
@ -513,6 +600,15 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
[[package]]
name = "winnow"
version = "0.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0e97b544156e9bebe1a0ffbc03484fc1ffe3100cbce3ffb17eac35f7cdd7ab36"
dependencies = [
"memchr",
]
[[package]]
name = "wit-bindgen-rt"
version = "0.33.0"

View file

@ -10,4 +10,6 @@ html-escape = "0.2.13"
pulldown-cmark = "0.13.0"
rayon = "1.10.0"
regex = "1.11.1"
serde = { version = "1.0.219", features = ["derive"] }
toml = "0.8.20"
uuid = { version = "1.15.1", features = [ "v4" ] }

View file

@ -2,7 +2,7 @@
Static site generator with infamous `<c>` tags.
## Run the example
Make sure you [have the Rust toolchain installed](https://www.rust-lang.org/learn/get-started) and `gcc` is available.
Make sure you [have the Rust toolchain installed](https://www.rust-lang.org/learn/get-started) and `gcc` and `convert` (ImageMagick) are available.
```sh
# Build compost
cargo build -r
@ -20,11 +20,26 @@ sh lib/build.sh
```
Built pages will be in example_site/out.
## Command Options
- `--prelude <filename>`: specifies the filename of the C prelude (before `main()`) to use. Default: `./prelude.c`.
- `--template <filename>`: specifies the filename of the HTML template to use inside `./templates/`. Default: `template.html`.
- `--thumb <URL>`: sets `META_THUMBNAIL` in templates, it should be an absolute URL because it is supposed to be used in meta tags. Default: empty.
- `--sync_to <path>`: runs `rsync -avh ./out/ <path>` after building. Does not perform rsync if not specified.
## Configuration
Look at example_site/compost.toml for an example. `compost` will look for `compost.toml` in the same directory by default. You can also provide a different config to `compost` via its first command-line argument.
```toml
cc = "gcc" # CC needs to be GCC-compatible (e.g. clang)
im = "convert" # ImageMagick
lib_dir = "./lib/"
include_dir = "./include/"
c_dir = "./bin/"
template_dir = "./templates/"
content_dir = "./content/"
output_dir = "./out/"
copy_year = 2025
root_url = "https://lachrymal.net/"
thumbnails_dir = "thumbnails/"
template_fn = "template.html"
default_thumb = "default.png"
prelude_path = "prelude.c"
font_fn = "lmroman10-regular.otf"
```
## Constructs
`<c>` tags are valid in `content/*.md` files, as well as templates themselves.

15
example_site/compost.toml Normal file
View file

@ -0,0 +1,15 @@
cc = "gcc"
im = "convert"
lib_dir = "./lib/"
include_dir = "./include/"
c_dir = "./bin/"
template_dir = "./templates/"
content_dir = "./content/"
output_dir = "./out/"
copy_year = 2025
root_url = "https://lachrymal.net/"
thumbnails_dir = "thumbnails/"
template_fn = "template.html"
default_thumb = "default.png"
prelude_path = "prelude.c"
font_fn = "lmroman10-regular.otf"

BIN
example_site/default.png Normal file

Binary file not shown.

After

(image error) Size: 623 KiB

Binary file not shown.

View file

@ -1,5 +1,6 @@
use std::env::{current_exe, set_current_dir, args};
use std::fs::{create_dir, remove_file, remove_dir_all, read_to_string, write};
use std::borrow::Cow;
use std::path::Path;
use std::process::Command;
use chrono::Datelike;
@ -7,10 +8,31 @@ use glob::glob;
use regex::Regex;
use uuid::Uuid;
use rayon::prelude::*;
use serde::{Serialize, Deserialize};
use toml;
const CC: &str = "gcc";
#[derive(Serialize, Deserialize)]
struct Config< 'a> {
cc: Cow<'a, str>,
im: Cow<'a, str>,
fn do_c(html: &mut String, basename: &str, lib_dir: &Path, include_dir: &Path, c_dir: &Path, c_prelude: &str) {
lib_dir: Cow<'a, Path>,
include_dir: Cow<'a, Path>,
c_dir: Cow<'a, Path>,
template_dir: Cow<'a, Path>,
content_dir: Cow<'a, Path>,
output_dir: Cow<'a, Path>,
copy_year: i32,
root_url: Cow<'a, str>,
thumbnails_dir: Cow<'a, str>,
template_fn: Cow<'a, str>,
default_thumb: Cow<'a, str>,
prelude_path: Cow<'a, Path>,
font_fn: Cow<'a, str>,
docroot_dir: Option<Cow<'a, str>>,
}
fn do_c(html: &mut String, basename: &str, config: &Config, c_prelude: &str) {
let c_re = Regex::new(r"(?s)<c>(.*?)</c>").unwrap();
while let Some(capture) = c_re.captures(&html) {
let source_match = capture.get(1).unwrap();
@ -29,16 +51,16 @@ fn do_c(html: &mut String, basename: &str, lib_dir: &Path, include_dir: &Path, c
let id = Uuid::new_v4();
let c_fn = c_dir.join(format!("src_{basename}_{id}.c"));
let c_fn = config.c_dir.join(format!("src_{basename}_{id}.c"));
write(&c_fn, source).unwrap();
let o_fn = c_dir.join(format!("out_{basename}_{id}"));
let out = match Command::new(CC).
let o_fn = config.c_dir.join(format!("out_{basename}_{id}"));
let out = match Command::new(config.cc.as_ref()).
arg(c_fn)
.args(glob(lib_dir.join("*.o").to_str().unwrap()).unwrap().map(|p| p.unwrap()))
.args(glob(config.lib_dir.join("*.o").to_str().unwrap()).unwrap().map(|p| p.unwrap()))
.arg("-lm")
.arg("-I")
.arg(include_dir)
.arg(config.include_dir.as_ref())
.arg("-o")
.arg(&o_fn)
.status() {
@ -92,13 +114,8 @@ fn do_typer_tags(contents: &mut String) {
fn compose(
filename: &Path,
lib_dir: &Path,
include_dir: &Path,
c_dir: &Path,
template: &Path,
output_dir: &Path,
default_thumb: &str,
copy_year: i32,
config: &Config,
template: &str,
c_prelude: &str
) {
let mut contents: String = read_to_string(&filename).unwrap();
@ -106,9 +123,38 @@ fn compose(
let mut lines = contents.lines();
let title = lines.next().unwrap()[2..].to_owned();
let description = lines.next().unwrap()[2..].to_owned();
let basename: String = Regex::new(r"(?i)(.*/)?([A-Za-z0-9_-]+)\.md").unwrap().captures(filename.to_str().unwrap()).unwrap().get(2).unwrap().as_str().to_owned();
let thumb = match lines.next() {
Some(line) if Regex::new(r"%\s").unwrap().is_match(line) => line[2..].to_owned(),
_ => default_thumb.to_string(),
_ => {
let thumb = format!("{}{basename}.png", config.thumbnails_dir);
format!("{}{}", config.root_url, match Command::new(config.im.as_ref())
.arg("default.png")
.arg("(")
.arg("-size").arg("1200x200")
.arg("gradient:transparent-black")
.arg(")")
.arg("-gravity").arg("South")
.arg("-compose").arg("Over")
.arg("-composite")
.arg("(")
.arg("-fill").arg("white")
.arg("-background").arg("transparent")
.arg("-size").arg("1150x150")
.arg("-font").arg("LMRoman10-Regular")
.arg("-gravity").arg("SouthWest")
.arg(format!("caption:{title}"))
.arg(")")
.arg("-gravity").arg("South")
.arg("-compose").arg("Over")
.arg("-composite")
.arg(config.output_dir.join(&thumb))
.status() {
Ok(_) => thumb.as_str(),
Err(_) => config.default_thumb.as_ref(),
}
)
},
};
println!("Composing {} (\"{}\")...", filename.display(), title);
@ -127,8 +173,7 @@ fn compose(
do_typer_tags(&mut contents);
let basename: String = Regex::new(r"(?i)(.*/)?([A-Za-z0-9_-]+)\.md").unwrap().captures(filename.to_str().unwrap()).unwrap().get(2).unwrap().as_str().to_owned();
do_c(&mut contents, &basename, &lib_dir, &include_dir, &c_dir, c_prelude);
do_c(&mut contents, &basename, &config, c_prelude);
let mut html = String::new();
pulldown_cmark::html::push_html(&mut html, pulldown_cmark::Parser::new(&contents));
@ -139,50 +184,45 @@ fn compose(
("`META_PAGE_DESCRIPTION`", html_escape::encode_safe(&description).to_string()),
("`PAGE_DESCRIPTION`", description),
("`CONTENT`", html),
("`YEAR`", copy_year.to_string()),
("`YEAR`", config.copy_year.to_string()),
("`META_THUMBNAIL`", thumb),
("`HEAD_INJECT`", head_injection)
];
let output_filename = output_dir.join(basename.clone() + ".html");
let output_filename = config.output_dir.join(basename.clone() + ".html");
let mut out = read_to_string(&template).expect("The specified template could not be found...");
let mut out = template.to_owned();
for (key, value) in map {
out = out.replace(key, value.as_str());
}
do_c(&mut out, &basename, &lib_dir, &include_dir, &c_dir, c_prelude);
do_c(&mut out, &basename, &config, c_prelude);
write(output_filename, out).unwrap();
}
fn main() -> Result<(), std::io::Error> {
let lib_dir = Path::new("./lib/");
let include_dir = Path::new("./include/");
let c_dir = Path::new("./bin/");
let template_dir = Path::new("./templates/");
let content_dir = Path::new("./content/");
let output_dir = Path::new("./out/");
let copy_year = chrono::Utc::now().year();
let config = toml::from_str(read_to_string(args().skip(1).next().as_ref().map_or("compost.toml", String::as_str)).unwrap().as_str()).unwrap_or(Config {
cc : Cow::Borrowed("gcc"),
im : Cow::Borrowed("convert"),
lib_dir : Cow::Borrowed(Path::new("./lib/")),
include_dir : Cow::Borrowed(Path::new("./include/")),
c_dir : Cow::Borrowed(Path::new("./bin/")),
template_dir : Cow::Borrowed(Path::new("./templates/")),
content_dir : Cow::Borrowed(Path::new("./content/")),
output_dir : Cow::Borrowed(Path::new("./out/")),
copy_year : chrono::Utc::now().year(),
root_url : Cow::Borrowed("https://lachrymal.net/"),
thumbnails_dir : Cow::Borrowed("thumbnails/"),
font_fn : Cow::Borrowed("Helvetica"),
template_fn : Cow::Borrowed("template.html"),
default_thumb : Cow::Borrowed("default.png"),
prelude_path : Cow::Borrowed(Path::new("prelude.c")),
docroot_dir : None,
});
let mut template_fn = "template.html";
let mut thumb = "";
let mut prelude_path = Path::new("prelude.c");
let mut docroot_dir: Option<&str> = None;
let args: Vec<_> = args().skip(1).collect();
for arg in args.chunks(2) {
match arg[0].as_str() {
"--thumb" => thumb = arg[1].as_str(),
"--sync_to" => docroot_dir = Some(arg[1].as_str()),
"--prelude" => prelude_path = Path::new(arg[1].as_str()),
"--template" => template_fn = arg[1].as_str(),
_ => panic!("Unrecognized option: {}", arg[0])
}
}
let c_prelude = read_to_string(prelude_path).expect("Could not find C prelude...");
let c_prelude = read_to_string(&config.prelude_path).expect("Could not find C prelude...");
let dir = current_exe().unwrap().parent().unwrap().to_owned();
set_current_dir(&dir).unwrap();
@ -197,26 +237,18 @@ fn main() -> Result<(), std::io::Error> {
println!("Processing directory {}...", dir.display());
println!("=============");
let _ = remove_dir_all(output_dir);
create_dir(output_dir).expect("Could not create output directory for pages.");
let _ = remove_dir_all(c_dir);
create_dir(c_dir).expect("Could not create output directory for C.");
_ = remove_dir_all(&config.output_dir);
create_dir(&config.output_dir).expect("Could not create output directory for pages.");
create_dir("./out/thumbnails").expect("Could not create output directory for thumbnails.");
_ = remove_dir_all(&config.c_dir);
create_dir(&config.c_dir).expect("Could not create output directory for C.");
let pages: Vec<_> = glob(content_dir.join("*").to_str().unwrap()).unwrap().collect();
pages.into_par_iter().for_each(|page| compose(
&page.unwrap(),
lib_dir,
include_dir,
c_dir,
&template_dir.join(template_fn),
output_dir,
thumb,
copy_year,
c_prelude.as_str()
));
let pages: Vec<_> = glob(config.content_dir.join("*").to_str().unwrap()).unwrap().collect();
let template = read_to_string(&config.template_dir.join(config.template_fn.as_ref())).expect("The specified template could not be found...");
pages.into_par_iter().for_each(|page| compose(&page.unwrap(), &config, &template, c_prelude.as_str()));
if let Some(dir) = docroot_dir {
let path = Path::new(dir);
if let Some(dir) = config.docroot_dir {
let path = Path::new(dir.as_ref());
println!("Updating website...");
println!("===================");
@ -230,7 +262,7 @@ fn main() -> Result<(), std::io::Error> {
}
Command::new("rsync")
.arg("-avh")
.arg(output_dir)
.arg(config.output_dir.as_ref())
.arg(path)
.spawn()
.unwrap();