version history MVP
implement basic version history support; there's now an icon in the footer that lets you see the previous versions and their sources I'm a bit worried about spoilers but honestly it's yet another way to hint yourself at the cool secrets so I don't mind
This commit is contained in:
parent
46dee56331
commit
c58c07d846
110
Cargo.lock
generated
110
Cargo.lock
generated
|
@ -267,9 +267,14 @@ checksum = "514de17de45fdb8dc022b1a7975556c53c86f9f0aa5f534b98977b171857c2c9"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cc"
|
name = "cc"
|
||||||
version = "1.0.97"
|
version = "1.1.21"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "099a5357d84c4c61eb35fc8eafa9a79a902c2f76911e5747ced4e032edd8d9b4"
|
checksum = "07b1695e2c7e8fc85310cde85aeaab7e3097f593c91d209d3f9df76c928100f0"
|
||||||
|
dependencies = [
|
||||||
|
"jobserver",
|
||||||
|
"libc",
|
||||||
|
"shlex",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cfg-if"
|
name = "cfg-if"
|
||||||
|
@ -599,6 +604,21 @@ version = "0.28.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253"
|
checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "git2"
|
||||||
|
version = "0.19.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b903b73e45dc0c6c596f2d37eccece7c1c8bb6e4407b001096387c63d0d93724"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags 2.5.0",
|
||||||
|
"libc",
|
||||||
|
"libgit2-sys",
|
||||||
|
"log",
|
||||||
|
"openssl-probe",
|
||||||
|
"openssl-sys",
|
||||||
|
"url",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "half"
|
name = "half"
|
||||||
version = "2.4.1"
|
version = "2.4.1"
|
||||||
|
@ -813,6 +833,15 @@ version = "1.0.11"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b"
|
checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "jobserver"
|
||||||
|
version = "0.1.32"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "48d1dbcbbeb6a7fec7e059840aa538bd62aaccf972c7346c4d9d2059312853d0"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "jotdown"
|
name = "jotdown"
|
||||||
version = "0.4.1"
|
version = "0.4.1"
|
||||||
|
@ -849,6 +878,46 @@ version = "0.2.155"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c"
|
checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "libgit2-sys"
|
||||||
|
version = "0.17.0+1.8.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "10472326a8a6477c3c20a64547b0059e4b0d086869eee31e6d7da728a8eb7224"
|
||||||
|
dependencies = [
|
||||||
|
"cc",
|
||||||
|
"libc",
|
||||||
|
"libssh2-sys",
|
||||||
|
"libz-sys",
|
||||||
|
"openssl-sys",
|
||||||
|
"pkg-config",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "libssh2-sys"
|
||||||
|
version = "0.3.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2dc8a030b787e2119a731f1951d6a773e2280c660f8ec4b0f5e1505a386e71ee"
|
||||||
|
dependencies = [
|
||||||
|
"cc",
|
||||||
|
"libc",
|
||||||
|
"libz-sys",
|
||||||
|
"openssl-sys",
|
||||||
|
"pkg-config",
|
||||||
|
"vcpkg",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "libz-sys"
|
||||||
|
version = "1.1.20"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d2d16453e800a8cf6dd2fc3eb4bc99b786a9b90c663b8559a5b1a041bf89e472"
|
||||||
|
dependencies = [
|
||||||
|
"cc",
|
||||||
|
"libc",
|
||||||
|
"pkg-config",
|
||||||
|
"vcpkg",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "lock_api"
|
name = "lock_api"
|
||||||
version = "0.4.12"
|
version = "0.4.12"
|
||||||
|
@ -938,6 +1007,24 @@ version = "1.19.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92"
|
checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "openssl-probe"
|
||||||
|
version = "0.1.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "openssl-sys"
|
||||||
|
version = "0.9.103"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7f9e8deee91df40a943c71b917e5874b951d32a802526c85721ce3b776c929d6"
|
||||||
|
dependencies = [
|
||||||
|
"cc",
|
||||||
|
"libc",
|
||||||
|
"pkg-config",
|
||||||
|
"vcpkg",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "parking_lot"
|
name = "parking_lot"
|
||||||
version = "0.12.2"
|
version = "0.12.2"
|
||||||
|
@ -1044,6 +1131,12 @@ version = "0.1.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
|
checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pkg-config"
|
||||||
|
version = "0.3.31"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "png"
|
name = "png"
|
||||||
version = "0.17.13"
|
version = "0.17.13"
|
||||||
|
@ -1295,6 +1388,12 @@ dependencies = [
|
||||||
"digest",
|
"digest",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "shlex"
|
||||||
|
version = "1.3.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "signal-hook-registry"
|
name = "signal-hook-registry"
|
||||||
version = "1.4.2"
|
version = "1.4.2"
|
||||||
|
@ -1546,6 +1645,7 @@ dependencies = [
|
||||||
"codespan-reporting",
|
"codespan-reporting",
|
||||||
"copy_dir",
|
"copy_dir",
|
||||||
"env_logger",
|
"env_logger",
|
||||||
|
"git2",
|
||||||
"handlebars",
|
"handlebars",
|
||||||
"http-body",
|
"http-body",
|
||||||
"image",
|
"image",
|
||||||
|
@ -1649,6 +1749,12 @@ version = "0.2.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a"
|
checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "vcpkg"
|
||||||
|
version = "0.2.15"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "version_check"
|
name = "version_check"
|
||||||
version = "0.9.4"
|
version = "0.9.4"
|
||||||
|
|
|
@ -11,8 +11,9 @@ mkfifo $reload_fifo
|
||||||
|
|
||||||
reload() {
|
reload() {
|
||||||
# This just kind of assumes regeneration doesn't take too long.
|
# This just kind of assumes regeneration doesn't take too long.
|
||||||
|
cargo build --release
|
||||||
kill "$treehouse_pid"
|
kill "$treehouse_pid"
|
||||||
cargo run --release -- serve --port 8082 > "$build_log" 2>&1 &
|
cargo run --release -- serve --port 8082 --commits-only > "$build_log" 2>&1 &
|
||||||
treehouse_pid="$!"
|
treehouse_pid="$!"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -16,6 +16,7 @@ clap = { version = "4.3.22", features = ["derive"] }
|
||||||
codespan-reporting = "0.11.1"
|
codespan-reporting = "0.11.1"
|
||||||
copy_dir = "0.1.3"
|
copy_dir = "0.1.3"
|
||||||
env_logger = "0.10.0"
|
env_logger = "0.10.0"
|
||||||
|
git2 = "0.19.0"
|
||||||
handlebars = "4.3.7"
|
handlebars = "4.3.7"
|
||||||
http-body = "1.0.0"
|
http-body = "1.0.0"
|
||||||
image = "0.24.8"
|
image = "0.24.8"
|
||||||
|
|
|
@ -44,7 +44,14 @@ pub enum Command {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Args)]
|
#[derive(Args)]
|
||||||
pub struct GenerateArgs {}
|
pub struct GenerateArgs {
|
||||||
|
/// Only use commits as sources. This will cause the latest revision to be taken from the
|
||||||
|
/// Git history instead of the working tree.
|
||||||
|
///
|
||||||
|
/// Recommended for deployment.
|
||||||
|
#[clap(long)]
|
||||||
|
pub commits_only: bool,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Args)]
|
#[derive(Args)]
|
||||||
pub struct FixArgs {
|
pub struct FixArgs {
|
||||||
|
|
|
@ -1,12 +1,13 @@
|
||||||
use std::{ffi::OsStr, ops::Range};
|
use std::{ffi::OsStr, ops::Range};
|
||||||
|
|
||||||
use anyhow::Context;
|
use anyhow::Context;
|
||||||
|
use codespan_reporting::diagnostic::Diagnostic;
|
||||||
use treehouse_format::ast::Branch;
|
use treehouse_format::ast::Branch;
|
||||||
use walkdir::WalkDir;
|
use walkdir::WalkDir;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
parse::{self, parse_toml_with_diagnostics, parse_tree_with_diagnostics},
|
parse::{self, parse_toml_with_diagnostics, parse_tree_with_diagnostics},
|
||||||
state::{FileId, Source, Treehouse},
|
state::{report_diagnostics, FileId, Source, Treehouse},
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::{FixAllArgs, FixArgs, Paths};
|
use super::{FixAllArgs, FixArgs, Paths};
|
||||||
|
@ -103,26 +104,32 @@ fn fix_indent_in_generated_toml(toml: &str, min_indent_level: usize) -> String {
|
||||||
|
|
||||||
pub fn fix_file(
|
pub fn fix_file(
|
||||||
treehouse: &mut Treehouse,
|
treehouse: &mut Treehouse,
|
||||||
|
diagnostics: &mut Vec<Diagnostic<FileId>>,
|
||||||
file_id: FileId,
|
file_id: FileId,
|
||||||
) -> Result<String, parse::ErrorsEmitted> {
|
) -> Result<String, parse::ErrorsEmitted> {
|
||||||
parse_tree_with_diagnostics(treehouse, file_id).map(|roots| {
|
parse_tree_with_diagnostics(treehouse, file_id)
|
||||||
let mut source = treehouse.source(file_id).input().to_owned();
|
.map(|roots| {
|
||||||
let mut state = State::default();
|
let mut source = treehouse.source(file_id).input().to_owned();
|
||||||
|
let mut state = State::default();
|
||||||
|
|
||||||
for branch in &roots.branches {
|
for branch in &roots.branches {
|
||||||
dfs_fix_branch(treehouse, file_id, &mut state, branch);
|
dfs_fix_branch(treehouse, file_id, &mut state, branch);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Doing a depth-first search of the branches yields fixes from the beginning of the file
|
// Doing a depth-first search of the branches yields fixes from the beginning of the file
|
||||||
// to its end. The most efficient way to apply all the fixes then is to reverse their order,
|
// to its end. The most efficient way to apply all the fixes then is to reverse their order,
|
||||||
// which lets us modify the source string in place because the fix ranges always stay
|
// which lets us modify the source string in place because the fix ranges always stay
|
||||||
// correct.
|
// correct.
|
||||||
for fix in state.fixes.iter().rev() {
|
for fix in state.fixes.iter().rev() {
|
||||||
source.replace_range(fix.range.clone(), &fix.replacement);
|
source.replace_range(fix.range.clone(), &fix.replacement);
|
||||||
}
|
}
|
||||||
|
|
||||||
source
|
source
|
||||||
})
|
})
|
||||||
|
.map_err(|mut new| {
|
||||||
|
diagnostics.append(&mut new);
|
||||||
|
parse::ErrorsEmitted
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn fix_file_cli(fix_args: FixArgs) -> anyhow::Result<()> {
|
pub fn fix_file_cli(fix_args: FixArgs) -> anyhow::Result<()> {
|
||||||
|
@ -130,9 +137,10 @@ pub fn fix_file_cli(fix_args: FixArgs) -> anyhow::Result<()> {
|
||||||
let file = std::fs::read_to_string(&fix_args.file).context("cannot read file to fix")?;
|
let file = std::fs::read_to_string(&fix_args.file).context("cannot read file to fix")?;
|
||||||
|
|
||||||
let mut treehouse = Treehouse::new();
|
let mut treehouse = Treehouse::new();
|
||||||
|
let mut diagnostics = vec![];
|
||||||
let file_id = treehouse.add_file(utf8_filename, Source::Other(file));
|
let file_id = treehouse.add_file(utf8_filename, Source::Other(file));
|
||||||
|
|
||||||
if let Ok(fixed) = fix_file(&mut treehouse, file_id) {
|
if let Ok(fixed) = fix_file(&mut treehouse, &mut diagnostics, file_id) {
|
||||||
if fix_args.apply {
|
if fix_args.apply {
|
||||||
// Try to write the backup first. If writing that fails, bail out without overwriting
|
// Try to write the backup first. If writing that fails, bail out without overwriting
|
||||||
// the source file.
|
// the source file.
|
||||||
|
@ -145,7 +153,7 @@ pub fn fix_file_cli(fix_args: FixArgs) -> anyhow::Result<()> {
|
||||||
println!("{fixed}");
|
println!("{fixed}");
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
treehouse.report_diagnostics()?;
|
report_diagnostics(&treehouse.files, &diagnostics)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
@ -160,9 +168,10 @@ pub fn fix_all_cli(fix_all_args: FixAllArgs, paths: &Paths<'_>) -> anyhow::Resul
|
||||||
let utf8_filename = entry.path().to_string_lossy();
|
let utf8_filename = entry.path().to_string_lossy();
|
||||||
|
|
||||||
let mut treehouse = Treehouse::new();
|
let mut treehouse = Treehouse::new();
|
||||||
|
let mut diagnostics = vec![];
|
||||||
let file_id = treehouse.add_file(utf8_filename.into_owned(), Source::Other(file));
|
let file_id = treehouse.add_file(utf8_filename.into_owned(), Source::Other(file));
|
||||||
|
|
||||||
if let Ok(fixed) = fix_file(&mut treehouse, file_id) {
|
if let Ok(fixed) = fix_file(&mut treehouse, &mut diagnostics, file_id) {
|
||||||
if fixed != treehouse.source(file_id).input() {
|
if fixed != treehouse.source(file_id).input() {
|
||||||
if fix_all_args.apply {
|
if fix_all_args.apply {
|
||||||
println!("fixing: {:?}", entry.path());
|
println!("fixing: {:?}", entry.path());
|
||||||
|
@ -174,7 +183,7 @@ pub fn fix_all_cli(fix_all_args: FixAllArgs, paths: &Paths<'_>) -> anyhow::Resul
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
treehouse.report_diagnostics()?;
|
report_diagnostics(&treehouse.files, &diagnostics)?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -193,9 +193,11 @@ async fn branch(RawQuery(named_id): RawQuery, State(state): State<Arc<Server>>)
|
||||||
.or_else(|| state.treehouse.branch_redirects.get(&named_id).copied());
|
.or_else(|| state.treehouse.branch_redirects.get(&named_id).copied());
|
||||||
if let Some(branch_id) = branch_id {
|
if let Some(branch_id) = branch_id {
|
||||||
let branch = state.treehouse.tree.branch(branch_id);
|
let branch = state.treehouse.tree.branch(branch_id);
|
||||||
if let Source::Tree { input, tree_path } = state.treehouse.source(branch.file_id) {
|
if let Source::Tree {
|
||||||
let file_path = state.target_dir.join(format!("{tree_path}.html"));
|
input, target_path, ..
|
||||||
match std::fs::read_to_string(&file_path) {
|
} = state.treehouse.source(branch.file_id)
|
||||||
|
{
|
||||||
|
match std::fs::read_to_string(target_path) {
|
||||||
Ok(content) => {
|
Ok(content) => {
|
||||||
let branch_markdown_content = input[branch.content.clone()].trim();
|
let branch_markdown_content = input[branch.content.clone()].trim();
|
||||||
let mut per_page_metadata =
|
let mut per_page_metadata =
|
||||||
|
@ -212,7 +214,7 @@ async fn branch(RawQuery(named_id): RawQuery, State(state): State<Arc<Server>>)
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
error!("error while reading file {file_path:?}: {e:?}");
|
error!("error while reading file {target_path:?}: {e:?}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,7 +6,7 @@ use walkdir::WalkDir;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
parse::parse_tree_with_diagnostics,
|
parse::parse_tree_with_diagnostics,
|
||||||
state::{Source, Treehouse},
|
state::{report_diagnostics, Source, Treehouse},
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::WcArgs;
|
use super::WcArgs;
|
||||||
|
@ -53,17 +53,20 @@ pub fn wc_cli(content_dir: &Path, mut wc_args: WcArgs) -> anyhow::Result<()> {
|
||||||
.to_string_lossy();
|
.to_string_lossy();
|
||||||
|
|
||||||
let file_id = treehouse.add_file(utf8_filename.into_owned(), Source::Other(file));
|
let file_id = treehouse.add_file(utf8_filename.into_owned(), Source::Other(file));
|
||||||
if let Ok(parsed) = parse_tree_with_diagnostics(&mut treehouse, file_id) {
|
match parse_tree_with_diagnostics(&mut treehouse, file_id) {
|
||||||
let source = treehouse.source(file_id);
|
Ok(parsed) => {
|
||||||
let word_count = wc_roots(source.input(), &parsed);
|
let source = treehouse.source(file_id);
|
||||||
println!("{word_count:>8} {}", treehouse.filename(file_id));
|
let word_count = wc_roots(source.input(), &parsed);
|
||||||
total += word_count;
|
println!("{word_count:>8} {}", treehouse.filename(file_id));
|
||||||
|
total += word_count;
|
||||||
|
}
|
||||||
|
Err(diagnostics) => {
|
||||||
|
report_diagnostics(&treehouse.files, &diagnostics)?;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
println!("{total:>8} total");
|
println!("{total:>8} total");
|
||||||
|
|
||||||
treehouse.report_diagnostics()?;
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
|
@ -26,6 +26,10 @@ pub struct Config {
|
||||||
/// TODO djot: Remove this once we transition to Djot fully.
|
/// TODO djot: Remove this once we transition to Djot fully.
|
||||||
pub markup: Markup,
|
pub markup: Markup,
|
||||||
|
|
||||||
|
/// This is used to generate a link in the footer that links to the page's source commit.
|
||||||
|
/// The final URL is `{commit_base_url}/{commit}/content/{tree_path}.tree`.
|
||||||
|
pub commit_base_url: String,
|
||||||
|
|
||||||
/// User-defined keys.
|
/// User-defined keys.
|
||||||
pub user: HashMap<String, String>,
|
pub user: HashMap<String, String>,
|
||||||
|
|
||||||
|
|
|
@ -19,6 +19,7 @@ use walkdir::WalkDir;
|
||||||
use crate::{
|
use crate::{
|
||||||
config::{Config, ConfigDerivedData},
|
config::{Config, ConfigDerivedData},
|
||||||
fun::seasons::Season,
|
fun::seasons::Season,
|
||||||
|
history::History,
|
||||||
html::{
|
html::{
|
||||||
breadcrumbs::breadcrumbs_to_html,
|
breadcrumbs::breadcrumbs_to_html,
|
||||||
navmap::{build_navigation_map, NavigationMap},
|
navmap::{build_navigation_map, NavigationMap},
|
||||||
|
@ -27,7 +28,7 @@ use crate::{
|
||||||
import_map::ImportMap,
|
import_map::ImportMap,
|
||||||
include_static::IncludeStatic,
|
include_static::IncludeStatic,
|
||||||
parse::parse_tree_with_diagnostics,
|
parse::parse_tree_with_diagnostics,
|
||||||
state::Source,
|
state::{has_errors, report_diagnostics, RevisionInfo, Source},
|
||||||
static_urls::StaticUrls,
|
static_urls::StaticUrls,
|
||||||
tree::SemaRoots,
|
tree::SemaRoots,
|
||||||
};
|
};
|
||||||
|
@ -36,14 +37,25 @@ use crate::state::{FileId, Treehouse};
|
||||||
|
|
||||||
use super::Paths;
|
use super::Paths;
|
||||||
|
|
||||||
#[derive(Default)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
struct Generator {
|
pub enum LatestRevision {
|
||||||
tree_files: Vec<PathBuf>,
|
/// The working tree is treated as the latest revision.
|
||||||
|
WorkingTree,
|
||||||
|
/// The latest commit is treated as the latest revision. The working tree is ignored.
|
||||||
|
LatestCommit,
|
||||||
}
|
}
|
||||||
|
|
||||||
struct Build {}
|
struct Generator {
|
||||||
|
tree_files: Vec<PathBuf>,
|
||||||
|
git: git2::Repository,
|
||||||
|
history: History,
|
||||||
|
latest_revision: LatestRevision,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
struct ParsedTree {
|
struct ParsedTree {
|
||||||
|
source_path: String,
|
||||||
|
root_key: String,
|
||||||
tree_path: String,
|
tree_path: String,
|
||||||
file_id: FileId,
|
file_id: FileId,
|
||||||
target_path: PathBuf,
|
target_path: PathBuf,
|
||||||
|
@ -63,6 +75,27 @@ pub struct Page {
|
||||||
pub breadcrumbs: String,
|
pub breadcrumbs: String,
|
||||||
pub tree_path: Option<String>,
|
pub tree_path: Option<String>,
|
||||||
pub tree: String,
|
pub tree: String,
|
||||||
|
|
||||||
|
pub revision: RevisionInfo,
|
||||||
|
pub revision_url: String,
|
||||||
|
pub source_url: String,
|
||||||
|
pub history_url: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
pub struct Commit {
|
||||||
|
pub revision_number: usize,
|
||||||
|
pub hash: String,
|
||||||
|
pub hash_short: String,
|
||||||
|
pub summary: String,
|
||||||
|
pub body: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
pub struct HistoryPage {
|
||||||
|
pub title: String,
|
||||||
|
pub commits: Vec<Commit>,
|
||||||
|
pub tree_path: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize)]
|
#[derive(Serialize)]
|
||||||
|
@ -85,6 +118,13 @@ struct PageTemplateData<'a> {
|
||||||
season: Option<Season>,
|
season: Option<Season>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
struct HistoryTemplateData<'a> {
|
||||||
|
config: &'a Config,
|
||||||
|
page: HistoryPage,
|
||||||
|
season: Option<Season>,
|
||||||
|
}
|
||||||
|
|
||||||
impl Generator {
|
impl Generator {
|
||||||
fn add_directory_rec(&mut self, directory: &Path) -> anyhow::Result<()> {
|
fn add_directory_rec(&mut self, directory: &Path) -> anyhow::Result<()> {
|
||||||
for entry in WalkDir::new(directory) {
|
for entry in WalkDir::new(directory) {
|
||||||
|
@ -96,136 +136,7 @@ impl Generator {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn register_template(
|
fn init_handlebars(handlebars: &mut Handlebars<'_>, paths: &Paths<'_>, config: &Config) {
|
||||||
handlebars: &mut Handlebars<'_>,
|
|
||||||
treehouse: &mut Treehouse,
|
|
||||||
name: &str,
|
|
||||||
path: &Path,
|
|
||||||
) -> anyhow::Result<FileId> {
|
|
||||||
let source = std::fs::read_to_string(path)
|
|
||||||
.with_context(|| format!("cannot read template file {path:?}"))?;
|
|
||||||
let file_id =
|
|
||||||
treehouse.add_file(path.to_string_lossy().into_owned(), Source::Other(source));
|
|
||||||
let source = treehouse.source(file_id);
|
|
||||||
if let Err(error) = handlebars.register_template_string(name, source) {
|
|
||||||
Self::wrangle_handlebars_error_into_diagnostic(
|
|
||||||
treehouse,
|
|
||||||
file_id,
|
|
||||||
error.line_no,
|
|
||||||
error.column_no,
|
|
||||||
error.reason().to_string(),
|
|
||||||
)?;
|
|
||||||
}
|
|
||||||
Ok(file_id)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn wrangle_handlebars_error_into_diagnostic(
|
|
||||||
treehouse: &mut Treehouse,
|
|
||||||
file_id: FileId,
|
|
||||||
line: Option<usize>,
|
|
||||||
column: Option<usize>,
|
|
||||||
message: String,
|
|
||||||
) -> anyhow::Result<()> {
|
|
||||||
if let (Some(line), Some(column)) = (line, column) {
|
|
||||||
let line_range = treehouse
|
|
||||||
.files
|
|
||||||
.line_range(file_id, line)
|
|
||||||
.expect("file was added to the list");
|
|
||||||
treehouse.diagnostics.push(Diagnostic {
|
|
||||||
severity: Severity::Error,
|
|
||||||
code: Some("template".into()),
|
|
||||||
message,
|
|
||||||
labels: vec![Label {
|
|
||||||
style: LabelStyle::Primary,
|
|
||||||
file_id,
|
|
||||||
range: line_range.start + column..line_range.start + column + 1,
|
|
||||||
message: String::new(),
|
|
||||||
}],
|
|
||||||
notes: vec![],
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
let file = treehouse.filename(file_id);
|
|
||||||
bail!("template error in {file}: {message}");
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn parse_trees(
|
|
||||||
&self,
|
|
||||||
config: &Config,
|
|
||||||
paths: &Paths<'_>,
|
|
||||||
) -> anyhow::Result<(Treehouse, Vec<ParsedTree>)> {
|
|
||||||
let mut treehouse = Treehouse::new();
|
|
||||||
let mut parsed_trees = vec![];
|
|
||||||
|
|
||||||
for path in &self.tree_files {
|
|
||||||
let utf8_filename = path.to_string_lossy();
|
|
||||||
|
|
||||||
let tree_path = path.strip_prefix(paths.content_dir).unwrap_or(path);
|
|
||||||
let target_path = if tree_path == OsStr::new("index.tree") {
|
|
||||||
paths.target_dir.join("index.html")
|
|
||||||
} else {
|
|
||||||
paths.target_dir.join(tree_path).with_extension("html")
|
|
||||||
};
|
|
||||||
debug!("generating: {path:?} -> {target_path:?}");
|
|
||||||
|
|
||||||
let source = match std::fs::read_to_string(path) {
|
|
||||||
Ok(source) => source,
|
|
||||||
Err(error) => {
|
|
||||||
treehouse.diagnostics.push(Diagnostic {
|
|
||||||
severity: Severity::Error,
|
|
||||||
code: None,
|
|
||||||
message: format!("{utf8_filename}: cannot read file: {error}"),
|
|
||||||
labels: vec![],
|
|
||||||
notes: vec![],
|
|
||||||
});
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
let tree_path = tree_path
|
|
||||||
.with_extension("")
|
|
||||||
.to_string_lossy()
|
|
||||||
.replace('\\', "/");
|
|
||||||
let file_id = treehouse.add_file(
|
|
||||||
utf8_filename.into_owned(),
|
|
||||||
Source::Tree {
|
|
||||||
input: source,
|
|
||||||
tree_path: tree_path.clone(),
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
if let Ok(roots) = parse_tree_with_diagnostics(&mut treehouse, file_id) {
|
|
||||||
let roots = SemaRoots::from_roots(&mut treehouse, config, file_id, roots);
|
|
||||||
treehouse.roots.insert(tree_path.clone(), roots);
|
|
||||||
parsed_trees.push(ParsedTree {
|
|
||||||
tree_path,
|
|
||||||
file_id,
|
|
||||||
target_path,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok((treehouse, parsed_trees))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn generate_all_files(
|
|
||||||
&self,
|
|
||||||
treehouse: &mut Treehouse,
|
|
||||||
config: &Config,
|
|
||||||
paths: &Paths<'_>,
|
|
||||||
navigation_map: &NavigationMap,
|
|
||||||
parsed_trees: Vec<ParsedTree>,
|
|
||||||
) -> anyhow::Result<()> {
|
|
||||||
let mut handlebars = Handlebars::new();
|
|
||||||
let mut config_derived_data = ConfigDerivedData {
|
|
||||||
image_sizes: Default::default(),
|
|
||||||
static_urls: StaticUrls::new(
|
|
||||||
// NOTE: Allow referring to generated static assets here.
|
|
||||||
paths.target_dir.join("static"),
|
|
||||||
format!("{}/static", config.site),
|
|
||||||
),
|
|
||||||
};
|
|
||||||
|
|
||||||
handlebars_helper!(cat: |a: String, b: String| a + &b);
|
handlebars_helper!(cat: |a: String, b: String| a + &b);
|
||||||
|
|
||||||
handlebars.register_helper("cat", Box::new(cat));
|
handlebars.register_helper("cat", Box::new(cat));
|
||||||
|
@ -245,6 +156,267 @@ impl Generator {
|
||||||
base_dir: paths.target_dir.join("static"),
|
base_dir: paths.target_dir.join("static"),
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn register_template(
|
||||||
|
handlebars: &mut Handlebars<'_>,
|
||||||
|
treehouse: &mut Treehouse,
|
||||||
|
diagnostics: &mut Vec<Diagnostic<FileId>>,
|
||||||
|
name: &str,
|
||||||
|
path: &Path,
|
||||||
|
) -> anyhow::Result<FileId> {
|
||||||
|
let source = std::fs::read_to_string(path)
|
||||||
|
.with_context(|| format!("cannot read template file {path:?}"))?;
|
||||||
|
let file_id =
|
||||||
|
treehouse.add_file(path.to_string_lossy().into_owned(), Source::Other(source));
|
||||||
|
let source = treehouse.source(file_id);
|
||||||
|
if let Err(error) = handlebars.register_template_string(name, source) {
|
||||||
|
Self::wrangle_handlebars_error_into_diagnostic(
|
||||||
|
treehouse,
|
||||||
|
diagnostics,
|
||||||
|
file_id,
|
||||||
|
error.line_no,
|
||||||
|
error.column_no,
|
||||||
|
error.reason().to_string(),
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
Ok(file_id)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn wrangle_handlebars_error_into_diagnostic(
|
||||||
|
treehouse: &mut Treehouse,
|
||||||
|
diagnostics: &mut Vec<Diagnostic<FileId>>,
|
||||||
|
file_id: FileId,
|
||||||
|
line: Option<usize>,
|
||||||
|
column: Option<usize>,
|
||||||
|
message: String,
|
||||||
|
) -> anyhow::Result<()> {
|
||||||
|
if let (Some(line), Some(column)) = (line, column) {
|
||||||
|
let line_range = treehouse
|
||||||
|
.files
|
||||||
|
.line_range(file_id, line)
|
||||||
|
.expect("file was added to the list");
|
||||||
|
diagnostics.push(Diagnostic {
|
||||||
|
severity: Severity::Error,
|
||||||
|
code: Some("template".into()),
|
||||||
|
message,
|
||||||
|
labels: vec![Label {
|
||||||
|
style: LabelStyle::Primary,
|
||||||
|
file_id,
|
||||||
|
range: line_range.start + column..line_range.start + column + 1,
|
||||||
|
message: String::new(),
|
||||||
|
}],
|
||||||
|
notes: vec![],
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
let file = treehouse.filename(file_id);
|
||||||
|
bail!("template error in {file}: {message}");
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_tree(
|
||||||
|
treehouse: &mut Treehouse,
|
||||||
|
config: &Config,
|
||||||
|
source: String,
|
||||||
|
source_path: String,
|
||||||
|
tree_path: String,
|
||||||
|
target_path: PathBuf,
|
||||||
|
revision: RevisionInfo,
|
||||||
|
) -> anyhow::Result<(Option<ParsedTree>, Vec<Diagnostic<FileId>>)> {
|
||||||
|
let file_id = treehouse.add_file(
|
||||||
|
format!("{source_path}@{}", revision.commit_short),
|
||||||
|
Source::Tree {
|
||||||
|
input: source,
|
||||||
|
target_path: target_path.clone(),
|
||||||
|
tree_path: tree_path.clone(),
|
||||||
|
revision_info: revision.clone(),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
match parse_tree_with_diagnostics(treehouse, file_id) {
|
||||||
|
Ok(roots) => {
|
||||||
|
let mut diagnostics = vec![];
|
||||||
|
let roots =
|
||||||
|
SemaRoots::from_roots(treehouse, &mut diagnostics, config, file_id, roots);
|
||||||
|
|
||||||
|
let root_key = if revision.is_latest {
|
||||||
|
tree_path.clone()
|
||||||
|
} else {
|
||||||
|
format!("{tree_path}@{}", revision.number)
|
||||||
|
};
|
||||||
|
treehouse.roots.insert(root_key.clone(), roots);
|
||||||
|
|
||||||
|
Ok((
|
||||||
|
Some(ParsedTree {
|
||||||
|
source_path,
|
||||||
|
root_key,
|
||||||
|
tree_path,
|
||||||
|
file_id,
|
||||||
|
target_path,
|
||||||
|
}),
|
||||||
|
diagnostics,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
Err(diagnostics) => Ok((None, diagnostics)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_trees(
|
||||||
|
&self,
|
||||||
|
config: &Config,
|
||||||
|
paths: &Paths<'_>,
|
||||||
|
) -> anyhow::Result<(Treehouse, Vec<ParsedTree>, Vec<Diagnostic<FileId>>)> {
|
||||||
|
let mut treehouse = Treehouse::new();
|
||||||
|
let mut diagnostics = vec![];
|
||||||
|
let mut parsed_trees = vec![];
|
||||||
|
|
||||||
|
for path in &self.tree_files {
|
||||||
|
let utf8_path = path.to_string_lossy();
|
||||||
|
|
||||||
|
let tree_path = path
|
||||||
|
.strip_prefix(paths.content_dir)
|
||||||
|
.unwrap_or(path)
|
||||||
|
.with_extension("")
|
||||||
|
.to_string_lossy()
|
||||||
|
.replace('\\', "/");
|
||||||
|
debug!("tree file: {path:?}");
|
||||||
|
|
||||||
|
let page_history = self.history.by_page.get(&utf8_path[..]);
|
||||||
|
let working_revision_number = page_history
|
||||||
|
.map(|history| history.revisions.len() + 1)
|
||||||
|
.unwrap_or(1);
|
||||||
|
|
||||||
|
if self.latest_revision == LatestRevision::WorkingTree {
|
||||||
|
let source = std::fs::read_to_string(path)?;
|
||||||
|
let target_path = paths.target_dir.join(&tree_path).with_extension("html");
|
||||||
|
let (parsed_tree, mut parse_diagnostics) = Self::parse_tree(
|
||||||
|
&mut treehouse,
|
||||||
|
config,
|
||||||
|
source,
|
||||||
|
utf8_path.clone().into_owned(),
|
||||||
|
tree_path.clone(),
|
||||||
|
target_path,
|
||||||
|
RevisionInfo {
|
||||||
|
is_latest: true,
|
||||||
|
number: working_revision_number,
|
||||||
|
commit: "working".into(),
|
||||||
|
commit_short: "working".into(),
|
||||||
|
},
|
||||||
|
)?;
|
||||||
|
diagnostics.append(&mut parse_diagnostics);
|
||||||
|
if let Some(parsed_tree) = parsed_tree {
|
||||||
|
parsed_trees.push(parsed_tree);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(page_history) = page_history {
|
||||||
|
for (i, revision) in page_history.revisions.iter().enumerate() {
|
||||||
|
let revision_number = page_history.revisions.len() - i;
|
||||||
|
|
||||||
|
let source = String::from_utf8(
|
||||||
|
self.git.find_blob(revision.blob_oid)?.content().to_owned(),
|
||||||
|
)?;
|
||||||
|
|
||||||
|
let target_path = paths
|
||||||
|
.target_dir
|
||||||
|
.join(format!("{tree_path}@{revision_number}"))
|
||||||
|
.with_extension("html");
|
||||||
|
|
||||||
|
let (parsed_tree, parse_diagnostics) = Self::parse_tree(
|
||||||
|
&mut treehouse,
|
||||||
|
config,
|
||||||
|
source,
|
||||||
|
utf8_path.clone().into_owned(),
|
||||||
|
tree_path.clone(),
|
||||||
|
target_path,
|
||||||
|
RevisionInfo {
|
||||||
|
is_latest: false,
|
||||||
|
number: revision_number,
|
||||||
|
commit: revision.commit_oid.to_string(),
|
||||||
|
commit_short: revision.commit_short(),
|
||||||
|
},
|
||||||
|
)?;
|
||||||
|
_ = parse_diagnostics; // We don't reemit diagnostics from old revisions.
|
||||||
|
if let Some(parsed_tree) = parsed_tree {
|
||||||
|
// If this commit is also considered to be the latest revision, we need
|
||||||
|
// to generate a second version of the page that will act as the
|
||||||
|
// latest one.
|
||||||
|
let is_latest =
|
||||||
|
self.latest_revision == LatestRevision::LatestCommit && i == 0;
|
||||||
|
if is_latest {
|
||||||
|
let root_key = parsed_tree.tree_path.clone();
|
||||||
|
treehouse.roots.insert(
|
||||||
|
root_key.clone(),
|
||||||
|
treehouse.roots.get(&parsed_tree.root_key).unwrap().clone(),
|
||||||
|
);
|
||||||
|
|
||||||
|
let target_path =
|
||||||
|
paths.target_dir.join(&tree_path).with_extension("html");
|
||||||
|
let file_id = {
|
||||||
|
let file = treehouse.files.get(parsed_tree.file_id).unwrap();
|
||||||
|
let filename = file.name().clone();
|
||||||
|
let Source::Tree {
|
||||||
|
input,
|
||||||
|
tree_path,
|
||||||
|
target_path,
|
||||||
|
revision_info,
|
||||||
|
} = file.source().clone()
|
||||||
|
else {
|
||||||
|
panic!(".tree files must have Tree sources")
|
||||||
|
};
|
||||||
|
treehouse.add_file(
|
||||||
|
filename,
|
||||||
|
Source::Tree {
|
||||||
|
input,
|
||||||
|
tree_path,
|
||||||
|
target_path: target_path.clone(),
|
||||||
|
revision_info: RevisionInfo {
|
||||||
|
is_latest: true,
|
||||||
|
..revision_info
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
|
parsed_trees.push(ParsedTree {
|
||||||
|
root_key,
|
||||||
|
target_path,
|
||||||
|
file_id,
|
||||||
|
..parsed_tree.clone()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
parsed_trees.push(parsed_tree);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok((treehouse, parsed_trees, diagnostics))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn generate_all_files(
|
||||||
|
&self,
|
||||||
|
treehouse: &mut Treehouse,
|
||||||
|
config: &Config,
|
||||||
|
paths: &Paths<'_>,
|
||||||
|
navigation_map: &NavigationMap,
|
||||||
|
parsed_trees: Vec<ParsedTree>,
|
||||||
|
) -> anyhow::Result<Vec<Diagnostic<FileId>>> {
|
||||||
|
let mut global_diagnostics = vec![];
|
||||||
|
|
||||||
|
let mut config_derived_data = ConfigDerivedData {
|
||||||
|
image_sizes: Default::default(),
|
||||||
|
static_urls: StaticUrls::new(
|
||||||
|
// NOTE: Allow referring to generated static assets here.
|
||||||
|
paths.target_dir.join("static"),
|
||||||
|
format!("{}/static", config.site),
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut handlebars = Handlebars::new();
|
||||||
|
Self::init_handlebars(&mut handlebars, paths, config);
|
||||||
|
|
||||||
let mut template_file_ids = HashMap::new();
|
let mut template_file_ids = HashMap::new();
|
||||||
for entry in WalkDir::new(paths.template_dir) {
|
for entry in WalkDir::new(paths.template_dir) {
|
||||||
|
@ -256,8 +428,13 @@ impl Generator {
|
||||||
.to_string_lossy()
|
.to_string_lossy()
|
||||||
.into_owned()
|
.into_owned()
|
||||||
.replace('\\', "/");
|
.replace('\\', "/");
|
||||||
let file_id =
|
let file_id = Self::register_template(
|
||||||
Self::register_template(&mut handlebars, treehouse, &relative_path, path)?;
|
&mut handlebars,
|
||||||
|
treehouse,
|
||||||
|
&mut global_diagnostics,
|
||||||
|
&relative_path,
|
||||||
|
path,
|
||||||
|
)?;
|
||||||
template_file_ids.insert(relative_path, file_id);
|
template_file_ids.insert(relative_path, file_id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -277,6 +454,7 @@ impl Generator {
|
||||||
Err(error) => {
|
Err(error) => {
|
||||||
Self::wrangle_handlebars_error_into_diagnostic(
|
Self::wrangle_handlebars_error_into_diagnostic(
|
||||||
treehouse,
|
treehouse,
|
||||||
|
&mut global_diagnostics,
|
||||||
file_id,
|
file_id,
|
||||||
error.line_no,
|
error.line_no,
|
||||||
error.column_no,
|
error.column_no,
|
||||||
|
@ -295,7 +473,7 @@ impl Generator {
|
||||||
let mut feeds = HashMap::new();
|
let mut feeds = HashMap::new();
|
||||||
|
|
||||||
for parsed_tree in &parsed_trees {
|
for parsed_tree in &parsed_trees {
|
||||||
let roots = &treehouse.roots[&parsed_tree.tree_path];
|
let roots = &treehouse.roots[&parsed_tree.root_key];
|
||||||
|
|
||||||
if let Some(feed_name) = &roots.attributes.feed {
|
if let Some(feed_name) = &roots.attributes.feed {
|
||||||
let mut feed = Feed {
|
let mut feed = Feed {
|
||||||
|
@ -310,13 +488,15 @@ impl Generator {
|
||||||
}
|
}
|
||||||
|
|
||||||
for parsed_tree in parsed_trees {
|
for parsed_tree in parsed_trees {
|
||||||
let breadcrumbs = breadcrumbs_to_html(config, navigation_map, &parsed_tree.tree_path);
|
debug!("generating: {:?}", parsed_tree.target_path);
|
||||||
|
|
||||||
|
let breadcrumbs = breadcrumbs_to_html(config, navigation_map, &parsed_tree.root_key);
|
||||||
|
|
||||||
let mut tree = String::new();
|
let mut tree = String::new();
|
||||||
// Temporarily steal the tree out of the treehouse.
|
// Temporarily steal the tree out of the treehouse.
|
||||||
let roots = treehouse
|
let roots = treehouse
|
||||||
.roots
|
.roots
|
||||||
.remove(&parsed_tree.tree_path)
|
.remove(&parsed_tree.root_key)
|
||||||
.expect("tree should have been added to the treehouse");
|
.expect("tree should have been added to the treehouse");
|
||||||
branches_to_html(
|
branches_to_html(
|
||||||
&mut tree,
|
&mut tree,
|
||||||
|
@ -328,6 +508,9 @@ impl Generator {
|
||||||
&roots.branches,
|
&roots.branches,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
let revision = treehouse
|
||||||
|
.revision_info(parsed_tree.file_id)
|
||||||
|
.expect(".tree files should have Tree sources");
|
||||||
let template_data = PageTemplateData {
|
let template_data = PageTemplateData {
|
||||||
config,
|
config,
|
||||||
page: Page {
|
page: Page {
|
||||||
|
@ -347,6 +530,14 @@ impl Generator {
|
||||||
.tree_path(parsed_tree.file_id)
|
.tree_path(parsed_tree.file_id)
|
||||||
.map(|s| s.to_owned()),
|
.map(|s| s.to_owned()),
|
||||||
tree,
|
tree,
|
||||||
|
|
||||||
|
revision_url: format!("{}/{}", config.site, parsed_tree.root_key),
|
||||||
|
source_url: format!(
|
||||||
|
"{}/{}/{}",
|
||||||
|
config.commit_base_url, revision.commit, parsed_tree.source_path,
|
||||||
|
),
|
||||||
|
history_url: format!("{}/h/{}", config.site, parsed_tree.tree_path),
|
||||||
|
revision: revision.clone(),
|
||||||
},
|
},
|
||||||
feeds: &feeds,
|
feeds: &feeds,
|
||||||
season: Season::current(),
|
season: Season::current(),
|
||||||
|
@ -357,13 +548,16 @@ impl Generator {
|
||||||
.clone()
|
.clone()
|
||||||
.unwrap_or_else(|| "_tree.hbs".into());
|
.unwrap_or_else(|| "_tree.hbs".into());
|
||||||
|
|
||||||
treehouse.roots.insert(parsed_tree.tree_path, roots);
|
// Reinsert the stolen roots.
|
||||||
|
treehouse.roots.insert(parsed_tree.root_key, roots);
|
||||||
|
|
||||||
let templated_html = match handlebars.render(&template_name, &template_data) {
|
let templated_html = match handlebars.render(&template_name, &template_data) {
|
||||||
Ok(html) => html,
|
Ok(html) => html,
|
||||||
Err(error) => {
|
Err(error) => {
|
||||||
Self::wrangle_handlebars_error_into_diagnostic(
|
Self::wrangle_handlebars_error_into_diagnostic(
|
||||||
treehouse,
|
treehouse,
|
||||||
|
// TODO: This should dump diagnostics out somewhere else.
|
||||||
|
&mut global_diagnostics,
|
||||||
template_file_ids[&template_name],
|
template_file_ids[&template_name],
|
||||||
error.line_no,
|
error.line_no,
|
||||||
error.column_no,
|
error.column_no,
|
||||||
|
@ -382,11 +576,78 @@ impl Generator {
|
||||||
std::fs::write(parsed_tree.target_path, templated_html)?;
|
std::fs::write(parsed_tree.target_path, templated_html)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
for (path, page_history) in &self.history.by_page {
|
||||||
|
let tree_path = path
|
||||||
|
.strip_prefix("content/")
|
||||||
|
.unwrap_or(path)
|
||||||
|
.strip_suffix(".tree")
|
||||||
|
.unwrap_or(path);
|
||||||
|
let target_path = paths
|
||||||
|
.target_dir
|
||||||
|
.join("h")
|
||||||
|
.join(path.strip_prefix("content/").unwrap_or(path))
|
||||||
|
.with_extension("html");
|
||||||
|
std::fs::create_dir_all(target_path.parent().unwrap())?;
|
||||||
|
|
||||||
|
let template_data = HistoryTemplateData {
|
||||||
|
config,
|
||||||
|
page: HistoryPage {
|
||||||
|
title: format!("page history: {tree_path}"),
|
||||||
|
commits: page_history
|
||||||
|
.revisions
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.map(|(i, revision)| Commit {
|
||||||
|
revision_number: page_history.revisions.len() - i,
|
||||||
|
hash: revision.commit_oid.to_string(),
|
||||||
|
hash_short: revision.commit_short(),
|
||||||
|
summary: self
|
||||||
|
.history
|
||||||
|
.commits
|
||||||
|
.get(&revision.commit_oid)
|
||||||
|
.map(|c| c.summary.as_str())
|
||||||
|
.unwrap_or("<no summary available>")
|
||||||
|
.to_owned(),
|
||||||
|
body: self
|
||||||
|
.history
|
||||||
|
.commits
|
||||||
|
.get(&revision.commit_oid)
|
||||||
|
.map(|c| c.body.as_str())
|
||||||
|
.unwrap_or("<no body available>")
|
||||||
|
.to_owned(),
|
||||||
|
})
|
||||||
|
.collect(),
|
||||||
|
tree_path: tree_path.to_owned(),
|
||||||
|
},
|
||||||
|
season: Season::current(),
|
||||||
|
};
|
||||||
|
let templated_html = match handlebars.render("_history.hbs", &template_data) {
|
||||||
|
Ok(html) => html,
|
||||||
|
Err(error) => {
|
||||||
|
Self::wrangle_handlebars_error_into_diagnostic(
|
||||||
|
treehouse,
|
||||||
|
// TODO: This should dump diagnostics out somewhere else.
|
||||||
|
&mut global_diagnostics,
|
||||||
|
template_file_ids["_history.hbs"],
|
||||||
|
error.line_no,
|
||||||
|
error.column_no,
|
||||||
|
error.desc,
|
||||||
|
)?;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
std::fs::write(target_path, templated_html)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(global_diagnostics)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn generate(paths: &Paths<'_>) -> anyhow::Result<(Config, Treehouse)> {
|
pub fn generate(
|
||||||
|
paths: &Paths<'_>,
|
||||||
|
latest_revision: LatestRevision,
|
||||||
|
) -> anyhow::Result<(Config, Treehouse)> {
|
||||||
let start = Instant::now();
|
let start = Instant::now();
|
||||||
|
|
||||||
info!("loading config");
|
info!("loading config");
|
||||||
|
@ -406,10 +667,23 @@ pub fn generate(paths: &Paths<'_>) -> anyhow::Result<(Config, Treehouse)> {
|
||||||
info!("creating static/generated directory");
|
info!("creating static/generated directory");
|
||||||
std::fs::create_dir_all(paths.target_dir.join("static/generated"))?;
|
std::fs::create_dir_all(paths.target_dir.join("static/generated"))?;
|
||||||
|
|
||||||
|
info!("getting history");
|
||||||
|
let git = git2::Repository::open(".")?;
|
||||||
|
let history = History::get(&git)?;
|
||||||
|
|
||||||
info!("parsing tree");
|
info!("parsing tree");
|
||||||
let mut generator = Generator::default();
|
let mut generator = Generator {
|
||||||
|
tree_files: vec![],
|
||||||
|
git,
|
||||||
|
history,
|
||||||
|
latest_revision,
|
||||||
|
};
|
||||||
generator.add_directory_rec(paths.content_dir)?;
|
generator.add_directory_rec(paths.content_dir)?;
|
||||||
let (mut treehouse, parsed_trees) = generator.parse_trees(&config, paths)?;
|
let (mut treehouse, parsed_trees, diagnostics) = generator.parse_trees(&config, paths)?;
|
||||||
|
report_diagnostics(&treehouse.files, &diagnostics)?;
|
||||||
|
if has_errors(&diagnostics) {
|
||||||
|
bail!("diagnostics emitted during parsing");
|
||||||
|
}
|
||||||
|
|
||||||
// NOTE: The navigation map is a legacy feature that is lazy-loaded when fragment-based
|
// NOTE: The navigation map is a legacy feature that is lazy-loaded when fragment-based
|
||||||
// navigation is used.
|
// navigation is used.
|
||||||
|
@ -431,30 +705,34 @@ pub fn generate(paths: &Paths<'_>) -> anyhow::Result<(Config, Treehouse)> {
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
info!("generating standalone pages");
|
info!("generating standalone pages");
|
||||||
generator.generate_all_files(
|
let diagnostics = generator.generate_all_files(
|
||||||
&mut treehouse,
|
&mut treehouse,
|
||||||
&config,
|
&config,
|
||||||
paths,
|
paths,
|
||||||
&navigation_map,
|
&navigation_map,
|
||||||
parsed_trees,
|
parsed_trees,
|
||||||
)?;
|
)?;
|
||||||
|
report_diagnostics(&treehouse.files, &diagnostics)?;
|
||||||
|
|
||||||
treehouse.report_diagnostics()?;
|
info!("generating change history pages");
|
||||||
|
|
||||||
let duration = start.elapsed();
|
let duration = start.elapsed();
|
||||||
info!("generation done in {duration:?}");
|
info!("generation done in {duration:?}");
|
||||||
|
|
||||||
if !treehouse.has_errors() {
|
if !has_errors(&diagnostics) {
|
||||||
Ok((config, treehouse))
|
Ok((config, treehouse))
|
||||||
} else {
|
} else {
|
||||||
bail!("generation errors occurred; diagnostics were emitted with detailed descriptions");
|
bail!("generation errors occurred; diagnostics were emitted with detailed descriptions");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn regenerate_or_report_error(paths: &Paths<'_>) -> anyhow::Result<(Config, Treehouse)> {
|
pub fn regenerate_or_report_error(
|
||||||
|
paths: &Paths<'_>,
|
||||||
|
latest_revision: LatestRevision,
|
||||||
|
) -> anyhow::Result<(Config, Treehouse)> {
|
||||||
info!("regenerating site content");
|
info!("regenerating site content");
|
||||||
|
|
||||||
let result = generate(paths);
|
let result = generate(paths, latest_revision);
|
||||||
if let Err(e) = &result {
|
if let Err(e) = &result {
|
||||||
error!("{e:?}");
|
error!("{e:?}");
|
||||||
}
|
}
|
||||||
|
|
106
crates/treehouse/src/history.rs
Normal file
106
crates/treehouse/src/history.rs
Normal file
|
@ -0,0 +1,106 @@
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
use indexmap::IndexMap;
|
||||||
|
use log::debug;
|
||||||
|
|
||||||
|
#[derive(Debug, Default, Clone)]
|
||||||
|
pub struct History {
|
||||||
|
// Sorted from newest to oldest.
|
||||||
|
pub commits: IndexMap<git2::Oid, Commit>,
|
||||||
|
pub by_page: HashMap<String, PageHistory>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct Commit {
|
||||||
|
pub summary: String,
|
||||||
|
pub body: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Default)]
|
||||||
|
pub struct PageHistory {
|
||||||
|
// Sorted from newest to oldest, so revision 0 is the current version.
|
||||||
|
// On the website these are sorted differently: 1 is the oldest revision, succeeding numbers are later revisions.
|
||||||
|
pub revisions: Vec<Revision>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct Revision {
|
||||||
|
pub commit_oid: git2::Oid,
|
||||||
|
pub blob_oid: git2::Oid,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl History {
|
||||||
|
pub fn get(git: &git2::Repository) -> anyhow::Result<Self> {
|
||||||
|
debug!("reading git history");
|
||||||
|
|
||||||
|
let mut history = History::default();
|
||||||
|
|
||||||
|
let mut revwalk = git.revwalk()?;
|
||||||
|
revwalk.push_head()?;
|
||||||
|
|
||||||
|
for commit_oid in revwalk {
|
||||||
|
let commit_oid = commit_oid?;
|
||||||
|
let commit = git.find_commit(commit_oid)?;
|
||||||
|
history.commits.insert(
|
||||||
|
commit_oid,
|
||||||
|
Commit {
|
||||||
|
summary: String::from_utf8_lossy(commit.summary_bytes().unwrap_or(&[]))
|
||||||
|
.into_owned(),
|
||||||
|
body: String::from_utf8_lossy(commit.body_bytes().unwrap_or(&[])).into_owned(),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
let tree = commit.tree()?;
|
||||||
|
tree.walk(git2::TreeWalkMode::PreOrder, |parent_path, entry| {
|
||||||
|
if parent_path.is_empty() && entry.name() != Some("content") {
|
||||||
|
// This is content-only history, so skip all directories that don't contain content.
|
||||||
|
git2::TreeWalkResult::Skip
|
||||||
|
} else if entry.kind() == Some(git2::ObjectType::Blob)
|
||||||
|
&& entry.name().is_some_and(|name| name.ends_with(".tree"))
|
||||||
|
{
|
||||||
|
let path = format!(
|
||||||
|
"{parent_path}{}",
|
||||||
|
String::from_utf8_lossy(entry.name_bytes())
|
||||||
|
);
|
||||||
|
let page_history = history.by_page.entry(path).or_default();
|
||||||
|
|
||||||
|
let unchanged = page_history
|
||||||
|
.revisions
|
||||||
|
.last()
|
||||||
|
.is_some_and(|rev| rev.blob_oid == entry.id());
|
||||||
|
if unchanged {
|
||||||
|
// Note again that the history is reversed as we're walking from HEAD
|
||||||
|
// backwards, so we need to find the _earliest_ commit with this revision.
|
||||||
|
// Therefore we update that current revision's commit oid with the
|
||||||
|
// current commit.
|
||||||
|
page_history.revisions.last_mut().unwrap().commit_oid = commit_oid;
|
||||||
|
} else {
|
||||||
|
page_history.revisions.push(Revision {
|
||||||
|
commit_oid,
|
||||||
|
blob_oid: entry.id(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
git2::TreeWalkResult::Ok
|
||||||
|
} else {
|
||||||
|
git2::TreeWalkResult::Ok
|
||||||
|
}
|
||||||
|
})?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(history)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn read_revision(
|
||||||
|
&self,
|
||||||
|
git: &git2::Repository,
|
||||||
|
revision: &Revision,
|
||||||
|
) -> anyhow::Result<Vec<u8>> {
|
||||||
|
Ok(git.find_blob(revision.blob_oid)?.content().to_owned())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Revision {
|
||||||
|
pub fn commit_short(&self) -> String {
|
||||||
|
self.commit_oid.to_string()[0..6].to_owned()
|
||||||
|
}
|
||||||
|
}
|
|
@ -33,7 +33,12 @@ pub struct Renderer<'a> {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> Renderer<'a> {
|
impl<'a> Renderer<'a> {
|
||||||
pub fn render(self, events: &[(Event, Range<usize>)], out: &mut String) {
|
#[must_use]
|
||||||
|
pub fn render(
|
||||||
|
self,
|
||||||
|
events: &[(Event, Range<usize>)],
|
||||||
|
out: &mut String,
|
||||||
|
) -> Vec<Diagnostic<FileId>> {
|
||||||
let mut writer = Writer {
|
let mut writer = Writer {
|
||||||
renderer: self,
|
renderer: self,
|
||||||
raw: Raw::None,
|
raw: Raw::None,
|
||||||
|
@ -42,6 +47,7 @@ impl<'a> Renderer<'a> {
|
||||||
list_tightness: vec![],
|
list_tightness: vec![],
|
||||||
not_first_line: false,
|
not_first_line: false,
|
||||||
ignore_next_event: false,
|
ignore_next_event: false,
|
||||||
|
diagnostics: vec![],
|
||||||
};
|
};
|
||||||
|
|
||||||
for (event, range) in events {
|
for (event, range) in events {
|
||||||
|
@ -49,6 +55,8 @@ impl<'a> Renderer<'a> {
|
||||||
.render_event(event, range.clone(), out)
|
.render_event(event, range.clone(), out)
|
||||||
.expect("formatting event into string should not fail");
|
.expect("formatting event into string should not fail");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
writer.diagnostics
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -85,6 +93,8 @@ struct Writer<'a> {
|
||||||
list_tightness: Vec<bool>,
|
list_tightness: Vec<bool>,
|
||||||
not_first_line: bool,
|
not_first_line: bool,
|
||||||
ignore_next_event: bool,
|
ignore_next_event: bool,
|
||||||
|
|
||||||
|
diagnostics: Vec<Diagnostic<FileId>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> Writer<'a> {
|
impl<'a> Writer<'a> {
|
||||||
|
@ -95,7 +105,7 @@ impl<'a> Writer<'a> {
|
||||||
out: &mut String,
|
out: &mut String,
|
||||||
) -> std::fmt::Result {
|
) -> std::fmt::Result {
|
||||||
if let Event::Start(Container::Footnote { label: _ }, ..) = e {
|
if let Event::Start(Container::Footnote { label: _ }, ..) = e {
|
||||||
self.renderer.treehouse.diagnostics.push(Diagnostic {
|
self.diagnostics.push(Diagnostic {
|
||||||
severity: Severity::Error,
|
severity: Severity::Error,
|
||||||
code: Some("djot".into()),
|
code: Some("djot".into()),
|
||||||
message: "Djot footnotes are not supported".into(),
|
message: "Djot footnotes are not supported".into(),
|
||||||
|
@ -523,7 +533,7 @@ impl<'a> Writer<'a> {
|
||||||
Raw::Other => {}
|
Raw::Other => {}
|
||||||
},
|
},
|
||||||
Event::FootnoteReference(_label) => {
|
Event::FootnoteReference(_label) => {
|
||||||
self.renderer.treehouse.diagnostics.push(Diagnostic {
|
self.diagnostics.push(Diagnostic {
|
||||||
severity: Severity::Error,
|
severity: Severity::Error,
|
||||||
code: Some("djot".into()),
|
code: Some("djot".into()),
|
||||||
message: "Djot footnotes are unsupported".into(),
|
message: "Djot footnotes are unsupported".into(),
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
use std::{borrow::Cow, fmt::Write};
|
use std::{borrow::Cow, fmt::Write};
|
||||||
|
|
||||||
use jotdown::Render;
|
|
||||||
use pulldown_cmark::{BrokenLink, LinkType};
|
use pulldown_cmark::{BrokenLink, LinkType};
|
||||||
use treehouse_format::pull::BranchKind;
|
use treehouse_format::pull::BranchKind;
|
||||||
|
|
||||||
|
@ -183,7 +182,7 @@ pub fn branch_to_html(
|
||||||
let events: Vec<_> = jotdown::Parser::new(&final_markup)
|
let events: Vec<_> = jotdown::Parser::new(&final_markup)
|
||||||
.into_offset_iter()
|
.into_offset_iter()
|
||||||
.collect();
|
.collect();
|
||||||
djot::Renderer {
|
let render_diagnostics = djot::Renderer {
|
||||||
page_id: treehouse
|
page_id: treehouse
|
||||||
.tree_path(file_id)
|
.tree_path(file_id)
|
||||||
.expect(".tree file expected")
|
.expect(".tree file expected")
|
||||||
|
@ -226,7 +225,7 @@ pub fn branch_to_html(
|
||||||
write!(
|
write!(
|
||||||
s,
|
s,
|
||||||
"<a class=\"icon icon-permalink\" href=\"/b?{}\" title=\"permalink\"></a>",
|
"<a class=\"icon icon-permalink\" href=\"/b?{}\" title=\"permalink\"></a>",
|
||||||
EscapeAttribute(&branch.attributes.id)
|
EscapeAttribute(&branch.named_id)
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,13 +7,14 @@ use cli::{
|
||||||
wc::wc_cli,
|
wc::wc_cli,
|
||||||
Command, Paths, ProgramArgs,
|
Command, Paths, ProgramArgs,
|
||||||
};
|
};
|
||||||
use generate::regenerate_or_report_error;
|
use generate::{regenerate_or_report_error, LatestRevision};
|
||||||
use log::{error, info, warn};
|
use log::{error, info, warn};
|
||||||
|
|
||||||
mod cli;
|
mod cli;
|
||||||
mod config;
|
mod config;
|
||||||
mod fun;
|
mod fun;
|
||||||
mod generate;
|
mod generate;
|
||||||
|
mod history;
|
||||||
mod html;
|
mod html;
|
||||||
mod import_map;
|
mod import_map;
|
||||||
mod include_static;
|
mod include_static;
|
||||||
|
@ -40,16 +41,24 @@ async fn fallible_main() -> anyhow::Result<()> {
|
||||||
};
|
};
|
||||||
|
|
||||||
match args.command {
|
match args.command {
|
||||||
Command::Generate(_generate_args) => {
|
Command::Generate(generate_args) => {
|
||||||
info!("regenerating using directories: {paths:#?}");
|
info!("regenerating using directories: {paths:#?}");
|
||||||
regenerate_or_report_error(&paths)?;
|
let latest_revision = match generate_args.commits_only {
|
||||||
|
true => LatestRevision::LatestCommit,
|
||||||
|
false => LatestRevision::WorkingTree,
|
||||||
|
};
|
||||||
|
regenerate_or_report_error(&paths, latest_revision)?;
|
||||||
warn!("`generate` is for debugging only and the files cannot be fully served using a static file server; use `treehouse serve` if you wish to start a treehouse server");
|
warn!("`generate` is for debugging only and the files cannot be fully served using a static file server; use `treehouse serve` if you wish to start a treehouse server");
|
||||||
}
|
}
|
||||||
Command::Serve {
|
Command::Serve {
|
||||||
generate: _,
|
generate: generate_args,
|
||||||
serve: serve_args,
|
serve: serve_args,
|
||||||
} => {
|
} => {
|
||||||
let (config, treehouse) = regenerate_or_report_error(&paths)?;
|
let latest_revision = match generate_args.commits_only {
|
||||||
|
true => LatestRevision::LatestCommit,
|
||||||
|
false => LatestRevision::WorkingTree,
|
||||||
|
};
|
||||||
|
let (config, treehouse) = regenerate_or_report_error(&paths, latest_revision)?;
|
||||||
serve(config, treehouse, &paths, serve_args.port).await?;
|
serve(config, treehouse, &paths, serve_args.port).await?;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -10,10 +10,10 @@ pub struct ErrorsEmitted;
|
||||||
pub fn parse_tree_with_diagnostics(
|
pub fn parse_tree_with_diagnostics(
|
||||||
treehouse: &mut Treehouse,
|
treehouse: &mut Treehouse,
|
||||||
file_id: FileId,
|
file_id: FileId,
|
||||||
) -> Result<Roots, ErrorsEmitted> {
|
) -> Result<Roots, Vec<Diagnostic<FileId>>> {
|
||||||
let input = &treehouse.source(file_id).input();
|
let input = &treehouse.source(file_id).input();
|
||||||
Roots::parse(&mut treehouse_format::pull::Parser { input, position: 0 }).map_err(|error| {
|
Roots::parse(&mut treehouse_format::pull::Parser { input, position: 0 }).map_err(|error| {
|
||||||
treehouse.diagnostics.push(Diagnostic {
|
vec![Diagnostic {
|
||||||
severity: Severity::Error,
|
severity: Severity::Error,
|
||||||
code: Some("tree".into()),
|
code: Some("tree".into()),
|
||||||
message: error.kind.to_string(),
|
message: error.kind.to_string(),
|
||||||
|
@ -24,8 +24,7 @@ pub fn parse_tree_with_diagnostics(
|
||||||
message: String::new(),
|
message: String::new(),
|
||||||
}],
|
}],
|
||||||
notes: vec![],
|
notes: vec![],
|
||||||
});
|
}]
|
||||||
ErrorsEmitted
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -33,17 +32,14 @@ pub fn parse_toml_with_diagnostics(
|
||||||
treehouse: &mut Treehouse,
|
treehouse: &mut Treehouse,
|
||||||
file_id: FileId,
|
file_id: FileId,
|
||||||
range: Range<usize>,
|
range: Range<usize>,
|
||||||
) -> Result<toml_edit::Document, ErrorsEmitted> {
|
) -> Result<toml_edit::Document, Vec<Diagnostic<FileId>>> {
|
||||||
let input = &treehouse.source(file_id).input()[range.clone()];
|
let input = &treehouse.source(file_id).input()[range.clone()];
|
||||||
toml_edit::Document::from_str(input).map_err(|error| {
|
toml_edit::Document::from_str(input).map_err(|error| {
|
||||||
treehouse
|
vec![toml_error_to_diagnostic(TomlError {
|
||||||
.diagnostics
|
message: error.message().to_owned(),
|
||||||
.push(toml_error_to_diagnostic(TomlError {
|
span: error.span(),
|
||||||
message: error.message().to_owned(),
|
file_id,
|
||||||
span: error.span(),
|
input_range: range.clone(),
|
||||||
file_id,
|
})]
|
||||||
input_range: range.clone(),
|
|
||||||
}));
|
|
||||||
ErrorsEmitted
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
use std::{collections::HashMap, ops::Range};
|
use std::{collections::HashMap, ops::Range, path::PathBuf};
|
||||||
|
|
||||||
use anyhow::Context;
|
use anyhow::Context;
|
||||||
use codespan_reporting::{
|
use codespan_reporting::{
|
||||||
|
@ -6,13 +6,27 @@ use codespan_reporting::{
|
||||||
files::SimpleFiles,
|
files::SimpleFiles,
|
||||||
term::termcolor::{ColorChoice, StandardStream},
|
term::termcolor::{ColorChoice, StandardStream},
|
||||||
};
|
};
|
||||||
|
use serde::Serialize;
|
||||||
use ulid::Ulid;
|
use ulid::Ulid;
|
||||||
|
|
||||||
use crate::tree::{SemaBranchId, SemaRoots, SemaTree};
|
use crate::tree::{SemaBranchId, SemaRoots, SemaTree};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize)]
|
||||||
|
pub struct RevisionInfo {
|
||||||
|
pub is_latest: bool,
|
||||||
|
pub number: usize,
|
||||||
|
pub commit: String,
|
||||||
|
pub commit_short: String,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub enum Source {
|
pub enum Source {
|
||||||
Tree { input: String, tree_path: String },
|
Tree {
|
||||||
|
input: String,
|
||||||
|
tree_path: String,
|
||||||
|
target_path: PathBuf,
|
||||||
|
revision_info: RevisionInfo,
|
||||||
|
},
|
||||||
Other(String),
|
Other(String),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -37,7 +51,6 @@ pub type FileId = <Files as codespan_reporting::files::Files<'static>>::FileId;
|
||||||
/// Treehouse compilation context.
|
/// Treehouse compilation context.
|
||||||
pub struct Treehouse {
|
pub struct Treehouse {
|
||||||
pub files: Files,
|
pub files: Files,
|
||||||
pub diagnostics: Vec<Diagnostic<FileId>>,
|
|
||||||
|
|
||||||
pub tree: SemaTree,
|
pub tree: SemaTree,
|
||||||
pub branches_by_named_id: HashMap<String, SemaBranchId>,
|
pub branches_by_named_id: HashMap<String, SemaBranchId>,
|
||||||
|
@ -52,7 +65,6 @@ impl Treehouse {
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
Self {
|
Self {
|
||||||
files: Files::new(),
|
files: Files::new(),
|
||||||
diagnostics: vec![],
|
|
||||||
|
|
||||||
tree: SemaTree::default(),
|
tree: SemaTree::default(),
|
||||||
branches_by_named_id: HashMap::new(),
|
branches_by_named_id: HashMap::new(),
|
||||||
|
@ -91,15 +103,11 @@ impl Treehouse {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn report_diagnostics(&self) -> anyhow::Result<()> {
|
pub fn revision_info(&self, file_id: FileId) -> Option<&RevisionInfo> {
|
||||||
let writer = StandardStream::stderr(ColorChoice::Auto);
|
match self.source(file_id) {
|
||||||
let config = codespan_reporting::term::Config::default();
|
Source::Tree { revision_info, .. } => Some(revision_info),
|
||||||
for diagnostic in &self.diagnostics {
|
Source::Other(_) => None,
|
||||||
codespan_reporting::term::emit(&mut writer.lock(), &config, &self.files, diagnostic)
|
|
||||||
.context("could not emit diagnostic")?;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn next_missingno(&mut self) -> Ulid {
|
pub fn next_missingno(&mut self) -> Ulid {
|
||||||
|
@ -107,12 +115,6 @@ impl Treehouse {
|
||||||
.generate()
|
.generate()
|
||||||
.expect("just how much disk space do you have?")
|
.expect("just how much disk space do you have?")
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn has_errors(&self) -> bool {
|
|
||||||
self.diagnostics
|
|
||||||
.iter()
|
|
||||||
.any(|diagnostic| diagnostic.severity == Severity::Error)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct TomlError {
|
pub struct TomlError {
|
||||||
|
@ -140,3 +142,18 @@ pub fn toml_error_to_diagnostic(error: TomlError) -> Diagnostic<FileId> {
|
||||||
notes: vec![],
|
notes: vec![],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn report_diagnostics(files: &Files, diagnostics: &[Diagnostic<FileId>]) -> anyhow::Result<()> {
|
||||||
|
let writer = StandardStream::stderr(ColorChoice::Auto);
|
||||||
|
let config = codespan_reporting::term::Config::default();
|
||||||
|
for diagnostic in diagnostics {
|
||||||
|
codespan_reporting::term::emit(&mut writer.lock(), &config, files, diagnostic)
|
||||||
|
.context("could not emit diagnostic")?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn has_errors(diagnostics: &[Diagnostic<FileId>]) -> bool {
|
||||||
|
diagnostics.iter().any(|d| d.severity == Severity::Error)
|
||||||
|
}
|
||||||
|
|
|
@ -3,7 +3,7 @@ use std::{
|
||||||
fs::File,
|
fs::File,
|
||||||
io::{self, BufReader},
|
io::{self, BufReader},
|
||||||
path::PathBuf,
|
path::PathBuf,
|
||||||
sync::RwLock,
|
sync::{Mutex, RwLock},
|
||||||
};
|
};
|
||||||
|
|
||||||
use handlebars::{Context, Handlebars, Helper, HelperDef, RenderContext, RenderError, ScopedJson};
|
use handlebars::{Context, Handlebars, Helper, HelperDef, RenderContext, RenderError, ScopedJson};
|
||||||
|
@ -18,6 +18,11 @@ pub struct StaticUrls {
|
||||||
// and required you to clone it over to different threads.
|
// and required you to clone it over to different threads.
|
||||||
// Stuff like this is why I really want to implement my own templating engine...
|
// Stuff like this is why I really want to implement my own templating engine...
|
||||||
hash_cache: RwLock<HashMap<String, String>>,
|
hash_cache: RwLock<HashMap<String, String>>,
|
||||||
|
missing_files: Mutex<Vec<MissingFile>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct MissingFile {
|
||||||
|
pub path: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl StaticUrls {
|
impl StaticUrls {
|
||||||
|
@ -26,6 +31,7 @@ impl StaticUrls {
|
||||||
base_dir,
|
base_dir,
|
||||||
base_url,
|
base_url,
|
||||||
hash_cache: RwLock::new(HashMap::new()),
|
hash_cache: RwLock::new(HashMap::new()),
|
||||||
|
missing_files: Mutex::new(vec![]),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -53,6 +59,10 @@ impl StaticUrls {
|
||||||
}
|
}
|
||||||
Ok(hash)
|
Ok(hash)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn take_missing_files(&self) -> Vec<MissingFile> {
|
||||||
|
std::mem::take(&mut self.missing_files.lock().unwrap())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl HelperDef for StaticUrls {
|
impl HelperDef for StaticUrls {
|
||||||
|
@ -65,9 +75,12 @@ impl HelperDef for StaticUrls {
|
||||||
) -> Result<ScopedJson<'reg, 'rc>, RenderError> {
|
) -> Result<ScopedJson<'reg, 'rc>, RenderError> {
|
||||||
if let Some(param) = helper.param(0).and_then(|v| v.value().as_str()) {
|
if let Some(param) = helper.param(0).and_then(|v| v.value().as_str()) {
|
||||||
return Ok(ScopedJson::Derived(Value::String(
|
return Ok(ScopedJson::Derived(Value::String(
|
||||||
self.get(param).map_err(|error| {
|
self.get(param).unwrap_or_else(|_| {
|
||||||
RenderError::new(format!("cannot get asset url for {param}: {error}"))
|
self.missing_files.lock().unwrap().push(MissingFile {
|
||||||
})?,
|
path: param.to_owned(),
|
||||||
|
});
|
||||||
|
format!("{}/{}", self.base_url, param)
|
||||||
|
}),
|
||||||
)));
|
)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -46,22 +46,24 @@ pub struct SemaRoots {
|
||||||
impl SemaRoots {
|
impl SemaRoots {
|
||||||
pub fn from_roots(
|
pub fn from_roots(
|
||||||
treehouse: &mut Treehouse,
|
treehouse: &mut Treehouse,
|
||||||
|
diagnostics: &mut Vec<Diagnostic<FileId>>,
|
||||||
config: &Config,
|
config: &Config,
|
||||||
file_id: FileId,
|
file_id: FileId,
|
||||||
roots: Roots,
|
roots: Roots,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
Self {
|
Self {
|
||||||
attributes: Self::parse_attributes(treehouse, config, file_id, &roots),
|
attributes: Self::parse_attributes(treehouse, diagnostics, config, file_id, &roots),
|
||||||
branches: roots
|
branches: roots
|
||||||
.branches
|
.branches
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|branch| SemaBranch::from_branch(treehouse, file_id, branch))
|
.map(|branch| SemaBranch::from_branch(treehouse, diagnostics, file_id, branch))
|
||||||
.collect(),
|
.collect(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_attributes(
|
fn parse_attributes(
|
||||||
treehouse: &mut Treehouse,
|
treehouse: &mut Treehouse,
|
||||||
|
diagnostics: &mut Vec<Diagnostic<FileId>>,
|
||||||
config: &Config,
|
config: &Config,
|
||||||
file_id: FileId,
|
file_id: FileId,
|
||||||
roots: &Roots,
|
roots: &Roots,
|
||||||
|
@ -72,14 +74,12 @@ impl SemaRoots {
|
||||||
let mut attributes = if let Some(attributes) = &roots.attributes {
|
let mut attributes = if let Some(attributes) = &roots.attributes {
|
||||||
toml_edit::de::from_str(&source.input()[attributes.data.clone()]).unwrap_or_else(
|
toml_edit::de::from_str(&source.input()[attributes.data.clone()]).unwrap_or_else(
|
||||||
|error| {
|
|error| {
|
||||||
treehouse
|
diagnostics.push(toml_error_to_diagnostic(TomlError {
|
||||||
.diagnostics
|
message: error.message().to_owned(),
|
||||||
.push(toml_error_to_diagnostic(TomlError {
|
span: error.span(),
|
||||||
message: error.message().to_owned(),
|
file_id,
|
||||||
span: error.span(),
|
input_range: attributes.data.clone(),
|
||||||
file_id,
|
}));
|
||||||
input_range: attributes.data.clone(),
|
|
||||||
}));
|
|
||||||
successfully_parsed = false;
|
successfully_parsed = false;
|
||||||
RootAttributes::default()
|
RootAttributes::default()
|
||||||
},
|
},
|
||||||
|
@ -98,7 +98,7 @@ impl SemaRoots {
|
||||||
|
|
||||||
if let Some(thumbnail) = &attributes.thumbnail {
|
if let Some(thumbnail) = &attributes.thumbnail {
|
||||||
if thumbnail.alt.is_none() {
|
if thumbnail.alt.is_none() {
|
||||||
treehouse.diagnostics.push(Diagnostic {
|
diagnostics.push(Diagnostic {
|
||||||
severity: Severity::Warning,
|
severity: Severity::Warning,
|
||||||
code: Some("sema".into()),
|
code: Some("sema".into()),
|
||||||
message: "thumbnail without alt text".into(),
|
message: "thumbnail without alt text".into(),
|
||||||
|
@ -116,7 +116,7 @@ impl SemaRoots {
|
||||||
}
|
}
|
||||||
|
|
||||||
if !config.pics.contains_key(&thumbnail.id) {
|
if !config.pics.contains_key(&thumbnail.id) {
|
||||||
treehouse.diagnostics.push(Diagnostic {
|
diagnostics.push(Diagnostic {
|
||||||
severity: Severity::Warning,
|
severity: Severity::Warning,
|
||||||
code: Some("sema".into()),
|
code: Some("sema".into()),
|
||||||
message: format!(
|
message: format!(
|
||||||
|
@ -149,20 +149,30 @@ pub struct SemaBranch {
|
||||||
pub content: Range<usize>,
|
pub content: Range<usize>,
|
||||||
|
|
||||||
pub html_id: String,
|
pub html_id: String,
|
||||||
|
pub named_id: String,
|
||||||
pub attributes: Attributes,
|
pub attributes: Attributes,
|
||||||
pub children: Vec<SemaBranchId>,
|
pub children: Vec<SemaBranchId>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl SemaBranch {
|
impl SemaBranch {
|
||||||
pub fn from_branch(treehouse: &mut Treehouse, file_id: FileId, branch: Branch) -> SemaBranchId {
|
pub fn from_branch(
|
||||||
let attributes = Self::parse_attributes(treehouse, file_id, &branch);
|
treehouse: &mut Treehouse,
|
||||||
|
diagnostics: &mut Vec<Diagnostic<FileId>>,
|
||||||
|
file_id: FileId,
|
||||||
|
branch: Branch,
|
||||||
|
) -> SemaBranchId {
|
||||||
|
let attributes = Self::parse_attributes(treehouse, diagnostics, file_id, &branch);
|
||||||
|
|
||||||
let named_id = attributes.id.clone();
|
let revision_info = treehouse
|
||||||
|
.revision_info(file_id)
|
||||||
|
.expect(".tree files must have Tree-type sources");
|
||||||
|
let named_id = match revision_info.is_latest {
|
||||||
|
true => attributes.id.to_owned(),
|
||||||
|
false => format!("{}@{}", attributes.id, revision_info.commit_short),
|
||||||
|
};
|
||||||
let html_id = format!(
|
let html_id = format!(
|
||||||
"{}:{}",
|
"{}:{}",
|
||||||
treehouse
|
treehouse.tree_path(file_id).unwrap(),
|
||||||
.tree_path(file_id)
|
|
||||||
.expect("file should have a tree path"),
|
|
||||||
attributes.id
|
attributes.id
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -175,11 +185,12 @@ impl SemaBranch {
|
||||||
kind_span: branch.kind_span,
|
kind_span: branch.kind_span,
|
||||||
content: branch.content,
|
content: branch.content,
|
||||||
html_id,
|
html_id,
|
||||||
|
named_id: named_id.clone(),
|
||||||
attributes,
|
attributes,
|
||||||
children: branch
|
children: branch
|
||||||
.children
|
.children
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|child| Self::from_branch(treehouse, file_id, child))
|
.map(|child| Self::from_branch(treehouse, diagnostics, file_id, child))
|
||||||
.collect(),
|
.collect(),
|
||||||
};
|
};
|
||||||
let new_branch_id = treehouse.tree.add_branch(branch);
|
let new_branch_id = treehouse.tree.add_branch(branch);
|
||||||
|
@ -191,7 +202,7 @@ impl SemaBranch {
|
||||||
let new_branch = treehouse.tree.branch(new_branch_id);
|
let new_branch = treehouse.tree.branch(new_branch_id);
|
||||||
let old_branch = treehouse.tree.branch(old_branch_id);
|
let old_branch = treehouse.tree.branch(old_branch_id);
|
||||||
|
|
||||||
treehouse.diagnostics.push(
|
diagnostics.push(
|
||||||
Diagnostic::warning()
|
Diagnostic::warning()
|
||||||
.with_code("sema")
|
.with_code("sema")
|
||||||
.with_message(format!("two branches share the same id `{}`", named_id))
|
.with_message(format!("two branches share the same id `{}`", named_id))
|
||||||
|
@ -220,7 +231,7 @@ impl SemaBranch {
|
||||||
let new_branch = treehouse.tree.branch(new_branch_id);
|
let new_branch = treehouse.tree.branch(new_branch_id);
|
||||||
let old_branch = treehouse.tree.branch(old_branch_id);
|
let old_branch = treehouse.tree.branch(old_branch_id);
|
||||||
|
|
||||||
treehouse.diagnostics.push(
|
diagnostics.push(
|
||||||
Diagnostic::warning()
|
Diagnostic::warning()
|
||||||
.with_code("sema")
|
.with_code("sema")
|
||||||
.with_message(format!(
|
.with_message(format!(
|
||||||
|
@ -247,21 +258,24 @@ impl SemaBranch {
|
||||||
new_branch_id
|
new_branch_id
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_attributes(treehouse: &mut Treehouse, file_id: FileId, branch: &Branch) -> Attributes {
|
fn parse_attributes(
|
||||||
|
treehouse: &mut Treehouse,
|
||||||
|
diagnostics: &mut Vec<Diagnostic<FileId>>,
|
||||||
|
file_id: FileId,
|
||||||
|
branch: &Branch,
|
||||||
|
) -> Attributes {
|
||||||
let source = treehouse.source(file_id);
|
let source = treehouse.source(file_id);
|
||||||
|
|
||||||
let mut successfully_parsed = true;
|
let mut successfully_parsed = true;
|
||||||
let mut attributes = if let Some(attributes) = &branch.attributes {
|
let mut attributes = if let Some(attributes) = &branch.attributes {
|
||||||
toml_edit::de::from_str(&source.input()[attributes.data.clone()]).unwrap_or_else(
|
toml_edit::de::from_str(&source.input()[attributes.data.clone()]).unwrap_or_else(
|
||||||
|error| {
|
|error| {
|
||||||
treehouse
|
diagnostics.push(toml_error_to_diagnostic(TomlError {
|
||||||
.diagnostics
|
message: error.message().to_owned(),
|
||||||
.push(toml_error_to_diagnostic(TomlError {
|
span: error.span(),
|
||||||
message: error.message().to_owned(),
|
file_id,
|
||||||
span: error.span(),
|
input_range: attributes.data.clone(),
|
||||||
file_id,
|
}));
|
||||||
input_range: attributes.data.clone(),
|
|
||||||
}));
|
|
||||||
successfully_parsed = false;
|
successfully_parsed = false;
|
||||||
Attributes::default()
|
Attributes::default()
|
||||||
},
|
},
|
||||||
|
@ -282,7 +296,7 @@ impl SemaBranch {
|
||||||
// Check that every block has an ID.
|
// Check that every block has an ID.
|
||||||
if attributes.id.is_empty() {
|
if attributes.id.is_empty() {
|
||||||
attributes.id = format!("treehouse-missingno-{}", treehouse.next_missingno());
|
attributes.id = format!("treehouse-missingno-{}", treehouse.next_missingno());
|
||||||
treehouse.diagnostics.push(Diagnostic {
|
diagnostics.push(Diagnostic {
|
||||||
severity: Severity::Warning,
|
severity: Severity::Warning,
|
||||||
code: Some("attr".into()),
|
code: Some("attr".into()),
|
||||||
message: "branch does not have an `id` attribute".into(),
|
message: "branch does not have an `id` attribute".into(),
|
||||||
|
@ -305,7 +319,7 @@ impl SemaBranch {
|
||||||
// Check that link-type blocks are `+`-type to facilitate lazy loading.
|
// Check that link-type blocks are `+`-type to facilitate lazy loading.
|
||||||
if let Content::Link(_) = &attributes.content {
|
if let Content::Link(_) = &attributes.content {
|
||||||
if branch.kind == BranchKind::Expanded {
|
if branch.kind == BranchKind::Expanded {
|
||||||
treehouse.diagnostics.push(Diagnostic {
|
diagnostics.push(Diagnostic {
|
||||||
severity: Severity::Warning,
|
severity: Severity::Warning,
|
||||||
code: Some("attr".into()),
|
code: Some("attr".into()),
|
||||||
message: "`content.link` branch is expanded by default".into(),
|
message: "`content.link` branch is expanded by default".into(),
|
||||||
|
|
7
scripts/mkicon.fish
Executable file
7
scripts/mkicon.fish
Executable file
|
@ -0,0 +1,7 @@
|
||||||
|
#!/usr/bin/env fish
|
||||||
|
|
||||||
|
set filename $argv[1]
|
||||||
|
set icon_name (basename $filename .svg)
|
||||||
|
set icon_base64 (svgcleaner --stdout $filename 2>/dev/null | base64 -w0)
|
||||||
|
|
||||||
|
printf "--icon-%s: url('data:image/svg+xml;base64,%s');" "$icon_name" "$icon_base64"
|
27
static/css/history.css
Normal file
27
static/css/history.css
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
.version-history {
|
||||||
|
|
||||||
|
&>ul.commits {
|
||||||
|
--recursive-mono: 1;
|
||||||
|
|
||||||
|
list-style: none;
|
||||||
|
padding-left: 0;
|
||||||
|
|
||||||
|
&>li {
|
||||||
|
padding-top: 0.2rem;
|
||||||
|
padding-bottom: 0.2rem;
|
||||||
|
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 4em min-content auto;
|
||||||
|
align-items: start;
|
||||||
|
gap: 0.5em;
|
||||||
|
|
||||||
|
&>.revision-number {
|
||||||
|
justify-self: end;
|
||||||
|
}
|
||||||
|
|
||||||
|
details>summary {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
24
static/css/icons.css
Normal file
24
static/css/icons.css
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
:root {
|
||||||
|
--icon-breadcrumb: url('data:image/svg+xml;base64,PHN2ZyBoZWlnaHQ9IjE2IiB3aWR0aD0iMTYiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHBhdGggZD0ibTYgMTIgNC00LTQtNCIgZmlsbD0ibm9uZSIgc3Ryb2tlPSIjNTU0MjNlIiBzdHJva2Utd2lkdGg9IjIiLz48L3N2Zz4=');
|
||||||
|
--icon-expand: url('data:image/svg+xml;base64,PHN2ZyBoZWlnaHQ9IjEyIiB3aWR0aD0iMTIiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHBhdGggY2xpcC1ydWxlPSJldmVub2RkIiBkPSJtNyA1di0zaC0ydjNoLTN2MmgzdjNoMnYtM2gzdi0yeiIgZmlsbD0iIzU1NDIzZSIgZmlsbC1vcGFjaXR5PSIuNSIgZmlsbC1ydWxlPSJldmVub2RkIi8+PC9zdmc+');
|
||||||
|
--icon-leaf: url('data:image/svg+xml;base64,PHN2ZyBoZWlnaHQ9IjEyIiB3aWR0aD0iMTIiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PGNpcmNsZSBjeD0iNiIgY3k9IjYiIGZpbGw9IiM1NTQyM2UiIGZpbGwtb3BhY2l0eT0iLjUiIHI9IjIiLz48L3N2Zz4=');
|
||||||
|
--icon-collapse: url('data:image/svg+xml;base64,PHN2ZyBoZWlnaHQ9IjEyIiB3aWR0aD0iMTIiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHBhdGggZD0ibTMgNmg2IiBzdHJva2U9IiM1NTQyM2UiIHN0cm9rZS1vcGFjaXR5PSIuNSIgc3Ryb2tlLXdpZHRoPSIyIi8+PC9zdmc+');
|
||||||
|
--icon-more: url('data:image/svg+xml;base64,PHN2ZyBoZWlnaHQ9IjE2IiB3aWR0aD0iMTYiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHBhdGggZD0ibTQgNiA0IDQgNC00IiBmaWxsPSJub25lIiBzdHJva2U9IiM1NTQyM2UiIHN0cm9rZS1vcGFjaXR5PSIuNSIgc3Ryb2tlLXdpZHRoPSIyIi8+PC9zdmc+');
|
||||||
|
--icon-permalink: url("data:image/svg+xml;base64,PHN2ZyBoZWlnaHQ9IjE2IiB3aWR0aD0iMTYiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHBhdGggZD0ibTcuNjU2ODYgMiAxLjQxNDIxIDEuNDE0MjJjLjY4MDUxLjY4MDUxIDEuMDY0NTMgMS41NDUyMSAxLjE1MjEzIDIuNDMzNjIuODg4NC4wODc2IDEuNzUzMS40NzE2NSAyLjQzMzcgMS4xNTIxNmwxLjQxNDIgMS40MTQyMmMxLjU2MjEgMS41NjIwOSAxLjU2MjEgNC4wOTQ3OCAwIDUuNjU2ODhzLTQuMDk0NzkgMS41NjIxLTUuNjU2ODggMGwtMS40MTQyMi0xLjQxNDJjLS42ODA1MS0uNjgwNi0xLjA2NDU2LTEuNTQ1My0xLjE1MjE2LTIuNDMzNy0uODg4NDEtLjA4NzYtMS43NTMxMS0uNDcxNjItMi40MzM2Mi0xLjE1MjEzbC0xLjQxNDIyLTEuNDE0MjFjLTEuNTYyMDk0LTEuNTYyMS0xLjU2MjA5NC00LjA5NDc2IDAtNS42NTY4NiAxLjU2MjEtMS41NjIwOTQgNC4wOTQ3Ni0xLjU2MjA5NCA1LjY1Njg2IDB6bS42MTggNy42ODkwN2MtLjE0NDMuMDg1MjItLjI5MjgxLjE2MDYxLS40NDQ1NS4yMjYxNi4wMjA4My40ODI1Ny4yMTU0Ni45NTg5Ny41ODM5MSAxLjMyNzM3bDEuNDE0MjEgMS40MTQzYy43ODEwNy43ODEgMi4wNDczNy43ODEgMi44Mjg0NyAwIC43ODEtLjc4MTEuNzgxLTIuMDQ3NCAwLTIuODI4NDdsLTEuNDE0My0xLjQxNDIxYy0uMzY4NC0uMzY4NDUtLjg0NDgtLjU2MzA4LTEuMzI3MzctLjU4MzkxLS4wNjU1NS4xNTE3My0uMTQwOTMuMzAwMjQtLjIyNjE2LjQ0NDU0bDEuODQ2NDMgMS44NDY0NS0xLjQxNDIgMS40MTQyem0tLjYxOC00Ljg2MDY0Yy4zNjg0NC4zNjg0NS41NjMwOC44NDQ4OC41ODM5MSAxLjMyNzQyLS4xNTE3NC4wNjU1NC0uMzAwMjQuMTQwOTMtLjQ0NDU0LjIyNjE1bC0xLjkxNzU0LTEuOTE3NTMtMS40MTQyMSAxLjQxNDIxIDEuOTE3NTMgMS45MTc1M2MtLjA4NTIzLjE0NDMxLS4xNjA2MS4yOTI4Mi0uMjI2MTYuNDQ0NTYtLjQ4MjU0LS4wMjA4My0uOTU4OTctLjIxNTQ3LTEuMzI3NDItLjU4MzkxbC0xLjQxNDIxLTEuNDE0MjJjLS43ODEwNS0uNzgxMDUtLjc4MTA1LTIuMDQ3MzcgMC0yLjgyODQyczIuMDQ3MzctLjc4MTA1IDIuODI4NDIgMHoiIGZpbGw9IiM1NTQyM2UiIGZpbGwtcnVsZT0iZXZlbm9kZCIvPjwvc3ZnPg==");
|
||||||
|
--icon-go: url("data:image/svg+xml;base64,PHN2ZyBoZWlnaHQ9IjE2IiB3aWR0aD0iMTYiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHBhdGggZD0ibTEwLjU4NTggNy0yLjI5Mjg5LTIuMjkyODkgMS40MTQyMS0xLjQxNDIyIDQuNzA3MDggNC43MDcxMS00LjcwNzA4IDQuNzA3MS0xLjQxNDIxLTEuNDE0MiAyLjI5Mjg5LTIuMjkyOWgtNy41ODU4di0yeiIgZmlsbD0iIzU1NDIzZSIvPjwvc3ZnPg==");
|
||||||
|
--icon-history: url('data:image/svg+xml;base64,PHN2ZyBmaWxsPSJub25lIiBoZWlnaHQ9IjE2IiB2aWV3Qm94PSIwIDAgMTYgMTYiIHdpZHRoPSIxNiIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cGF0aCBjbGlwLXJ1bGU9ImV2ZW5vZGQiIGQ9Im0xMi4yNDI2IDMuNzU3MzZjLTEuMDg1Ny0xLjA4NTc5LTIuNTg1NzUtMS43NTczNi00LjI0MjYtMS43NTczNi0zLjMxMzcxIDAtNiAyLjY4NjI5LTYgNiAwIDMuMzEzNyAyLjY4NjI5IDYgNiA2IDMuMzEzNyAwIDYtMi42ODYzIDYtNmgybC0zLTMtMyAzaDJjMCAyLjIwOTEtMS43OTA5IDQtNCA0LTIuMjA5MTQgMC00LTEuNzkwOS00LTQgMC0yLjIwOTE0IDEuNzkwODYtNCA0LTQgMS4xMDQ1NyAwIDIuMTA0Ni40NDc3MiAyLjgyODQgMS4xNzE1N3ptLTUuMjQyNTkgMS4yNDI2NHYyLjU4NTc5bC0xLjIwNzEgMS4yMDcxIDEuNDE0MjEgMS40MTQyMSAxLjc5Mjg5LTEuNzkyODl2LTMuNDE0MjF6IiBmaWxsPSIjNTU0MjNlIiBmaWxsLXJ1bGU9ImV2ZW5vZGQiLz48L3N2Zz4=');
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
:root {
|
||||||
|
--icon-breadcrumb: url('data:image/svg+xml;base64,PHN2ZyBoZWlnaHQ9IjE2IiB3aWR0aD0iMTYiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHBhdGggZD0ibTYgMTIgNC00LTQtNCIgZmlsbD0ibm9uZSIgc3Ryb2tlPSIjZDdjZGJmIiBzdHJva2Utd2lkdGg9IjIiLz48L3N2Zz4=');
|
||||||
|
--icon-expand: url('data:image/svg+xml;base64,PHN2ZyBoZWlnaHQ9IjEyIiB3aWR0aD0iMTIiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHBhdGggY2xpcC1ydWxlPSJldmVub2RkIiBkPSJtNyA1di0zaC0ydjNoLTN2MmgzdjNoMnYtM2gzdi0yeiIgZmlsbD0iI2Q3Y2RiZiIgZmlsbC1vcGFjaXR5PSIuNSIgZmlsbC1ydWxlPSJldmVub2RkIi8+PC9zdmc+');
|
||||||
|
--icon-leaf: url('data:image/svg+xml;base64,PHN2ZyBoZWlnaHQ9IjEyIiB3aWR0aD0iMTIiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PGNpcmNsZSBjeD0iNiIgY3k9IjYiIGZpbGw9IiNkN2NkYmYiIGZpbGwtb3BhY2l0eT0iLjUiIHI9IjIiLz48L3N2Zz4=');
|
||||||
|
--icon-collapse: url('data:image/svg+xml;base64,PHN2ZyBoZWlnaHQ9IjEyIiB3aWR0aD0iMTIiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHBhdGggZD0ibTMgNmg2IiBzdHJva2U9IiNkN2NkYmYiIHN0cm9rZS1vcGFjaXR5PSIuNSIgc3Ryb2tlLXdpZHRoPSIyIi8+PC9zdmc+');
|
||||||
|
--icon-permalink: url("data:image/svg+xml;base64,PHN2ZyBoZWlnaHQ9IjE2IiB3aWR0aD0iMTYiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHBhdGggZD0ibTcuNjU2ODYgMiAxLjQxNDIxIDEuNDE0MjJjLjY4MDUxLjY4MDUxIDEuMDY0NTMgMS41NDUyMSAxLjE1MjEzIDIuNDMzNjIuODg4NC4wODc2IDEuNzUzMS40NzE2NSAyLjQzMzcgMS4xNTIxNmwxLjQxNDIgMS40MTQyMmMxLjU2MjEgMS41NjIwOSAxLjU2MjEgNC4wOTQ3OCAwIDUuNjU2ODhzLTQuMDk0NzkgMS41NjIxLTUuNjU2ODggMGwtMS40MTQyMi0xLjQxNDJjLS42ODA1MS0uNjgwNi0xLjA2NDU2LTEuNTQ1My0xLjE1MjE2LTIuNDMzNy0uODg4NDEtLjA4NzYtMS43NTMxMS0uNDcxNjItMi40MzM2Mi0xLjE1MjEzbC0xLjQxNDIyLTEuNDE0MjFjLTEuNTYyMDk0LTEuNTYyMS0xLjU2MjA5NC00LjA5NDc2IDAtNS42NTY4NiAxLjU2MjEtMS41NjIwOTQgNC4wOTQ3Ni0xLjU2MjA5NCA1LjY1Njg2IDB6bS42MTggNy42ODkwN2MtLjE0NDMuMDg1MjItLjI5MjgxLjE2MDYxLS40NDQ1NS4yMjYxNi4wMjA4My40ODI1Ny4yMTU0Ni45NTg5Ny41ODM5MSAxLjMyNzM3bDEuNDE0MjEgMS40MTQzYy43ODEwNy43ODEgMi4wNDczNy43ODEgMi44Mjg0NyAwIC43ODEtLjc4MTEuNzgxLTIuMDQ3NCAwLTIuODI4NDdsLTEuNDE0My0xLjQxNDIxYy0uMzY4NC0uMzY4NDUtLjg0NDgtLjU2MzA4LTEuMzI3MzctLjU4MzkxLS4wNjU1NS4xNTE3My0uMTQwOTMuMzAwMjQtLjIyNjE2LjQ0NDU0bDEuODQ2NDMgMS44NDY0NS0xLjQxNDIgMS40MTQyem0tLjYxOC00Ljg2MDY0Yy4zNjg0NC4zNjg0NS41NjMwOC44NDQ4OC41ODM5MSAxLjMyNzQyLS4xNTE3NC4wNjU1NC0uMzAwMjQuMTQwOTMtLjQ0NDU0LjIyNjE1bC0xLjkxNzU0LTEuOTE3NTMtMS40MTQyMSAxLjQxNDIxIDEuOTE3NTMgMS45MTc1M2MtLjA4NTIzLjE0NDMxLS4xNjA2MS4yOTI4Mi0uMjI2MTYuNDQ0NTYtLjQ4MjU0LS4wMjA4My0uOTU4OTctLjIxNTQ3LTEuMzI3NDItLjU4MzkxbC0xLjQxNDIxLTEuNDE0MjJjLS43ODEwNS0uNzgxMDUtLjc4MTA1LTIuMDQ3MzcgMC0yLjgyODQyczIuMDQ3MzctLjc4MTA1IDIuODI4NDIgMHoiIGZpbGw9IiNkN2NkYmYiIGZpbGwtcnVsZT0iZXZlbm9kZCIvPjwvc3ZnPg==");
|
||||||
|
--icon-go: url("data:image/svg+xml;base64,PHN2ZyBoZWlnaHQ9IjE2IiB3aWR0aD0iMTYiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHBhdGggZD0ibTEwLjU4NTggNy0yLjI5Mjg5LTIuMjkyODkgMS40MTQyMS0xLjQxNDIyIDQuNzA3MDggNC43MDcxMS00LjcwNzA4IDQuNzA3MS0xLjQxNDIxLTEuNDE0MiAyLjI5Mjg5LTIuMjkyOWgtNy41ODU4di0yeiIgZmlsbD0iI2Q3Y2RiZiIvPjwvc3ZnPg==");
|
||||||
|
--icon-more: url('data:image/svg+xml;base64,PHN2ZyBoZWlnaHQ9IjE2IiB3aWR0aD0iMTYiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHBhdGggZD0ibTQgNiA0IDQgNC00IiBmaWxsPSJub25lIiBzdHJva2U9IiNkN2NkYmYiIHN0cm9rZS1vcGFjaXR5PSIuNSIgc3Ryb2tlLXdpZHRoPSIyIi8+PC9zdmc+');
|
||||||
|
--icon-history: url('data:image/svg+xml;base64,PHN2ZyBmaWxsPSJub25lIiBoZWlnaHQ9IjE2IiB2aWV3Qm94PSIwIDAgMTYgMTYiIHdpZHRoPSIxNiIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cGF0aCBjbGlwLXJ1bGU9ImV2ZW5vZGQiIGQ9Im0xMi4yNDI2IDMuNzU3MzZjLTEuMDg1Ny0xLjA4NTc5LTIuNTg1NzUtMS43NTczNi00LjI0MjYtMS43NTczNi0zLjMxMzcxIDAtNiAyLjY4NjI5LTYgNiAwIDMuMzEzNyAyLjY4NjI5IDYgNiA2IDMuMzEzNyAwIDYtMi42ODYzIDYtNmgybC0zLTMtMyAzaDJjMCAyLjIwOTEtMS43OTA5IDQtNCA0LTIuMjA5MTQgMC00LTEuNzkwOS00LTQgMC0yLjIwOTE0IDEuNzkwODYtNCA0LTQgMS4xMDQ1NyAwIDIuMTA0Ni40NDc3MiAyLjgyODQgMS4xNzE1N3ptLTUuMjQyNTkgMS4yNDI2NHYyLjU4NTc5bC0xLjIwNzEgMS4yMDcxIDEuNDE0MjEgMS40MTQyMSAxLjc5Mjg5LTEuNzkyODl2LTMuNDE0MjF6IiBmaWxsPSIjZDdjZGJmIiBmaWxsLXJ1bGU9ImV2ZW5vZGQiLz48L3N2Zz4=');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -538,12 +538,70 @@ span.badge {
|
||||||
/* Style the footer */
|
/* Style the footer */
|
||||||
footer {
|
footer {
|
||||||
margin-top: 4rem;
|
margin-top: 4rem;
|
||||||
|
padding-right: 1.75rem;
|
||||||
text-align: right;
|
text-align: right;
|
||||||
opacity: 40%;
|
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
|
||||||
|
&>section:first-child {
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
&>section:last-child {
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
& #version-info {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: end;
|
||||||
|
opacity: 50%;
|
||||||
|
padding-left: 1.75rem;
|
||||||
|
transition: var(--transition-duration) opacity;
|
||||||
|
|
||||||
|
& .icon-history {
|
||||||
|
display: inline-block;
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
padding-right: 1.75rem;
|
||||||
|
background-image: var(--icon-history);
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-position: 50% 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
&>ul {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
list-style: none;
|
||||||
|
padding-left: 0;
|
||||||
|
opacity: 0%;
|
||||||
|
transition: var(--transition-duration) opacity;
|
||||||
|
}
|
||||||
|
|
||||||
|
&>ul>li:not(:first-child)::before {
|
||||||
|
content: '·';
|
||||||
|
text-decoration: none;
|
||||||
|
display: inline-block;
|
||||||
|
padding-left: 0.75em;
|
||||||
|
padding-right: 0.75em;
|
||||||
|
}
|
||||||
|
|
||||||
|
& a {
|
||||||
|
display: inline-block;
|
||||||
|
color: var(--text-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover>ul {
|
||||||
|
opacity: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
& #footer-icon {
|
& #footer-icon {
|
||||||
color: var(--text-color);
|
color: var(--text-color);
|
||||||
padding-right: 1.75rem;
|
padding-right: 1.75rem;
|
||||||
|
opacity: 40%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,27 +1,3 @@
|
||||||
/*** Icons ***/
|
|
||||||
|
|
||||||
:root {
|
|
||||||
--icon-breadcrumb: url('data:image/svg+xml;base64,PHN2ZyBoZWlnaHQ9IjE2IiB3aWR0aD0iMTYiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHBhdGggZD0ibTYgMTIgNC00LTQtNCIgZmlsbD0ibm9uZSIgc3Ryb2tlPSIjNTU0MjNlIiBzdHJva2Utd2lkdGg9IjIiLz48L3N2Zz4=');
|
|
||||||
--icon-expand: url('data:image/svg+xml;base64,PHN2ZyBoZWlnaHQ9IjEyIiB3aWR0aD0iMTIiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHBhdGggY2xpcC1ydWxlPSJldmVub2RkIiBkPSJtNyA1di0zaC0ydjNoLTN2MmgzdjNoMnYtM2gzdi0yeiIgZmlsbD0iIzU1NDIzZSIgZmlsbC1vcGFjaXR5PSIuNSIgZmlsbC1ydWxlPSJldmVub2RkIi8+PC9zdmc+');
|
|
||||||
--icon-leaf: url('data:image/svg+xml;base64,PHN2ZyBoZWlnaHQ9IjEyIiB3aWR0aD0iMTIiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PGNpcmNsZSBjeD0iNiIgY3k9IjYiIGZpbGw9IiM1NTQyM2UiIGZpbGwtb3BhY2l0eT0iLjUiIHI9IjIiLz48L3N2Zz4=');
|
|
||||||
--icon-collapse: url('data:image/svg+xml;base64,PHN2ZyBoZWlnaHQ9IjEyIiB3aWR0aD0iMTIiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHBhdGggZD0ibTMgNmg2IiBzdHJva2U9IiM1NTQyM2UiIHN0cm9rZS1vcGFjaXR5PSIuNSIgc3Ryb2tlLXdpZHRoPSIyIi8+PC9zdmc+');
|
|
||||||
--icon-more: url('data:image/svg+xml;base64,PHN2ZyBoZWlnaHQ9IjE2IiB3aWR0aD0iMTYiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHBhdGggZD0ibTQgNiA0IDQgNC00IiBmaWxsPSJub25lIiBzdHJva2U9IiM1NTQyM2UiIHN0cm9rZS1vcGFjaXR5PSIuNSIgc3Ryb2tlLXdpZHRoPSIyIi8+PC9zdmc+');
|
|
||||||
--icon-permalink: url("data:image/svg+xml;base64,PHN2ZyBoZWlnaHQ9IjE2IiB3aWR0aD0iMTYiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHBhdGggZD0ibTcuNjU2ODYgMiAxLjQxNDIxIDEuNDE0MjJjLjY4MDUxLjY4MDUxIDEuMDY0NTMgMS41NDUyMSAxLjE1MjEzIDIuNDMzNjIuODg4NC4wODc2IDEuNzUzMS40NzE2NSAyLjQzMzcgMS4xNTIxNmwxLjQxNDIgMS40MTQyMmMxLjU2MjEgMS41NjIwOSAxLjU2MjEgNC4wOTQ3OCAwIDUuNjU2ODhzLTQuMDk0NzkgMS41NjIxLTUuNjU2ODggMGwtMS40MTQyMi0xLjQxNDJjLS42ODA1MS0uNjgwNi0xLjA2NDU2LTEuNTQ1My0xLjE1MjE2LTIuNDMzNy0uODg4NDEtLjA4NzYtMS43NTMxMS0uNDcxNjItMi40MzM2Mi0xLjE1MjEzbC0xLjQxNDIyLTEuNDE0MjFjLTEuNTYyMDk0LTEuNTYyMS0xLjU2MjA5NC00LjA5NDc2IDAtNS42NTY4NiAxLjU2MjEtMS41NjIwOTQgNC4wOTQ3Ni0xLjU2MjA5NCA1LjY1Njg2IDB6bS42MTggNy42ODkwN2MtLjE0NDMuMDg1MjItLjI5MjgxLjE2MDYxLS40NDQ1NS4yMjYxNi4wMjA4My40ODI1Ny4yMTU0Ni45NTg5Ny41ODM5MSAxLjMyNzM3bDEuNDE0MjEgMS40MTQzYy43ODEwNy43ODEgMi4wNDczNy43ODEgMi44Mjg0NyAwIC43ODEtLjc4MTEuNzgxLTIuMDQ3NCAwLTIuODI4NDdsLTEuNDE0My0xLjQxNDIxYy0uMzY4NC0uMzY4NDUtLjg0NDgtLjU2MzA4LTEuMzI3MzctLjU4MzkxLS4wNjU1NS4xNTE3My0uMTQwOTMuMzAwMjQtLjIyNjE2LjQ0NDU0bDEuODQ2NDMgMS44NDY0NS0xLjQxNDIgMS40MTQyem0tLjYxOC00Ljg2MDY0Yy4zNjg0NC4zNjg0NS41NjMwOC44NDQ4OC41ODM5MSAxLjMyNzQyLS4xNTE3NC4wNjU1NC0uMzAwMjQuMTQwOTMtLjQ0NDU0LjIyNjE1bC0xLjkxNzU0LTEuOTE3NTMtMS40MTQyMSAxLjQxNDIxIDEuOTE3NTMgMS45MTc1M2MtLjA4NTIzLjE0NDMxLS4xNjA2MS4yOTI4Mi0uMjI2MTYuNDQ0NTYtLjQ4MjU0LS4wMjA4My0uOTU4OTctLjIxNTQ3LTEuMzI3NDItLjU4MzkxbC0xLjQxNDIxLTEuNDE0MjJjLS43ODEwNS0uNzgxMDUtLjc4MTA1LTIuMDQ3MzcgMC0yLjgyODQyczIuMDQ3MzctLjc4MTA1IDIuODI4NDIgMHoiIGZpbGw9IiM1NTQyM2UiIGZpbGwtcnVsZT0iZXZlbm9kZCIvPjwvc3ZnPg==");
|
|
||||||
--icon-go: url("data:image/svg+xml;base64,PHN2ZyBoZWlnaHQ9IjE2IiB3aWR0aD0iMTYiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHBhdGggZD0ibTEwLjU4NTggNy0yLjI5Mjg5LTIuMjkyODkgMS40MTQyMS0xLjQxNDIyIDQuNzA3MDggNC43MDcxMS00LjcwNzA4IDQuNzA3MS0xLjQxNDIxLTEuNDE0MiAyLjI5Mjg5LTIuMjkyOWgtNy41ODU4di0yeiIgZmlsbD0iIzU1NDIzZSIvPjwvc3ZnPg==");
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (prefers-color-scheme: dark) {
|
|
||||||
:root {
|
|
||||||
--icon-breadcrumb: url('data:image/svg+xml;base64,PHN2ZyBoZWlnaHQ9IjE2IiB3aWR0aD0iMTYiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHBhdGggZD0ibTYgMTIgNC00LTQtNCIgZmlsbD0ibm9uZSIgc3Ryb2tlPSIjZDdjZGJmIiBzdHJva2Utd2lkdGg9IjIiLz48L3N2Zz4=');
|
|
||||||
--icon-expand: url('data:image/svg+xml;base64,PHN2ZyBoZWlnaHQ9IjEyIiB3aWR0aD0iMTIiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHBhdGggY2xpcC1ydWxlPSJldmVub2RkIiBkPSJtNyA1di0zaC0ydjNoLTN2MmgzdjNoMnYtM2gzdi0yeiIgZmlsbD0iI2Q3Y2RiZiIgZmlsbC1vcGFjaXR5PSIuNSIgZmlsbC1ydWxlPSJldmVub2RkIi8+PC9zdmc+');
|
|
||||||
--icon-leaf: url('data:image/svg+xml;base64,PHN2ZyBoZWlnaHQ9IjEyIiB3aWR0aD0iMTIiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PGNpcmNsZSBjeD0iNiIgY3k9IjYiIGZpbGw9IiNkN2NkYmYiIGZpbGwtb3BhY2l0eT0iLjUiIHI9IjIiLz48L3N2Zz4=');
|
|
||||||
--icon-collapse: url('data:image/svg+xml;base64,PHN2ZyBoZWlnaHQ9IjEyIiB3aWR0aD0iMTIiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHBhdGggZD0ibTMgNmg2IiBzdHJva2U9IiNkN2NkYmYiIHN0cm9rZS1vcGFjaXR5PSIuNSIgc3Ryb2tlLXdpZHRoPSIyIi8+PC9zdmc+');
|
|
||||||
--icon-permalink: url("data:image/svg+xml;base64,PHN2ZyBoZWlnaHQ9IjE2IiB3aWR0aD0iMTYiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHBhdGggZD0ibTcuNjU2ODYgMiAxLjQxNDIxIDEuNDE0MjJjLjY4MDUxLjY4MDUxIDEuMDY0NTMgMS41NDUyMSAxLjE1MjEzIDIuNDMzNjIuODg4NC4wODc2IDEuNzUzMS40NzE2NSAyLjQzMzcgMS4xNTIxNmwxLjQxNDIgMS40MTQyMmMxLjU2MjEgMS41NjIwOSAxLjU2MjEgNC4wOTQ3OCAwIDUuNjU2ODhzLTQuMDk0NzkgMS41NjIxLTUuNjU2ODggMGwtMS40MTQyMi0xLjQxNDJjLS42ODA1MS0uNjgwNi0xLjA2NDU2LTEuNTQ1My0xLjE1MjE2LTIuNDMzNy0uODg4NDEtLjA4NzYtMS43NTMxMS0uNDcxNjItMi40MzM2Mi0xLjE1MjEzbC0xLjQxNDIyLTEuNDE0MjFjLTEuNTYyMDk0LTEuNTYyMS0xLjU2MjA5NC00LjA5NDc2IDAtNS42NTY4NiAxLjU2MjEtMS41NjIwOTQgNC4wOTQ3Ni0xLjU2MjA5NCA1LjY1Njg2IDB6bS42MTggNy42ODkwN2MtLjE0NDMuMDg1MjItLjI5MjgxLjE2MDYxLS40NDQ1NS4yMjYxNi4wMjA4My40ODI1Ny4yMTU0Ni45NTg5Ny41ODM5MSAxLjMyNzM3bDEuNDE0MjEgMS40MTQzYy43ODEwNy43ODEgMi4wNDczNy43ODEgMi44Mjg0NyAwIC43ODEtLjc4MTEuNzgxLTIuMDQ3NCAwLTIuODI4NDdsLTEuNDE0My0xLjQxNDIxYy0uMzY4NC0uMzY4NDUtLjg0NDgtLjU2MzA4LTEuMzI3MzctLjU4MzkxLS4wNjU1NS4xNTE3My0uMTQwOTMuMzAwMjQtLjIyNjE2LjQ0NDU0bDEuODQ2NDMgMS44NDY0NS0xLjQxNDIgMS40MTQyem0tLjYxOC00Ljg2MDY0Yy4zNjg0NC4zNjg0NS41NjMwOC44NDQ4OC41ODM5MSAxLjMyNzQyLS4xNTE3NC4wNjU1NC0uMzAwMjQuMTQwOTMtLjQ0NDU0LjIyNjE1bC0xLjkxNzU0LTEuOTE3NTMtMS40MTQyMSAxLjQxNDIxIDEuOTE3NTMgMS45MTc1M2MtLjA4NTIzLjE0NDMxLS4xNjA2MS4yOTI4Mi0uMjI2MTYuNDQ0NTYtLjQ4MjU0LS4wMjA4My0uOTU4OTctLjIxNTQ3LTEuMzI3NDItLjU4MzkxbC0xLjQxNDIxLTEuNDE0MjJjLS43ODEwNS0uNzgxMDUtLjc4MTA1LTIuMDQ3MzcgMC0yLjgyODQyczIuMDQ3MzctLjc4MTA1IDIuODI4NDIgMHoiIGZpbGw9IiNkN2NkYmYiIGZpbGwtcnVsZT0iZXZlbm9kZCIvPjwvc3ZnPg==");
|
|
||||||
--icon-go: url("data:image/svg+xml;base64,PHN2ZyBoZWlnaHQ9IjE2IiB3aWR0aD0iMTYiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHBhdGggZD0ibTEwLjU4NTggNy0yLjI5Mjg5LTIuMjkyODkgMS40MTQyMS0xLjQxNDIyIDQuNzA3MDggNC43MDcxMS00LjcwNzA4IDQuNzA3MS0xLjQxNDIxLTEuNDE0MiAyLjI5Mjg5LTIuMjkyOWgtNy41ODU4di0yeiIgZmlsbD0iI2Q3Y2RiZiIvPjwvc3ZnPg==");
|
|
||||||
--icon-more: url('data:image/svg+xml;base64,PHN2ZyBoZWlnaHQ9IjE2IiB3aWR0aD0iMTYiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHBhdGggZD0ibTQgNiA0IDQgNC00IiBmaWxsPSJub25lIiBzdHJva2U9IiNkN2NkYmYiIHN0cm9rZS1vcGFjaXR5PSIuNSIgc3Ryb2tlLXdpZHRoPSIyIi8+PC9zdmc+');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/*** Variables ***/
|
/*** Variables ***/
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
|
|
3
static/svg/dark/history.svg
Normal file
3
static/svg/dark/history.svg
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M12.2426 3.75736C11.1569 2.67157 9.65685 2 8 2C4.68629 2 2 4.68629 2 8C2 11.3137 4.68629 14 8 14C11.3137 14 14 11.3137 14 8H16L13 5L10 8H12C12 10.2091 10.2091 12 8 12C5.79086 12 4 10.2091 4 8C4 5.79086 5.79086 4 8 4C9.10457 4 10.1046 4.44772 10.8284 5.17157L12.2426 3.75736ZM7.00001 5V7.58579L5.79291 8.79289L7.20712 10.2071L9.00001 8.41421V5H7.00001Z" fill="#d7cdbf"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 522 B |
3
static/svg/light/history.svg
Normal file
3
static/svg/light/history.svg
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M12.2426 3.75736C11.1569 2.67157 9.65685 2 8 2C4.68629 2 2 4.68629 2 8C2 11.3137 4.68629 14 8 14C11.3137 14 14 11.3137 14 8H16L13 5L10 8H12C12 10.2091 10.2091 12 8 12C5.79086 12 4 10.2091 4 8C4 5.79086 5.79086 4 8 4C9.10457 4 10.1046 4.44772 10.8284 5.17157L12.2426 3.75736ZM7.00001 5V7.58579L5.79291 8.79289L7.20712 10.2071L9.00001 8.41421V5H7.00001Z" fill="#55423e"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 522 B |
42
template/_history.hbs
Normal file
42
template/_history.hbs
Normal file
|
@ -0,0 +1,42 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
|
||||||
|
<html lang="en-US" prefix="og: https://ogp.me/ns#">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
{{> components/_head.hbs }}
|
||||||
|
|
||||||
|
<link rel="stylesheet" href="{{ asset 'css/history.css' }}">
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
{{#> components/_nav.hbs }}
|
||||||
|
{{/ components/_nav.hbs }}
|
||||||
|
|
||||||
|
{{> components/_noscript.hbs }}
|
||||||
|
|
||||||
|
<main class="version-history">
|
||||||
|
<p>{{ len page.commits }} commits</p>
|
||||||
|
|
||||||
|
<ul class="commits">
|
||||||
|
{{#each page.commits}}
|
||||||
|
<li>
|
||||||
|
<a class="revision-number" href="{{ ../config.site }}/{{ ../page.tree_path }}@{{ revision_number }}">#{{ revision_number }}</a>
|
||||||
|
<a href="{{ ../config.commit_base_url }}/{{ hash }}/content/{{ ../page.tree_path }}.tree"><code>{{ hash_short }}</code></a>
|
||||||
|
|
||||||
|
{{#if body}}
|
||||||
|
<details>
|
||||||
|
<summary class="summary">{{ summary }}</summary>
|
||||||
|
{{ body }}
|
||||||
|
</details>
|
||||||
|
{{else}}
|
||||||
|
<span class="summary">{{ summary }}</span>
|
||||||
|
{{/if}}
|
||||||
|
</li>
|
||||||
|
{{/each}}
|
||||||
|
</ul>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
{{> components/_footer.hbs }}
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
|
@ -1,49 +1,65 @@
|
||||||
<footer>
|
<footer>
|
||||||
<a href="{{ config.site }}/treehouse">
|
<section id="version-info">
|
||||||
<svg id="footer-icon" width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
{{#if page.revision}}
|
||||||
<g id="all">
|
<ul>
|
||||||
<mask id="mask">
|
<li><a class="revision" href="{{ page.revision_url }}" title="permalink to this revision of the page">
|
||||||
<rect width="32" height="32" fill="black" />
|
revision {{ page.revision.number }}
|
||||||
|
{{#if page.revision.is_latest}}(latest){{/if}}
|
||||||
|
</a></li>
|
||||||
|
<li><a class="git" href="{{ page.source_url }}" title="source code">git <code>{{ page.revision.commit_short }}</code></a></li>
|
||||||
|
<li><a class="history" href="{{ page.history_url }}">history</a></li>
|
||||||
|
</ul>
|
||||||
|
<a class="icon-history" href="{{ page.history_url }}" title="version history"></a>
|
||||||
|
{{/if}}
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<a href="{{ config.site }}/treehouse">
|
||||||
|
<svg id="footer-icon" width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<g id="all">
|
||||||
|
<mask id="mask">
|
||||||
|
<rect width="32" height="32" fill="black" />
|
||||||
|
|
||||||
<clipPath id="treehouse">
|
<clipPath id="treehouse">
|
||||||
<path fill-rule="evenodd" clip-rule="evenodd" fill="white" transform="translate(0.5 12)"
|
<path fill-rule="evenodd" clip-rule="evenodd" fill="white" transform="translate(0.5 12)"
|
||||||
d="M2.95266 3.95816C2.74074 1.83892 4.40494 0 6.53475 0C8.68036 0 10.3496 1.86501 10.1127 3.9975L10.0568 4.5L10.352 4.37352C11.7717 3.76506 13.316 4.92718 13.1244 6.45988L13.0568 7C14.1537 6.56127 15.3084 7.4907 15.1142 8.65595L15.0449 9.07153C14.7633 10.7614 13.3012 12 11.588 12H4.05892C2.0541 12 0.358966 10.5159 0.0940032 8.52866L0.0241185 8.00452C-0.210422 6.24546 1.30006 4.74903 3.05685 5L2.95266 3.95816ZM4.55685 7H2.55685V8H4.55685V7ZM4.55685 9H2.55685V10H4.55685V9ZM5.55685 7H7.55685V8H5.55685V7ZM7.55685 9H5.55685V10H7.55685V9ZM5.55685 13H7.55685L8.05685 16L9.55685 13H10.5569L9.49201 16.5495C9.21835 17.4617 9.39407 18.4496 9.96549 19.2115L10.5569 20H7.55685V18H6.55685V20H4.55685L5.35542 18.9352C5.80652 18.3338 6.01534 17.5848 5.94053 16.8367L5.55685 13Z" />
|
d="M2.95266 3.95816C2.74074 1.83892 4.40494 0 6.53475 0C8.68036 0 10.3496 1.86501 10.1127 3.9975L10.0568 4.5L10.352 4.37352C11.7717 3.76506 13.316 4.92718 13.1244 6.45988L13.0568 7C14.1537 6.56127 15.3084 7.4907 15.1142 8.65595L15.0449 9.07153C14.7633 10.7614 13.3012 12 11.588 12H4.05892C2.0541 12 0.358966 10.5159 0.0940032 8.52866L0.0241185 8.00452C-0.210422 6.24546 1.30006 4.74903 3.05685 5L2.95266 3.95816ZM4.55685 7H2.55685V8H4.55685V7ZM4.55685 9H2.55685V10H4.55685V9ZM5.55685 7H7.55685V8H5.55685V7ZM7.55685 9H5.55685V10H7.55685V9ZM5.55685 13H7.55685L8.05685 16L9.55685 13H10.5569L9.49201 16.5495C9.21835 17.4617 9.39407 18.4496 9.96549 19.2115L10.5569 20H7.55685V18H6.55685V20H4.55685L5.35542 18.9352C5.80652 18.3338 6.01534 17.5848 5.94053 16.8367L5.55685 13Z" />
|
||||||
</clipPath>
|
</clipPath>
|
||||||
|
|
||||||
<clipPath id="rectangleClip">
|
<clipPath id="rectangleClip">
|
||||||
<rect id="rectangle1" width="16" height="16" />
|
<rect id="rectangle1" width="16" height="16" />
|
||||||
</clipPath>
|
</clipPath>
|
||||||
|
|
||||||
<clipPath id="rectangleTreehouseClip" clip-path="url(#treehouse)">
|
<clipPath id="rectangleTreehouseClip" clip-path="url(#treehouse)">
|
||||||
<rect id="rectangle2" width="16" height="16" />
|
<rect id="rectangle2" width="16" height="16" />
|
||||||
</clipPath>
|
</clipPath>
|
||||||
|
|
||||||
<g transform="translate(3 0)">
|
<g transform="translate(3 0)">
|
||||||
<rect width="32" height="32" fill="white" clip-path="url(#treehouse)" />
|
<rect width="32" height="32" fill="white" clip-path="url(#treehouse)" />
|
||||||
<rect width="32" height="32" fill="white" clip-path="url(#rectangleClip)" />
|
<rect width="32" height="32" fill="white" clip-path="url(#rectangleClip)" />
|
||||||
<rect width="32" height="32" fill="black" clip-path="url(#rectangleTreehouseClip)" />
|
<rect width="32" height="32" fill="black" clip-path="url(#rectangleTreehouseClip)" />
|
||||||
</g>
|
</g>
|
||||||
</mask>
|
</mask>
|
||||||
|
|
||||||
<rect width="32" height="32" fill="currentColor" mask="url(#mask)" />
|
<rect width="32" height="32" fill="currentColor" mask="url(#mask)" />
|
||||||
</g>
|
</g>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
#rectangle1,
|
#rectangle1,
|
||||||
#rectangle2 {
|
#rectangle2 {
|
||||||
transform: translate(16px, 12px) rotate(15deg) translate(-8px, -8px);
|
transform: translate(16px, 12px) rotate(15deg) translate(-8px, -8px);
|
||||||
rx: 0px;
|
rx: 0px;
|
||||||
transition: all 1s;
|
transition: all 1s;
|
||||||
}
|
}
|
||||||
|
|
||||||
#all:hover #rectangle1,
|
#all:hover #rectangle1,
|
||||||
#all:hover #rectangle2 {
|
#all:hover #rectangle2 {
|
||||||
transform: translate(22px, 24px) rotate(360deg) translate(-2px, -2px);
|
transform: translate(22px, 24px) rotate(360deg) translate(-2px, -2px);
|
||||||
width: 4px;
|
width: 4px;
|
||||||
height: 4px;
|
height: 4px;
|
||||||
rx: 4px;
|
rx: 4px;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
</svg>
|
</svg>
|
||||||
</a>
|
</a>
|
||||||
|
</section>
|
||||||
</footer>
|
</footer>
|
||||||
|
|
|
@ -7,6 +7,7 @@
|
||||||
<link rel="preload" href="{{ asset 'font/Recursive_VF_1.085.woff2' }}" as="font" type="font/woff2"
|
<link rel="preload" href="{{ asset 'font/Recursive_VF_1.085.woff2' }}" as="font" type="font/woff2"
|
||||||
crossorigin="anonymous">
|
crossorigin="anonymous">
|
||||||
<link rel="stylesheet" href="{{ asset 'css/main.css' }}">
|
<link rel="stylesheet" href="{{ asset 'css/main.css' }}">
|
||||||
|
<link rel="stylesheet" href="{{ asset 'css/icons.css' }}">
|
||||||
<link rel="stylesheet" href="{{ asset 'css/tree.css' }}">
|
<link rel="stylesheet" href="{{ asset 'css/tree.css' }}">
|
||||||
|
|
||||||
{{!-- Import maps currently don't support the src="" attribute. Unless we come up with something
|
{{!-- Import maps currently don't support the src="" attribute. Unless we come up with something
|
||||||
|
|
|
@ -8,6 +8,10 @@ site = ""
|
||||||
# TODO djot: Remove once transition is over.
|
# TODO djot: Remove once transition is over.
|
||||||
markup = "Djot"
|
markup = "Djot"
|
||||||
|
|
||||||
|
# This is used to generate a link in the footer that links to the page's source commit.
|
||||||
|
# The final URL is `{commit_base_url}/{commit}/content/{tree_path}.tree`.
|
||||||
|
commit_base_url = "https://src.liquidev.net/liquidex/treehouse/src/commit"
|
||||||
|
|
||||||
[user]
|
[user]
|
||||||
title = "liquidex's treehouse"
|
title = "liquidex's treehouse"
|
||||||
author = "liquidex"
|
author = "liquidex"
|
||||||
|
|
Loading…
Reference in a new issue