summary history branches tags files
commit:48b6dd056a3e4ab8818ca950ae84b87d5edffe24
author:Trevor Bentley
committer:Trevor Bentley
date:Tue Jan 17 00:23:18 2023 +0100
parents:f787ec91b302d03368c900cf7fdb79de2f4ae529
add support for %NAME% and url_string (permalinks)
diff --git a/README.md b/README.md
line changes: +32/-11
index fb659ce..7d5082a
--- a/README.md
+++ b/README.md
@@ -56,8 +56,9 @@ Feedback, bug reports, and feature requests are welcome.
     * optional Syntax highlighting
   * Error page
 * Configurable output
-  * configurable file names
+  * configurable file names, with variable substitution
   * configurable directory structure
+* Paginated output
 * Configurable limits for RAM and disk space usage
 * Site-wide and per-repository asset files
 
@@ -107,6 +108,18 @@ The top of the file contains global, site-wide settings like the site name, desc
 
 This should be followed by the `[gitsy_outputs]` section, which defines which input templates to use, and which files to output.  Input templates that are not specified will not be generated, so you can disable any output types that you don't need.  Templates can be used as many times as desired, generating arbitrarily many outputs.
 
+A few special variables can be used in output filenames:
+
+| Variable     | Purpose                                                                |
+|--------------|------------------------------------------------------------------------|
+| `%REPO%`     | Replaced with the name of the current repository.                      |
+| `%ID%`       | Replaced with the ID/hash of the current object (commit, branch, tag). |
+| `%PAGE%`     | Replaced with the current page, if pagination is enabled.              |
+| `%NAME%`     | Replaced with the name of the current object (file)                    |
+| `%PATH%`     | Replaced with the full path of the current object (file)               |
+| `%REF%`      | Replaced with the reference name of the current object (branch, tag)   |
+| `%TEMPLATE%` | Replaced with the template directory path (asset files)                |
+
 An optional `[gitsy_extra]` section can be used to provide global, user-defined key/value pairs to all of the templates.  Use this if you want to add custom site-wide variables for use in your templates.
 
 Finally, zero or more sections with arbitrary names define individual Git repositories to index.  Here, you can override most of the global settings at a per-repository level.  This is more powerful and allows specifying more metadata than bulk-import.
@@ -140,15 +153,18 @@ Any templates that are not specified in the configuration file are not evaluated
 
 Tera templates support custom functions and filters, and Itsy-Gitsy defines a few for convenience:
 
-| Name                | Type     | Purpose                                    | Example                                          |
-|---------------------|----------|--------------------------------------------|--------------------------------------------------|
-| only_files          | filter   | Filter the file tree into only files       | {{ all_files \| only_files }}                    |
-| only_dirs           | filter   | Filter the file tree into only directories | {{ all_files \| only_dirs }}                     |
-| hex                 | filter   | Output a number as a hex string            | {{ 17 \| hex }}                                  |
-| oct                 | filter   | Output a number as an octal string         | {{ 17 \| oct }}                                  |
-| mask                | filter   | Bitwise mask a number with another number  | {{ 17 \| mask(mask="0x77") }}                    |
-| ts_to_date          | function | Convert a timestamp and offset to a date   | {{ts_to_date(ts=ts_utc, tz=ts_offset)}}          |
-| ts_to_git_timestamp | function | Same, but print in standard Git format     | {{ts_to_git_timestamp(ts=ts_utc, tz=ts_offset)}} |
+| Name                | Type     | Purpose                                    | Example                                          |              |
+|---------------------|----------|--------------------------------------------|--------------------------------------------------|--------------|
+| only_files          | filter   | Filter the file tree into only files       | {{ all_files \| only_files }}                    |              |
+| only_dirs           | filter   | Filter the file tree into only directories | {{ all_files \| only_dirs }}                     |              |
+| hex                 | filter   | Output a number as a hex string            | {{ 17 \| hex }}                                  |              |
+| oct                 | filter   | Output a number as an octal string         | {{ 17 \| oct }}                                  |              |
+| mask                | filter   | Bitwise mask a number with another number  | {{ 17 \| mask(mask="0x77") }}                    |              |
+| url_string          | filter   | Convert a string to a url-friendly "slug"  | {{ file.path                                     | url_string}} |
+| ts_to_date          | function | Convert a timestamp and offset to a date   | {{ts_to_date(ts=ts_utc, tz=ts_offset)}}          |              |
+| ts_to_git_timestamp | function | Same, but print in standard Git format     | {{ts_to_git_timestamp(ts=ts_utc, tz=ts_offset)}} |              |
+
+`url_string` can be used in conjunction with the `%PATH%` and `%REF%` filename variables.  Both use a very primitive form of "slugifying" the strings into a format that can be used in a URL.  This allows for basic permalinks.
 
 ## Security
 
@@ -171,10 +187,15 @@ All metadata of all repositories, except for file contents, is held in memory.  
 
 Small repositories with dozens to hundreds of commits can be generated on the order of a few seconds or less.  Large repositories take *considerably* longer; parsing 1,000,000 commits from the Linux kernel repository with `limit_tree_depth = 3`, `limit_context = 10` and `limit_diffs = 100` took ~30 minutes on a fast laptop, and produced a ~2GB website.
 
+## Other Considerations
+
+The default templates are provided as a starting point, and demonstrate most features.  It is fully expected that you will customize or replace them.
+
+The default templates use the `%ID%` variable for outputting files, directories, branches, and tags.  These are guaranteed to be unique and URL-friendly.  Links to files and directories, however, are invalidated when changes are made to the repository.  To get "permalinks", you can change the output variables to use `%PATH%` instead, and replace `{{file.id}}` and `{{dir.id}}` with `{{file.path | url_string}}` and `{{dir.path | url_string}}` in all of the template files.
+
 ## Limitations
 
 * Only indexes history of one branch.
-* No permalinks.  Links to file contents are invalidated if the file changes.
 * High memory usage for large repositories.
 * Limited to the pre-defined set of input templates.
 * Leaves output in unknown, partial state in case of errors.

diff --git a/src/generate.rs b/src/generate.rs
line changes: +2/-1
index a34f70e..5703133
--- a/src/generate.rs
+++ b/src/generate.rs
@@ -25,7 +25,7 @@ use crate::{
     git::{dir_listing, parse_repo, GitFile, GitRepo, GitsyMetadata},
     loud, louder, loudest, normal, normal_noln,
     settings::{GitsyCli, GitsyRepoDescriptions, GitsySettings, GitsySettingsRepo},
-    template::{DirFilter, FileFilter, HexFilter, MaskFilter, OctFilter, Pagination, TsDateFn, TsTimestampFn},
+    template::{DirFilter, FileFilter, HexFilter, MaskFilter, OctFilter, Pagination, TsDateFn, TsTimestampFn, UrlStringFilter},
     util::{GitsyError, GitsyErrorKind, VERBOSITY},
 };
 use git2::{Error, Repository};
@@ -289,6 +289,7 @@ impl GitsyGenerator {
         tera.register_filter("hex", HexFilter {});
         tera.register_filter("oct", OctFilter {});
         tera.register_filter("mask", MaskFilter {});
+        tera.register_filter("url_string", UrlStringFilter {});
         tera.register_function("ts_to_date", TsDateFn {});
         tera.register_function("ts_to_git_timestamp", TsTimestampFn {});
         Ok(tera)

diff --git a/src/git.rs b/src/git.rs
line changes: +1/-1
index 596639f..f522c8e
--- a/src/git.rs
+++ b/src/git.rs
@@ -143,7 +143,7 @@ impl SafePathVar for GitObject {
         let mut dst = PathBuf::new();
         let safe_full_hash = sanitize_path_component(&self.full_hash);
         let safe_ref = self.ref_name.as_deref()
-            .map(|v| sanitize_path_component(&v))
+            .map(|v| sanitize_path_component(&urlify_path(v)))
             .unwrap_or("%REF%".to_string());
         for cmp in src.components() {
             let cmp = cmp.as_os_str().to_string_lossy()

diff --git a/src/main.rs b/src/main.rs
line changes: +0/-1
index e12f48d..e5ee47c
--- a/src/main.rs
+++ b/src/main.rs
@@ -32,7 +32,6 @@ use settings::{GitsyCli, GitsySettings};
 // TODO:
 //
 //   * favicon
-//   * permalinks (at least for README?)
 //   * better error propagation
 //   * automated tests
 //   * documentation + examples

diff --git a/src/template.rs b/src/template.rs
line changes: +10/-0
index f8d3823..146333d
--- a/src/template.rs
+++ b/src/template.rs
@@ -21,6 +21,7 @@
  * along with Itsy-Gitsy.  If not, see <http://www.gnu.org/licenses/>.
  */
 use crate::git::GitFile;
+use crate::util::{sanitize_path_component, urlify_path};
 use chrono::{naive::NaiveDateTime, offset::FixedOffset, DateTime};
 use serde::Serialize;
 use std::collections::HashMap;
@@ -116,6 +117,15 @@ impl Filter for MaskFilter {
     }
 }
 
+pub struct UrlStringFilter;
+impl Filter for UrlStringFilter {
+    fn filter(&self, value: &Value, _args: &HashMap<String, Value>) -> Result<Value, tera::Error> {
+        let v: String = try_get_value!("url_string", "value", String, value);
+        let sanitized = sanitize_path_component(&urlify_path(&v));
+        Ok(to_value(sanitize_path_component(&sanitized)).unwrap())
+    }
+}
+
 pub struct TsDateFn;
 impl Function for TsDateFn {
     fn call(&self, args: &HashMap<String, Value>) -> Result<Value, tera::Error> {