use super::{HtmlConfig, HtmlWikiConfig};
use crate::Link;
use chrono::NaiveDate;
use derive_more::{Display, Error};
use relative_path::RelativePathBuf;
use serde::{de, Deserialize};
use std::{
borrow::Cow,
convert::TryFrom,
ffi::OsStr,
path::{Component, Path, PathBuf},
};
use uriparse::{
Fragment, RelativeReference, RelativeReferenceError, URIReference,
};
use voca_rs::escape;
pub fn deserialize_absolute_path<'de, D>(d: D) -> Result<PathBuf, D::Error>
where
D: de::Deserializer<'de>,
{
let value = PathBuf::deserialize(d)?;
let value = PathBuf::from(
shellexpand::full(&value.to_string_lossy())
.map_err(|x| {
de::Error::invalid_value(
de::Unexpected::Str(value.to_string_lossy().as_ref()),
&x.to_string().as_str(),
)
})?
.to_string(),
);
let value = normalize_path(value.as_path());
if !value.is_absolute() {
return Err(de::Error::invalid_value(
de::Unexpected::Str(value.to_string_lossy().as_ref()),
&"path must be absolute",
));
}
Ok(value)
}
pub fn normalize_id(id: &str) -> String {
escape::escape_html(
id.to_lowercase()
.split(|c: char| c.is_whitespace())
.filter(|s| !s.is_empty())
.collect::<Vec<&str>>()
.join("-")
.as_str(),
)
}
pub fn normalize_path(path: &Path) -> PathBuf {
let mut components = path.components().peekable();
let mut ret =
if let Some(c @ Component::Prefix(..)) = components.peek().cloned() {
components.next();
PathBuf::from(c.as_os_str())
} else {
PathBuf::new()
};
for component in components {
match component {
Component::Prefix(..) => unreachable!(),
Component::RootDir => {
ret.push(component.as_os_str());
}
Component::CurDir => {}
Component::ParentDir => {
ret.pop();
}
Component::Normal(c) => {
ret.push(c);
}
}
}
ret
}
pub fn path_to_uri_string(path: &Path) -> String {
let out = path
.components()
.filter_map(|c| {
match c {
Component::Prefix(_) => None,
Component::RootDir => None,
Component::CurDir => Some(Cow::Borrowed(".")),
Component::ParentDir => Some(Cow::Borrowed("..")),
Component::Normal(x) => Some(x.to_string_lossy()),
}
})
.collect::<Vec<Cow<'_, str>>>()
.join("/");
if path.is_absolute() {
format!("/{}", out)
} else {
out
}
}
#[derive(Clone, Debug, PartialEq, Eq, Display, Error)]
pub enum LinkResolutionError {
MissingWikiWithIndex {
#[error(not(source))]
index: usize,
},
MissingWikiWithName {
#[error(not(source))]
name: String,
},
RelativeReference {
#[error(source)]
source: RelativeReferenceError,
},
}
pub fn resolve_link(
config: &HtmlConfig,
src_wiki: &HtmlWikiConfig,
src: &Path,
target: &Link<'_>,
) -> Result<URIReference<'static>, LinkResolutionError> {
let ext = "html";
let src_out = src_wiki.make_output_path(src, ext);
let target_is_dir = is_directory_uri(&target.data().uri_ref);
let uri_ref = match target {
Link::Wiki { data } => {
if data.is_local() {
let mut path = data.to_path_buf();
if target_is_dir {
path.push("index");
}
let target_out = if data.uri_ref.path().is_absolute() {
src_wiki.make_output_path(path.as_path(), ext)
} else {
src_wiki.make_output_path(
src.parent()
.map(Path::to_path_buf)
.unwrap_or_default()
.join(path.as_path())
.as_path(),
ext,
)
};
let mut uri_ref = make_relative_link(src_out, target_out)
.map(URIReference::from)
.map_err(|source| {
LinkResolutionError::RelativeReference { source }
})?;
if let Some(anchor) = data.to_anchor() {
uri_ref.map_fragment(|_| Fragment::try_from(anchor).ok());
}
uri_ref
} else {
data.uri_ref.clone()
}
}
Link::IndexedInterWiki { index, data } => {
let index = *index as usize;
let wiki = config.find_wiki_by_index(index).ok_or({
LinkResolutionError::MissingWikiWithIndex { index }
})?;
let mut path = data.to_path_buf();
if target_is_dir {
path.push("index");
}
let target_out =
wiki.make_output_path(data.to_path_buf().as_path(), ext);
let mut uri_ref = make_relative_link(src_out, target_out)
.map(URIReference::from)
.map_err(|source| LinkResolutionError::RelativeReference {
source,
})?;
if let Some(anchor) = data.to_anchor() {
uri_ref.map_fragment(|_| Fragment::try_from(anchor).ok());
}
uri_ref
}
Link::NamedInterWiki { name, data } => {
let wiki = config.find_wiki_by_name(name).ok_or_else(|| {
LinkResolutionError::MissingWikiWithName {
name: name.to_string(),
}
})?;
let mut path = data.to_path_buf();
if target_is_dir {
path.push("index");
}
let target_out =
wiki.make_output_path(data.to_path_buf().as_path(), ext);
let mut uri_ref = make_relative_link(src_out, target_out)
.map(URIReference::from)
.map_err(|source| LinkResolutionError::RelativeReference {
source,
})?;
if let Some(anchor) = data.to_anchor() {
uri_ref.map_fragment(|_| Fragment::try_from(anchor).ok());
}
uri_ref
}
Link::Diary { date, data } => {
let diary_out =
make_diary_absolute_output_path(src_wiki, *date, ext);
let mut uri_ref = make_relative_link(src_out, diary_out)
.map(URIReference::from)
.map_err(|source| LinkResolutionError::RelativeReference {
source,
})?;
if let Some(anchor) = data.to_anchor() {
uri_ref.map_fragment(|_| Fragment::try_from(anchor).ok());
}
uri_ref
}
Link::Raw { data } => data.uri_ref.clone(),
Link::Transclusion { data } => {
if data.is_local() {
let path = data.to_path_buf();
let ext =
path.extension().and_then(OsStr::to_str).unwrap_or("");
let target_out = if data.uri_ref.path().is_absolute() {
src_wiki.make_output_path(path.as_path(), ext)
} else {
src_wiki.make_output_path(
src.parent()
.map(Path::to_path_buf)
.unwrap_or_default()
.join(path.as_path())
.as_path(),
ext,
)
};
make_relative_link(src_out, target_out)
.map(URIReference::from)
.map_err(|source| {
LinkResolutionError::RelativeReference { source }
})?
} else {
data.uri_ref.clone()
}
}
};
Ok(uri_ref.into_owned())
}
fn make_diary_absolute_output_path(
config: &HtmlWikiConfig,
date: NaiveDate,
ext: &str,
) -> PathBuf {
let input = config
.path
.join(config.diary_rel_path.as_path())
.join(date.format("%Y-%m-%d").to_string());
config.make_output_path(input.as_path(), ext)
}
#[inline]
fn make_relative_link<P1: AsRef<Path>, P2: AsRef<Path>>(
src: P1,
target: P2,
) -> Result<RelativeReference<'static>, RelativeReferenceError> {
let src_rel = RelativePathBuf::from_path(make_path_relative(src))
.expect("Impossible: relative path should always succeed");
let target_rel = RelativePathBuf::from_path(make_path_relative(target))
.expect("Impossible: relative path should always succeed");
let relative_path = src_rel.relative(target_rel);
let res = RelativeReference::try_from(
relative_path
.strip_prefix("..")
.unwrap_or(&relative_path)
.as_str(),
)
.map(RelativeReference::into_owned);
res
}
pub fn make_path_relative<P: AsRef<Path>>(path: P) -> PathBuf {
path.as_ref()
.components()
.filter(|c| {
matches!(
c,
Component::CurDir | Component::ParentDir | Component::Normal(_)
)
})
.collect()
}
fn is_directory_uri(uri_ref: &URIReference<'_>) -> bool {
uri_ref
.path()
.segments()
.last()
.map_or(false, |s| s.as_str().is_empty())
}