use crate::{Parser, SafeMode, content::substitute_attributes_in_text, document::InterpretedValue};
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub enum DocinfoLocation {
Head,
Header,
Footer,
}
impl DocinfoLocation {
fn token(self) -> &'static str {
match self {
Self::Head => "head",
Self::Header => "header",
Self::Footer => "footer",
}
}
fn name_infix(self) -> &'static str {
match self {
Self::Head => "",
Self::Header => "-header",
Self::Footer => "-footer",
}
}
}
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub(crate) struct Docinfo {
head: String,
header: String,
footer: String,
}
impl Docinfo {
pub(crate) fn content(&self, location: DocinfoLocation) -> &str {
match location {
DocinfoLocation::Head => &self.head,
DocinfoLocation::Header => &self.header,
DocinfoLocation::Footer => &self.footer,
}
}
pub(crate) fn resolve(parser: &Parser) -> Self {
if parser.safe_mode() >= SafeMode::Secure {
return Self::default();
}
let Some(handler) = parser.docinfo_file_handler.as_ref() else {
return Self::default();
};
let tokens: Vec<String> = match parser.attribute_value("docinfo") {
InterpretedValue::Unset => return Self::default(),
InterpretedValue::Set => vec!["private".to_string()],
InterpretedValue::Value(v) => v
.split(',')
.map(|t| t.trim().to_ascii_lowercase())
.filter(|t| !t.is_empty())
.collect(),
};
if tokens.is_empty() {
return Self::default();
}
let suffix = match parser.attribute_value("outfilesuffix") {
InterpretedValue::Value(v) => v,
_ => ".html".to_string(),
};
let docinfodir = match parser.attribute_value("docinfodir") {
InterpretedValue::Value(v) => Some(v),
_ => None,
};
let docname = parser.docname();
let apply_attribute_subs = docinfosubs_includes_attributes(parser);
let resolve_location = |location: DocinfoLocation| -> String {
let token = location.token();
let infix = location.name_infix();
let shared_token = format!("shared-{token}");
let private_token = format!("private-{token}");
let shared_enabled = tokens.iter().any(|t| t == "shared" || *t == shared_token);
let private_enabled = tokens.iter().any(|t| t == "private" || *t == private_token);
let mut parts: Vec<String> = vec![];
if shared_enabled {
let file_name = format!("docinfo{infix}{suffix}");
if let Some(content) =
handler.resolve_docinfo(docinfodir.as_deref(), &file_name, parser)
{
parts.push(content);
}
}
if private_enabled && let Some(docname) = docname.as_deref() {
let file_name = format!("{docname}-docinfo{infix}{suffix}");
if let Some(content) =
handler.resolve_docinfo(docinfodir.as_deref(), &file_name, parser)
{
parts.push(content);
}
}
if parts.is_empty() {
return String::new();
}
let joined = parts.join("\n");
if !apply_attribute_subs {
return joined;
}
let saved = parser.substitution_warnings_len();
let substituted = substitute_attributes_in_text(&joined, parser);
parser.truncate_substitution_warnings(saved);
substituted
};
Self {
head: resolve_location(DocinfoLocation::Head),
header: resolve_location(DocinfoLocation::Header),
footer: resolve_location(DocinfoLocation::Footer),
}
}
}
fn docinfosubs_includes_attributes(parser: &Parser) -> bool {
match parser.attribute_value("docinfosubs") {
InterpretedValue::Unset => true,
InterpretedValue::Set => false,
InterpretedValue::Value(v) => v.split(',').any(|s| s.trim() == "attributes"),
}
}
#[cfg(test)]
mod tests {
use std::collections::HashMap;
use crate::{Parser, SafeMode, document::DocinfoLocation, parser::DocinfoFileHandler};
#[derive(Debug)]
struct MapHandler(HashMap<String, String>);
impl MapHandler {
fn new(pairs: &[(&str, &str)]) -> Self {
Self(
pairs
.iter()
.map(|(k, v)| (k.to_string(), v.to_string()))
.collect(),
)
}
}
impl DocinfoFileHandler for MapHandler {
fn resolve_docinfo(
&self,
_docinfodir: Option<&str>,
file_name: &str,
_parser: &Parser,
) -> Option<String> {
self.0.get(file_name).cloned()
}
}
fn head_for(src: &str, files: &[(&str, &str)]) -> String {
Parser::default()
.with_safe_mode(SafeMode::Server)
.with_primary_file_name("mydoc.adoc")
.with_docinfo_file_handler(MapHandler::new(files))
.parse(src)
.docinfo(DocinfoLocation::Head)
.to_string()
}
#[test]
fn empty_docinfosubs_disables_substitution() {
let head = head_for(
"= Doc\n:license-url: https://example.org\n:docinfo: shared-head\n:docinfosubs:\n\nBody.",
&[("docinfo.html", "{license-url}")],
);
assert_eq!(head, "{license-url}");
}
#[test]
fn blank_docinfo_value_resolves_to_no_locations() {
let head = head_for(
"= Doc\n:docinfo: ,\n\nBody.",
&[("docinfo.html", "X"), ("mydoc-docinfo.html", "Y")],
);
assert_eq!(head, "");
}
#[test]
fn multi_line_content_mixes_references_and_plain_lines() {
let head = head_for(
"= Doc\n:name: World\n:docinfo: shared-head\n\nBody.",
&[(
"docinfo.html",
"<p>Hello {name}</p>\n<p>plain line</p>\n<p>{name} again</p>",
)],
);
assert_eq!(
head,
"<p>Hello World</p>\n<p>plain line</p>\n<p>World again</p>"
);
}
#[test]
fn drop_line_removes_docinfo_lines_with_missing_references() {
let head = head_for(
"= Doc\n:attribute-missing: drop-line\n:docinfo: shared-head\n\nBody.",
&[("docinfo.html", "keep one\n{nope}\nkeep two")],
);
assert_eq!(head, "keep one\nkeep two");
}
#[test]
fn outfilesuffix_falls_back_to_html() {
let head = head_for(
"= Doc\n:docinfo: shared-head\n:outfilesuffix:\n\nBody.",
&[("docinfo.html", "HEAD")],
);
assert_eq!(head, "HEAD");
}
}