use crate::md_elem::elem::*;
use crate::md_elem::*;
use crate::output::find_numbered_links::find_reserved_link_numbers;
use crate::output::fmt_md_inlines::{InlineElemOptions, MdInlinesWriter};
use crate::util::number_assigner::NumberAssigner;
use crate::util::output::Output;
use clap::ValueEnum;
use std::borrow::Cow;
use std::collections::hash_map::Entry;
use std::collections::HashMap;
#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Default, ValueEnum)]
#[non_exhaustive]
pub enum LinkTransform {
Keep,
Inline,
#[default]
NeverInline,
}
pub(crate) struct LinkTransformer {
delegate: LinkTransformerStrategy,
}
#[derive(Debug, PartialEq, Eq, Hash, Clone)]
pub(crate) enum LinkLabel<'md> {
Text(Cow<'md, str>),
Inline(&'md Vec<Inline>),
}
impl<'md> LinkLabel<'md> {
pub(crate) fn get_sort_string(&self, ctx: &'md MdContext) -> String {
match self {
LinkLabel::Text(s) => s.to_string(),
LinkLabel::Inline(inlines) => {
let mut inline_writer = MdInlinesWriter::new(
ctx,
InlineElemOptions {
link_format: LinkTransform::Keep,
renumber_footnotes: false,
},
&[], );
inlines_to_string(&mut inline_writer, inlines)
}
}
}
}
enum LinkTransformerStrategy {
Keep,
Inline,
NeverInline(ReferenceAssigner),
}
impl LinkTransformer {
pub(crate) fn new(transform: LinkTransform, nodes: &[MdElem], ctx: &MdContext) -> Self {
let delegate = match transform {
LinkTransform::Keep => LinkTransformerStrategy::Keep,
LinkTransform::Inline => LinkTransformerStrategy::Inline,
LinkTransform::NeverInline => LinkTransformerStrategy::NeverInline(ReferenceAssigner::new(nodes, ctx)),
};
Self { delegate }
}
pub(crate) fn apply(&mut self, link: &LinkReference) -> LinkReference {
match &mut self.delegate {
LinkTransformerStrategy::Keep => Cow::Borrowed(link),
LinkTransformerStrategy::Inline => Cow::Owned(LinkReference::Inline),
LinkTransformerStrategy::NeverInline(assigner) => match &link {
LinkReference::Inline => assigner.assign_new(),
LinkReference::Full(link_id) => {
if is_numeric(link_id) {
assigner.reassign(link_id)
} else {
Cow::Borrowed(link)
}
}
LinkReference::Collapsed | LinkReference::Shortcut => {
Cow::Borrowed(link)
}
},
}
.into_owned()
}
}
struct ReferenceAssigner {
index_assigner: NumberAssigner,
reorderings: HashMap<String, u64>,
}
impl ReferenceAssigner {
fn new(nodes: &[MdElem], ctx: &MdContext) -> Self {
let reserved_ints = find_reserved_link_numbers(nodes.iter(), ctx);
let index_assigner = NumberAssigner::new(reserved_ints);
Self {
index_assigner,
reorderings: HashMap::with_capacity(16), }
}
fn reassign<'md>(&mut self, prev: &str) -> Cow<'md, LinkReference> {
match self.reorderings.entry(String::from(prev)) {
Entry::Occupied(map_to) => Cow::Owned(LinkReference::Full(map_to.get().to_string())),
Entry::Vacant(e) => {
let num = self.index_assigner.next_num();
e.insert(num);
Cow::Owned(LinkReference::Full(num.to_string()))
}
}
}
fn assign_new<'md>(&mut self) -> Cow<'md, LinkReference> {
let idx_str = self.index_assigner.next_num().to_string();
Cow::Owned(LinkReference::Full(idx_str))
}
}
pub(crate) fn inlines_to_string<'md>(inline_writer: &mut MdInlinesWriter<'md>, inlines: &'md Vec<Inline>) -> String {
let mut string_writer = Output::without_text_wrapping(String::with_capacity(32)); inline_writer.write_line(&mut string_writer, inlines);
string_writer
.take_underlying()
.expect("internal error while parsing collapsed- or shortcut-style link")
}
fn is_numeric(text: &str) -> bool {
if text.is_empty() {
false
} else if text.as_bytes()[0] == b'0' {
false
} else {
text.as_bytes().iter().all(u8::is_ascii_digit)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::util::utils_for_test::*;
enum Combo {
Of(LinkTransform, LinkReference),
}
variants_checker!(VARIANTS_CHECKER = Combo {
Of(LinkTransform::Keep, LinkReference::Inline),
Of(LinkTransform::Keep, LinkReference::Collapsed),
Of(LinkTransform::Keep, LinkReference::Full(_)),
Of(LinkTransform::Keep, LinkReference::Shortcut),
Of(LinkTransform::Inline, LinkReference::Shortcut),
Of(LinkTransform::Inline, LinkReference::Collapsed),
Of(LinkTransform::Inline, LinkReference::Full(_)),
Of(LinkTransform::Inline, LinkReference::Inline),
Of(LinkTransform::NeverInline, LinkReference::Collapsed),
Of(LinkTransform::NeverInline, LinkReference::Full(_)),
Of(LinkTransform::NeverInline, LinkReference::Inline),
Of(LinkTransform::NeverInline, LinkReference::Shortcut),
});
mod keep {
use super::*;
#[test]
fn inline() {
check_keep(LinkReference::Inline);
}
#[test]
fn collapsed() {
check_keep(LinkReference::Collapsed);
}
#[test]
fn full() {
check_keep(LinkReference::Full("5".to_string()));
}
#[test]
fn shortcut() {
check_keep(LinkReference::Shortcut);
}
fn check_keep(link_ref: LinkReference) {
Given {
transform: LinkTransform::Keep,
label: mdq_inline!("doesn't matter"),
orig_reference: link_ref.clone(),
}
.expect(link_ref);
}
}
mod inline {
use super::*;
#[test]
fn inline() {
check_inline(LinkReference::Inline);
}
#[test]
fn collapsed() {
check_inline(LinkReference::Collapsed);
}
#[test]
fn full() {
check_inline(LinkReference::Full("5".to_string()));
}
#[test]
fn shortcut() {
check_inline(LinkReference::Shortcut);
}
fn check_inline(link_ref: LinkReference) {
Given {
transform: LinkTransform::Inline,
label: mdq_inline!("doesn't matter"),
orig_reference: link_ref,
}
.expect(LinkReference::Inline);
}
}
mod never_inline {
use super::*;
#[test]
fn inline() {
check_never_inline(LinkReference::Inline, LinkReference::Full("1".to_string()));
}
#[test]
fn collapsed() {
check_never_inline(LinkReference::Collapsed, LinkReference::Collapsed);
}
#[test]
fn full() {
check_never_inline(
LinkReference::Full("5".to_string()),
LinkReference::Full("1".to_string()), );
}
#[test]
fn full_with_numeric_display() {
Given {
transform: LinkTransform::NeverInline,
label: mdq_inline!("123"),
orig_reference: LinkReference::Full("a".to_string()),
}
.expect(LinkReference::Full("a".to_string()));
}
#[test]
fn shortcut() {
check_never_inline(LinkReference::Shortcut, LinkReference::Shortcut);
}
fn check_never_inline(link_ref: LinkReference, expect: LinkReference) {
Given {
transform: LinkTransform::NeverInline,
label: mdq_inline!("doesn't matter"),
orig_reference: link_ref,
}
.expect(expect);
}
}
#[test]
fn smoke_test_multi_with_never_inline() {
let a_collapsed = make_link("alpha", LinkReference::Collapsed);
let b_inline = make_link("bravo", LinkReference::Inline);
let c_full_2 = make_link("charlie", LinkReference::Full("2".to_string()));
let d_full_1 = make_link("delta", LinkReference::Full("1".to_string()));
let e_4_collapsed = make_link("4", LinkReference::Collapsed);
let f_3_shortcut = make_link("3", LinkReference::Shortcut);
let g_inline = make_link("golf", LinkReference::Inline);
let as_md_elem: [MdElem; 7] = [
&a_collapsed,
&b_inline,
&c_full_2,
&d_full_1,
&e_4_collapsed,
&f_3_shortcut,
&g_inline,
]
.map(Link::clone)
.map(Link::into);
let ctx = MdContext::empty();
let mut transformer = LinkTransformer::new(LinkTransform::NeverInline, as_md_elem.as_slice(), &ctx);
assert_eq!(transform(&mut transformer, &a_collapsed), LinkReference::Collapsed);
assert_eq!(
transform(&mut transformer, &b_inline),
LinkReference::Full("1".to_string())
);
assert_eq!(
transform(&mut transformer, &c_full_2),
LinkReference::Full("2".to_string())
);
assert_eq!(
transform(&mut transformer, &d_full_1),
LinkReference::Full("5".to_string())
);
assert_eq!(transform(&mut transformer, &e_4_collapsed), LinkReference::Collapsed);
assert_eq!(transform(&mut transformer, &f_3_shortcut), LinkReference::Shortcut);
assert_eq!(
transform(&mut transformer, &g_inline),
LinkReference::Full("6".to_string())
);
}
mod is_numeric {
use super::*;
#[test]
fn only_digits() {
assert!(is_numeric("123"));
}
#[test]
fn leading_zero() {
assert!(!is_numeric("012"));
}
#[test]
fn empty() {
assert!(!is_numeric(""));
}
#[test]
fn non_digits() {
assert!(!is_numeric("hello"));
}
#[test]
fn unicode_numerics() {
assert!(!is_numeric("\u{2174}")); }
}
fn transform(transformer: &mut LinkTransformer, link: &Link) -> LinkReference {
match link {
Link::Standard(standard_link) => transformer.apply(&standard_link.link.reference),
Link::Autolink(autolink) => {
panic!("unexpected autolink: {autolink:?}")
}
}
}
fn make_link(label: &str, link_ref: LinkReference) -> Link {
Link::Standard(StandardLink {
display: vec![Inline::Text(Text {
variant: TextVariant::Plain,
value: label.to_string(),
})],
link: LinkDefinition {
url: "https://example.com".to_string(),
title: None,
reference: link_ref,
},
})
}
struct Given {
transform: LinkTransform,
label: Inline,
orig_reference: LinkReference,
}
impl Given {
fn expect(self, expected: LinkReference) {
let Given {
transform,
label,
orig_reference: reference,
} = self;
let link = Link::Standard(StandardLink {
display: vec![label],
link: LinkDefinition {
url: "https://example.com".to_string(),
title: None,
reference: reference.clone(),
},
});
let link_md_slice: [MdElem; 1] = [link.clone().into()];
let ctx = MdContext::empty();
let mut transformer = LinkTransformer::new(transform, &link_md_slice, &ctx);
let actual = self::transform(&mut transformer, &link);
VARIANTS_CHECKER.see(&Combo::Of(transform, reference.clone()));
assert_eq!(actual, expected);
}
}
}