summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Cargo.lock4
-rw-r--r--Cargo.toml2
-rw-r--r--src/collect_files.rs188
-rw-r--r--src/generate_html.rs8
-rw-r--r--src/main.rs207
5 files changed, 229 insertions, 180 deletions
diff --git a/Cargo.lock b/Cargo.lock
index 8241476..c13ad81 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -18,8 +18,8 @@ dependencies = [
[[package]]
name = "vagabond"
-version = "1.0.1"
-source = "git+git://benbridle.com/vagabond?tag=v1.0.1#08f3153fea62ea81a42438347eeee058f5bec199"
+version = "1.0.2"
+source = "git+git://benbridle.com/vagabond?tag=v1.0.2#740df75e2bf62d067ca76bd661f9c46b4a228678"
[[package]]
name = "xflags"
diff --git a/Cargo.toml b/Cargo.toml
index 139574e..d83f0d4 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -4,7 +4,7 @@ version = "1.2.0"
edition = "2021"
[dependencies]
-vagabond = { git = "git://benbridle.com/vagabond", tag = "v1.0.1" }
+vagabond = { git = "git://benbridle.com/vagabond", tag = "v1.0.2" }
markdown = { git = "git://benbridle.com/markdown", tag = "v2.1.0" }
xflags = "0.4.0-pre.1"
diff --git a/src/collect_files.rs b/src/collect_files.rs
new file mode 100644
index 0000000..88d065f
--- /dev/null
+++ b/src/collect_files.rs
@@ -0,0 +1,188 @@
+use crate::*;
+
+use vagabond::*;
+
+
+pub struct Website {
+ pub name: String,
+ pub pages: Vec<Page>,
+ pub static_files: Vec<StaticItem>,
+ pub static_dirs: Vec<StaticItem>,
+}
+
+pub struct Page {
+ pub name: String, // Display name
+ pub parent_url: String, // URL base for relative links
+ pub file_url: String, // Safe file name, no extension
+ pub full_url: String, // Safe full URL, no extension
+ pub source_path: PathBuf, // Absolute path to source file
+ pub document: MarkdownDocument, // File content parsed as markdown
+ pub headings: Vec<String>, // Safe name of each document heading
+}
+
+pub struct StaticItem {
+ pub full_url: String, // Safe full URL, with extension
+ pub source_path: PathBuf, // Absolute path to source file
+}
+
+
+
+impl Page {
+ pub fn back_string(&self) -> String {
+ let mut back = String::new();
+ for c in self.full_url.chars() {
+ if c == '/' {
+ back.push_str("../");
+ }
+ }
+ return back;
+ }
+}
+
+
+
+impl Website {
+ pub fn from_path(path: &Path) -> Self {
+ let mut new = Self {
+ pages: Vec::new(),
+ static_files: Vec::new(),
+ static_dirs: Vec::new(),
+ name: match Entry::from_path(path) {
+ Ok(entry) => entry.name,
+ Err(err) => error!("Couldn't open {:?}: {:?}", &path, err),
+ },
+ };
+ new.collect_entry(path, path);
+ return new;
+ }
+
+ fn collect_entry(&mut self, path: &Path, prefix: &Path) {
+ let entry = Entry::from_path(path).unwrap();
+ // Ignore dotted entries.
+ if entry.name.starts_with('.') {
+ return;
+ }
+ // Get name and extension.
+ let (mut name, extension) = entry.split_name();
+ if let Some((prefix, suffix)) = name.split_once(' ') {
+ if prefix.chars().all(|c| "0123456789-".contains(c)) {
+ name = suffix.to_string();
+ }
+ }
+ let file_url = make_url_safe(&name);
+ // Generate parent URL, used only for files.
+ let source_path = entry.original_path.clone();
+ let relative_path = source_path.strip_prefix(prefix).unwrap_or_else(
+ |_| error!("Path doesn't start with {:?}: {:?}", prefix, source_path));
+ let mut parent_url = String::new();
+ let mut components: Vec<_> = relative_path.components().collect();
+ components.pop(); // Remove file segment.
+ for c in &components {
+ let segment = &make_url_safe(&c.as_os_str().to_string_lossy());
+ parent_url.push_str(segment); parent_url.push('/')
+ };
+
+ // Process each entry.
+ if entry.is_directory() {
+ if let Some(stripped) = entry.name.strip_prefix("!") {
+ let full_url = make_url_safe(stripped);
+ self.static_dirs.push(StaticItem { full_url, source_path });
+ } else {
+ for child in list_directory(entry.original_path).unwrap() {
+ self.collect_entry(&child.original_path, prefix);
+ }
+ }
+ } else {
+ match extension.as_str() {
+ "md" => {
+ let markdown = std::fs::read_to_string(&source_path).unwrap();
+ let document = MarkdownDocument::from_str(&markdown);
+ let headings = document.blocks.iter()
+ .filter_map(|block| if let Block::Heading { line, .. } = block {
+ Some(make_url_safe(&line.to_string()))
+ } else {
+ None
+ }).collect();
+ // Change name and path if this is an index file.
+ let mut name = name;
+ let mut file_url = file_url;
+ let mut full_url = format!("{parent_url}{file_url}");
+ if file_url == "+index" {
+ if components.is_empty() {
+ // This is the index file for the whole site.
+ name = String::from("Home");
+ file_url = String::from("index");
+ full_url = String::from("index");
+ } else {
+ // This is an index file for a directory.
+ name = components[components.len()-1]
+ .as_os_str().to_string_lossy().to_string();
+ file_url = make_url_safe(&name);
+ full_url = parent_url.strip_suffix('/').unwrap_or(&parent_url).to_string();
+ }
+ }
+ self.pages.push(
+ Page { name, parent_url, file_url, full_url, source_path, document, headings });
+ },
+ _ => {
+ let full_url = format!("{parent_url}{file_url}.{extension}");
+ self.static_files.push(StaticItem { full_url, source_path });
+ },
+ }
+ }
+ }
+
+ // Ext is extension without a dot.
+ // Checks if a relative link to an internal page name can be reached from
+ // the current page, and returns a resolved absolute link to the page with extension.
+ pub fn has_page(&self, from: &Page, path: &str, ext: &str) -> Option<String> {
+ // Remove heading fragment and file extension.
+ let (path, heading) = match path.rsplit_once('#') {
+ Some((path, heading)) => match heading.is_empty() {
+ true => (path, None),
+ false => (path, Some(heading)),
+ }
+ None => (path, None),
+ };
+ let path = path.strip_suffix(&format!(".{ext}")).unwrap_or(path);
+
+ // Attach parent of current page to given path.
+ let directory = match from.parent_url.rsplit_once('/') {
+ Some((parent, _)) => parent,
+ None => &from.parent_url,
+ };
+ let full_path = match path.starts_with("/") {
+ true => path.to_string(),
+ false => format!("{directory}/{path}"),
+ };
+
+ // Remove relative portions of path.
+ let segments: Vec<&str> = full_path.split("/")
+ .filter(|seg| !seg.is_empty() && *seg != ".")
+ .collect();
+ let mut reduced_segments: Vec<&str> = segments.windows(2)
+ .filter(|w| w[1] != "..")
+ .map(|w| w[1])
+ .collect();
+ // The first segment is always skipped by the previous step.
+ if !segments.is_empty() && segments.get(1) != Some(&"..") {
+ if segments[0] != ".." {
+ reduced_segments.insert(0, segments[0]);
+ }
+ }
+ let path = reduced_segments.join("/");
+
+ for page in &self.pages {
+ if page.full_url == path {
+ if let Some(heading) = heading {
+ if !page.headings.contains(&make_url_safe(heading)) {
+ warn!("Page {:?} contains link to nonexistent heading {heading:?} on page {path:?}", from.name);
+ }
+ }
+ return Some(format!("{path}.{ext}"));
+ }
+ }
+ return None;
+ }
+}
+
diff --git a/src/generate_html.rs b/src/generate_html.rs
index f14d5f9..05456d0 100644
--- a/src/generate_html.rs
+++ b/src/generate_html.rs
@@ -3,7 +3,7 @@ use crate::*;
use markdown::*;
-pub fn generate_html(document: &MarkdownDocument, page: &SourceFile, website: &Website) -> String {
+pub fn generate_html(document: &MarkdownDocument, page: &Page, website: &Website) -> String {
format!("\
<!DOCTYPE html>
<head>
@@ -27,7 +27,7 @@ pub fn generate_html(document: &MarkdownDocument, page: &SourceFile, website: &W
-pub fn get_html_head(document: &MarkdownDocument, page: &SourceFile) -> String {
+pub fn get_html_head(document: &MarkdownDocument, page: &Page) -> String {
if let Some(Block::Fragment { language, content }) = document.blocks.first() {
if language == "embed-html-head" {
return content.to_string();
@@ -43,7 +43,7 @@ pub fn get_html_head(document: &MarkdownDocument, page: &SourceFile) -> String {
-pub fn document_to_html(document: &MarkdownDocument, page: &SourceFile, website: &Website) -> String {
+pub fn document_to_html(document: &MarkdownDocument, page: &Page, website: &Website) -> String {
let mut html = String::new();
macro_rules! line_to_html {
@@ -158,7 +158,7 @@ pub fn document_to_html(document: &MarkdownDocument, page: &SourceFile, website:
-fn line_to_html(line: &Line, page: &SourceFile, website: &Website) -> String {
+fn line_to_html(line: &Line, page: &Page, website: &Website) -> String {
let mut html = String::new();
for line_element in &line.tokens {
match line_element {
diff --git a/src/main.rs b/src/main.rs
index 0a62ec2..2950ee9 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -1,5 +1,7 @@
#![feature(path_add_extension)]
+mod collect_files;
+pub use collect_files::*;
mod generate_html;
pub use generate_html::*;
@@ -50,101 +52,32 @@ fn main() {
let source_directory = args.source.unwrap().canonicalize().unwrap();
let destination_directory = args.destination.unwrap();
- let mut website = Website {
- source_files: Vec::new(),
- static_files: Vec::new(),
- name: match Entry::from_path(&source_directory) {
- Ok(entry) => entry.name,
- Err(err) => error!("Couldn't open {:?}: {:?}", &source_directory, err),
- },
- error: false,
- };
-
- // Collect all website files.
- match traverse_directory(&source_directory) {
- Ok(entries) => for entry in entries {
- // Ignore dot files.
- if entry.name.starts_with(".") {
- continue;
- }
- // Generate name, stripping any leading digit sequence.
- let (mut name, extension) = entry.split_name();
- if let Some((prefix, suffix)) = name.split_once(' ') {
- if prefix.chars().all(|c| "0123456789-".contains(c)) {
- name = suffix.to_string();
- }
- }
- // Generate full URL with stripped name, no extension.
- let source_path = entry.original_path;
- let relative_path = source_path.strip_prefix(&source_directory).unwrap_or_else(
- // Probably unreachable.
- |_| error!("Path doesn't start with {:?}: {:?}", source_directory, source_path));
- let mut full_url = String::new();
- let mut components: Vec<_> = relative_path.components().collect();
- components.pop(); // Remove file segment, use the stripped name instead.
- for c in components {
- full_url.push_str(&make_url_safe(&c.as_os_str().to_string_lossy()));
- full_url.push('/')
- };
- let parent_url = full_url.clone();
- full_url.push_str(&make_url_safe(&name));
-
-
- if extension == "md" {
- // Rename and relocate index files.
- let mut file_url = make_url_safe(&name);
- if file_url == "+index" {
- let components: Vec<_> = relative_path.components().collect();
- if components.len() == 1 {
- name = String::from("Home");
- file_url = String::from("index");
- full_url = String::from("index");
- } else {
- let parent = components[components.len()-2];
- let parent_string = parent.as_os_str().to_string_lossy().to_string();
- name = parent_string;
- file_url = make_url_safe(&name);
- full_url.clear();
- for c in &components[..components.len()-2] {
- full_url.push_str(&make_url_safe(&c.as_os_str().to_string_lossy()));
- full_url.push('/')
- };
- full_url.push_str(&file_url);
- }
- }
- // Load and parse the markdown.
- let markdown = std::fs::read_to_string(&source_path).unwrap();
- let document = MarkdownDocument::from_str(&markdown);
- let headings = document.blocks.iter().filter_map(|block| match block {
- Block::Heading { line, .. } => Some(make_url_safe(&line.to_string())),
- _ => None,
- }).collect();
- website.source_files.push(
- SourceFile { name, parent_url, file_url, full_url, source_path, document, headings });
- } else {
- full_url.push('.'); full_url.push_str(&extension);
- website.static_files.push(StaticFile { full_url, source_path });
- }
- }
- Err(err) => error!("Could not read from source directory: {:?}", err),
- }
+ let website = Website::from_path(&source_directory);
let mut destination = destination_directory.clone();
destination.push(make_url_safe(&website.name));
- for source_file in &website.source_files {
+ if args.delete && Entry::from_path(&destination).is_ok() {
+ verbose!("Deleting existing destination directory {destination:?}");
+ remove(&destination).unwrap_or_else(|_|
+ error!("Failed to delete existing destination directory {destination:?}"));
+ }
+
+ for page in &website.pages {
let mut destination = destination.clone();
- destination.push(&source_file.full_url);
+ destination.push(&page.full_url);
// Convert document to different formats.
if args.html {
- let html = generate_html(&source_file.document, source_file, &website);
+ let html = generate_html(&page.document, page, &website);
write_file(&html, &destination, "html");
}
// Copy original markdown file.
destination.add_extension("md");
verbose!("Copying original markdown file to {destination:?}");
- copy(&source_file.source_path, &destination).unwrap();
+ copy(&page.source_path, &destination).unwrap_or_else(|_|
+ error!("Failed to copy original markdown file {:?} to {:?}",
+ page.source_path, destination));
}
for static_file in &website.static_files {
@@ -152,7 +85,19 @@ fn main() {
destination.push(&static_file.full_url);
verbose!("Copying static file to {destination:?}");
make_parent_directory(&destination).unwrap();
- copy(&static_file.source_path, &destination).unwrap();
+ copy(&static_file.source_path, &destination).unwrap_or_else(|_|
+ error!("Failed to copy static file {:?} to {:?}",
+ static_file.source_path, destination));
+ }
+
+ for static_dir in &website.static_dirs {
+ let mut destination = destination.clone();
+ destination.push(&static_dir.full_url);
+ verbose!("Copying static directory to {destination:?}");
+ make_parent_directory(&destination).unwrap();
+ copy(&static_dir.source_path, &destination).unwrap_or_else(|_|
+ error!("Failed to copy static directory {:?} to {:?}",
+ static_dir.source_path, destination));
}
}
@@ -162,8 +107,10 @@ pub fn write_file(text: &str, destination: &PathBuf, ext: &str) {
let mut destination = destination.clone();
destination.add_extension(ext);
verbose!("Generating {destination:?}");
- make_parent_directory(&destination).unwrap();
- write_to_file(destination, text).unwrap();
+ make_parent_directory(&destination).unwrap_or_else(|_|
+ error!("Failed to create parent directories for {destination:?}"));
+ write_to_file(&destination, text).unwrap_or_else(|_|
+ error!("Failed to write generated {ext} file to {destination:?}"));
}
pub fn make_url_safe(text: &str) -> String {
@@ -175,94 +122,6 @@ pub fn make_url_safe(text: &str) -> String {
}
-pub struct Website {
- pub name: String,
- pub source_files: Vec<SourceFile>,
- pub static_files: Vec<StaticFile>,
- pub error: bool,
-}
-
-impl Website {
- // Ext is extension without a dot.
- // Checks if a relative link to an internal page name can be reached from
- // the current page, and returns a resolved absolute link to the page with extension.
- pub fn has_page(&self, from: &SourceFile, path: &str, ext: &str) -> Option<String> {
- // Remove heading fragment and file extension.
- let (path, heading) = match path.rsplit_once('#') {
- Some((path, heading)) => match heading.is_empty() {
- true => (path, None),
- false => (path, Some(heading)),
- }
- None => (path, None),
- };
- let path = path.strip_suffix(&format!(".{ext}")).unwrap_or(path);
-
- // Attach parent of current page to given path.
- let directory = match from.parent_url.rsplit_once('/') {
- Some((parent, _)) => parent,
- None => &from.parent_url,
- };
- let full_path = match path.starts_with("/") {
- true => path.to_string(),
- false => format!("{directory}/{path}"),
- };
-
- // Remove relative portions of path.
- let segments: Vec<&str> = full_path.split("/")
- .filter(|seg| !seg.is_empty() && *seg != ".")
- .collect();
- let mut reduced_segments: Vec<&str> = segments.windows(2)
- .filter(|w| w[1] != "..")
- .map(|w| w[1])
- .collect();
- // The first segment is always skipped by the previous step.
- if !segments.is_empty() && segments.get(1) != Some(&"..") {
- if segments[0] != ".." {
- reduced_segments.insert(0, segments[0]);
- }
- }
- let path = reduced_segments.join("/");
-
- for source_file in &self.source_files {
- if source_file.full_url == path {
- if let Some(heading) = heading {
- if !source_file.headings.contains(&make_url_safe(heading)) {
- warn!("Page {:?} contains link to nonexistent heading {heading:?} on page {path:?}", from.name);
- }
- }
- return Some(format!("{path}.{ext}"));
- }
- }
- return None;
- }
-}
-
-pub struct SourceFile {
- pub name: String,
- pub parent_url: String, // URL base of child pages
- pub file_url: String, // URL file segment, no extension
- pub full_url: String, // URL full path, no extension
- pub source_path: PathBuf,
- pub document: MarkdownDocument,
- pub headings: Vec<String>,
-}
-
-impl SourceFile {
- pub fn back_string(&self) -> String {
- let mut back = String::new();
- for c in self.full_url.chars() {
- if c == '/' {
- back.push_str("../");
- }
- }
- return back;
- }
-}
-
-pub struct StaticFile {
- pub full_url: String, // URL full path, with extension
- pub source_path: PathBuf,
-}
xflags::xflags! {
/// Generate a website from a structured directory of markdown files.
@@ -271,6 +130,8 @@ xflags::xflags! {
optional source: PathBuf
/// Path to output directory
optional destination: PathBuf
+ /// Delete the destination directory first if it exists
+ optional --delete
/// Generate HTML output
optional --html
/// Generate Gemtext output