use std::borrow::Cow;
use crate::{
Anchor, AttributeValue, BlockMetadata, Location, ProcessedContent, Title,
document_attribute::AttributeDeclaration,
grammar::{ParserState, passthrough_processing::replace_passthrough_placeholders},
model::{PositionalAttribute, SectionLevel, substitution::SubstitutionPlan},
};
#[derive(Debug)]
pub(crate) struct PositionWithOffset {
pub(crate) offset: usize,
pub(crate) position: crate::Position,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum MacroAttributeContext {
General,
Image,
}
#[derive(Debug)]
pub(crate) enum BlockMetadataLine<'input> {
Anchor(Anchor<'input>),
Attributes((bool, Box<BlockMetadata<'input>>)),
Title {
source: &'input str,
start: usize,
end: usize,
},
DocumentAttribute(AttributeDeclaration<'input>, Location),
}
#[derive(Debug)]
pub(crate) enum AttributeOrAnchorLine<'input> {
Anchor(Anchor<'input>),
Attributes((bool, Box<BlockMetadata<'input>>)),
}
#[derive(Debug, Default)]
pub(crate) struct BlockParsingMetadata<'input> {
pub(crate) metadata: BlockMetadata<'input>,
pub(crate) title: Title<'input>,
pub(crate) parent_section_level: Option<SectionLevel>,
pub(crate) substitutions: SubstitutionPlan,
pub(crate) hardbreaks: bool,
pub(crate) discrete: bool,
}
#[derive(Debug)]
pub(crate) enum Shorthand<'input> {
Id(Cow<'input, str>),
Role(Cow<'input, str>),
}
pub(crate) const RESERVED_NAMED_ATTRIBUTE_ID: &str = "id";
pub(crate) const RESERVED_NAMED_ATTRIBUTE_ROLE: &str = "role";
pub(crate) const RESERVED_NAMED_ATTRIBUTE_OPTIONS: &str = "opts";
pub(crate) const RESERVED_NAMED_ATTRIBUTE_SUBS: &str = "subs";
pub(crate) fn is_valid_bibliography_id(id: &str) -> bool {
let mut chars = id.chars();
chars
.next()
.is_some_and(|first| first.is_alphabetic() || matches!(first, '_' | ':'))
&& chars.all(|character| {
character.is_alphanumeric() || matches!(character, '_' | '-' | ':' | '.')
})
}
pub(crate) fn strip_url_backslash_escapes(text: &str) -> Cow<'_, str> {
if !text.contains('\\') {
return Cow::Borrowed(text);
}
Cow::Owned(
text.replace("\\...", "...")
.replace("\\->", "->")
.replace("\\<-", "<-")
.replace("\\=>", "=>")
.replace("\\<=", "<=")
.replace("\\--", "--"),
)
}
pub(crate) fn restore_url_path(processed: ProcessedContent<'_>) -> String {
let text = if processed.passthroughs.is_empty() {
processed.text
} else {
replace_passthrough_placeholders(&processed.text, &processed).into()
};
strip_url_backslash_escapes(&text).into_owned()
}
pub(crate) fn parse_comma_separated_values<'a>(
state: &ParserState<'a>,
value: &str,
) -> Vec<&'a str> {
value
.split(',')
.map(str::trim)
.filter(|s| !s.is_empty())
.map(|s| state.intern_str(s))
.collect()
}
pub(crate) fn process_attribute_list<'input>(
attrs: impl IntoIterator<
Item = Option<(
Cow<'input, str>,
AttributeValue<'input>,
Option<(usize, usize)>,
)>,
>,
metadata: &mut BlockMetadata<'input>,
state: &ParserState<'input>,
fallback_start: usize,
fallback_end: usize,
context: MacroAttributeContext,
) -> Option<(usize, usize)> {
let mut title_position = None;
let mut first_positional = true;
for (key, value, pos) in attrs.into_iter().flatten() {
match key.as_ref() {
k if k == RESERVED_NAMED_ATTRIBUTE_ID && metadata.id.is_none() => {
let (id_start, id_end) = pos.unwrap_or((fallback_start, fallback_end));
let id: &'input str = match value {
AttributeValue::String(s) => state.intern_cow(s),
AttributeValue::Bool(_) | AttributeValue::None => {
state.intern_fmt(format_args!("{value}"))
}
};
metadata.id = Some(Anchor {
bibliography_label: None,
id,
xreflabel: None,
location: state.create_location(id_start, id_end),
bibliography: false,
});
}
k if k == RESERVED_NAMED_ATTRIBUTE_ROLE => {
if let AttributeValue::String(ref s) = value {
for role in s.split_whitespace() {
if !role.is_empty() {
metadata.roles.push(state.intern_str(role));
}
}
}
}
k if k == RESERVED_NAMED_ATTRIBUTE_OPTIONS => {
if let AttributeValue::String(ref s) = value {
metadata
.options
.extend(parse_comma_separated_values(state, s));
}
}
k if k == RESERVED_NAMED_ATTRIBUTE_SUBS => {}
"title" => {
if let AttributeValue::String(ref s) = value {
if pos.is_some() {
title_position = pos;
}
metadata
.attributes
.insert(key, AttributeValue::String(s.clone()));
}
}
_ => {
if let AttributeValue::String(ref s) = value {
if context == MacroAttributeContext::Image && key == "link" {
metadata
.attributes
.set(key, AttributeValue::String(s.clone()));
} else {
metadata
.attributes
.insert(key, AttributeValue::String(s.clone()));
}
} else if value == AttributeValue::None {
let key_str: &'input str = state.intern_cow(key);
if first_positional {
metadata.style = Some(key_str);
first_positional = false;
} else {
metadata.positional_attributes.push(PositionalAttribute {
value: key_str,
substitutions: false,
location: None,
});
}
}
}
}
}
title_position
}
pub(crate) fn title_looks_like_description_list(title: &str) -> bool {
let trimmed = title.trim();
for marker in &["::::", ":::", "::", ";;"] {
if let Some(pos) = trimmed.find(marker) &&
pos > 0 &&
let Some(after) = trimmed.get(pos + marker.len()..)
&& (after.is_empty() || after.starts_with(' ') || after.starts_with('\t'))
{
return true;
}
}
false
}