some hard coded working output.
Some checks failed
/ clippy (push) Failing after 1m53s
/ test (push) Failing after 1m57s

Depends on my fork of orgize in order to not get hung up on blocks
having empty lines
This commit is contained in:
Chris Cochrun 2026-03-31 14:42:18 -05:00
parent 4e894bbbca
commit 31eb7c9b2e
6 changed files with 126 additions and 31 deletions

View file

@ -2,10 +2,12 @@ pub mod org;
pub mod publish;
pub mod watcher;
use std::time::Duration;
use std::{path::PathBuf, sync::mpsc};
use clap::Parser;
use notify::{Event, RecursiveMode, Result, Watcher};
use notify_debouncer_full::notify::{Event, RecursiveMode, Result, Watcher};
use notify_debouncer_full::{DebouncedEvent, new_debouncer};
use tracing::{error, info, level_filters::LevelFilter};
use tracing_subscriber::EnvFilter;
@ -41,21 +43,28 @@ fn main() -> Result<()> {
.init();
if let Some(path) = args.path {
let (tx, rx) = mpsc::channel::<Result<Event>>();
let (tx, rx) = mpsc::channel();
// Automatically select the best implementation for your platform.
let mut watcher = notify::recommended_watcher(tx)?;
let mut watcher = new_debouncer(Duration::from_millis(500), None, tx).unwrap();
watcher.watch(&path, RecursiveMode::Recursive)?;
for res in rx {
match res {
Ok(event) => match watcher::watch(event, &path) {
Ok(()) => (),
Err(e) => error!("internal error: {:?}", e),
},
loop {
match rx.recv() {
Ok(event) => event.map_or_else(
|e| error!("Error: {:?}", e),
|events| {
for event in events {
match watcher::watch(&event, &path) {
Ok(()) => (),
Err(e) => error!("internal error: {:?}", e),
}
}
},
),
Err(e) => error!("watch error: {:?}", e),
};
}
}
} else {
info!("Nothing to do")
info!("Nothing to do");
}
Ok(())

View file

@ -2,6 +2,8 @@ use slugify::slugify;
use std::cmp::min;
use std::fmt;
use std::fmt::Write as _;
use std::sync::Arc;
use tracing::{debug, info};
use orgize::export::{Container, Event, TraversalContext, Traverser};
use orgize::{SyntaxElement, SyntaxKind, SyntaxNode};
@ -52,6 +54,8 @@ pub struct OrgHtmlExporter {
in_descriptive_list: Vec<bool>,
table_row: TableRow,
files: Arc<Vec<String>>,
}
#[derive(Default, PartialEq, Eq)]
@ -64,6 +68,12 @@ enum TableRow {
}
impl OrgHtmlExporter {
pub fn new(files: Arc<Vec<String>>) -> Self {
let mut new = Self::default();
new.files = files;
new
}
pub fn push_str(&mut self, s: impl AsRef<str>) {
self.output += s.as_ref();
}
@ -87,6 +97,26 @@ impl OrgHtmlExporter {
let mut ctx = TraversalContext::default();
self.element(SyntaxElement::Node(node.clone()), &mut ctx);
}
fn convert_denote_links(&self, link: impl AsRef<str>) -> String {
let link = link.as_ref().to_string();
if !link.starts_with("denote") {
return link;
};
link.as_str()
.to_string()
.strip_prefix("denote:")
.map_or(link.clone(), |key| {
self.files
.iter()
.find(|file| file.contains(key))
.map_or(link, |s| {
let mut final_link = "/".to_string();
final_link.push_str(s);
final_link
})
})
}
}
impl Traverser for OrgHtmlExporter {
@ -281,7 +311,7 @@ impl Traverser for OrgHtmlExporter {
Event::Leave(Container::OrgTableCell(_)) => self.output += "</td>",
Event::Enter(Container::Link(link)) => {
let path = link.path();
let path = self.convert_denote_links(link.path());
let path = path.trim_start_matches("file:");
let path = if let Some(path) = path.strip_suffix("org") {
let mut path = path.to_string();
@ -290,6 +320,7 @@ impl Traverser for OrgHtmlExporter {
} else {
path.to_string()
};
let path = path.trim_start_matches("//");
if link.is_image() {
let _ = write!(&mut self.output, r#"<img src="{}">"#, HtmlEscape(&path));

View file

@ -4,6 +4,7 @@ use std::collections::HashMap;
use std::fs;
use std::io::Error as IOError;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use tracing::{debug, error};
use walkdir::{DirEntry, Error as WDError, WalkDir};
@ -57,7 +58,22 @@ pub fn build_site(
}
}
if base_path.is_dir() {
WalkDir::new(&base_path)
let files = WalkDir::new(&base_path)
.follow_links(true)
.into_iter()
.filter_map(|e| {
e.ok().map(|e| {
let path = e
.path()
.strip_prefix(&base_path)
.unwrap_or_else(|_| e.path());
path.to_string_lossy().to_string()
})
})
.collect::<Vec<String>>();
let files = Arc::new(files);
let walk_dir = WalkDir::new(&base_path);
walk_dir
.follow_links(true)
.into_iter()
.collect::<Vec<Result<walkdir::DirEntry, WDError>>>()
@ -67,8 +83,8 @@ pub fn build_site(
let path = entry.path();
let file_name = path.file_name().unwrap_or_default().to_string_lossy();
if path.is_file() && !path.ends_with("~") && !file_name.starts_with(".#") {
let mut handler = OrgHtmlExporter::default();
debug!(?path);
let mut handler = OrgHtmlExporter::new(Arc::clone(&files));
// debug!(?path);
if path
.extension()
.iter()
@ -76,7 +92,7 @@ pub fn build_site(
.collect::<String>()
== "org"
{
debug!("It ends with org");
// debug!("It ends with org");
let org_string = fs::read_to_string(path).unwrap_or_default();
let org = orgize::Org::parse(&org_string);
let mut keywords = org.keywords();
@ -154,7 +170,17 @@ fn build_html(inner_html: String, keywords: HashMap<String, String>) -> String {
display: none;
}
</style>"#;
format!("{head}<body>\n{inner_html}\n</body></html>")
let preamble = r#"<nav>
<ul>
<li><strong>Chris Cochrun</strong></li>
</ul>
<ul>
<li><a href="/notes">Notes</a></li>
<li><a href="/teaching">Teaching</a></li>
</ul>
</nav>
"#;
format!("{head}<body>\n{preamble}{inner_html}\n</body></html>")
}
fn extract_keywords<I>(keywords: &mut I) -> HashMap<String, String>
@ -168,3 +194,7 @@ where
}
map
}
fn conver_denote_links(inner_html: String) -> String {
todo!()
}

View file

@ -1,11 +1,11 @@
use std::path::{Path, PathBuf};
use notify::{Event, EventKind};
use notify_debouncer_full::{DebouncedEvent, notify::EventKind};
use tracing::{debug, info};
use crate::publish::publish::{Error, build_site};
pub fn watch(event: Event, path: impl AsRef<Path>) -> Result<(), Error> {
pub fn watch(event: &DebouncedEvent, path: impl AsRef<Path>) -> Result<(), Error> {
if !event
.paths
.clone()
@ -16,7 +16,10 @@ pub fn watch(event: Event, path: impl AsRef<Path>) -> Result<(), Error> {
false
} else {
e.file_name().is_some_and(|base_name| {
let name = base_name.to_os_string().into_string().unwrap_or("".into());
let name = base_name
.to_os_string()
.into_string()
.unwrap_or_else(|_| String::new());
!(name.starts_with(".#") | name.ends_with("~"))
})
}
@ -28,14 +31,14 @@ pub fn watch(event: Event, path: impl AsRef<Path>) -> Result<(), Error> {
match event.kind {
EventKind::Create(_) | EventKind::Modify(_) | EventKind::Remove(_) => {
// rebuild function
debug!(?event);
for path in event.paths {
for path in &event.paths {
if !path.exists() {
debug!("file deleted");
}
}
let begin = chrono::Local::now();
let mut base_path = path.as_ref().to_path_buf();
info!("Rebuilding site at: {:?}", base_path);
base_path.push("content");
let mut publish_path = path.as_ref().to_path_buf();
publish_path.push("public");
@ -43,10 +46,10 @@ pub fn watch(event: Event, path: impl AsRef<Path>) -> Result<(), Error> {
let end = chrono::Local::now();
let elapsed = end - begin;
info!(
"Time to build: {:?} minutes, {:?} seconds, and {:?} nanoseconds",
"Time to build: {:?} minutes, {:?} seconds, and {:?} milliseconds",
elapsed.num_minutes(),
elapsed.num_seconds(),
elapsed.num_nanoseconds()
elapsed.subsec_millis()
);
}
_ => (),