use super::{AbsoluteLinksOption, CURRENT_DIR, MD057Config, MD057ExistingRelativeLinks, PROTOCOL_DOMAIN_REGEX};
use crate::lint_context::LinkTargetPolicy;
use crate::utils::project_root::project_root;
use std::path::{Path, PathBuf};
use std::sync::Arc;
pub(crate) fn is_external_url(url: &str) -> bool {
if url.is_empty() {
return false;
}
if PROTOCOL_DOMAIN_REGEX.is_match(url) || url.starts_with("www.") {
return true;
}
if url.starts_with("{{") || url.starts_with("{%") {
return true;
}
if url.contains('@') {
return true;
}
if !url.contains('/') && url.ends_with(".com") {
return true;
}
if url.starts_with('~') || url.starts_with('@') {
return true;
}
false
}
pub(crate) fn is_non_file_destination(url: &str, flavor: crate::config::MarkdownFlavor) -> bool {
is_external_url(url)
|| (flavor == crate::config::MarkdownFlavor::GhAw && crate::utils::gh_aw::is_output_placeholder(url))
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum LinkResolution {
Target(PathBuf),
Missing,
Unchecked,
}
#[derive(Debug, Clone, Default)]
pub struct LinkResolver {
config: MD057Config,
markdown_config: Option<Arc<crate::config::Config>>,
}
impl LinkResolver {
pub fn new(config: MD057Config, markdown_config: Option<Arc<crate::config::Config>>) -> Self {
Self {
config,
markdown_config,
}
}
pub fn from_config(config: &crate::config::Config) -> Self {
let md057 = crate::rule_config_serde::load_rule_config::<MD057Config>(config);
Self::new(md057, Some(Arc::new(config.clone())))
}
pub fn resolve(&self, source_file: &Path, url: &str, policy: Option<&LinkTargetPolicy>) -> LinkResolution {
self.for_document(source_file).resolve(url, policy)
}
pub fn for_document(&self, source_file: &Path) -> DocumentLinks<'_> {
let flavor = self
.markdown_config
.as_ref()
.map_or(crate::config::MarkdownFlavor::Standard, |config| {
config.get_flavor_for_file(source_file)
});
let base_path = source_file
.canonicalize()
.ok()
.and_then(|file| file.parent().map(Path::to_path_buf))
.or_else(|| source_file.parent().map(Path::to_path_buf))
.unwrap_or_else(|| CURRENT_DIR.clone());
let project_root = project_root();
let search_paths = MD057ExistingRelativeLinks::search_paths_for(
&self.config.search_paths,
flavor,
Some(source_file),
&base_path,
project_root,
);
DocumentLinks {
resolver: self,
flavor,
base_path,
project_root,
search_paths,
}
}
}
pub struct DocumentLinks<'a> {
resolver: &'a LinkResolver,
flavor: crate::config::MarkdownFlavor,
base_path: PathBuf,
project_root: &'static Path,
search_paths: Vec<PathBuf>,
}
impl DocumentLinks<'_> {
pub fn resolve(&self, url: &str, policy: Option<&LinkTargetPolicy>) -> LinkResolution {
if url.is_empty() || url.starts_with('#') || is_non_file_destination(url, self.flavor) {
return LinkResolution::Unchecked;
}
if MD057ExistingRelativeLinks::is_absolute_path(url) {
return self
.resolver
.resolve_absolute(url, &self.base_path, self.project_root, policy);
}
match MD057ExistingRelativeLinks::resolve_relative(url, &self.base_path, &self.search_paths, policy) {
Some(target) => LinkResolution::Target(target),
None => LinkResolution::Missing,
}
}
}
impl LinkResolver {
fn resolve_absolute(
&self,
url: &str,
base_path: &Path,
project_root: &Path,
policy: Option<&LinkTargetPolicy>,
) -> LinkResolution {
match self.config.absolute_links {
AbsoluteLinksOption::Ignore | AbsoluteLinksOption::Warn => LinkResolution::Unchecked,
AbsoluteLinksOption::RelativeToDocs => {
match MD057ExistingRelativeLinks::resolve_absolute_via_docs_dir(url, base_path, policy) {
None => LinkResolution::Unchecked,
Some(super::Resolution::Found(target)) => LinkResolution::Target(target),
Some(_) => LinkResolution::Missing,
}
}
AbsoluteLinksOption::RelativeToRoots => {
match MD057ExistingRelativeLinks::resolve_absolute_via_roots(
url,
&self.config.roots,
project_root,
policy,
) {
Some(target) => LinkResolution::Target(target),
None => LinkResolution::Missing,
}
}
}
}
}