use crate::parser::parse::take_link;
use crate::parser::Link;
use std::borrow::Cow;
use std::collections::HashMap;
use std::mem::swap;
#[derive(Debug, PartialEq)]
struct HyperlinkCollection<'a> {
text2dest_label: Vec<(usize, usize, Link<'a>)>,
label2label: Vec<(Cow<'a, str>, Cow<'a, str>)>,
label2dest: HashMap<Cow<'a, str>, (Cow<'a, str>, Cow<'a, str>)>,
}
impl<'a> HyperlinkCollection<'a> {
fn new() -> Self {
Self {
text2dest_label: Vec::new(),
label2label: Vec::new(),
label2dest: HashMap::new(),
}
}
#[inline]
fn from(input: &'a str, render_label2dest: bool) -> Self {
let mut i = input;
let mut hc = HyperlinkCollection::new();
let mut anonymous_text2label_counter = 0;
let mut anonymous_label2x_counter = 0;
let mut input_idx = 0;
while let Ok((j, (skipped, res))) = take_link(i) {
match res {
l if matches!(l, Link::Text2Dest { .. }) => {
let link_offset = input_idx + skipped.len();
let link_len = i.len() - j.len() - skipped.len();
hc.text2dest_label.push((link_offset, link_len, l));
}
Link::Text2Label(text, mut label) => {
if label == "_" {
anonymous_text2label_counter += 1;
label = Cow::Owned(format!("_{}", anonymous_text2label_counter));
}
let link_offset = input_idx + skipped.len();
let link_len = i.len() - j.len() - skipped.len();
hc.text2dest_label
.push((link_offset, link_len, Link::Text2Label(text, label)))
}
Link::TextLabel2Dest(tl, d, t) => {
let link_offset = input_idx + skipped.len();
let link_len = i.len() - j.len() - skipped.len();
hc.text2dest_label.push((
link_offset,
link_len,
Link::Text2Dest(tl.clone(), d.clone(), t.clone()),
));
hc.label2dest.insert(tl, (d, t));
}
Link::Label2Label(mut from, to) => {
if from == "_" {
anonymous_label2x_counter += 1;
from = Cow::Owned(format!("_{}", anonymous_label2x_counter));
}
hc.label2label.push((from, to));
}
Link::Label2Dest(mut l, d, t) => {
if l == "_" {
anonymous_label2x_counter += 1;
l = Cow::Owned(format!("_{}", anonymous_label2x_counter));
}
if render_label2dest {
let link_offset = input_idx + skipped.len();
let link_len = i.len() - j.len() - skipped.len();
hc.text2dest_label.push((
link_offset,
link_len,
Link::Text2Dest(
Cow::from(&input[link_offset..link_offset + link_len]),
d.clone(),
t.clone(),
),
));
};
hc.label2dest.insert(l, (d, t));
}
_ => unreachable!(),
};
input_idx += i.len() - j.len();
i = j;
}
hc
}
#[inline]
fn resolve_label2label_references(&mut self) {
let mut nb_no_match = 0;
let mut idx = 0;
while !self.label2label.is_empty() && nb_no_match < self.label2label.len() {
let (key_alias, key) = &self.label2label[idx];
if let Some(value) = self.label2dest.get(key) {
let found_new_key = key_alias.clone();
let found_value = value.clone();
self.label2label.remove(idx);
self.label2dest.insert(found_new_key, found_value);
nb_no_match = 0;
} else {
idx += 1;
nb_no_match += 1;
};
if idx >= self.label2label.len() {
idx = 0;
}
}
}
#[inline]
fn resolve_text2label_references(&mut self) {
let mut idx = 0;
while idx < self.text2dest_label.len() {
if let (input_offset, len, Link::Text2Label(text, label)) = &self.text2dest_label[idx] {
if let Some((dest, title)) = &self.label2dest.get(&*label) {
let new_link = if text == "" {
(
*input_offset,
*len,
Link::Text2Dest(dest.clone(), dest.clone(), title.clone()),
)
} else {
(
*input_offset,
*len,
Link::Text2Dest(text.clone(), dest.clone(), title.clone()),
)
};
self.text2dest_label[idx] = new_link;
};
};
idx += 1;
}
}
}
#[derive(Debug, PartialEq)]
enum Status<'a> {
Init,
DirectSearch(&'a str),
ResolvedLinks(Vec<(usize, usize, Link<'a>)>),
End,
}
#[derive(Debug, PartialEq)]
pub struct Hyperlink<'a> {
input: &'a str,
status: Status<'a>,
last_output_offset: usize,
last_output_len: usize,
render_label: bool,
}
impl<'a> Hyperlink<'a> {
#[inline]
pub fn new(input: &'a str, render_label: bool) -> Self {
Self {
input,
status: Status::Init,
last_output_offset: 0,
last_output_len: 0,
render_label,
}
}
}
impl<'a> Iterator for Hyperlink<'a> {
#[allow(clippy::type_complexity)]
type Item = (
(&'a str, &'a str, &'a str),
(Cow<'a, str>, Cow<'a, str>, Cow<'a, str>),
);
fn next(&mut self) -> Option<Self::Item> {
let mut output = None;
let mut status = Status::Init;
swap(&mut status, &mut self.status);
let mut again = true;
while again {
status = match status {
Status::Init => Status::DirectSearch(self.input),
Status::DirectSearch(input) => {
if let Ok((remaining_input, (skipped, Link::Text2Dest(te, de, ti)))) =
take_link(input)
{
let consumed = &input[skipped.len()..input.len() - remaining_input.len()];
output = Some(((skipped, consumed, remaining_input), (te, de, ti)));
debug_assert_eq!(input, {
let mut s = "".to_string();
s.push_str(skipped);
s.push_str(consumed);
s.push_str(remaining_input);
s
});
again = false;
Status::DirectSearch(remaining_input)
} else {
self.input = input;
let mut hc = HyperlinkCollection::from(input, self.render_label);
hc.resolve_label2label_references();
hc.resolve_text2label_references();
let mut resolved_links = Vec::new();
swap(&mut hc.text2dest_label, &mut resolved_links);
Status::ResolvedLinks(resolved_links)
}
}
Status::ResolvedLinks(mut resolved_links) => {
while !resolved_links.is_empty() {
if let (input_offset, len, Link::Text2Dest(te, de, ti)) =
resolved_links.remove(0)
{
let skipped = &self.input
[(self.last_output_offset + self.last_output_len)..input_offset];
let consumed = &self.input[input_offset..input_offset + len];
let remaining_input = &self.input[input_offset + len..];
output = Some(((skipped, consumed, remaining_input), (te, de, ti)));
debug_assert_eq!(self.input, {
let mut s = (&self.input
[..self.last_output_offset + self.last_output_len])
.to_string();
s.push_str(skipped);
s.push_str(consumed);
s.push_str(remaining_input);
s
});
self.last_output_offset = input_offset;
self.last_output_len = len;
break;
};
}
again = false;
if output.is_some() {
Status::ResolvedLinks(resolved_links)
} else {
Status::End
}
}
Status::End => {
again = false;
output = None;
Status::End
}
}
}
swap(&mut status, &mut self.status);
output
}
}
pub fn first_hyperlink(i: &str) -> Option<(Cow<str>, Cow<str>, Cow<str>)> {
if let Some((_, (text, dest, title))) = Hyperlink::new(i, false).next() {
Some((text, dest, title))
} else {
None
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_populate_collection() {
let i = r#"[md label1]: md_destination1 "md title1"
abc [md text2](md_destination2 "md title2")[md text3]: abc[md text4]: abc
[md label5]: md_destination5 "md title5"
abc `rst text1 <rst_destination1>`__abc
abc `rst text2 <rst_label2_>`_ .. _norst: no .. _norst: no
.. _rst label3: rst_destination3
.. _rst label4: rst_d
estination4
__ rst_label5_
__ rst_label6_
abc `rst text5`__abc
abc `rst text6`__abc
abc `rst text_label7 <rst_destination7>`_abc
"#;
let hc = HyperlinkCollection::from(i, false);
let expected = r#"[
(
45,
39,
Text2Dest(
"md text2",
"md_destination2",
"md title2",
),
),
(
84,
10,
Text2Label(
"md text3",
"md text3",
),
),
(
99,
10,
Text2Label(
"md text4",
"md text4",
),
),
(
163,
32,
Text2Dest(
"rst text1",
"rst_destination1",
"",
),
),
(
203,
54,
Text2Label(
"rst text2",
"rst_label2",
),
),
(
366,
13,
Text2Label(
"rst text5",
"_1",
),
),
(
387,
13,
Text2Label(
"rst text6",
"_2",
),
),
(
408,
37,
Text2Dest(
"rst text_label7",
"rst_destination7",
"",
),
),
]"#;
let res = format!("{:#?}", hc.text2dest_label);
assert_eq!(hc.text2dest_label.len(), 8);
assert_eq!(res, expected);
let expected = r#"[
(
"_1",
"rst_label5",
),
(
"_2",
"rst_label6",
),
]"#;
let res = format!("{:#?}", hc.label2label);
assert_eq!(hc.label2label.len(), 2);
assert_eq!(res, expected);
assert_eq!(hc.label2dest.len(), 5);
assert_eq!(
*hc.label2dest.get("md label1").unwrap(),
(Cow::from("md_destination1"), Cow::from("md title1"))
);
assert_eq!(
*hc.label2dest.get("md label5").unwrap(),
(Cow::from("md_destination5"), Cow::from("md title5"))
);
assert_eq!(
*hc.label2dest.get("rst label3").unwrap(),
(Cow::from("rst_destination3"), Cow::from(""))
);
assert_eq!(
*hc.label2dest.get("rst label4").unwrap(),
(Cow::from("rst_destination4"), Cow::from(""))
);
assert_eq!(
*hc.label2dest.get("rst text_label7").unwrap(),
(Cow::from("rst_destination7"), Cow::from(""))
);
}
#[test]
fn test_resolve_label2label_references() {
let i = r#"label2_
.. _label2: rst_destination2
.. _label5: label4_
.. _label1: nolabel_
.. _label4: label3_
.. _label3: label2_
"#;
let mut hc = HyperlinkCollection::from(i, false);
hc.resolve_label2label_references();
assert_eq!(hc.label2label.len(), 1);
assert_eq!(
hc.label2label[0],
(Cow::from("label1"), Cow::from("nolabel"))
);
assert_eq!(hc.label2dest.len(), 4);
assert_eq!(
*hc.label2dest.get("label2").unwrap(),
(Cow::from("rst_destination2"), Cow::from(""))
);
assert_eq!(
*hc.label2dest.get("label3").unwrap(),
(Cow::from("rst_destination2"), Cow::from(""))
);
assert_eq!(
*hc.label2dest.get("label4").unwrap(),
(Cow::from("rst_destination2"), Cow::from(""))
);
assert_eq!(
*hc.label2dest.get("label5").unwrap(),
(Cow::from("rst_destination2"), Cow::from(""))
);
}
#[test]
fn test_resolve_text2label_references() {
let i = r#"abc[text1][label1]abc
abc [text2](destination2 "title2")
[label3]: destination3 "title3"
[label1]: destination1 "title1"
.. _label4: label3_
abc[label3]abc[label5]abc
label4_
"#;
let mut hc = HyperlinkCollection::from(i, false);
hc.resolve_label2label_references();
hc.resolve_text2label_references();
let expected = vec![
(
3,
15,
Link::Text2Dest(
Cow::from("text1"),
Cow::from("destination1"),
Cow::from("title1"),
),
),
(
34,
30,
Link::Text2Dest(
Cow::from("text2"),
Cow::from("destination2"),
Cow::from("title2"),
),
),
(
191,
8,
Link::Text2Dest(
Cow::from("label3"),
Cow::from("destination3"),
Cow::from("title3"),
),
),
(
202,
8,
Link::Text2Label(Cow::from("label5"), Cow::from("label5")),
),
(
222,
7,
Link::Text2Dest(
Cow::from("label4"),
Cow::from("destination3"),
Cow::from("title3"),
),
),
];
assert_eq!(hc.text2dest_label, expected);
}
#[test]
fn test_resolve_text2label_references2() {
let i = r#"
abc `text1 <label1_>`_abc
abc text_label2_ abc
abc text3__ abc
abc text_label4_ abc
abc text5__ abc
.. _label1: destination1
.. _text_label2: destination2
.. __: destination3
__ destination5
"#;
let mut hc = HyperlinkCollection::from(i, false);
hc.resolve_label2label_references();
hc.resolve_text2label_references();
let expected = vec![
(
5,
18,
Link::Text2Dest(Cow::from("text1"), Cow::from("destination1"), Cow::from("")),
),
(
31,
12,
Link::Text2Dest(
Cow::from("text_label2"),
Cow::from("destination2"),
Cow::from(""),
),
),
(
52,
7,
Link::Text2Dest(Cow::from("text3"), Cow::from("destination3"), Cow::from("")),
),
(
68,
12,
Link::Text2Label(Cow::from("text_label4"), Cow::from("text_label4")),
),
(
89,
7,
Link::Text2Dest(Cow::from("text5"), Cow::from("destination5"), Cow::from("")),
),
];
assert_eq!(hc.text2dest_label, expected);
}
#[test]
fn test_resolve_text2label_references3() {
let i = r#"
abc[my homepage]abc
abc
[my homepage]: https://getreu.net
abc"#;
let mut hc = HyperlinkCollection::from(i, false);
eprintln!("{:#?}", hc);
hc.resolve_label2label_references();
hc.resolve_text2label_references();
let expected = vec![(
4,
13,
Link::Text2Dest(
Cow::from("my homepage"),
Cow::from("https://getreu.net"),
Cow::from(""),
),
)];
assert_eq!(hc.text2dest_label, expected);
}
#[test]
fn test_next() {
let i = r#"abc[text0](destination0)abc
abc[text1][label1]abc
abc [text2](destination2 "title2")
[label3]: destination3 "title3"
[label1]: destination1 "title1"
.. _label4: label3_
abc[label3]abc[label5]abc
label4_
"#;
let mut iter = Hyperlink::new(i, false);
let expected = (Cow::from("text0"), Cow::from("destination0"), Cow::from(""));
let item = iter.next().unwrap();
assert_eq!(item.1, expected);
let expected = (
Cow::from("text1"),
Cow::from("destination1"),
Cow::from("title1"),
);
let item = iter.next().unwrap();
assert_eq!(item.1, expected);
let expected = (
Cow::from("text2"),
Cow::from("destination2"),
Cow::from("title2"),
);
let item = iter.next().unwrap();
assert_eq!(item.1, expected);
let expected = (
Cow::from("label3"),
Cow::from("destination3"),
Cow::from("title3"),
);
let item = iter.next().unwrap();
assert_eq!(item.1, expected);
let expected = (
Cow::from("label4"),
Cow::from("destination3"),
Cow::from("title3"),
);
let item = iter.next().unwrap();
assert_eq!(item.1, expected);
let expected = None;
let item = iter.next();
assert_eq!(item, expected);
let expected = None;
let item = iter.next();
assert_eq!(item, expected);
}
}