use std::fs;
use std::io;
use std::path::Path;
use camino::Utf8Component;
use camino::{Utf8Path, Utf8PathBuf};
use crate::Many;
use crate::One;
use crate::core::Dynamic;
use crate::engine::Handle;
use crate::engine::Map;
use crate::engine::TrackerPtr;
pub fn source_to_bundle(path: impl AsRef<Utf8Path>) -> Utf8PathBuf {
let path = path.as_ref().with_extension("");
if let Some("index") = path.file_name() {
if let Some(parent) = path.parent() {
return parent.to_path_buf();
}
}
path.to_path_buf()
}
pub fn source_to_href(path: &Utf8Path, offset: Option<&str>) -> String {
let path = if let Some(offset) = offset {
path.strip_prefix(offset).unwrap_or(path)
} else {
path
};
let mut url = String::from("/");
if let Some(parent) = path.parent() {
url.push_str(parent.as_str());
}
let stem = path.file_stem().unwrap_or_default();
if stem != "index" {
if !url.ends_with('/') {
url.push('/');
}
url.push_str(stem);
}
if !url.ends_with('/') {
url.push('/');
}
if url.starts_with("//") {
url.replace("//", "/")
} else {
url
}
}
pub fn href_to_dist(href: &str, dist_root: impl AsRef<Utf8Path>) -> Utf8PathBuf {
dist_root
.as_ref()
.join(href.trim_start_matches('/'))
.join("index.html")
}
pub(crate) fn normalize_path(path: &Utf8Path) -> Utf8PathBuf {
let mut components = path.components().peekable();
let mut ret = if let Some(c @ Utf8Component::Prefix(..)) = components.peek().cloned() {
components.next();
Utf8PathBuf::from(c.as_str())
} else {
Utf8PathBuf::new()
};
for component in components {
match component {
Utf8Component::Prefix(..) => unreachable!(),
Utf8Component::RootDir => {
ret.push(Utf8Component::RootDir);
}
Utf8Component::CurDir => {}
Utf8Component::ParentDir => {
if ret.ends_with(Utf8Component::ParentDir) {
ret.push(Utf8Component::ParentDir);
} else {
let popped = ret.pop();
if !popped && !ret.has_root() {
ret.push(Utf8Component::ParentDir);
}
}
}
Utf8Component::Normal(c) => {
ret.push(c);
}
}
}
ret
}
fn normalize_path_html(path: impl AsRef<Utf8Path>) -> Utf8PathBuf {
let mut buffer = path.as_ref().to_path_buf();
if let Some(file_name) = buffer.file_name() {
if file_name == "index" || file_name.starts_with("index.") {
buffer.set_extension("html");
} else {
buffer.set_extension("");
buffer.push("index.html");
}
} else {
buffer.push("index.html");
}
buffer
}
#[derive(Debug, Clone, Hash)]
pub enum OutputData {
Utf8(String),
Binary(Vec<u8>),
}
impl AsRef<[u8]> for OutputData {
fn as_ref(&self) -> &[u8] {
match self {
OutputData::Utf8(s) => s.as_bytes(),
OutputData::Binary(b) => b.as_slice(),
}
}
}
#[derive(Debug, Clone, Hash)]
pub struct Output {
pub path: Utf8PathBuf,
pub data: OutputData,
}
impl Output {
pub fn mapper(source: impl Into<Utf8PathBuf>) -> OutputBuilder {
OutputBuilder {
current: source.into(),
}
}
pub fn html(path: impl AsRef<Utf8Path>, data: impl Into<String>) -> Self {
Self {
path: normalize_path_html(path),
data: OutputData::Utf8(data.into()),
}
}
pub fn binary(path: impl Into<Utf8PathBuf>, data: impl Into<Vec<u8>>) -> Self {
Self {
path: path.into(),
data: OutputData::Binary(data.into()),
}
}
}
pub struct OutputBuilder {
current: Utf8PathBuf,
}
impl OutputBuilder {
pub fn strip_prefix(
mut self,
prefix: impl AsRef<Utf8Path>,
) -> Result<Self, crate::error::HauchiwaError> {
self.current = self
.current
.strip_prefix(prefix.as_ref())
.map(|p| p.to_path_buf())
.map_err(|_| {
anyhow::anyhow!(
"Path {} does not start with prefix {}",
self.current,
prefix.as_ref().as_str()
)
})
.map_err(|e| crate::error::HauchiwaError::Build(crate::error::BuildError::Other(e)))?;
Ok(self)
}
pub fn html(mut self) -> Self {
self.current = source_to_bundle(&self.current)
.join("index")
.with_extension("html");
self
}
pub fn ext(mut self, extension: &str) -> Self {
self.current.set_extension(extension);
self
}
pub fn content(self, body: impl Into<String>) -> Output {
let path = if (self.current.extension() == Some("html"))
|| self.current.file_name() == Some("index")
{
normalize_path_html(&self.current)
} else {
normalize_path(&self.current)
};
Output {
path,
data: OutputData::Utf8(body.into()),
}
}
}
pub trait OutputHandle: Handle {
fn resolve_refs(item: &Dynamic) -> (Option<TrackerPtr>, Vec<&Output>);
}
impl OutputHandle for One<Output> {
fn resolve_refs(item: &Dynamic) -> (Option<TrackerPtr>, Vec<&Output>) {
match item.downcast_ref::<Output>() {
Some(item) => (None, vec![item]),
None => unreachable!(),
}
}
}
impl OutputHandle for One<Vec<Output>> {
fn resolve_refs(item: &Dynamic) -> (Option<TrackerPtr>, Vec<&Output>) {
match item.downcast_ref::<Vec<Output>>() {
Some(item) => (None, item.iter().collect()),
None => unreachable!(),
}
}
}
impl OutputHandle for Many<Output> {
fn resolve_refs(item: &Dynamic) -> (Option<TrackerPtr>, Vec<&Output>) {
match item.downcast_ref::<Map<Output>>() {
Some(map) => {
let ptr = TrackerPtr::default();
let mut items = Vec::new();
{
let mut tracker = ptr.ptr.lock().unwrap();
for (key, (output, provenance)) in &map.map {
tracker.accessed.insert(key.clone(), *provenance);
items.push(output);
}
}
(Some(ptr), items)
}
None => unreachable!(),
}
}
}
pub(crate) fn save_pages_to_dist(pages: &[Output]) -> io::Result<()> {
let output_dir = Path::new("dist");
fs::create_dir_all(output_dir)?;
for page in pages {
let file_path = output_dir.join(&page.path);
if let Some(parent_dir) = file_path.parent() {
fs::create_dir_all(parent_dir)?;
}
fs::write(&file_path, &page.data)?;
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_source_to_href() {
assert_eq!(
source_to_href(Utf8Path::new("content/posts/hello.md"), Some("content")),
"/posts/hello/"
);
assert_eq!(
source_to_href(Utf8Path::new("content/posts/index.md"), Some("content")),
"/posts/"
);
assert_eq!(
source_to_href(Utf8Path::new("posts/hello.md"), None),
"/posts/hello/"
);
assert_eq!(source_to_href(Utf8Path::new("index.md"), None), "/");
assert_eq!(
source_to_href(Utf8Path::new("content/hello.md"), Some("content")),
"/hello/"
);
assert_eq!(
source_to_href(Utf8Path::new("content/index.md"), Some("content")),
"/"
);
assert_eq!(
source_to_href(Utf8Path::new("content/a/b/c.md"), Some("content")),
"/a/b/c/"
);
}
#[test]
fn test_source_to_bundle() {
assert_eq!(
source_to_bundle("content/foo/bar.md"),
Utf8Path::new("content/foo/bar")
);
assert_eq!(
source_to_bundle("content/foo/index.md"),
Utf8Path::new("content/foo")
);
assert_eq!(source_to_bundle("index.md"), Utf8Path::new(""));
}
#[test]
fn test_href_to_dist() {
assert_eq!(
href_to_dist("/posts/hello/", "dist"),
Utf8Path::new("dist/posts/hello/index.html")
);
assert_eq!(href_to_dist("/", "dist"), Utf8Path::new("dist/index.html"));
assert_eq!(
href_to_dist("/a/b/c/", "dist"),
Utf8Path::new("dist/a/b/c/index.html")
);
}
}