use crate::{Parser, parser::DerivedReference};
const ASCIIDOC_EXTENSIONS: [&str; 5] = [".adoc", ".asciidoc", ".asc", ".ad", ".txt"];
#[derive(Clone, Debug, Eq, PartialEq)]
pub(crate) enum XrefTarget {
SameDocument(String),
OtherDocument {
path: String,
source: bool,
fragment: Option<String>,
},
}
pub(crate) fn interpret_xref_target(target: &str, macro_form: bool) -> XrefTarget {
let hash_index = target
.find('#')
.filter(|index| *index == 0 || !target[..*index].ends_with('&'));
match hash_index {
Some(index) if index > 0 => {
let (path, fragment) = target.split_at(index);
let fragment = &fragment[1..];
let (path, source) = strip_asciidoc_extension(path, macro_form);
XrefTarget::OtherDocument {
path: path.to_string(),
source,
fragment: if fragment.is_empty() {
None
} else {
Some(fragment.to_string())
},
}
}
Some(_) => XrefTarget::SameDocument(target[1..].to_string()),
None if macro_form => {
if let Some(path) = target.strip_suffix(".adoc") {
XrefTarget::OtherDocument {
path: path.to_string(),
source: true,
fragment: None,
}
} else if has_extension(target) {
XrefTarget::OtherDocument {
path: target.to_string(),
source: false,
fragment: None,
}
} else {
XrefTarget::SameDocument(target.to_string())
}
}
None => XrefTarget::SameDocument(target.to_string()),
}
}
fn strip_asciidoc_extension(path: &str, macro_form: bool) -> (&str, bool) {
if macro_form {
match path.strip_suffix(".adoc") {
Some(stem) => (stem, true),
None => (path, !has_extension(path)),
}
} else if let Some(index) = path.rfind('.')
&& ASCIIDOC_EXTENSIONS.contains(&&path[index..])
{
(&path[..index], true)
} else {
(path, true)
}
}
fn has_extension(path: &str) -> bool {
match path.rfind('.') {
Some(index) => !path[index..].contains('/'),
None => false,
}
}
const SELF_REFERENCE_FALLBACK_TEXT: &str = "[^top]";
pub(crate) fn other_document_reference(
parser: &Parser,
path: &str,
source: bool,
fragment: Option<&str>,
) -> DerivedReference {
let prefix = parser
.attribute_value("relfileprefix")
.as_maybe_str()
.unwrap_or_default()
.to_string();
let suffix = if source {
parser
.attribute_value("relfilesuffix")
.as_maybe_str()
.unwrap_or_default()
.to_string()
} else {
String::new()
};
let path = format!("{prefix}{path}{suffix}");
let href = match fragment {
Some(fragment) => format!("{path}#{fragment}"),
None => path.clone(),
};
DerivedReference { href, text: path }
}
pub(crate) fn this_document_reference(parser: &Parser) -> DerivedReference {
let reftext = parser.attribute_value("reftext");
let doctitle = parser.attribute_value("doctitle");
let text = reftext
.as_maybe_str()
.filter(|reftext| !reftext.is_empty())
.or_else(|| doctitle.as_maybe_str().filter(|title| !title.is_empty()))
.unwrap_or(SELF_REFERENCE_FALLBACK_TEXT)
.to_string();
DerivedReference {
href: "#".to_string(),
text,
}
}
#[cfg(test)]
mod tests {
use super::{XrefTarget, interpret_xref_target, this_document_reference};
use crate::{Parser, parser::ModificationContext};
fn other(path: &str, source: bool, fragment: Option<&str>) -> XrefTarget {
XrefTarget::OtherDocument {
path: path.to_string(),
source,
fragment: fragment.map(str::to_string),
}
}
#[test]
fn shorthand_without_hash_is_same_document() {
assert_eq!(
interpret_xref_target("tigers", false),
XrefTarget::SameDocument("tigers".to_string())
);
assert_eq!(
interpret_xref_target("tigers.adoc", false),
XrefTarget::SameDocument("tigers.adoc".to_string())
);
}
#[test]
fn explicit_hash_prefix_is_same_document() {
assert_eq!(
interpret_xref_target("#tigers", false),
XrefTarget::SameDocument("tigers".to_string())
);
assert_eq!(
interpret_xref_target("#using-.net-web-services", true),
XrefTarget::SameDocument("using-.net-web-services".to_string())
);
}
#[test]
fn shorthand_with_hash_is_inter_document() {
assert_eq!(
interpret_xref_target("tigers#", false),
other("tigers", true, None)
);
assert_eq!(
interpret_xref_target("tigers#about", false),
other("tigers", true, Some("about"))
);
assert_eq!(
interpret_xref_target("tigers.adoc#about", false),
other("tigers", true, Some("about"))
);
assert_eq!(
interpret_xref_target("using-.net-web-services.adoc#", false),
other("using-.net-web-services", true, None)
);
assert_eq!(
interpret_xref_target("asciidoctor.1#", false),
other("asciidoctor.1", true, None)
);
}
#[test]
fn macro_form_reads_extension_as_path() {
assert_eq!(
interpret_xref_target("tigers.adoc", true),
other("tigers", true, None)
);
assert_eq!(
interpret_xref_target("refcard.pdf", true),
other("refcard.pdf", false, None)
);
assert_eq!(
interpret_xref_target("sections.d/first", true),
XrefTarget::SameDocument("sections.d/first".to_string())
);
}
#[test]
fn macro_form_with_hash_keeps_non_asciidoc_extension() {
assert_eq!(
interpret_xref_target("asciidoctor.1#", true),
other("asciidoctor.1", false, None)
);
assert_eq!(
interpret_xref_target("document#", true),
other("document", true, None)
);
assert_eq!(
interpret_xref_target("include.d/document#", true),
other("include.d/document", true, None)
);
}
#[test]
fn hash_in_numeric_character_reference_is_not_a_separator() {
assert_eq!(
interpret_xref_target("C++", true),
XrefTarget::SameDocument("C++".to_string())
);
assert_eq!(
interpret_xref_target("Cub ⇒ Tiger", false),
XrefTarget::SameDocument("Cub ⇒ Tiger".to_string())
);
assert_eq!(
interpret_xref_target("+.adoc#frag", false),
XrefTarget::SameDocument("+.adoc#frag".to_string())
);
}
#[test]
fn this_document_is_named_by_its_reftext_then_its_title() {
let parser = Parser::default();
assert_eq!(this_document_reference(&parser).href, "#");
assert_eq!(this_document_reference(&parser).text, "[^top]");
let parser =
parser.with_intrinsic_attribute("doctitle", "Doc Title", ModificationContext::Anywhere);
assert_eq!(this_document_reference(&parser).text, "Doc Title");
let parser = parser.with_intrinsic_attribute("reftext", "", ModificationContext::Anywhere);
assert_eq!(this_document_reference(&parser).text, "Doc Title");
let parser =
parser.with_intrinsic_attribute("reftext", "Ref Text", ModificationContext::Anywhere);
assert_eq!(this_document_reference(&parser).text, "Ref Text");
}
}