#[derive(Debug, Clone, PartialEq, Eq)]
pub enum RefSyntax {
WikilinkStem,
WikilinkPath,
WikilinkStemEmbed,
WikilinkPathEmbed,
WikilinkAliased { display: String },
WikilinkAliasedEmbed { display: String },
MarkdownLink { label: String },
MarkdownImage { alt: String },
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RawRef {
pub text: String,
pub syntax: RefSyntax,
pub byte_from: usize,
pub byte_to: usize,
pub ref_from: usize,
pub ref_to: usize,
}
pub fn extract_md_references(source: &str) -> Vec<RawRef> {
let mask = crate::inert_regions::mask_inert(source);
let mut refs = Vec::new();
scan_range(source, mask.as_bytes(), 0, source.len(), &mut refs);
refs
}
fn scan_range(source: &str, bytes: &[u8], from: usize, to: usize, refs: &mut Vec<RawRef>) {
let len = to;
let mut i = from;
while i < len {
if bytes[i] == b'\\' {
i += 1; if i < len {
#[allow(clippy::string_slice)]
if let Some(ch) = source[i..].chars().next() {
i += ch.len_utf8();
}
}
continue;
}
let is_embed_wikilink = i + 4 < len
&& bytes[i] == b'!'
&& bytes[i+1] == b'['
&& bytes[i+2] == b'[';
let is_wikilink = !is_embed_wikilink
&& i + 3 < len
&& bytes[i] == b'['
&& bytes[i+1] == b'[';
if is_embed_wikilink || is_wikilink {
let token_start = i;
let inner_start = if is_embed_wikilink { i + 3 } else { i + 2 };
if let Some(close) = find_double_bracket(bytes, inner_start, len) {
#[allow(clippy::string_slice)]
let inner = &source[inner_start..close];
let token_end = close + 2;
let (path_part, pipe_part) = match inner.split_once('|') {
Some((before, after)) => (before, Some(after)),
None => (inner, None),
};
if !path_part.trim().is_empty() {
let text = path_part.trim().to_string();
let has_slash = text.contains('/');
let syntax = match (is_embed_wikilink, pipe_part) {
(false, None) => {
if has_slash { RefSyntax::WikilinkPath } else { RefSyntax::WikilinkStem }
}
(false, Some(alias)) => RefSyntax::WikilinkAliased { display: alias.to_string() },
(true, None) => {
if has_slash { RefSyntax::WikilinkPathEmbed } else { RefSyntax::WikilinkStemEmbed }
}
(true, Some(pot)) => RefSyntax::WikilinkAliasedEmbed { display: pot.to_string() },
};
let ref_from = inner_start + (path_part.len() - path_part.trim_start().len());
let ref_to = ref_from + text.len();
refs.push(RawRef {
text,
syntax,
byte_from: token_start,
byte_to: token_end,
ref_from,
ref_to,
});
}
i = token_end;
continue;
}
}
if i + 3 < len && bytes[i] == b'!' && bytes[i+1] == b'[' {
if let Some(link) = parse_md_link(source, bytes, i + 1, len) {
let token_start = i;
refs.push(RawRef {
text: link.path,
syntax: RefSyntax::MarkdownImage { alt: link.label },
byte_from: token_start,
byte_to: link.token_end,
ref_from: link.path_from,
ref_to: link.path_to,
});
i = link.token_end;
continue;
}
}
if bytes[i] == b'[' {
if i + 1 < len && bytes[i+1] != b'[' {
if let Some(link) = parse_md_link(source, bytes, i, len) {
let (label_from, label_to) = (link.label_from, link.label_to);
let end = link.token_end;
refs.push(RawRef {
text: link.path,
syntax: RefSyntax::MarkdownLink { label: link.label },
byte_from: i,
byte_to: end,
ref_from: link.path_from,
ref_to: link.path_to,
});
scan_range(source, bytes, label_from, label_to, refs);
i = end;
continue;
}
}
}
i += 1;
}
}
fn find_double_bracket(bytes: &[u8], start: usize, limit: usize) -> Option<usize> {
let bytes = &bytes[..limit];
let mut j = start;
while j + 1 < bytes.len() {
if bytes[j] == b']' && bytes[j+1] == b']' {
return Some(j);
}
if bytes[j] == b'\n' {
return None;
}
j += 1;
}
None
}
struct ParsedLink {
label: String,
label_from: usize,
label_to: usize,
path: String,
path_from: usize,
path_to: usize,
token_end: usize,
}
fn parse_md_link(
source: &str,
bytes: &[u8],
bracket_pos: usize,
limit: usize,
) -> Option<ParsedLink> {
let len = limit;
let mut depth = 0usize;
let mut j = bracket_pos;
while j < len {
match bytes[j] {
b'[' => { depth += 1; j += 1; }
b']' => {
depth -= 1;
if depth == 0 { break; }
j += 1;
}
b'\n' => return None,
_ => { j += 1; }
}
}
if j >= len || bytes[j] != b']' { return None; }
let label_start = bracket_pos + 1;
let label_end = j;
#[allow(clippy::string_slice)]
let label = source[label_start..label_end].to_string();
let paren_open = j + 1;
if paren_open >= len || bytes[paren_open] != b'(' { return None; }
let mut depth = 0usize;
let mut k = paren_open;
while k < len {
match bytes[k] {
b'(' => { depth += 1; k += 1; }
b')' => {
depth -= 1;
if depth == 0 { break; }
k += 1;
}
b'\n' => return None,
_ => { k += 1; }
}
}
if k >= len || bytes[k] != b')' { return None; }
let path_start = paren_open + 1;
let path_end = k;
#[allow(clippy::string_slice)]
let raw = &source[path_start..path_end];
let trimmed = raw.trim();
let path_from = path_start + (raw.len() - raw.trim_start().len());
let path = strip_link_title(trimmed);
let path_to = path_from + path.len();
Some(ParsedLink {
label,
label_from: label_start,
label_to: label_end,
path,
path_from,
path_to,
token_end: k + 1,
})
}
fn strip_link_title(raw: &str) -> String {
let raw = raw.trim();
if let Some(ws) = raw.rfind(|c: char| c.is_ascii_whitespace()) {
let (path_part, maybe_title) = raw.split_at(ws);
let maybe_title = maybe_title.trim();
let is_title = (maybe_title.starts_with('"') && maybe_title.ends_with('"'))
|| (maybe_title.starts_with('\'') && maybe_title.ends_with('\''))
|| (maybe_title.starts_with('(') && maybe_title.ends_with(')'));
if is_title {
return path_part.trim().to_string();
}
}
raw.to_string()
}
#[cfg(test)]
#[path = "md_extract_tests.rs"]
mod tests;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum PathContainer {
GalleryBody,
HeroBodyMedia,
HeroDirective,
ShortcodeAttr { key: String },
FrontmatterField { key: String },
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AssetPathSpan {
pub path: String,
pub attrs: String,
pub quote: Option<char>,
pub value: std::ops::Range<usize>,
pub outer: std::ops::Range<usize>,
pub container: PathContainer,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct MediaLineSpan {
pub path: String,
pub alt: String,
pub attrs: String,
pub value: std::ops::Range<usize>,
pub value_attrs: String,
pub is_token: bool,
}
pub(crate) fn line_table(source: &str) -> Vec<(usize, usize, usize)> {
let bytes = source.as_bytes();
let mut table = Vec::new();
let mut base = 0usize;
while base <= bytes.len() {
let nl = bytes[base..].iter().position(|&b| b == b'\n');
match nl {
Some(off) => {
let mut content = off;
if content > 0 && bytes[base + content - 1] == b'\r' {
content -= 1;
}
table.push((base, content, off - content + 1));
base += off + 1;
}
None => {
if base < bytes.len() {
table.push((base, bytes.len() - base, 0));
}
break;
}
}
}
table
}
pub fn extract_structural_asset_refs(source: &str) -> Vec<AssetPathSpan> {
let mut v = crate::ast::shortcode_extract::shortcode_asset_spans(source);
v.extend(crate::frontmatter::frontmatter_asset_spans(source));
v.sort_by_key(|s| s.value.start);
v
}