diff --git a/src/log.rs b/src/log.rs
@@ -50,14 +50,6 @@ pub(crate) fn log(level: Level, args: &Arguments<'_>, newline: bool) {
#[macro_export]
macro_rules! info {
- // info!(key1:? = 42, key2 = true; "a {} event", "log");
- ($($key:tt $(:$capture:tt)? $(= $value:expr)?),+; $($arg:tt)+) => ({
- $crate::log::log(
- $crate::log::Level::Info,
- &std::format_args!($($arg)+),
- false,
- );
- });
// info!("a {} event", "log");
($($arg:tt)+) => ({
$crate::log::log(
@@ -70,14 +62,6 @@ macro_rules! info {
#[macro_export]
macro_rules! infoln {
- // infoln!(key1:? = 42, key2 = true; "a {} event", "log");
- ($($key:tt $(:$capture:tt)? $(= $value:expr)?),+; $($arg:tt)+) => ({
- $crate::log::log(
- $crate::log::Level::Info,
- &std::format_args!($($arg)+),
- true,
- );
- });
// infoln!("a {} event", "log");
($($arg:tt)+) => ({
$crate::log::log(
@@ -90,21 +74,22 @@ macro_rules! infoln {
#[macro_export]
macro_rules! info_done {
- () => {
+ () => ({
let _ = writeln!(io::stdout().lock(), " Done!");
- };
+ });
+
+ // infoln!("a {} event", "log");
+ ($($arg:tt)+) => ({
+ let _ = writeln!(
+ io::stdout().lock(),
+ " {}",
+ &std::format_args!($($arg)+)
+ );
+ });
}
#[macro_export]
macro_rules! error {
- // error!(key1:? = 42, key2 = true; "a {} event", "log");
- ($($key:tt $(:$capture:tt)? $(= $value:expr)?),+; $($arg:tt)+) => ({
- $crate::log::log(
- $crate::log::Level::Error,
- &std::format_args!($($arg)+),
- false,
- );
- });
// info!("a {} event", "log");
($($arg:tt)+) => ({
$crate::log::log(
@@ -117,14 +102,6 @@ macro_rules! error {
#[macro_export]
macro_rules! errorln {
- // errorln!(key1:? = 42, key2 = true; "a {} event", "log");
- ($($key:tt $(:$capture:tt)? $(= $value:expr)?),+; $($arg:tt)+) => ({
- $crate::log::log(
- $crate::log::Level::Error,
- &std::format_args!($($arg)+),
- true,
- );
- });
// errorln!("a {} event", "log");
($($arg:tt)+) => ({
$crate::log::log(
@@ -137,14 +114,6 @@ macro_rules! errorln {
#[macro_export]
macro_rules! warnln {
- // info!(key1:? = 42, key2 = true; "a {} event", "log");
- ($($key:tt $(:$capture:tt)? $(= $value:expr)?),+; $($arg:tt)+) => ({
- $crate::log::log(
- $crate::log::Level::Warn,
- &std::format_args!($($arg)+),
- true,
- );
- });
// info!("a {} event", "log");
($($arg:tt)+) => ({
$crate::log::log(
@@ -161,7 +130,7 @@ macro_rules! usage {
let mut stderr = io::stderr();
let _ = writeln!(
stderr,
- "{usage_header_msg} {} config.yml",
+ "{usage_header_msg} {} config.yml [--full-build]",
$program,
usage_header_msg = "[USAGE]".yellow().bold()
);
diff --git a/src/main.rs b/src/main.rs
@@ -6,7 +6,7 @@ use std::{
fmt::{self, Display},
fs::{self, File},
io::{self, Write},
- path::PathBuf,
+ path::{PathBuf, Path},
process::{ExitCode, Command},
sync::mpsc,
os::unix,
@@ -24,6 +24,18 @@ pub struct ThumbPath<'a>(pub &'a GalleryEntry);
/// A wrapper for HTML-escaped strings
pub struct Escaped<'a>(pub &'a str);
+/// A wrapper to display lists of command line arguments
+struct ArgList<'a>(pub &'a [String]);
+
+#[derive(Clone, Copy, PartialEq, Eq)]
+enum RenderResult {
+ Skipped,
+ Success,
+ Failure,
+}
+
+const FULL_BUILD_OPT: &str = "--full-build";
+
const TARGET_PATH: &str = "./site";
const PAGES_PATH: &str = "figures";
const IMAGES_PATH: &str = "assets/images";
@@ -52,15 +64,18 @@ fn main() -> ExitCode {
let args: Vec<String> = env::args().collect();
- let (program, config) = match &args[..] {
- [program, config] => (program, config),
- [program] => {
- error!("Expected 1 command line argument, found none");
+ let (program, config, full_build) = match &args[..] {
+ [program, config] => (program, config, false),
+ [program, config, opt] if opt == FULL_BUILD_OPT => {
+ (program, config, true)
+ }
+ [program, _config, ..] => {
+ errorln!("Unknown arguments: {}", ArgList(&args[2..]));
usage!(program);
return ExitCode::FAILURE;
}
- [program, ..] => {
- error!("Expected 1 command line argument, found many");
+ [program] => {
+ errorln!("Expected 1 command line argument, found none");
usage!(program);
return ExitCode::FAILURE;
}
@@ -71,22 +86,22 @@ fn main() -> ExitCode {
match f.map(serde_yaml::from_reader::<_, Vec<GalleryEntry>>) {
// Error opening the config file
Err(err) => {
- error!("Couldn't open {config:?}: {err}");
+ errorln!("Couldn't open {config:?}: {err}");
usage!(program);
ExitCode::FAILURE
}
// Error parsing the config file
Ok(Err(err)) => {
- error!("Couldn't parse {config:?}: {err}");
+ errorln!("Couldn't parse {config:?}: {err}");
usage_config!();
ExitCode::FAILURE
}
- Ok(Ok(pics)) => render_gallery(pics),
+ Ok(Ok(pics)) => render_gallery(pics, full_build),
}
}
/// Coordinates the rendering of all the pages and file conversions
-fn render_gallery(pics: Vec<GalleryEntry>) -> ExitCode {
+fn render_gallery(pics: Vec<GalleryEntry>, full_build: bool) -> ExitCode {
info!("Copying image files to the target directory...");
for pic in &pics {
@@ -98,8 +113,8 @@ fn render_gallery(pics: Vec<GalleryEntry>) -> ExitCode {
errorln!(
"Couldn't copy file {src:?} to {target:?}: {err}",
src = pic.path,
- target = target_path
- );
+ target = target_path,
+ );
return ExitCode::FAILURE;
}
}
@@ -130,15 +145,15 @@ fn render_gallery(pics: Vec<GalleryEntry>) -> ExitCode {
let sender = sender.clone();
let pic = pic.clone();
rendering_pool.execute(move || {
- sender.send(render_thumbnail(pic))
+ sender.send(render_thumbnail(pic, full_build))
.expect("channel should still be alive awaiting for the completion of this task");
});
}
for _ in 0..pics.len() {
match reciever.recv() {
- Ok(false) => return ExitCode::FAILURE,
- Ok(true) => {}
+ Ok(RenderResult::Failure) => return ExitCode::FAILURE,
+ Ok(RenderResult::Success | RenderResult::Skipped) => {}
Err(_) => {
// Propagate the panic to the main thread: reciever.recv should
// only fail if some of the rendering threads panicked
@@ -158,10 +173,13 @@ fn render_gallery(pics: Vec<GalleryEntry>) -> ExitCode {
for pic in pics {
info!("Rendering HTML page for {name:?}...", name = pic.file_name);
- if render_pic_page(&pic).is_err() {
- return ExitCode::FAILURE;
+ match render_pic_page(&pic, full_build) {
+ RenderResult::Success => info_done!(),
+ RenderResult::Skipped => {
+ info_done!("Skipped! (use {FULL_BUILD_OPT} to overwrite)");
+ }
+ RenderResult::Failure => return ExitCode::FAILURE,
}
- info_done!();
}
ExitCode::SUCCESS
@@ -230,114 +248,130 @@ fn render_index(pics: &Vec<GalleryEntry>) -> io::Result<()> {
writeln!(f, "</html>")
}
-fn render_pic_page(pic: &GalleryEntry) -> io::Result<()> {
+fn render_pic_page(pic: &GalleryEntry, full_build: bool) -> RenderResult {
let mut path = PathBuf::from(TARGET_PATH);
path.push(PAGES_PATH);
path.push(pic.file_name.clone() + ".html");
+ // Only try to re-render HTML page in case the page is older than the
+ // image file
+ if !full_build && !needs_update(&path, &pic.path) {
+ return RenderResult::Skipped;
+ }
+
let mut f = match File::create(&path) {
Ok(file) => file,
Err(err) => {
errorln!("Could not open file {path:?}: {err}");
- return Err(err);
+ return RenderResult::Failure;
}
};
- writeln!(f, "<!DOCTYPE html>")?;
- write_license(&mut f)?;
- writeln!(f, "<html lang=\"en\">")?;
- writeln!(f, "<head>")?;
- writeln!(
- f,
- "<title>{PAGE_TITLE} ‐ {name}</title>",
- name = Escaped(&pic.file_name)
- )?;
- write_head(&mut f)?;
- writeln!(
- f,
- "<link rel=\"preload\" as=\"image\" href=\"{path}\">",
- path = ThumbPath(pic),
- )?;
- writeln!(f, "</head>")?;
+ /// Does the deeds
+ fn write_file(f: &mut File, pic: &GalleryEntry) -> io::Result<()> {
+ writeln!(f, "<!DOCTYPE html>")?;
+ write_license(f)?;
+ writeln!(f, "<html lang=\"en\">")?;
+ writeln!(f, "<head>")?;
+ writeln!(
+ f,
+ "<title>{PAGE_TITLE} ‐ {name}</title>",
+ name = Escaped(&pic.file_name)
+ )?;
+ write_head(f)?;
+ writeln!(
+ f,
+ "<link rel=\"preload\" as=\"image\" href=\"{path}\">",
+ path = ThumbPath(pic),
+ )?;
+ writeln!(f, "</head>")?;
- writeln!(f, "<body>")?;
- writeln!(f, "<main>")?;
- writeln!(
- f,
- "<h1 class=\"picture-title\">{name}</h1>",
- name = Escaped(&pic.file_name)
- )?;
+ writeln!(f, "<body>")?;
+ writeln!(f, "<main>")?;
+ writeln!(
+ f,
+ "<h1 class=\"picture-title\">{name}</h1>",
+ name = Escaped(&pic.file_name)
+ )?;
- if pic.caption.is_some() {
- writeln!(f, "<figure>")?;
- } else {
- writeln!(f, "<figure aria-label=\"File {name}\">",
- name = Escaped(&pic.file_name))?;
- }
- writeln!(f, "<div id=\"picture\">")?;
- writeln!(f, "<div>")?;
+ if pic.caption.is_some() {
+ writeln!(f, "<figure>")?;
+ } else {
+ writeln!(f, "<figure aria-label=\"File {name}\">",
+ name = Escaped(&pic.file_name))?;
+ }
+ writeln!(f, "<div id=\"picture\">")?;
+ writeln!(f, "<div>")?;
- writeln!(f, "<div class=\"picture-container\">")?;
- writeln!(
- f,
- "<img alt=\"{alt}\" src=\"{path}\">",
- alt = Escaped(&pic.alt),
- path = ThumbPath(pic),
- )?;
- writeln!(f, "</div>")?;
+ writeln!(f, "<div class=\"picture-container\">")?;
+ writeln!(
+ f,
+ "<img alt=\"{alt}\" src=\"{path}\">",
+ alt = Escaped(&pic.alt),
+ path = ThumbPath(pic),
+ )?;
+ writeln!(f, "</div>")?;
- writeln!(f, "<nav id=\"picture-nav\">")?;
- writeln!(f, "<ul>")?;
- writeln!(
- f,
- "<li><a href=\"/{IMAGES_PATH}/{name}\">download</a></li>",
- name = Escaped(&pic.file_name),
- )?;
- if let Some(src) = &pic.source {
- writeln!(f, "<li><a href=\"{src}\">original source</a></li>")?;
- }
- writeln!(f, "</ul>")?;
- writeln!(f, "</nav>")?;
+ writeln!(f, "<nav id=\"picture-nav\">")?;
+ writeln!(f, "<ul>")?;
+ writeln!(
+ f,
+ "<li><a href=\"/{IMAGES_PATH}/{name}\">download</a></li>",
+ name = Escaped(&pic.file_name),
+ )?;
+ if let Some(src) = &pic.source {
+ writeln!(f, "<li><a href=\"{src}\">original source</a></li>")?;
+ }
+ writeln!(f, "</ul>")?;
+ writeln!(f, "</nav>")?;
+
+ writeln!(f, "</div>")?;
+ writeln!(f, "</div>")?;
+ if let Some(caption) = &pic.caption {
+ writeln!(f, "<figcaption>")?;
+ writeln!(f, "{}", Escaped(caption))?;
+ writeln!(f, "</figcaption>")?;
+ }
+ writeln!(f, "</figure>")?;
+ writeln!(f, "</main>")?;
+
+ writeln!(f, "<footer>")?;
+ write!(f, "original work by ")?;
+ if let Some(url) = &pic.author_url {
+ writeln!(f, "<a role=\"author\" href=\"{url}\">{author}</a>",
+ author = Escaped(&pic.author))?;
+ } else {
+ writeln!(f, "{}", Escaped(&pic.author))?;
+ }
+ writeln!(f, "<br>")?;
+ match &pic.license {
+ LicenseType::Cc(license) => {
+ writeln!(
+ f,
+ "licensed under <a role=\"license\" href=\"{url}\">{license}</a>",
+ url = license.url()
+ )?;
+ }
+ LicenseType::PublicDomain => writeln!(f, "this is public domain")?,
+ LicenseType::Proprietary => {
+ writeln!(
+ f,
+ "this is distributed under a proprietary license"
+ )?;
+ }
+ }
+ writeln!(f, "</footer>")?;
- writeln!(f, "</div>")?;
- writeln!(f, "</div>")?;
- if let Some(caption) = &pic.caption {
- writeln!(f, "<figcaption>")?;
- writeln!(f, "{}", Escaped(caption))?;
- writeln!(f, "</figcaption>")?;
+ writeln!(f, "</body>")?;
+ writeln!(f, "</html>")
}
- writeln!(f, "</figure>")?;
- writeln!(f, "</main>")?;
- writeln!(f, "<footer>")?;
- write!(f, "original work by ")?;
- if let Some(url) = &pic.author_url {
- writeln!(f, "<a role=\"author\" href=\"{url}\">{author}</a>",
- author = Escaped(&pic.author))?;
+ if let Err(err) = write_file(&mut f, pic) {
+ errorln!("Could not write to {path:?}: {err}");
+ RenderResult::Failure
} else {
- writeln!(f, "{}", Escaped(&pic.author))?;
+ RenderResult::Success
}
- writeln!(f, "<br>")?;
- match &pic.license {
- LicenseType::Cc(license) => {
- writeln!(
- f,
- "licensed under <a role=\"license\" href=\"{url}\">{license}</a>",
- url = license.url()
- )?;
- }
- LicenseType::PublicDomain => writeln!(f, "this is public domain")?,
- LicenseType::Proprietary => {
- writeln!(
- f,
- "this is distributed under a proprietary license"
- )?;
- }
- }
- writeln!(f, "</footer>")?;
-
- writeln!(f, "</body>")?;
- writeln!(f, "</html>")
}
/// Prints the common head elements to a given file
@@ -396,27 +430,17 @@ fn write_license(f: &mut File) -> io::Result<()> {
)
}
-/// Returns `true` if rendering succeded (or was skipped) and `false`
-/// otherwise. All warinings and error messages should be logged in here.
-fn render_thumbnail(pic: GalleryEntry) -> bool {
+fn render_thumbnail(pic: GalleryEntry, full_build: bool) -> RenderResult {
let thumb_path = thumb_path(&pic);
// Only try to render thumbnail in case the thumbnail file in the machine
// is older than the source file
- if let (Ok(thumb_meta), Ok(img_meta)) = (fs::metadata(&thumb_path), fs::metadata(&pic.path)) {
- let thumb_mod_date = thumb_meta.modified()
- .expect("os should support file modification date");
- let img_mod_date = img_meta.modified()
- .expect("os should support file modification date");
-
- if thumb_mod_date > img_mod_date {
- warnln!(
- "Skipped generating the thumbnail for {name:?} (update {path:?} to overwrite)",
- name = pic.file_name,
- path = pic.path
- );
- return true;
- }
+ if !full_build && !needs_update(&thumb_path, &pic.path) {
+ warnln!(
+ "Skipped rendering the thumbnail for {name:?} (use {FULL_BUILD_OPT} to overwrite)",
+ name = pic.file_name
+ );
+ return RenderResult::Skipped;
}
match pic.file_format {
@@ -451,11 +475,11 @@ fn render_thumbnail(pic: GalleryEntry) -> bool {
command = tikztosvg_cmd,
code = c
);
- return false;
+ return RenderResult::Failure;
}
Err(err) => {
errorln!("Failed to run tikztosvg: {err}");
- return false;
+ return RenderResult::Failure;
}
_ => {}
}
@@ -476,17 +500,18 @@ fn render_thumbnail(pic: GalleryEntry) -> bool {
src = src_path,
err = err,
);
- return false;
+ return RenderResult::Failure;
}
};
+ // TODO: Remove symlink and remake it if necessary
if let Err(err) = unix::fs::symlink(&src_path, &thumb_path) {
errorln!(
"Failed to create symlink {thumb:?} -> {src:?}: {err}",
thumb = thumb_path,
src = src_path,
);
- return false;
+ return RenderResult::Failure;
}
},
FileFormat::Jpeg | FileFormat::Png => {
@@ -496,7 +521,7 @@ fn render_thumbnail(pic: GalleryEntry) -> bool {
errorln!(
"Couldn't open thumbnail file {thumb_path:?}: {err}"
);
- return false;
+ return RenderResult::Failure;
}
};
@@ -507,7 +532,7 @@ fn render_thumbnail(pic: GalleryEntry) -> bool {
"Couldn't open file {path:?} to render thumbnail: {err}",
path = pic.file_name,
);
- return false;
+ return RenderResult::Failure;
}
};
@@ -518,7 +543,7 @@ fn render_thumbnail(pic: GalleryEntry) -> bool {
"Faileded to decode image file {name:?}: {err}",
name = pic.file_name,
);
- return false;
+ return RenderResult::Failure;
}
};
@@ -537,13 +562,13 @@ fn render_thumbnail(pic: GalleryEntry) -> bool {
"Couldn't write thumnail to file {path:?}: {err}",
path = thumb_path
);
- return false;
+ return RenderResult::Failure;
}
}
}
infoln!("Rendered thumbnail for {name:?}", name = pic.file_name);
- true
+ RenderResult::Success
}
/// Helper to get the correct thumbnail path for a given entry
@@ -566,6 +591,18 @@ fn thumb_path(pic: &GalleryEntry) -> PathBuf {
result
}
+/// Returns `false` if both `p1` and `p2` exist and and `p1` is newer than
+/// `f2`. Returns `true` otherwise
+fn needs_update<P1: AsRef<Path>, P2: AsRef<Path>>(p1: P1, p2: P2) -> bool {
+ if let (Ok(m1), Ok(m2)) = (fs::metadata(&p1), fs::metadata(&p2)) {
+ if m1.modified().unwrap() > m2.modified().unwrap() {
+ return false;
+ }
+ }
+
+ true
+}
+
impl<'a> Display for ThumbPath<'a> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
write!(f, "/{THUMBS_PATH}/{name}", name = Escaped(&self.0.file_name))?;
@@ -596,3 +633,20 @@ impl<'a> Display for Escaped<'a> {
Ok(())
}
}
+
+impl<'a> Display for ArgList<'a> {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
+ let mut first = true;
+
+ for arg in self.0 {
+ if first {
+ first = false;
+ write!(f, "{:?}", arg)?;
+ } else {
+ write!(f, " {:?}", arg)?;
+ }
+ }
+
+ Ok(())
+ }
+}