thumbnails!
This commit is contained in:
parent
939abec73f
commit
4397a7a17c
7 changed files with 230 additions and 70 deletions
96
Cargo.lock
generated
96
Cargo.lock
generated
|
@ -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"
|
||||
|
|
|
@ -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" ] }
|
||||
|
|
27
README.md
27
README.md
|
@ -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
15
example_site/compost.toml
Normal 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
BIN
example_site/default.png
Normal file
Binary file not shown.
After ![]() (image error) Size: 623 KiB |
BIN
example_site/lmroman10-regular.otf
Normal file
BIN
example_site/lmroman10-regular.otf
Normal file
Binary file not shown.
160
src/main.rs
160
src/main.rs
|
@ -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();
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue