use crate::{
ElementAttributes, InlineNode,
grammar::{
ParserState,
helpers::BlockParsingMetadata,
inline_preprocessor::{ProcessedKind, SourceMap},
inline_processing::process_inlines_no_autolinks,
inlines::{
index_terms::IndexTermSegment,
text::{RestoredRange, registration_source, restored_label_offset},
},
state::InlineRules,
},
};
use std::{borrow::Cow, ops::Range};
pub(super) struct XrefMacroText<'s> {
pub(super) text: Cow<'s, str>,
pub(super) offset: usize,
pub(super) source_map: SourceMap,
pub(super) as_written: bool,
pub(super) xrefstyle: Option<String>,
pub(super) role: Option<String>,
}
pub(super) fn xref_macro_text(raw: &str, compat_mode: bool) -> XrefMacroText<'_> {
if compat_mode || !raw.contains('=') {
return XrefMacroText {
text: Cow::Borrowed(raw),
offset: 0,
source_map: SourceMap::default(),
as_written: true,
xrefstyle: None,
role: None,
};
}
let mut parsed = XrefMacroText {
text: Cow::Borrowed(""),
offset: 0,
source_map: SourceMap::default(),
as_written: false,
xrefstyle: None,
role: None,
};
for (index, attribute) in crate::grammar::attributes::scan_attribute_list(raw, &[])
.into_iter()
.enumerate()
{
match attribute.name.as_deref() {
Some("xrefstyle") => parsed.xrefstyle = Some(attribute.value),
Some("role") => parsed.role = Some(attribute.value),
None if index == 0 => {
let slice = raw
.get(attribute.value_start..attribute.value_end)
.unwrap_or_default();
parsed.text = if slice == attribute.value {
Cow::Borrowed(slice)
} else {
if let Some(quote @ ('\'' | '"')) = raw
.get(..attribute.value_start)
.and_then(|prefix| prefix.chars().next_back())
{
for (offset, _) in slice.match_indices(&format!("\\{quote}")) {
parsed.source_map.add_replacement(
offset,
offset + 2,
1,
ProcessedKind::Escape,
);
}
}
Cow::Owned(attribute.value)
};
parsed.offset = attribute.value_start;
}
Some(_) | None => {}
}
}
parsed
}
#[derive(Debug)]
pub(super) struct LinkContent<'a> {
pub(super) raw: &'a str,
pub(super) protected: Vec<Range<usize>>,
}
impl LinkContent<'_> {
fn registered_protected_ranges(&self, restored: &[RestoredRange]) -> Cow<'_, [Range<usize>]> {
if restored.is_empty() {
Cow::Borrowed(self.protected.as_slice())
} else {
let reversed: Vec<_> = restored
.iter()
.map(|(a, b)| (b.clone(), a.clone()))
.collect();
Cow::Owned(
self.protected
.iter()
.map(|range| {
restored_label_offset(range.start, &reversed, false)
..restored_label_offset(range.end, &reversed, false)
})
.collect(),
)
}
}
}
#[derive(Default)]
pub(super) struct ProcessedLinkContent<'a> {
pub(super) text: Vec<InlineNode<'a>>,
pub(super) attributes: ElementAttributes<'a>,
pub(super) subject: Option<&'a str>,
pub(super) body: Option<&'a str>,
}
pub(super) fn process_link_content<'a>(
state: &mut ParserState<'a>,
metadata: &BlockParsingMetadata<'_>,
start: usize,
end: usize,
content: &LinkContent<'a>,
mailto: bool,
) -> Result<ProcessedLinkContent<'a>, crate::Error> {
let (raw, restored) = if mailto {
registration_source(
state,
IndexTermSegment {
text: content.raw,
start,
},
)
} else {
(content.raw, Vec::new())
};
let protected = content.registered_protected_ranges(&restored);
let mut text = content.raw;
let mut offset = 0;
let mut quote = None;
let mut result = ProcessedLinkContent::default();
if !state.document_attributes.contains_key("compat-mode")
&& raw.contains(if mailto { ',' } else { '=' })
{
text = "";
for (index, attribute) in crate::grammar::attributes::scan_attribute_list(raw, &protected)
.into_iter()
.enumerate()
{
let attribute_quote = raw[..attribute.value_start]
.chars()
.next_back()
.filter(|character| matches!(character, '\'' | '"'));
if let Some(name) = attribute.name {
let name = match name.as_str() {
"roles" => Cow::Borrowed("role"),
"opts" => Cow::Borrowed("options"),
_ => Cow::Owned(name),
};
let value = if restored.is_empty() {
attribute.value
} else {
let start = restored_label_offset(attribute.value_start, &restored, false);
let end = restored_label_offset(attribute.value_end, &restored, false);
let value = &content.raw[start..end];
attribute_quote.map_or_else(
|| value.to_string(),
|quote| crate::grammar::attributes::unescape_attribute_quote(value, quote),
)
};
result.attributes.set(name, value.into());
} else if index == 0 {
quote = attribute_quote;
offset = restored_label_offset(attribute.value_start, &restored, false);
let end = restored_label_offset(attribute.value_end, &restored, false);
text = &content.raw[offset..end];
} else if mailto && matches!(index, 1 | 2) {
let value = state.intern_str(&attribute.value.replace("\\]", "]"));
if index == 1 {
result.subject = Some(value);
} else {
result.body = Some(value);
}
}
}
}
if let Some(label) = text.strip_suffix('^') {
text = label;
result.attributes.insert("window".into(), "_blank".into());
}
if text.is_empty() {
return Ok(result);
}
let rules = state.inline_ctx.rules;
state.inline_ctx.rules.insert(InlineRules::BRACKET_LABEL);
state
.inline_ctx
.rules
.set(InlineRules::DOUBLE_QUOTED_LINK, quote == Some('"'));
state
.inline_ctx
.rules
.set(InlineRules::SINGLE_QUOTED_LINK, quote == Some('\''));
let parsed = process_inlines_no_autolinks(
state,
metadata,
start + offset,
end,
state.inline_ctx.offset,
text,
);
state.inline_ctx.rules = rules;
result.text = parsed?;
Ok(result)
}