use crate::{
HasSpan, Parser, Span,
attributes::{Attrlist, AttrlistContext},
blocks::metadata::block_title_text,
content::{Content, SubstitutionGroup, substitute_attributes_in_reftext},
document::{
Attribute, Author, AuthorLine, InterpretedValue, RefType, RevisionLine,
is_attribute_entry_pass_macro, matches_author_pattern, set_author_metadata,
},
internal::{debug::DebugSliceReference, opaque_iter::opaque_slice_iter},
span::MatchedItem,
warnings::{MatchAndWarnings, Warning, WarningType},
};
opaque_slice_iter! {
pub struct HeaderAttributes<'a> yielding Attribute<'a>;
}
opaque_slice_iter! {
pub struct Comments<'a> yielding Span<'a>;
}
#[derive(Clone, Eq, PartialEq)]
pub struct Header<'src> {
title_source: Option<Span<'src>>,
title: Option<String>,
doctitle: Option<String>,
main_title: Option<String>,
subtitle: Option<String>,
id: Option<String>,
roles: Vec<String>,
attributes: Vec<Attribute<'src>>,
author_line: Option<AuthorLine<'src>>,
authors: Vec<Author>,
revision_line: Option<RevisionLine<'src>>,
comments: Vec<Span<'src>>,
source: Span<'src>,
}
impl<'src> Header<'src> {
pub(crate) fn parse(
mut source: Span<'src>,
parser: &mut Parser,
) -> MatchAndWarnings<'src, MatchedItem<'src, Self>> {
let original_source = source.discard_empty_lines();
let mut title_source: Option<Span<'src>> = None;
let mut title: Option<String> = None;
let mut saw_implicit_title = false;
let mut implicit_overridden_from_above = false;
let mut implicit_doctitle_str: Option<String> = None;
let mut doctitle_entry_after_title = false;
let mut id: Option<String> = None;
let mut roles: Vec<String> = vec![];
let mut attributes: Vec<Attribute> = vec![];
let mut author_line: Option<AuthorLine<'src>> = None;
let mut author_attribute: Option<Author> = None;
let mut authorinitials_from_entry = false;
let mut revision_line: Option<RevisionLine<'src>> = None;
let mut comments: Vec<Span<'src>> = vec![];
let mut warnings: Vec<Warning<'src>> = vec![];
while !source.is_empty() {
let line_mi = source.take_normalized_line();
let line = line_mi.item;
if line.is_empty() {
if title.is_some() {
break;
}
source = line_mi.after;
} else if line.starts_with("//") && !line.starts_with("///") {
comments.push(line);
source = line_mi.after;
} else if title.is_some()
&& let Some((after, terminated)) = skip_block_comment(line, line_mi.after)
{
comments.push(source.trim_remainder(after).trim_trailing_line_end());
if !terminated {
warnings.push(Warning::new(line, WarningType::UnterminatedDelimitedBlock));
}
source = after;
} else if line.starts_with(':')
&& let Some(attr) = Attribute::parse(source, parser)
{
if attr
.item
.name()
.data()
.eq_ignore_ascii_case("authorinitials")
{
authorinitials_from_entry =
!matches!(attr.item.value(), InterpretedValue::Unset);
}
let mut author_name_override: Option<String> = None;
if attr.item.name().data().eq_ignore_ascii_case("author")
&& let Some(raw_value) = attr.item.raw_value()
&& let Some(author) = Author::parse_from_entry(
raw_value.data(),
attr.item.value().as_maybe_str(),
parser,
)
{
parser.set_attribute_by_value_from_header("firstname", author.firstname());
if let Some(middlename) = author.middlename() {
parser.set_attribute_by_value_from_header("middlename", middlename);
}
if let Some(lastname) = author.lastname() {
parser.set_attribute_by_value_from_header("lastname", lastname);
}
if !authorinitials_from_entry {
parser.set_attribute_by_value_from_header(
"authorinitials",
author.initials(),
);
}
if let Some(email) = author.email() {
parser.set_attribute_by_value_from_header("email", email);
}
let raw = raw_value.data();
if is_attribute_entry_pass_macro(raw)
|| (!raw.contains('<')
&& !raw.contains('{')
&& !matches_author_pattern(raw))
{
author_name_override = Some(author.name().to_string());
}
author_attribute = Some(author);
}
parser.set_attribute_from_header(&attr.item, &mut warnings);
if let Some(author_name) = author_name_override {
parser.set_attribute_by_value_from_header("author", author_name);
}
if title.is_some() && attr.item.name().data().eq_ignore_ascii_case("doctitle") {
doctitle_entry_after_title = true;
}
attributes.push(attr.item);
source = attr.after;
} else if title.is_none()
&& line.starts_with('[')
&& line.ends_with(']')
&& document_title_follows_block_metadata(source, parser.level_offset())
&& let Some((metadata, metadata_warnings)) = parse_document_metadata(line, parser)
{
warnings.extend(metadata_warnings);
if let Some(doc_id) = metadata.id {
id = Some(doc_id);
}
if let Some(separator) = metadata.separator {
parser.set_attribute_by_value_from_header("title-separator", separator);
}
if let Some(reftext) = metadata.reftext {
parser.set_attribute_by_value_from_header("reftext", reftext);
}
if !metadata.roles.is_empty() {
roles.extend(metadata.roles);
parser.set_attribute_by_value_from_header("role", roles.join(" "));
}
for option in metadata.options {
parser.set_attribute_by_value_from_header(format!("{option}-option"), "");
}
source = line_mi.after;
} else if title.is_none()
&& block_title_text(line).is_some()
&& document_title_follows_block_metadata(source, parser.level_offset())
{
break;
} else if title.is_none()
&& let Some((marker, count)) = document_title_marker(line, parser.level_offset())
{
let title_span = crate::blocks::strip_symmetric_title_close(
line.discard(count).discard_whitespace(),
marker,
count,
);
saw_implicit_title = true;
title_source = Some(title_span);
if let InterpretedValue::Value(existing) = parser.attribute_value("doctitle")
&& !existing.is_empty()
{
implicit_overridden_from_above = true;
implicit_doctitle_str = Some(existing.clone());
title = Some(existing);
} else {
let title_str = apply_header_subs(title_span.data(), parser);
parser.set_attribute_by_value_from_header("doctitle", &title_str);
implicit_doctitle_str = Some(title_str.clone());
title = Some(title_str);
}
source = line_mi.after;
} else if title.is_some() && author_line.is_none() {
author_line = Some(AuthorLine::parse(line, parser));
source = line_mi.after;
} else if title.is_some() && author_line.is_some() && revision_line.is_none() {
revision_line = Some(RevisionLine::parse(line, parser));
source = line_mi.after;
} else {
if title.is_some() {
warnings.push(Warning::new(line, WarningType::DocumentHeaderNotTerminated));
}
break;
}
}
let after = source.discard_empty_lines();
let source = original_source.trim_remainder(source);
let final_doctitle_attr = match parser.attribute_value("doctitle") {
InterpretedValue::Value(v) if !v.is_empty() => Some(v),
_ => None,
};
title = if saw_implicit_title {
let base = if !implicit_overridden_from_above
&& let Some(raw) = title_source
&& implicit_doctitle_str
.as_deref()
.is_some_and(|s| s.contains('{'))
{
Some(apply_header_subs(raw.data(), parser))
} else {
title
};
if doctitle_entry_after_title
&& let Some(ref dt) = final_doctitle_attr
&& Some(dt) != implicit_doctitle_str.as_ref()
{
Some(dt.clone())
} else {
base
}
} else {
final_doctitle_attr
};
let (main_title, subtitle) = match &title {
Some(title) => {
let (main_title, subtitle) = partition_title(title, parser);
(Some(main_title), subtitle)
}
None => (None, None),
};
let doctitle = match parser.attribute_value("title") {
InterpretedValue::Value(v) => Some(v),
InterpretedValue::Set => Some(String::new()),
InterpretedValue::Unset => title.clone(),
};
if let Some(doc_id) = id.as_deref() {
let reftext = match parser.attribute_value("reftext") {
InterpretedValue::Value(reftext) if !reftext.is_empty() => Some(reftext),
_ => doctitle.clone().filter(|title| !title.is_empty()),
};
let _ = parser.register_ref(doc_id, reftext.as_deref(), RefType::Section);
}
let authors = resolve_authors(
author_line.as_ref(),
author_attribute,
!attributes.is_empty(),
parser,
);
if !authors.is_empty() {
parser.set_attribute_by_value_from_header("authorcount", authors.len().to_string());
}
MatchAndWarnings {
item: MatchedItem {
item: Self {
title_source,
title,
doctitle,
main_title,
subtitle,
id,
roles,
attributes,
author_line,
authors,
revision_line,
comments,
source: source.trim_trailing_whitespace(),
},
after,
},
warnings,
}
}
pub fn title_source(&'src self) -> Option<Span<'src>> {
self.title_source
}
pub fn title(&self) -> Option<&str> {
self.title.as_deref()
}
pub(crate) fn doctitle(&self) -> Option<&str> {
self.doctitle.as_deref()
}
pub fn main_title(&self) -> Option<&str> {
self.main_title.as_deref()
}
pub fn subtitle(&self) -> Option<&str> {
self.subtitle.as_deref()
}
pub fn id(&self) -> Option<&str> {
self.id.as_deref()
}
pub fn roles(&self) -> Vec<&str> {
self.roles.iter().map(String::as_str).collect()
}
pub fn attributes(&'src self) -> HeaderAttributes<'src> {
HeaderAttributes::new(&self.attributes)
}
pub fn author_line(&self) -> Option<&AuthorLine<'src>> {
self.author_line.as_ref()
}
pub fn authors(&self) -> &[Author] {
&self.authors
}
pub fn revision_line(&self) -> Option<&RevisionLine<'src>> {
self.revision_line.as_ref()
}
pub fn comments(&'src self) -> Comments<'src> {
Comments::new(&self.comments)
}
}
impl<'src> HasSpan<'src> for Header<'src> {
fn span(&self) -> Span<'src> {
self.source
}
}
fn skip_block_comment<'src>(line: Span<'src>, after: Span<'src>) -> Option<(Span<'src>, bool)> {
let delimiter = line.data();
if delimiter.len() < 4 || !delimiter.bytes().all(|b| b == b'/') {
return None;
}
let mut next = after;
let mut terminated = false;
while !next.is_empty() {
let line_mi = next.take_normalized_line();
next = line_mi.after;
if line_mi.item.data() == delimiter {
terminated = true;
break;
}
}
Some((next, terminated))
}
fn document_title_marker(line: Span<'_>, level_offset: i32) -> Option<(char, usize)> {
let data = line.data();
let marker = if data.starts_with('=') {
'='
} else if data.starts_with('#') {
'#'
} else {
return None;
};
let count = data.chars().take_while(|&c| c == marker).count();
if count > 6 {
return None;
}
if !data[count..].starts_with([' ', '\t']) {
return None;
}
let syntactic_level = (count as i32) - 1;
if syntactic_level.saturating_add(level_offset) != 0 {
return None;
}
Some((marker, count))
}
fn document_title_follows_block_metadata(source: Span<'_>, level_offset: i32) -> bool {
let mut next = source;
let mut effective_style_is_discrete = false;
while !next.is_empty() {
let line_mi = next.take_normalized_line();
let line = line_mi.item;
if document_title_marker(line, level_offset).is_some() {
return !effective_style_is_discrete;
}
if block_title_text(line).is_some() {
next = line_mi.after;
continue;
}
if !is_document_metadata_line(line) {
return false;
}
if let Some(is_discrete) = metadata_line_block_style_is_discrete(line) {
effective_style_is_discrete = is_discrete;
}
next = line_mi.after;
}
false
}
fn metadata_line_block_style_is_discrete(line: Span<'_>) -> Option<bool> {
let inner = line.slice(1..line.len() - 1).data();
if inner.starts_with('[') {
return None;
}
let token_len = inner
.find(|c: char| !(c.is_ascii_alphanumeric() || c == '-' || c == '_'))
.unwrap_or(inner.len());
if token_len == 0 || inner[token_len..].starts_with('=') {
return None;
}
let token = &inner[..token_len];
Some(token == "discrete" || token == "float")
}
fn is_document_metadata_line(line: Span<'_>) -> bool {
if !(line.starts_with('[') && line.ends_with(']')) {
return false;
}
let inner = line.slice(1..line.len() - 1);
if inner.is_empty() || inner.starts_with(' ') || inner.starts_with('\t') {
return false;
}
if inner.starts_with('[') && inner.ends_with(']') {
return inner.len() > 2;
}
true
}
struct DocumentMetadata {
id: Option<String>,
separator: Option<String>,
reftext: Option<String>,
roles: Vec<String>,
options: Vec<String>,
}
fn parse_document_metadata<'src>(
line: Span<'src>,
parser: &Parser,
) -> Option<(DocumentMetadata, Vec<Warning<'src>>)> {
if !is_document_metadata_line(line) {
return None;
}
let inner = line.slice(1..line.len() - 1);
if inner.starts_with('[') && inner.ends_with(']') {
return parse_document_metadata_anchor(inner.slice(1..inner.len() - 1), parser);
}
let MatchAndWarnings {
item: MatchedItem {
item: attrlist,
after: _,
},
warnings,
} = Attrlist::parse(inner, parser, AttrlistContext::Block);
let metadata = DocumentMetadata {
id: attrlist.id().map(str::to_string),
separator: attrlist
.named_attribute("separator")
.map(|attr| attr.value().to_string()),
reftext: attrlist
.named_attribute("reftext")
.map(|attr| attr.value().to_string()),
roles: attrlist.roles().iter().map(|r| r.to_string()).collect(),
options: attrlist.options().iter().map(|o| o.to_string()).collect(),
};
Some((metadata, warnings))
}
fn parse_document_metadata_anchor<'src>(
anchor: Span<'src>,
parser: &Parser,
) -> Option<(DocumentMetadata, Vec<Warning<'src>>)> {
let (id, reftext) = match anchor.position(|c| c == ',') {
Some(comma) if comma < anchor.len() - 1 => (
anchor.slice(0..comma),
Some(substitute_attributes_in_reftext(
anchor.slice(comma + 1..anchor.len()),
parser,
)),
),
_ => (anchor, None),
};
if !id.is_xml_name() {
return None;
}
let metadata = DocumentMetadata {
id: Some(id.data().to_string()),
separator: None,
reftext: reftext.map(|r| r.to_string()),
roles: vec![],
options: vec![],
};
Some((metadata, vec![]))
}
fn partition_title(title: &str, parser: &Parser) -> (String, Option<String>) {
let separator = match parser.effective_attribute("title-separator") {
Some(av) => match &av.value {
InterpretedValue::Value(value) if !value.is_empty() => value.clone(),
_ => ":".to_string(),
},
None => ":".to_string(),
};
let separator = format!("{separator} ");
match title.rfind(&separator) {
Some(index) => {
let main_title = title[..index].to_string();
let subtitle = title[index + separator.len()..].to_string();
(main_title, Some(subtitle))
}
None => (title.to_string(), None),
}
}
fn resolve_authors(
author_line: Option<&AuthorLine>,
author_attribute: Option<Author>,
header_has_attributes: bool,
parser: &mut Parser,
) -> Vec<Author> {
if let Some(author_line) = author_line {
let implicit_authors: Vec<Author> = author_line.authors().cloned().collect();
if let Some(authors_value) = attribute_string(parser, "authors") {
let computed = implicit_authors
.iter()
.map(Author::name)
.collect::<Vec<_>>()
.join(", ");
if authors_value != computed
&& let Some(authors) = authors_from_authors_attribute(&authors_value, parser)
{
set_author_metadata(parser, &authors);
return authors;
}
}
if attribute_string(parser, "author_1").is_some() {
let mut reconciled: Vec<Author> = Vec::new();
let mut any_override = false;
let mut index = 1;
while let Some(current) = attribute_string(parser, &format!("author_{index}")) {
match implicit_authors.get(index - 1) {
Some(implicit) if current == implicit.name() => {
reconciled.push(implicit.clone());
}
_ => {
any_override = true;
if let Some(author) = Author::parse(¤t, parser, true) {
reconciled.push(author);
}
}
}
index += 1;
}
if any_override {
let reconciled = collect_indexed_authors(reconciled.into_iter(), parser);
set_author_metadata(parser, &reconciled);
return reconciled;
}
}
return implicit_authors;
}
if !header_has_attributes {
return vec![];
}
if attribute_string(parser, "author").is_some()
&& let Some(author) = author_attribute
{
let author = author.with_email(attribute_string(parser, "email"));
parser.set_attribute_by_value_from_header("authors", author.name());
return vec![author];
}
if let Some(authors_value) = attribute_string(parser, "authors")
&& let Some(authors) = authors_from_authors_attribute(&authors_value, parser)
{
set_author_metadata(parser, &authors);
return authors;
}
let mut raw_names = vec![];
let mut index = 1;
while let Some(name) = attribute_string(parser, &format!("author_{index}")) {
raw_names.push(name);
index += 1;
}
let authors = collect_indexed_authors(
raw_names
.iter()
.filter_map(|name| Author::parse(name, parser, true)),
parser,
);
if !authors.is_empty() {
set_author_metadata(parser, &authors);
}
authors
}
fn authors_from_authors_attribute(value: &str, parser: &Parser) -> Option<Vec<Author>> {
let authors = collect_indexed_authors(
split_author_entries(value)
.into_iter()
.filter_map(|entry| Author::parse(entry, parser, true)),
parser,
);
if authors.is_empty() {
None
} else {
Some(authors)
}
}
fn attribute_string(parser: &Parser, name: &str) -> Option<String> {
match parser.attribute_value(name) {
InterpretedValue::Value(value) => Some(value),
_ => None,
}
}
fn collect_indexed_authors(authors: impl Iterator<Item = Author>, parser: &Parser) -> Vec<Author> {
authors
.enumerate()
.map(|(idx, author)| {
author.with_email(attribute_string(parser, &format!("email_{}", idx + 1)))
})
.collect()
}
fn split_author_entries(value: &str) -> Vec<&str> {
let bytes = value.as_bytes();
let mut entries: Vec<&str> = Vec::new();
let mut start = 0;
for (index, c) in value.char_indices() {
if c != ';' {
continue;
}
let is_separator = match bytes.get(index + 1) {
Some(next) => *next == b' ',
None => true,
};
if is_separator {
entries.push(&value[start..index]);
start = index + 1;
}
}
entries.push(&value[start..]);
entries
}
fn apply_header_subs(source: &str, parser: &Parser) -> String {
let span = Span::new(source);
let mut content = Content::from(span);
SubstitutionGroup::Header.apply(&mut content, parser, None);
content.rendered().to_string()
}
impl std::fmt::Debug for Header<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Header")
.field("title_source", &self.title_source)
.field("title", &self.title)
.field("doctitle", &self.doctitle)
.field("main_title", &self.main_title)
.field("subtitle", &self.subtitle)
.field("id", &self.id)
.field("roles", &self.roles)
.field("attributes", &DebugSliceReference(&self.attributes))
.field("author_line", &self.author_line)
.field("authors", &self.authors)
.field("revision_line", &self.revision_line)
.field("comments", &DebugSliceReference(&self.comments))
.field("source", &self.source)
.finish()
}
}
#[cfg(test)]
mod tests {
#![allow(clippy::unwrap_used)]
use crate::tests::prelude::*;
#[test]
fn attributes_iterator_supports_exact_size_double_ended_and_nth() {
let doc = Parser::default().parse(":alpha: 1\n:bravo: 2\n:charlie: 3\n\nbody\n");
let header = doc.header();
let names: Vec<_> = header
.attributes()
.map(|a| a.name().data().to_string())
.collect();
assert!(names.len() >= 3);
assert_eq!(names.first().map(String::as_str), Some("alpha"));
assert_eq!(header.attributes().len(), names.len());
assert_eq!(
header.attributes().next_back().map(|a| a.name().data()),
names.last().map(String::as_str),
);
assert_eq!(
header.attributes().nth(1).map(|a| a.name().data()),
Some("bravo"),
);
}
#[test]
fn leveloffset_does_not_coerce_an_over_deep_heading_to_the_doctitle() {
let doc = Parser::default().parse(":leveloffset: -6\n======= Not A Title");
assert_eq!(doc.doctitle(), None);
}
#[test]
fn impl_clone() {
let mut parser = Parser::default();
let h1 = crate::document::Header::parse(crate::Span::new("= Title"), &mut parser)
.unwrap_if_no_warnings();
let h2 = h1.clone();
assert_eq!(h1, h2);
}
#[test]
fn only_title() {
let mut parser = Parser::default();
let mi = crate::document::Header::parse(crate::Span::new("= Just the Title"), &mut parser)
.unwrap_if_no_warnings();
assert_eq!(
mi.item,
Header {
title_source: Some(Span {
data: "Just the Title",
line: 1,
col: 3,
offset: 2,
}),
title: Some("Just the Title"),
attributes: &[],
author_line: None,
revision_line: None,
comments: &[],
source: Span {
data: "= Just the Title",
line: 1,
col: 1,
offset: 0,
}
}
);
assert_eq!(
mi.after,
Span {
data: "",
line: 1,
col: 17,
offset: 16
}
);
}
#[test]
fn trims_leading_spaces_in_title() {
let mut parser = Parser::default();
let mi =
crate::document::Header::parse(crate::Span::new("= Just the Title"), &mut parser)
.unwrap_if_no_warnings();
assert_eq!(
mi.item,
Header {
title_source: Some(Span {
data: "Just the Title",
line: 1,
col: 6,
offset: 5,
}),
title: Some("Just the Title"),
attributes: &[],
author_line: None,
revision_line: None,
comments: &[],
source: Span {
data: "= Just the Title",
line: 1,
col: 1,
offset: 0,
}
}
);
assert_eq!(
mi.after,
Span {
data: "",
line: 1,
col: 20,
offset: 19
}
);
}
#[test]
fn trims_trailing_spaces_in_title() {
let mut parser = Parser::default();
let mi =
crate::document::Header::parse(crate::Span::new("= Just the Title "), &mut parser)
.unwrap_if_no_warnings();
assert_eq!(
mi.item,
Header {
title_source: Some(Span {
data: "Just the Title",
line: 1,
col: 3,
offset: 2,
}),
title: Some("Just the Title"),
attributes: &[],
author_line: None,
revision_line: None,
comments: &[],
source: Span {
data: "= Just the Title",
line: 1,
col: 1,
offset: 0,
}
}
);
assert_eq!(
mi.after,
Span {
data: "",
line: 1,
col: 20,
offset: 19
}
);
}
#[test]
fn title_and_attribute() {
let mut parser = Parser::default();
let mi = crate::document::Header::parse(
crate::Span::new("= Just the Title\n:foo: bar\n\nblah"),
&mut parser,
)
.unwrap_if_no_warnings();
assert_eq!(
mi.item,
Header {
title_source: Some(Span {
data: "Just the Title",
line: 1,
col: 3,
offset: 2,
}),
title: Some("Just the Title"),
attributes: &[Attribute {
name: Span {
data: "foo",
line: 2,
col: 2,
offset: 18,
},
value_source: Some(Span {
data: "bar",
line: 2,
col: 7,
offset: 23,
}),
value: InterpretedValue::Value("bar"),
source: Span {
data: ":foo: bar",
line: 2,
col: 1,
offset: 17,
}
}],
author_line: None,
revision_line: None,
comments: &[],
source: Span {
data: "= Just the Title\n:foo: bar",
line: 1,
col: 1,
offset: 0,
}
}
);
assert_eq!(
mi.after,
Span {
data: "blah",
line: 4,
col: 1,
offset: 28
}
);
}
#[test]
fn title_applies_header_substitutions() {
let mut parser = Parser::default();
let mi = crate::document::Header::parse(
crate::Span::new("= The Title & Some{sp}Nonsense\n:foo: bar\n\nblah"),
&mut parser,
)
.unwrap_if_no_warnings();
assert_eq!(
mi.item,
Header {
title_source: Some(Span {
data: "The Title & Some{sp}Nonsense",
line: 1,
col: 3,
offset: 2,
}),
title: Some("The Title & Some Nonsense"),
attributes: &[Attribute {
name: Span {
data: "foo",
line: 2,
col: 2,
offset: 32,
},
value_source: Some(Span {
data: "bar",
line: 2,
col: 7,
offset: 37,
}),
value: InterpretedValue::Value("bar"),
source: Span {
data: ":foo: bar",
line: 2,
col: 1,
offset: 31,
}
}],
author_line: None,
revision_line: None,
comments: &[],
source: Span {
data: "= The Title & Some{sp}Nonsense\n:foo: bar",
line: 1,
col: 1,
offset: 0,
}
}
);
assert_eq!(
mi.after,
Span {
data: "blah",
line: 4,
col: 1,
offset: 42
}
);
}
#[test]
fn attribute_without_title() {
let mut parser = Parser::default();
let mi = crate::document::Header::parse(crate::Span::new(":foo: bar\n\nblah"), &mut parser)
.unwrap_if_no_warnings();
assert_eq!(
mi.item,
Header {
title_source: None,
title: None,
attributes: &[Attribute {
name: Span {
data: "foo",
line: 1,
col: 2,
offset: 1,
},
value_source: Some(Span {
data: "bar",
line: 1,
col: 7,
offset: 6,
}),
value: InterpretedValue::Value("bar"),
source: Span {
data: ":foo: bar",
line: 1,
col: 1,
offset: 0,
}
}],
author_line: None,
revision_line: None,
comments: &[],
source: Span {
data: ":foo: bar",
line: 1,
col: 1,
offset: 0,
}
}
);
assert_eq!(
mi.after,
Span {
data: "blah",
line: 3,
col: 1,
offset: 11
}
);
}
#[test]
fn sets_doctitle_attribute() {
let mut parser = Parser::default();
let _doc = parser.parse("= Document Title Goes Here");
assert_eq!(
parser.attribute_value("doctitle"),
InterpretedValue::Value("Document Title Goes Here")
);
}
#[test]
fn sets_author_attributes_from_author_attribute() {
let mut parser = Parser::default();
let _doc = parser.parse(":author: John Q. Smith <john@example.com>");
assert_eq!(
parser.attribute_value("firstname"),
InterpretedValue::Value("John")
);
assert_eq!(
parser.attribute_value("middlename"),
InterpretedValue::Value("Q.")
);
assert_eq!(
parser.attribute_value("lastname"),
InterpretedValue::Value("Smith")
);
assert_eq!(
parser.attribute_value("authorinitials"),
InterpretedValue::Value("JQS")
);
assert_eq!(
parser.attribute_value("email"),
InterpretedValue::Value("john@example.com")
);
assert_eq!(
parser.attribute_value("author"),
InterpretedValue::Value("John Q. Smith <john@example.com>")
);
}
#[test]
fn author_attribute_with_four_or_more_parts_is_partitioned() {
let mut parser = Parser::default();
let _doc = parser.parse(":author: Leroy Harold Scherer, Jr.");
assert_eq!(
parser.attribute_value("author"),
InterpretedValue::Value("Leroy Harold Scherer, Jr.")
);
assert_eq!(
parser.attribute_value("firstname"),
InterpretedValue::Value("Leroy")
);
assert_eq!(
parser.attribute_value("middlename"),
InterpretedValue::Value("Harold")
);
assert_eq!(
parser.attribute_value("lastname"),
InterpretedValue::Value("Scherer, Jr.")
);
assert_eq!(
parser.attribute_value("authorinitials"),
InterpretedValue::Value("LHS")
);
}
#[test]
fn author_attribute_two_part_fallback_partitions_lastname() {
let mut parser = Parser::default();
let _doc = parser.parse(":author: Jane, Doe");
assert_eq!(
parser.attribute_value("author"),
InterpretedValue::Value("Jane, Doe")
);
assert_eq!(
parser.attribute_value("firstname"),
InterpretedValue::Value("Jane,")
);
assert_eq!(
parser.attribute_value("middlename"),
InterpretedValue::Unset
);
assert_eq!(
parser.attribute_value("lastname"),
InterpretedValue::Value("Doe")
);
}
#[test]
fn author_attribute_single_part_fallback_is_firstname_only() {
let mut parser = Parser::default();
let _doc = parser.parse(":author: Jane,");
assert_eq!(
parser.attribute_value("author"),
InterpretedValue::Value("Jane,")
);
assert_eq!(
parser.attribute_value("firstname"),
InterpretedValue::Value("Jane,")
);
assert_eq!(
parser.attribute_value("middlename"),
InterpretedValue::Unset
);
assert_eq!(parser.attribute_value("lastname"), InterpretedValue::Unset);
}
#[test]
fn author_attribute_four_or_more_parts_with_inline_email() {
let mut parser = Parser::default();
let _doc = parser.parse(":author: Leroy Harold Scherer, Jr. <leroy@example.com>");
assert_eq!(
parser.attribute_value("firstname"),
InterpretedValue::Value("Leroy")
);
assert_eq!(
parser.attribute_value("middlename"),
InterpretedValue::Value("Harold")
);
assert_eq!(
parser.attribute_value("lastname"),
InterpretedValue::Value("Scherer, Jr.")
);
assert_eq!(
parser.attribute_value("email"),
InterpretedValue::Value("leroy@example.com")
);
assert_eq!(
parser.attribute_value("authorinitials"),
InterpretedValue::Value("LHS")
);
}
#[test]
fn author_attribute_reference_expands_and_partitions() {
let mut parser = Parser::default();
let _doc = parser.parse(":full-name: Leroy Harold Scherer, Jr.\n:author: {full-name}");
assert_eq!(
parser.attribute_value("firstname"),
InterpretedValue::Value("Leroy")
);
assert_eq!(
parser.attribute_value("middlename"),
InterpretedValue::Value("Harold")
);
assert_eq!(
parser.attribute_value("lastname"),
InterpretedValue::Value("Scherer, Jr.")
);
assert_eq!(
parser.attribute_value("authorinitials"),
InterpretedValue::Value("LHS")
);
}
#[test]
fn author_attribute_reference_within_larger_value_expands_and_partitions() {
let mut parser = Parser::default();
let _doc = parser.parse(":rest: Harold Scherer, Jr.\n:author: Leroy {rest}");
assert_eq!(
parser.attribute_value("firstname"),
InterpretedValue::Value("Leroy")
);
assert_eq!(
parser.attribute_value("middlename"),
InterpretedValue::Value("Harold")
);
assert_eq!(
parser.attribute_value("lastname"),
InterpretedValue::Value("Scherer, Jr.")
);
}
#[test]
fn author_attribute_non_breaking_space_is_not_a_name_separator() {
let mut parser = Parser::default();
let _doc = parser.parse(":author: John\u{a0}Doe Scherer, Jr.");
assert_eq!(
parser.attribute_value("firstname"),
InterpretedValue::Value("John\u{a0}Doe")
);
assert_eq!(
parser.attribute_value("middlename"),
InterpretedValue::Value("Scherer,")
);
assert_eq!(
parser.attribute_value("lastname"),
InterpretedValue::Value("Jr.")
);
}
#[test]
fn sets_author_attributes_from_author_attribute_two_names() {
let mut parser = Parser::default();
let _doc = parser.parse(":author: Jane Doe");
assert_eq!(
parser.attribute_value("firstname"),
InterpretedValue::Value("Jane")
);
assert_eq!(
parser.attribute_value("middlename"),
InterpretedValue::Unset
);
assert_eq!(
parser.attribute_value("lastname"),
InterpretedValue::Value("Doe")
);
assert_eq!(
parser.attribute_value("authorinitials"),
InterpretedValue::Value("JD")
);
assert_eq!(parser.attribute_value("email"), InterpretedValue::Unset);
}
#[test]
fn sets_author_attributes_from_author_attribute_single_name() {
let mut parser = Parser::default();
let _doc = parser.parse(":author: Cher");
assert_eq!(
parser.attribute_value("firstname"),
InterpretedValue::Value("Cher")
);
assert_eq!(
parser.attribute_value("middlename"),
InterpretedValue::Unset
);
assert_eq!(parser.attribute_value("lastname"), InterpretedValue::Unset);
assert_eq!(
parser.attribute_value("authorinitials"),
InterpretedValue::Value("C")
);
assert_eq!(parser.attribute_value("email"), InterpretedValue::Unset);
}
#[test]
fn sets_author_attributes_from_empty_string() {
let mut parser = Parser::default();
let _doc = parser.parse(":author:");
assert_eq!(parser.attribute_value("firstname"), InterpretedValue::Unset);
assert_eq!(
parser.attribute_value("middlename"),
InterpretedValue::Unset
);
assert_eq!(parser.attribute_value("lastname"), InterpretedValue::Unset);
assert_eq!(
parser.attribute_value("authorinitials"),
InterpretedValue::Unset
);
assert_eq!(parser.attribute_value("email"), InterpretedValue::Unset);
assert_eq!(parser.attribute_value("author"), InterpretedValue::Set);
}
#[test]
fn authors_from_author_line() {
let doc = Parser::default().parse("= Title\nKismet R. Lee <kismet@asciidoctor.org>");
assert_eq!(doc.authors().len(), 1);
let author = doc.authors().first().unwrap();
assert_eq!(author.name(), "Kismet R. Lee");
assert_eq!(author.email(), Some("kismet@asciidoctor.org"));
assert_eq!(author.initials(), "KRL");
}
#[test]
fn authors_from_author_attribute() {
let doc =
Parser::default().parse("= Title\n:author: Jane Q. Public\n:email: jane@example.com");
assert_eq!(doc.authors().len(), 1);
let author = doc.authors().first().unwrap();
assert_eq!(author.name(), "Jane Q. Public");
assert_eq!(author.firstname(), "Jane");
assert_eq!(author.middlename(), Some("Q."));
assert_eq!(author.lastname(), Some("Public"));
assert_eq!(author.email(), Some("jane@example.com"));
assert_eq!(author.initials(), "JQP");
}
#[test]
fn authors_from_author_attribute_with_inline_email() {
let doc = Parser::default().parse("= Title\n:author: John Q. Smith <john@example.com>");
assert_eq!(doc.authors().len(), 1);
let author = doc.authors().first().unwrap();
assert_eq!(author.name(), "John Q. Smith");
assert_eq!(author.firstname(), "John");
assert_eq!(author.middlename(), Some("Q."));
assert_eq!(author.lastname(), Some("Smith"));
assert_eq!(author.email(), Some("john@example.com"));
assert_eq!(author.initials(), "JQS");
}
#[test]
fn authors_is_empty_without_author_info() {
let doc = Parser::default().parse("= Title\n\nBody.");
assert!(doc.authors().is_empty());
}
#[test]
fn authorcount_reflects_author_line() {
let doc = Parser::default().parse("= Title\nJane Doe; John Smith\n\nBody.");
assert_eq!(doc.authors().len(), 2);
assert_eq!(
doc.attribute_value("authorcount"),
InterpretedValue::Value("2")
);
let doc = Parser::default().parse(":author: Jane Doe\n\nBody.");
assert_eq!(
doc.attribute_value("authorcount"),
InterpretedValue::Value("1")
);
let doc = Parser::default().parse("= Title\n\nBody.");
assert_eq!(
doc.attribute_value("authorcount"),
InterpretedValue::Value("0")
);
}
#[test]
fn explicit_authorinitials_after_author_still_wins() {
let doc = Parser::default().parse(":author: Doc Writer\n:authorinitials: DOC\n\nBody.");
assert_eq!(
doc.attribute_value("authorinitials"),
InterpretedValue::Value("DOC")
);
let doc = Parser::default()
.parse(":author: Jane Roe\n:authorinitials: DOC\n:author: Doc Writer\n\nBody.");
assert_eq!(
doc.attribute_value("author"),
InterpretedValue::Value("Doc Writer")
);
assert_eq!(
doc.attribute_value("authorinitials"),
InterpretedValue::Value("DOC")
);
}
#[test]
fn later_author_entry_redrives_initials_without_explicit_override() {
let doc = Parser::default().parse(":author: Jane Roe\n:author: Doc Writer\n\nBody.");
assert_eq!(
doc.attribute_value("authorinitials"),
InterpretedValue::Value("DW")
);
}
#[test]
fn single_author_entry_sets_combined_authors_attribute() {
let doc = Parser::default().parse(":author: Doc Writer\n\nBody.");
assert_eq!(
doc.attribute_value("author"),
InterpretedValue::Value("Doc Writer")
);
assert_eq!(
doc.attribute_value("authors"),
InterpretedValue::Value("Doc Writer")
);
let doc = Parser::default()
.parse("= T\n:authorinitials: DOC\n:author: Kismet R. Chameleon\n\nBody.");
assert_eq!(
doc.attribute_value("author"),
InterpretedValue::Value("Kismet R. Chameleon")
);
assert_eq!(
doc.attribute_value("authors"),
InterpretedValue::Value("Kismet R. Chameleon")
);
assert_eq!(
doc.attribute_value("authorinitials"),
InterpretedValue::Value("DOC")
);
}
#[test]
fn author_unset_after_entry_leaves_authors_unset() {
let doc = Parser::default().parse(":author: Jane Doe\n:author!:\n\nBody.");
assert_eq!(doc.attribute_value("author"), InterpretedValue::Unset);
assert_eq!(doc.attribute_value("authors"), InterpretedValue::Unset);
assert!(doc.authors().is_empty());
}
#[test]
fn explicit_authorinitials_not_preserved_for_indexed_or_authors_forms() {
let doc = Parser::default().parse(":authorinitials: DOC\n:author_1: Doc Writer\n\nBody.");
assert_eq!(
doc.attribute_value("author"),
InterpretedValue::Value("Doc Writer")
);
assert_eq!(
doc.attribute_value("authorinitials"),
InterpretedValue::Value("DW")
);
}
#[test]
fn authors_attribute_splits_into_indexed_authors() {
let doc = Parser::default().parse(":authors: Jane Doe; John Q. Smith\n\nBody.");
assert_eq!(doc.authors().len(), 2);
assert_eq!(
doc.attribute_value("authors"),
InterpretedValue::Value("Jane Doe, John Q. Smith")
);
assert_eq!(
doc.attribute_value("author"),
InterpretedValue::Value("Jane Doe")
);
assert_eq!(
doc.attribute_value("author_2"),
InterpretedValue::Value("John Q. Smith")
);
assert_eq!(
doc.attribute_value("middlename_2"),
InterpretedValue::Value("Q.")
);
assert_eq!(
doc.attribute_value("authorinitials_2"),
InterpretedValue::Value("JQS")
);
}
#[test]
fn authors_attribute_attaches_companion_emails_and_base_middlename() {
let doc = Parser::default().parse(
":authors: Jane Q. Doe; John Smith\n:email_1: jane@example.com\n:email_2: john@example.com\n\nBody.",
);
let authors = doc.authors();
assert_eq!(authors.len(), 2);
assert_eq!(authors.first().unwrap().email(), Some("jane@example.com"));
assert_eq!(authors.get(1).unwrap().email(), Some("john@example.com"));
assert_eq!(
doc.attribute_value("middlename"),
InterpretedValue::Value("Q.")
);
assert_eq!(
doc.attribute_value("email"),
InterpretedValue::Value("jane@example.com")
);
assert_eq!(
doc.attribute_value("email_2"),
InterpretedValue::Value("john@example.com")
);
}
#[test]
fn authors_attribute_semicolon_without_space_is_one_author() {
let doc = Parser::default().parse(":authors: Joe Doe;Smith Johnson\n\nBody.");
assert_eq!(doc.authors().len(), 1);
assert_eq!(
doc.attribute_value("authorcount"),
InterpretedValue::Value("1")
);
}
#[test]
fn authors_attribute_single_name_authors_and_trailing_separator() {
let doc = Parser::default().parse(":authors: Cher; Madonna;\n\nBody.");
assert_eq!(doc.authors().len(), 2);
assert_eq!(
doc.attribute_value("authors"),
InterpretedValue::Value("Cher, Madonna")
);
assert_eq!(
doc.attribute_value("author"),
InterpretedValue::Value("Cher")
);
assert_eq!(doc.attribute_value("lastname"), InterpretedValue::Unset);
assert_eq!(
doc.attribute_value("authorinitials"),
InterpretedValue::Value("C")
);
assert_eq!(
doc.attribute_value("author_2"),
InterpretedValue::Value("Madonna")
);
assert_eq!(doc.attribute_value("lastname_2"), InterpretedValue::Unset);
assert_eq!(
doc.attribute_value("authorcount"),
InterpretedValue::Value("2")
);
}
#[test]
fn authors_attribute_with_only_empty_entries_yields_no_authors() {
let doc = Parser::default().parse(":authors: ;\n\nBody.");
assert!(doc.authors().is_empty());
assert_eq!(doc.attribute_value("author"), InterpretedValue::Unset);
assert_eq!(
doc.attribute_value("authorcount"),
InterpretedValue::Value("0")
);
assert_eq!(doc.attribute_value("authors"), InterpretedValue::Value(";"));
}
#[test]
fn author_attribute_takes_precedence_over_authors() {
let doc = Parser::default()
.parse(":author: Solo Writer\n:authors: Jane Doe; John Smith\n\nBody.");
assert_eq!(doc.authors().len(), 1);
assert_eq!(
doc.attribute_value("author"),
InterpretedValue::Value("Solo Writer")
);
assert_eq!(doc.attribute_value("author_2"), InterpretedValue::Unset);
}
#[test]
fn author_unset_after_being_assigned_yields_no_authors() {
let doc = Parser::default().parse("= Title\n:author: Jane Doe\n:author!:\n\nBody.");
assert_eq!(doc.attribute_value("author"), InterpretedValue::Unset);
assert!(doc.authors().is_empty());
}
#[test]
fn authors_entry_replacing_a_longer_implicit_list_leaves_stale_attributes() {
let mut parser = Parser::default();
let doc = parser.parse(
"= T\nKismet R. Lee <kismet@example.com>; Junior Writer; Third Author\n:authors: Stuart Rackham; Dan Allen\n",
);
let authors = doc.header().authors();
assert_eq!(authors.len(), 2);
assert_eq!(authors.first().unwrap().name(), "Stuart Rackham");
assert_eq!(authors.get(1).unwrap().name(), "Dan Allen");
assert_eq!(
parser.attribute_value("authorcount"),
InterpretedValue::Value("2")
);
assert_eq!(
parser.attribute_value("authors"),
InterpretedValue::Value("Stuart Rackham, Dan Allen")
);
assert_eq!(
parser.attribute_value("author_1"),
InterpretedValue::Value("Stuart Rackham")
);
assert_eq!(
parser.attribute_value("author_2"),
InterpretedValue::Value("Dan Allen")
);
assert_eq!(
parser.attribute_value("author_3"),
InterpretedValue::Value("Third Author")
);
assert_eq!(
parser.attribute_value("email"),
InterpretedValue::Value("kismet@example.com")
);
assert_eq!(
parser.attribute_value("middlename"),
InterpretedValue::Value("R.")
);
}
#[test]
fn impl_debug() {
let doc = Parser::default().parse("= Example Title\n\nabc\n\ndef");
let header = doc.header();
assert_eq!(
format!("{header:#?}"),
r#"Header {
title_source: Some(
Span {
data: "Example Title",
line: 1,
col: 3,
offset: 2,
},
),
title: Some(
"Example Title",
),
doctitle: Some(
"Example Title",
),
main_title: Some(
"Example Title",
),
subtitle: None,
id: None,
roles: [],
attributes: &[],
author_line: None,
authors: [],
revision_line: None,
comments: &[],
source: Span {
data: "= Example Title",
line: 1,
col: 1,
offset: 0,
},
}"#
);
}
#[test]
fn no_subtitle() {
let doc = Parser::default().parse("= Just the Title");
let header = doc.header();
assert_eq!(header.title(), Some("Just the Title"));
assert_eq!(header.main_title(), Some("Just the Title"));
assert_eq!(header.subtitle(), None);
}
#[test]
fn no_title() {
let doc = Parser::default().parse(":foo: bar\n\nbody");
let header = doc.header();
assert_eq!(header.title(), None);
assert_eq!(header.main_title(), None);
assert_eq!(header.subtitle(), None);
}
#[test]
fn colon_without_space_is_not_a_separator() {
let doc = Parser::default().parse("= Ratio 3:1 Explained");
let header = doc.header();
assert_eq!(header.main_title(), Some("Ratio 3:1 Explained"));
assert_eq!(header.subtitle(), None);
}
#[test]
fn subtitle_available_on_document() {
let doc = Parser::default().parse("= Main Title: Subtitle");
assert_eq!(doc.doctitle(), Some("Main Title: Subtitle"));
assert_eq!(doc.subtitle(), Some("Subtitle"));
}
#[test]
fn separator_block_attribute_above_title() {
let doc = Parser::default().parse("[separator=::]\n= Main Title:: Subtitle");
let header = doc.header();
assert_eq!(header.main_title(), Some("Main Title"));
assert_eq!(header.subtitle(), Some("Subtitle"));
let doc = Parser::default().parse("[separator=::]\n= Main: Title:: Subtitle");
let header = doc.header();
assert_eq!(header.main_title(), Some("Main: Title"));
assert_eq!(header.subtitle(), Some("Subtitle"));
}
#[test]
fn separator_attribute_entry_overrides_block_attribute() {
let doc = Parser::default()
.parse("[separator=::]\n= Main Title;; Subtitle\n:title-separator: ;;");
let header = doc.header();
assert_eq!(header.main_title(), Some("Main Title"));
assert_eq!(header.subtitle(), Some("Subtitle"));
}
#[test]
fn unrecognized_block_attribute_above_title_is_consumed() {
let doc = Parser::default().parse("[foo=bar]\n= A Header Title");
let header = doc.header();
assert_eq!(header.title(), Some("A Header Title"));
assert_eq!(header.subtitle(), None);
assert_eq!(doc.attribute_value("foo"), InterpretedValue::Unset);
}
#[test]
fn reftext_block_attribute_above_title() {
let doc =
Parser::default().parse("[reftext=\"Links and Stuff\"]\n= Links & Stuff\n\nBody.");
let header = doc.header();
assert_eq!(header.title(), Some("Links & Stuff"));
assert_eq!(
doc.attribute_value("reftext"),
InterpretedValue::Value("Links and Stuff")
);
assert_eq!(rendered_paragraphs(&doc), vec!["Body."]);
}
#[test]
fn id_block_attribute_above_title() {
let doc = Parser::default().parse("[#docid]\n= Document Title\n\nBody.");
let header = doc.header();
assert_eq!(header.title(), Some("Document Title"));
assert_eq!(header.id(), Some("docid"));
assert_eq!(doc.id(), Some("docid"));
let doc = Parser::default().parse("[id=docid]\n= Document Title");
assert_eq!(doc.header().id(), Some("docid"));
}
#[test]
fn bracket_anchor_above_title() {
let doc = Parser::default().parse("[[idname]]\n= Document Title\n\ncontent");
let header = doc.header();
assert_eq!(header.title(), Some("Document Title"));
assert_eq!(header.id(), Some("idname"));
assert_eq!(doc.id(), Some("idname"));
assert_eq!(rendered_paragraphs(&doc), vec!["content"]);
let doc = Parser::default()
.parse(":product: Widgets\n[[guide,{product} Guide]]\n= User Guide\n\ncontent");
let header = doc.header();
assert_eq!(header.title(), Some("User Guide"));
assert_eq!(header.id(), Some("guide"));
assert_eq!(
doc.attribute_value("reftext"),
InterpretedValue::Value("Widgets Guide")
);
}
#[test]
fn bracket_anchor_above_title_requires_a_valid_name() {
let doc = Parser::default().parse("[[bad name]]\n= Document Title\n\ncontent");
let header = doc.header();
assert_eq!(header.title(), None);
assert_eq!(header.id(), None);
}
#[test]
fn stacked_block_attributes_above_title() {
let doc = Parser::default()
.parse("[#docid]\n[reftext=\"Links and Stuff\"]\n= Links & Stuff\n\nBody.");
let header = doc.header();
assert_eq!(header.title(), Some("Links & Stuff"));
assert_eq!(header.id(), Some("docid"));
assert_eq!(doc.id(), Some("docid"));
assert_eq!(
doc.attribute_value("reftext"),
InterpretedValue::Value("Links and Stuff")
);
assert_eq!(rendered_paragraphs(&doc), vec!["Body."]);
}
#[test]
fn stacked_block_attributes_combine_roles() {
let doc = Parser::default().parse("[#docid]\n[.one]\n[.two]\n= Document Title");
let header = doc.header();
assert_eq!(header.title(), Some("Document Title"));
assert_eq!(header.id(), Some("docid"));
assert_eq!(
doc.attribute_value("role"),
InterpretedValue::Value("one two")
);
assert_eq!(header.roles(), vec!["one", "two"]);
}
#[test]
fn stacked_block_attributes_require_a_following_title() {
let doc = Parser::default().parse("[#docid]\n[reftext=\"Stuff\"]\n\nBody.");
let header = doc.header();
assert_eq!(header.title(), None);
assert_eq!(header.id(), None);
assert_eq!(doc.attribute_value("reftext"), InterpretedValue::Unset);
}
#[test]
fn stacked_block_attributes_fold_a_block_anchor() {
let doc = Parser::default().parse("[#docid]\n[[anchor]]\n= Some Title");
let header = doc.header();
assert_eq!(header.title(), Some("Some Title"));
assert_eq!(header.id(), Some("anchor"));
}
#[test]
fn stacked_block_styles_gate_doctitle_by_effective_style() {
let doc = Parser::default().parse("[float]\n[normal]\n= Some Title\n\nbody");
assert_eq!(doc.header().title(), Some("Some Title"));
assert!(all_sections(&doc).is_empty());
let doc = Parser::default().parse("[normal]\n[float]\n= Some Title\n\nbody");
assert_eq!(doc.header().title(), None);
let sec = first_section(&doc);
assert_eq!(sec.section_type(), SectionType::Discrete);
assert_eq!(sec.level(), 0);
assert_eq!(sec.section_title(), "Some Title");
}
#[test]
fn rejected_metadata_run_does_not_fire_counter() {
let doc =
Parser::default().parse("[reftext=\"See {counter:item}\"]\nBody.\n\n{counter:item}");
assert_eq!(doc.header().title(), None);
assert_eq!(rendered_paragraphs(&doc), vec!["Body.", "2"]);
}
#[test]
fn role_block_attribute_above_title() {
let doc = Parser::default().parse("[role=special]\n= Document Title\n\nBody.");
let header = doc.header();
assert_eq!(header.title(), Some("Document Title"));
assert_eq!(
doc.attribute_value("role"),
InterpretedValue::Value("special")
);
assert_eq!(header.roles(), vec!["special"]);
assert_eq!(doc.roles(), vec!["special"]);
let doc = Parser::default().parse("[.one.two]\n= Document Title");
assert_eq!(
doc.attribute_value("role"),
InterpretedValue::Value("one two")
);
assert_eq!(doc.header().roles(), vec!["one", "two"]);
assert_eq!(doc.roles(), vec!["one", "two"]);
}
#[test]
fn roles_empty_without_block_attribute() {
let doc = Parser::default().parse("= Document Title\n\nBody.");
assert!(doc.header().roles().is_empty());
assert!(doc.roles().is_empty());
}
#[test]
fn options_block_attribute_above_title() {
let doc = Parser::default().parse("[opts=\"noheader,autowidth\"]\n= Document Title");
assert!(doc.is_attribute_set("noheader-option"));
assert!(doc.is_attribute_set("autowidth-option"));
let doc = Parser::default().parse("[%hardbreaks]\n= Document Title");
assert!(doc.is_attribute_set("hardbreaks-option"));
}
#[test]
fn bracketed_line_that_is_not_a_separator_attribute_list() {
let doc = Parser::default().parse("[[]]\n= Some Title: Subtitle");
let header = doc.header();
assert_eq!(header.title(), None);
assert_eq!(header.subtitle(), None);
let doc = Parser::default().parse("[ separator=::]\n= Main Title:: Subtitle");
let header = doc.header();
assert_eq!(header.title(), None);
assert_eq!(header.subtitle(), None);
}
#[test]
fn empty_title_separator_falls_back_to_default() {
let doc = Parser::default().parse("= Main Title: Subtitle\n:title-separator:");
let header = doc.header();
assert_eq!(header.main_title(), Some("Main Title"));
assert_eq!(header.subtitle(), Some("Subtitle"));
}
#[test]
fn counter_does_not_shadow_title_separator() {
let doc = Parser::default().parse("= Main Title: Subtitle {counter:title-separator}");
let header = doc.header();
assert_eq!(header.main_title(), Some("Main Title"));
assert_eq!(header.subtitle(), Some("Subtitle 1"));
}
#[test]
fn skips_block_comment_before_author() {
let doc = Parser::default()
.parse("= Title\n////\nAsciidoctor\nrelease artist\n////\nRyan Waldron");
let header = doc.header();
let author = header.authors().first().unwrap();
assert_eq!(author.name(), "Ryan Waldron");
assert_eq!(header.comments().count(), 1);
assert_eq!(
header.comments().next().unwrap().data(),
"////\nAsciidoctor\nrelease artist\n////"
);
}
#[test]
fn skips_block_comment_with_blank_lines() {
let doc = Parser::default().parse("= Title\n////\n\nAsciidoctor\n\n////\nRyan Waldron");
let header = doc.header();
assert_eq!(header.authors().first().unwrap().name(), "Ryan Waldron");
assert_eq!(header.comments().count(), 1);
}
#[test]
fn unterminated_block_comment_consumes_rest_of_header() {
let doc = Parser::default().parse("= Title\n////\nAsciidoctor\nRyan Waldron");
let header = doc.header();
assert!(header.authors().is_empty());
assert_eq!(header.comments().count(), 1);
}
#[test]
fn longer_block_comment_delimiter_requires_matching_close() {
let doc = Parser::default()
.parse("= Title\n/////\nAsciidoctor\n////\nstill comment\n/////\nRyan Waldron");
let header = doc.header();
assert_eq!(header.authors().first().unwrap().name(), "Ryan Waldron");
assert_eq!(header.comments().count(), 1);
}
#[test]
fn three_slashes_is_not_a_block_comment() {
let mut parser = Parser::default();
let _ = parser.parse("= Title\nJoe Cool\nv1.0\n///\nstuff");
assert_eq!(
parser.attribute_value("author"),
InterpretedValue::Value("Joe Cool")
);
assert_eq!(
parser.attribute_value("revnumber"),
InterpretedValue::Value("1.0")
);
}
mod markdown_style_document_title {
use crate::tests::prelude::*;
#[test]
fn hash_marker_is_a_document_title() {
let mut parser = Parser::default();
let mi =
crate::document::Header::parse(crate::Span::new("# Just the Title"), &mut parser)
.unwrap_if_no_warnings();
assert_eq!(
mi.item,
Header {
title_source: Some(Span {
data: "Just the Title",
line: 1,
col: 3,
offset: 2,
}),
title: Some("Just the Title"),
attributes: &[],
author_line: None,
revision_line: None,
comments: &[],
source: Span {
data: "# Just the Title",
line: 1,
col: 1,
offset: 0,
}
}
);
assert_eq!(
mi.after,
Span {
data: "",
line: 1,
col: 17,
offset: 16
}
);
}
#[test]
fn sets_doctitle_attribute() {
let doc = Parser::default().parse("# Doc Title\n\n{doctitle}");
assert_eq!(doc.header().title(), Some("Doc Title"));
assert_eq!(rendered_paragraphs(&doc), vec!["Doc Title"]);
}
#[test]
fn strips_symmetric_close() {
let doc = Parser::default().parse("# Doc Title #");
assert_eq!(doc.header().title(), Some("Doc Title"));
}
#[test]
fn does_not_strip_mismatched_close() {
let doc = Parser::default().parse("# Doc Title =");
assert_eq!(doc.header().title(), Some("Doc Title ="));
}
#[test]
fn requires_whitespace_after_marker() {
let doc = Parser::default().parse("#Doc Title");
assert_eq!(doc.header().title(), None);
assert_eq!(rendered_paragraphs(&doc), vec!["#Doc Title"]);
}
#[test]
fn carries_the_rest_of_the_header() {
let doc = Parser::default()
.parse("# Doc Title\n:foo: bar\nKismet R. Lee <kismet@asciidoctor.org>\nv1.0\n");
let header = doc.header();
assert_eq!(header.title(), Some("Doc Title"));
assert_eq!(header.authors().first().unwrap().firstname(), "Kismet");
assert_eq!(header.revision_line().unwrap().revnumber().unwrap(), "1.0");
}
#[test]
fn partitions_subtitle() {
let doc = Parser::default().parse("# Main Title: Subtitle");
let header = doc.header();
assert_eq!(header.main_title(), Some("Main Title"));
assert_eq!(header.subtitle(), Some("Subtitle"));
}
#[test]
fn separator_block_attribute_above_title() {
let doc = Parser::default().parse("[separator=::]\n# Main Title:: Subtitle");
let header = doc.header();
assert_eq!(header.main_title(), Some("Main Title"));
assert_eq!(header.subtitle(), Some("Subtitle"));
}
#[test]
fn markdown_title_followed_by_markdown_sections() {
let doc = Parser::default().parse("# Doc Title\n\n## Section One\n\nblah blah\n");
assert_eq!(doc.header().title(), Some("Doc Title"));
let section = first_section(&doc);
assert_eq!(section.level(), 1);
assert_eq!(section.section_title(), "Section One");
}
}
}