tikz-gallery-generator

Custum build of stapix for tikz.pablopie.xyz

Commit
9a1bf4442cb206612c62a89085dc8a46a69eddd8
Parent
3986bf768f93d2f74211203d475f00aa14666850
Author
Pablo <pablo-pie@riseup.net>
Date

Pulled changes from upstream

Cleaned the logging code and made it more customizable

Implemented incremental builds and made them the default

Diffstat

3 files changed, 205 insertions, 177 deletions

Status File Name N° Changes Insertions Deletions
Modified README.md 7 6 1
Modified src/log.rs 55 12 43
Modified src/main.rs 320 187 133
diff --git a/README.md b/README.md
@@ -8,7 +8,7 @@ Custum build of [`stapix`](https://git.pablopie.xyz/stapix) for
 Run:
 
 ```console
-$ tikz_gallery_generator config.yml
+$ tikz_gallery_generator config.yml [--full-build]
 ```
 
 The configuration file `config.yml` should consist of a list of struct entries
@@ -56,6 +56,11 @@ was taken) to be displayed for all users. **The `alt` and `caption` attributes
 should not be the same!** See
 <https://www.htmhell.dev/adventcalendar/2022/22/> for further details.
 
+### Options
+
+* **`--full-build`:** Disables incremental builds. Re-renders all pages and
+  thumbnails.
+
 ## Installation
 
 `tikz_gallery_generator` can be installed via Cargo by cloning this directory,
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} &dash; {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} &dash; {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(())
+    }
+}