use super::core::parse_inline_text;
use crate::options::ParserOptions;
use crate::syntax::SyntaxKind;
use rowan::GreenNodeBuilder;
use crate::parser::utils::attributes::try_parse_trailing_attributes;
pub fn try_parse_inline_image(text: &str) -> Option<(usize, &str, &str, Option<&str>)> {
if !text.starts_with("![") {
return None;
}
let mut bracket_depth = 0;
let mut escape_next = false;
let mut close_bracket_pos = None;
for (i, ch) in text[2..].char_indices() {
if escape_next {
escape_next = false;
continue;
}
match ch {
'\\' => escape_next = true,
'[' => bracket_depth += 1,
']' => {
if bracket_depth == 0 {
close_bracket_pos = Some(i + 2);
break;
}
bracket_depth -= 1;
}
_ => {}
}
}
let close_bracket = close_bracket_pos?;
let alt_text = &text[2..close_bracket];
let after_bracket = close_bracket + 1;
if text.len() <= after_bracket || !text[after_bracket..].starts_with('(') {
return None;
}
let dest_start = after_bracket + 1;
let remaining = &text[dest_start..];
let mut paren_depth = 0;
let mut escape_next = false;
let mut in_quotes = false;
let mut close_paren_pos = None;
for (i, ch) in remaining.char_indices() {
if escape_next {
escape_next = false;
continue;
}
match ch {
'\\' => escape_next = true,
'"' => in_quotes = !in_quotes,
'(' if !in_quotes => paren_depth += 1,
')' if !in_quotes => {
if paren_depth == 0 {
close_paren_pos = Some(i);
break;
}
paren_depth -= 1;
}
_ => {}
}
}
let close_paren = close_paren_pos?;
let dest_content = &remaining[..close_paren];
let after_paren = dest_start + close_paren + 1;
let after_close = &text[after_paren..];
if after_close.starts_with('{') {
if let Some(close_brace_pos) = after_close.find('}') {
let attr_text = &after_close[..=close_brace_pos];
if let Some((_attrs, _)) = try_parse_trailing_attributes(attr_text) {
let total_len = after_paren + close_brace_pos + 1;
let raw_attrs = attr_text;
return Some((total_len, alt_text, dest_content, Some(raw_attrs)));
}
}
}
let total_len = after_paren;
Some((total_len, alt_text, dest_content, None))
}
pub fn emit_inline_image(
builder: &mut GreenNodeBuilder,
_text: &str,
alt_text: &str,
dest: &str,
raw_attributes: Option<&str>,
config: &ParserOptions,
) {
builder.start_node(SyntaxKind::IMAGE_LINK.into());
builder.start_node(SyntaxKind::IMAGE_LINK_START.into());
builder.token(SyntaxKind::IMAGE_LINK_START.into(), "![");
builder.finish_node();
builder.start_node(SyntaxKind::IMAGE_ALT.into());
parse_inline_text(builder, alt_text, config, false);
builder.finish_node();
builder.token(SyntaxKind::IMAGE_ALT_END.into(), "]");
builder.token(SyntaxKind::IMAGE_DEST_START.into(), "(");
builder.start_node(SyntaxKind::LINK_DEST.into());
builder.token(SyntaxKind::TEXT.into(), dest);
builder.finish_node();
builder.token(SyntaxKind::IMAGE_DEST_END.into(), ")");
if let Some(raw_attrs) = raw_attributes {
builder.start_node(SyntaxKind::ATTRIBUTE.into());
builder.token(SyntaxKind::ATTRIBUTE.into(), raw_attrs);
builder.finish_node();
}
builder.finish_node();
}
pub fn try_parse_autolink(text: &str) -> Option<(usize, &str)> {
if !text.starts_with('<') {
return None;
}
let close_pos = text[1..].find('>')?;
let content = &text[1..1 + close_pos];
if content.contains(|c: char| c.is_whitespace()) {
return None;
}
if content.is_empty() {
return None;
}
let is_url = content.contains("://") || content.contains(':');
let is_email = content.contains('@');
if !is_url && !is_email {
return None;
}
Some((close_pos + 2, content))
}
pub fn emit_autolink(builder: &mut GreenNodeBuilder, _text: &str, url: &str) {
builder.start_node(SyntaxKind::AUTO_LINK.into());
builder.start_node(SyntaxKind::AUTO_LINK_MARKER.into());
builder.token(SyntaxKind::AUTO_LINK_MARKER.into(), "<");
builder.finish_node();
builder.token(SyntaxKind::TEXT.into(), url);
builder.start_node(SyntaxKind::AUTO_LINK_MARKER.into());
builder.token(SyntaxKind::AUTO_LINK_MARKER.into(), ">");
builder.finish_node();
builder.finish_node();
}
pub fn try_parse_bare_uri(text: &str) -> Option<(usize, &str)> {
let mut chars = text.char_indices();
let (_, first) = chars.next()?;
if !first.is_ascii_alphabetic() {
return None;
}
let mut scheme_end = None;
for (idx, ch) in text.char_indices() {
if ch == ':' {
scheme_end = Some(idx);
break;
}
if !ch.is_ascii_alphanumeric() && ch != '+' && ch != '-' && ch != '.' {
return None;
}
}
let scheme_end = scheme_end?;
if scheme_end == 0 {
return None;
}
let mut end = scheme_end + 1;
let bytes = text.as_bytes();
while end < text.len() {
let b = bytes[end];
if b.is_ascii_whitespace() {
break;
}
if matches!(b, b'<' | b'>' | b'`' | b'"' | b'\'') {
break;
}
end += 1;
}
if end == scheme_end + 1 {
return None;
}
let mut trimmed = end;
while trimmed > scheme_end + 1 {
let ch = text[..trimmed].chars().last().unwrap();
if matches!(ch, '.' | ',' | ';' | ':' | ')' | ']' | '}') {
trimmed -= ch.len_utf8();
} else {
break;
}
}
if trimmed <= scheme_end + 1 {
return None;
}
if text[..trimmed].ends_with('\\') {
return None;
}
Some((trimmed, &text[..trimmed]))
}
pub fn try_parse_inline_link(text: &str) -> Option<(usize, &str, &str, Option<&str>)> {
if !text.starts_with('[') {
return None;
}
let mut bracket_depth = 0;
let mut escape_next = false;
let mut close_bracket_pos = None;
for (i, ch) in text[1..].char_indices() {
if escape_next {
escape_next = false;
continue;
}
match ch {
'\\' => escape_next = true,
'[' => bracket_depth += 1,
']' => {
if bracket_depth == 0 {
close_bracket_pos = Some(i + 1);
break;
}
bracket_depth -= 1;
}
_ => {}
}
}
let close_bracket = close_bracket_pos?;
let link_text = &text[1..close_bracket];
let after_bracket = close_bracket + 1;
if text.len() <= after_bracket || !text[after_bracket..].starts_with('(') {
return None;
}
let dest_start = after_bracket + 1;
let remaining = &text[dest_start..];
let mut paren_depth = 0;
let mut escape_next = false;
let mut in_quotes = false;
let mut close_paren_pos = None;
for (i, ch) in remaining.char_indices() {
if escape_next {
escape_next = false;
continue;
}
match ch {
'\\' => escape_next = true,
'"' => in_quotes = !in_quotes,
'(' if !in_quotes => paren_depth += 1,
')' if !in_quotes => {
if paren_depth == 0 {
close_paren_pos = Some(i);
break;
}
paren_depth -= 1;
}
_ => {}
}
}
let close_paren = close_paren_pos?;
let dest_content = &remaining[..close_paren];
let after_paren = dest_start + close_paren + 1;
let after_close = &text[after_paren..];
if after_close.starts_with('{') {
if let Some(close_brace_pos) = after_close.find('}') {
let attr_text = &after_close[..=close_brace_pos];
if let Some((_attrs, _)) = try_parse_trailing_attributes(attr_text) {
let total_len = after_paren + close_brace_pos + 1;
let raw_attrs = attr_text;
return Some((total_len, link_text, dest_content, Some(raw_attrs)));
}
}
}
let total_len = after_paren;
Some((total_len, link_text, dest_content, None))
}
pub fn emit_inline_link(
builder: &mut GreenNodeBuilder,
_text: &str,
link_text: &str,
dest: &str,
raw_attributes: Option<&str>,
config: &ParserOptions,
) {
builder.start_node(SyntaxKind::LINK.into());
builder.start_node(SyntaxKind::LINK_START.into());
builder.token(SyntaxKind::LINK_START.into(), "[");
builder.finish_node();
builder.start_node(SyntaxKind::LINK_TEXT.into());
parse_inline_text(builder, link_text, config, false);
builder.finish_node();
builder.token(SyntaxKind::LINK_TEXT_END.into(), "]");
builder.token(SyntaxKind::LINK_DEST_START.into(), "(");
builder.start_node(SyntaxKind::LINK_DEST.into());
builder.token(SyntaxKind::TEXT.into(), dest);
builder.finish_node();
builder.token(SyntaxKind::LINK_DEST_END.into(), ")");
if let Some(raw_attrs) = raw_attributes {
builder.start_node(SyntaxKind::ATTRIBUTE.into());
builder.token(SyntaxKind::ATTRIBUTE.into(), raw_attrs);
builder.finish_node();
}
builder.finish_node();
}
pub fn emit_bare_uri_link(builder: &mut GreenNodeBuilder, uri: &str, _config: &ParserOptions) {
builder.start_node(SyntaxKind::LINK.into());
builder.start_node(SyntaxKind::LINK_START.into());
builder.token(SyntaxKind::LINK_START.into(), "[");
builder.finish_node();
builder.start_node(SyntaxKind::LINK_TEXT.into());
builder.token(SyntaxKind::TEXT.into(), uri);
builder.finish_node();
builder.token(SyntaxKind::LINK_TEXT_END.into(), "]");
builder.token(SyntaxKind::LINK_DEST_START.into(), "(");
builder.start_node(SyntaxKind::LINK_DEST.into());
builder.token(SyntaxKind::TEXT.into(), uri);
builder.finish_node();
builder.token(SyntaxKind::LINK_DEST_END.into(), ")");
builder.finish_node();
}
pub fn try_parse_reference_link(
text: &str,
allow_shortcut: bool,
) -> Option<(usize, &str, String, bool)> {
if !text.starts_with('[') {
return None;
}
if text.len() > 1 {
let bytes = text.as_bytes();
if bytes[1] == b'@' {
return None;
}
if bytes[1] == b'-' && text.len() > 2 && bytes[2] == b'@' {
return None;
}
}
let mut bracket_depth = 0;
let mut escape_next = false;
let mut close_bracket_pos = None;
for (i, ch) in text[1..].char_indices() {
if escape_next {
escape_next = false;
continue;
}
match ch {
'\\' => escape_next = true,
'[' => bracket_depth += 1,
']' => {
if bracket_depth == 0 {
close_bracket_pos = Some(i + 1);
break;
}
bracket_depth -= 1;
}
_ => {}
}
}
let close_bracket = close_bracket_pos?;
let link_text = &text[1..close_bracket];
let after_bracket = close_bracket + 1;
if after_bracket < text.len() && text[after_bracket..].starts_with('(') {
return None;
}
if after_bracket < text.len() && text[after_bracket..].starts_with('{') {
return None;
}
if after_bracket < text.len() && text[after_bracket..].starts_with('[') {
let label_start = after_bracket + 1;
let mut label_end = None;
for (i, ch) in text[label_start..].char_indices() {
if ch == ']' {
label_end = Some(i + label_start);
break;
}
if ch == '\n' {
return None;
}
}
let label_end = label_end?;
let label = &text[label_start..label_end];
let total_len = label_end + 1;
if label.is_empty() {
return Some((total_len, link_text, String::new(), false));
}
Some((total_len, link_text, label.to_string(), false))
} else if allow_shortcut {
if link_text.is_empty() {
return None;
}
Some((after_bracket, link_text, link_text.to_string(), true))
} else {
None
}
}
pub fn emit_reference_link(
builder: &mut GreenNodeBuilder,
link_text: &str,
label: &str,
is_shortcut: bool,
config: &ParserOptions,
) {
builder.start_node(SyntaxKind::LINK.into());
builder.start_node(SyntaxKind::LINK_START.into());
builder.token(SyntaxKind::LINK_START.into(), "[");
builder.finish_node();
builder.start_node(SyntaxKind::LINK_TEXT.into());
parse_inline_text(builder, link_text, config, false);
builder.finish_node();
builder.token(SyntaxKind::TEXT.into(), "]");
if !is_shortcut {
builder.token(SyntaxKind::TEXT.into(), "[");
builder.start_node(SyntaxKind::LINK_REF.into());
if !label.is_empty() {
builder.token(SyntaxKind::TEXT.into(), label);
}
builder.finish_node();
builder.token(SyntaxKind::TEXT.into(), "]");
}
builder.finish_node();
}
pub fn try_parse_reference_image(
text: &str,
allow_shortcut: bool,
) -> Option<(usize, &str, String, bool)> {
let bytes = text.as_bytes();
if bytes.len() < 4 || bytes[0] != b'!' || bytes[1] != b'[' {
return None;
}
let mut pos = 2;
let mut bracket_depth = 1;
let alt_start = pos;
while pos < bytes.len() && bracket_depth > 0 {
match bytes[pos] {
b'[' => bracket_depth += 1,
b']' => bracket_depth -= 1,
b'\\' if pos + 1 < bytes.len() => pos += 1, _ => {}
}
pos += 1;
}
if bracket_depth > 0 {
return None; }
let alt_text = &text[alt_start..pos - 1];
if pos >= bytes.len() {
return None;
}
if bytes[pos] == b'[' {
pos += 1;
let label_start = pos;
while pos < bytes.len() && bytes[pos] != b']' && bytes[pos] != b'\n' && bytes[pos] != b'\r'
{
pos += 1;
}
if pos >= bytes.len() || bytes[pos] != b']' {
return None;
}
let label_text = &text[label_start..pos];
pos += 1;
let label = if label_text.is_empty() {
alt_text.to_string() } else {
label_text.to_string() };
return Some((pos, alt_text, label, false));
}
if allow_shortcut {
if pos < bytes.len() && bytes[pos] == b'(' {
return None;
}
let label = alt_text.to_string();
return Some((pos, alt_text, label, true));
}
None
}
pub fn emit_reference_image(
builder: &mut GreenNodeBuilder,
alt_text: &str,
label: &str,
is_shortcut: bool,
config: &ParserOptions,
) {
builder.start_node(SyntaxKind::IMAGE_LINK.into());
builder.start_node(SyntaxKind::IMAGE_LINK_START.into());
builder.token(SyntaxKind::IMAGE_LINK_START.into(), "![");
builder.finish_node();
builder.start_node(SyntaxKind::IMAGE_ALT.into());
parse_inline_text(builder, alt_text, config, false);
builder.finish_node();
builder.token(SyntaxKind::TEXT.into(), "]");
if !is_shortcut {
builder.token(SyntaxKind::TEXT.into(), "[");
builder.start_node(SyntaxKind::LINK_REF.into());
if label != alt_text {
builder.token(SyntaxKind::TEXT.into(), label);
}
builder.finish_node();
builder.token(SyntaxKind::TEXT.into(), "]");
}
builder.finish_node();
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_autolink_url() {
let input = "<https://example.com>";
let result = try_parse_autolink(input);
assert_eq!(result, Some((21, "https://example.com")));
}
#[test]
fn test_parse_autolink_email() {
let input = "<user@example.com>";
let result = try_parse_autolink(input);
assert_eq!(result, Some((18, "user@example.com")));
}
#[test]
fn test_parse_autolink_no_close() {
let input = "<https://example.com";
let result = try_parse_autolink(input);
assert_eq!(result, None);
}
#[test]
fn test_parse_autolink_with_space() {
let input = "<https://example.com >";
let result = try_parse_autolink(input);
assert_eq!(result, None);
}
#[test]
fn test_parse_autolink_not_url_or_email() {
let input = "<notaurl>";
let result = try_parse_autolink(input);
assert_eq!(result, None);
}
#[test]
fn test_parse_inline_link_simple() {
let input = "[text](url)";
let result = try_parse_inline_link(input);
assert_eq!(result, Some((11, "text", "url", None)));
}
#[test]
fn test_parse_inline_link_with_title() {
let input = r#"[text](url "title")"#;
let result = try_parse_inline_link(input);
assert_eq!(result, Some((19, "text", r#"url "title""#, None)));
}
#[test]
fn test_parse_inline_link_with_nested_brackets() {
let input = "[outer [inner] text](url)";
let result = try_parse_inline_link(input);
assert_eq!(result, Some((25, "outer [inner] text", "url", None)));
}
#[test]
fn test_parse_inline_link_no_space_between_brackets_and_parens() {
let input = "[text] (url)";
let result = try_parse_inline_link(input);
assert_eq!(result, None);
}
#[test]
fn test_parse_inline_link_no_closing_bracket() {
let input = "[text(url)";
let result = try_parse_inline_link(input);
assert_eq!(result, None);
}
#[test]
fn test_parse_inline_link_no_closing_paren() {
let input = "[text](url";
let result = try_parse_inline_link(input);
assert_eq!(result, None);
}
#[test]
fn test_parse_inline_link_escaped_bracket() {
let input = r"[text\]more](url)";
let result = try_parse_inline_link(input);
assert_eq!(result, Some((17, r"text\]more", "url", None)));
}
#[test]
fn test_parse_inline_link_parens_in_url() {
let input = "[text](url(with)parens)";
let result = try_parse_inline_link(input);
assert_eq!(result, Some((23, "text", "url(with)parens", None)));
}
#[test]
fn test_parse_inline_image_simple() {
let input = "";
let result = try_parse_inline_image(input);
assert_eq!(result, Some((17, "alt", "image.jpg", None)));
}
#[test]
fn test_parse_inline_image_with_title() {
let input = r#""#;
let result = try_parse_inline_image(input);
assert_eq!(result, Some((27, "alt", r#"image.jpg "A title""#, None)));
}
#[test]
fn test_parse_inline_image_with_nested_brackets() {
let input = "![outer [inner] alt](image.jpg)";
let result = try_parse_inline_image(input);
assert_eq!(result, Some((31, "outer [inner] alt", "image.jpg", None)));
}
#[test]
fn test_parse_bare_uri_rejects_dangling_backslash_after_trim() {
let input = r"a:\]";
let result = try_parse_bare_uri(input);
assert_eq!(result, None);
}
#[test]
fn test_parse_inline_image_no_space_between_brackets_and_parens() {
let input = "![alt] (image.jpg)";
let result = try_parse_inline_image(input);
assert_eq!(result, None);
}
#[test]
fn test_parse_inline_image_no_closing_bracket() {
let input = "![alt(image.jpg)";
let result = try_parse_inline_image(input);
assert_eq!(result, None);
}
#[test]
fn test_parse_inline_image_no_closing_paren() {
let input = ";
assert_eq!(result, None);
}
#[test]
fn test_parse_inline_image_with_simple_class() {
let input = "{.large}";
let result = try_parse_inline_image(input);
let (len, alt, dest, attrs) = result.unwrap();
assert_eq!(len, 23);
assert_eq!(alt, "alt");
assert_eq!(dest, "img.png");
assert!(attrs.is_some());
let attrs = attrs.unwrap();
assert_eq!(attrs, "{.large}");
}
#[test]
fn test_parse_inline_image_with_id() {
let input = "{#fig-1}";
let result = try_parse_inline_image(input);
let (len, alt, dest, attrs) = result.unwrap();
assert_eq!(len, 29);
assert_eq!(alt, "Figure 1");
assert_eq!(dest, "fig1.png");
assert!(attrs.is_some());
let attrs = attrs.unwrap();
assert_eq!(attrs, "{#fig-1}");
}
#[test]
fn test_parse_inline_image_with_full_attributes() {
let input = "{#fig .large width=\"80%\"}";
let result = try_parse_inline_image(input);
let (len, alt, dest, attrs) = result.unwrap();
assert_eq!(len, 40);
assert_eq!(alt, "alt");
assert_eq!(dest, "img.png");
assert!(attrs.is_some());
let attrs = attrs.unwrap();
assert_eq!(attrs, "{#fig .large width=\"80%\"}");
}
#[test]
fn test_parse_inline_image_attributes_must_be_adjacent() {
let input = " {.large}";
let result = try_parse_inline_image(input);
assert_eq!(result, Some((15, "alt", "img.png", None)));
}
#[test]
fn test_parse_inline_link_with_id() {
let input = "[text](url){#link-1}";
let result = try_parse_inline_link(input);
let (len, text, dest, attrs) = result.unwrap();
assert_eq!(len, 20);
assert_eq!(text, "text");
assert_eq!(dest, "url");
assert!(attrs.is_some());
let attrs = attrs.unwrap();
assert_eq!(attrs, "{#link-1}");
}
#[test]
fn test_parse_inline_link_with_full_attributes() {
let input = "[text](url){#link .external target=\"_blank\"}";
let result = try_parse_inline_link(input);
let (len, text, dest, attrs) = result.unwrap();
assert_eq!(len, 44);
assert_eq!(text, "text");
assert_eq!(dest, "url");
assert!(attrs.is_some());
let attrs = attrs.unwrap();
assert_eq!(attrs, "{#link .external target=\"_blank\"}");
}
#[test]
fn test_parse_inline_link_attributes_must_be_adjacent() {
let input = "[text](url) {.class}";
let result = try_parse_inline_link(input);
assert_eq!(result, Some((11, "text", "url", None)));
}
#[test]
fn test_parse_inline_link_with_title_and_attributes() {
let input = r#"[text](url "title"){.external}"#;
let result = try_parse_inline_link(input);
let (len, text, dest, attrs) = result.unwrap();
assert_eq!(len, 30);
assert_eq!(text, "text");
assert_eq!(dest, r#"url "title""#);
assert!(attrs.is_some());
let attrs = attrs.unwrap();
assert_eq!(attrs, "{.external}");
}
#[test]
fn test_parse_reference_link_explicit() {
let input = "[link text][label]";
let result = try_parse_reference_link(input, false);
assert_eq!(result, Some((18, "link text", "label".to_string(), false)));
}
#[test]
fn test_parse_reference_link_implicit() {
let input = "[link text][]";
let result = try_parse_reference_link(input, false);
assert_eq!(result, Some((13, "link text", String::new(), false)));
}
#[test]
fn test_parse_reference_link_explicit_same_label_as_text() {
let input = "[stack][stack]";
let result = try_parse_reference_link(input, false);
assert_eq!(result, Some((14, "stack", "stack".to_string(), false)));
}
#[test]
fn test_parse_reference_link_shortcut() {
let input = "[link text] rest";
let result = try_parse_reference_link(input, true);
assert_eq!(
result,
Some((11, "link text", "link text".to_string(), true))
);
}
#[test]
fn test_parse_reference_link_shortcut_rejects_empty_label() {
let input = "[] rest";
let result = try_parse_reference_link(input, true);
assert_eq!(result, None);
}
#[test]
fn test_parse_reference_link_shortcut_disabled() {
let input = "[link text] rest";
let result = try_parse_reference_link(input, false);
assert_eq!(result, None);
}
#[test]
fn test_parse_reference_link_not_inline_link() {
let input = "[text](url)";
let result = try_parse_reference_link(input, true);
assert_eq!(result, None);
}
#[test]
fn test_parse_reference_link_with_nested_brackets() {
let input = "[outer [inner] text][ref]";
let result = try_parse_reference_link(input, false);
assert_eq!(
result,
Some((25, "outer [inner] text", "ref".to_string(), false))
);
}
#[test]
fn test_parse_reference_link_label_no_newline() {
let input = "[text][label\nmore]";
let result = try_parse_reference_link(input, false);
assert_eq!(result, None);
}
#[test]
fn test_parse_reference_image_explicit() {
let input = "![alt text][label]";
let result = try_parse_reference_image(input, false);
assert_eq!(result, Some((18, "alt text", "label".to_string(), false)));
}
#[test]
fn test_parse_reference_image_implicit() {
let input = "![alt text][]";
let result = try_parse_reference_image(input, false);
assert_eq!(
result,
Some((13, "alt text", "alt text".to_string(), false))
);
}
#[test]
fn test_parse_reference_image_shortcut() {
let input = "![alt text] rest";
let result = try_parse_reference_image(input, true);
assert_eq!(result, Some((11, "alt text", "alt text".to_string(), true)));
}
#[test]
fn test_parse_reference_image_shortcut_disabled() {
let input = "![alt text] rest";
let result = try_parse_reference_image(input, false);
assert_eq!(result, None);
}
#[test]
fn test_parse_reference_image_not_inline() {
let input = "";
let result = try_parse_reference_image(input, true);
assert_eq!(result, None);
}
#[test]
fn test_parse_reference_image_with_nested_brackets() {
let input = "![alt [nested] text][ref]";
let result = try_parse_reference_image(input, false);
assert_eq!(
result,
Some((25, "alt [nested] text", "ref".to_string(), false))
);
}
#[test]
fn test_reference_link_label_with_crlf() {
let input = "[foo\r\nbar]";
let result = try_parse_reference_link(input, false);
assert_eq!(
result, None,
"Should not parse reference link with CRLF in label"
);
}
#[test]
fn test_reference_link_label_with_lf() {
let input = "[foo\nbar]";
let result = try_parse_reference_link(input, false);
assert_eq!(
result, None,
"Should not parse reference link with LF in label"
);
}
#[test]
fn test_parse_inline_link_multiline_text() {
let input = "[text on\nline two](url)";
let result = try_parse_inline_link(input);
assert_eq!(
result,
Some((23, "text on\nline two", "url", None)),
"Link text should allow newlines"
);
}
#[test]
fn test_parse_inline_link_multiline_with_formatting() {
let input =
"[A network graph. Different edges\nwith probability](../images/networkfig.png)";
let result = try_parse_inline_link(input);
assert!(result.is_some(), "Link text with newlines should parse");
let (len, text, _dest, _attrs) = result.unwrap();
assert!(text.contains('\n'), "Link text should preserve newline");
assert_eq!(len, input.len());
}
#[test]
fn test_parse_inline_image_multiline_alt() {
let input = "";
let result = try_parse_inline_image(input);
assert_eq!(
result,
Some((27, "alt on\nline two", "img.png", None)),
"Image alt text should allow newlines"
);
}
#[test]
fn test_parse_inline_image_multiline_with_attributes() {
let input = "{width=70%}";
let result = try_parse_inline_image(input);
assert!(
result.is_some(),
"Image alt with newlines and attributes should parse"
);
let (len, alt, dest, attrs) = result.unwrap();
assert!(alt.contains('\n'), "Alt text should preserve newline");
assert_eq!(dest, "../images/fig.png");
assert_eq!(attrs, Some("{width=70%}"));
assert_eq!(len, input.len());
}
#[test]
fn test_parse_inline_link_with_attributes_after_newline() {
let input = "[A network graph.](../images/networkfig.png){width=70%}\nA word\n";
let result = try_parse_inline_link(input);
assert!(
result.is_some(),
"Link with attributes should parse even with following text"
);
let (len, text, dest, attrs) = result.unwrap();
assert_eq!(text, "A network graph.");
assert_eq!(dest, "../images/networkfig.png");
assert_eq!(attrs, Some("{width=70%}"), "Attributes should be captured");
assert_eq!(
len, 55,
"Length should include attributes (up to closing brace)"
);
}
}