use panache_parser::{parse, syntax::SyntaxKind};
use panache_parser::syntax::{
AstNode, Figure, GridTable, Heading, ImageAlt, ImageLink, Link, LinkDest, LinkRef, LinkText,
MultilineTable, PipeTable, SimpleTable, TableCaption, TableCell, TableRow,
};
#[test]
fn link_cast_and_accessors() {
let input = "[text](url)\n";
let tree = parse(input, None);
let link_node = tree
.descendants()
.find(|n| n.kind() == SyntaxKind::LINK)
.expect("Should find LINK node");
let link = Link::cast(link_node).expect("Should cast to Link");
let link_text = link.text().expect("Link should have text");
let text_content = link_text.text_content();
assert!(
text_content.contains("text"),
"Link text should contain 'text'"
);
let link_dest = link.dest().expect("Link should have destination");
let url = link_dest.url_content();
assert_eq!(url, "url", "Link destination should be 'url'");
assert!(
link.reference().is_none(),
"Inline link should not have reference"
);
}
#[test]
fn link_reference_style() {
let input = "[text][ref]\n\n[ref]: url\n";
let tree = parse(input, None);
let link_node = tree
.descendants()
.find(|n| n.kind() == SyntaxKind::LINK)
.expect("Should find LINK node");
let link = Link::cast(link_node).expect("Should cast to Link");
assert!(link.text().is_some(), "Reference link should have text");
let link_ref = link
.reference()
.expect("Reference link should have reference");
let label = link_ref.label();
assert!(
label.contains("ref"),
"Reference label should contain 'ref'"
);
}
#[test]
fn link_text_content() {
let input = "[hello world](url)\n";
let tree = parse(input, None);
let link_text_node = tree
.descendants()
.find(|n| n.kind() == SyntaxKind::LINK_TEXT)
.expect("Should find LINK_TEXT node");
let link_text = LinkText::cast(link_text_node).expect("Should cast to LinkText");
let content = link_text.text_content();
assert!(
content.contains("hello") && content.contains("world"),
"Link text should contain 'hello world'"
);
}
#[test]
fn link_dest_url_with_parentheses() {
let input = "[text](https://example.com)\n";
let tree = parse(input, None);
let dest_node = tree
.descendants()
.find(|n| n.kind() == SyntaxKind::LINK_DEST)
.expect("Should find LINK_DEST node");
let dest = LinkDest::cast(dest_node).expect("Should cast to LinkDest");
let url_with_parens = dest.url();
assert!(
url_with_parens.contains("https://example.com"),
"URL should contain the full URL"
);
let url_content = dest.url_content();
assert_eq!(
url_content, "https://example.com",
"URL content should not include parentheses"
);
}
#[test]
fn link_can_cast_rejects_wrong_kind() {
assert!(
!Link::can_cast(SyntaxKind::IMAGE_LINK),
"Link should not accept IMAGE_LINK kind"
);
assert!(
!Link::can_cast(SyntaxKind::PARAGRAPH),
"Link should not accept PARAGRAPH kind"
);
}
#[test]
fn image_link_cast_and_accessors() {
let input = "\n";
let tree = parse(input, None);
let image_node = tree
.descendants()
.find(|n| n.kind() == SyntaxKind::IMAGE_LINK)
.expect("Should find IMAGE_LINK node");
let image = ImageLink::cast(image_node).expect("Should cast to ImageLink");
let alt = image.alt().expect("Image should have alt text");
let alt_text = alt.text();
assert!(
alt_text.contains("alt text"),
"Image alt should contain 'alt text'"
);
let dest = image.dest().expect("Image should have destination");
let url = dest.url_content();
assert_eq!(url, "image.png", "Image destination should be 'image.png'");
}
#[test]
fn image_alt_text_extraction() {
let input = "\n";
let tree = parse(input, None);
let alt_node = tree
.descendants()
.find(|n| n.kind() == SyntaxKind::IMAGE_ALT)
.expect("Should find IMAGE_ALT node");
let alt = ImageAlt::cast(alt_node).expect("Should cast to ImageAlt");
let text = alt.text();
assert!(
text.contains("hello") && text.contains("world"),
"Alt text should contain 'hello world'"
);
}
#[test]
fn image_reference_emits_link_ref_child() {
let input = "![alt][img]\n\n[img]: image.png\n";
let tree = parse(input, None);
let image_node = tree
.descendants()
.find(|n| n.kind() == SyntaxKind::IMAGE_LINK)
.expect("Should find IMAGE_LINK node");
let image = ImageLink::cast(image_node).expect("Should cast to ImageLink");
let link_ref = image
.reference()
.expect("Reference-style image should have LINK_REF child");
assert_eq!(link_ref.label(), "img");
}
#[test]
fn image_implicit_reference_emits_empty_link_ref_child() {
let input = "![alt][]\n\n[alt]: image.png\n";
let tree = parse(input, None);
let image_node = tree
.descendants()
.find(|n| n.kind() == SyntaxKind::IMAGE_LINK)
.expect("Should find IMAGE_LINK node");
let image = ImageLink::cast(image_node).expect("Should cast to ImageLink");
let link_ref = image
.reference()
.expect("Implicit reference image should have LINK_REF child");
assert_eq!(link_ref.label(), "");
}
#[test]
fn figure_with_image() {
let input = "\n\n";
let tree = parse(input, None);
if let Some(figure_node) = tree.descendants().find(|n| n.kind() == SyntaxKind::FIGURE) {
let figure = Figure::cast(figure_node).expect("Should cast to Figure");
let image = figure.image().expect("Figure should contain image");
assert!(
image.syntax().kind() == SyntaxKind::IMAGE_LINK,
"Figure should contain IMAGE_LINK"
);
}
}
#[test]
fn pipe_table_cast_and_rows() {
let input = "| A | B |\n|---|---|\n| 1 | 2 |\n";
let tree = parse(input, None);
let table_node = tree
.descendants()
.find(|n| n.kind() == SyntaxKind::PIPE_TABLE)
.expect("Should find PIPE_TABLE node");
let table = PipeTable::cast(table_node).expect("Should cast to PipeTable");
assert!(
table.rows().next().is_some(),
"Table should have at least 1 row"
);
}
#[test]
fn pipe_table_with_caption() {
let input = "| A | B |\n|---|---|\n| 1 | 2 |\n\n: Table caption\n";
let tree = parse(input, None);
if let Some(table_node) = tree
.descendants()
.find(|n| n.kind() == SyntaxKind::PIPE_TABLE)
{
let table = PipeTable::cast(table_node).expect("Should cast to PipeTable");
if let Some(caption) = table.caption() {
let text = caption.text();
assert!(
text.contains("Table caption") || text.contains("caption"),
"Caption should contain text"
);
}
}
}
#[test]
fn table_row_and_cells() {
let input = "| A | B | C |\n|---|---|---|\n| 1 | 2 | 3 |\n";
let tree = parse(input, None);
let row_node = tree
.descendants()
.find(|n| n.kind() == SyntaxKind::TABLE_ROW)
.expect("Should find TABLE_ROW node");
let row = TableRow::cast(row_node).expect("Should cast to TableRow");
assert!(row.cells().count() >= 2, "Row should have at least 2 cells");
}
#[test]
fn table_cell_cast() {
let input = "| A | B |\n|---|---|\n| 1 | 2 |\n";
let tree = parse(input, None);
let cell_node = tree
.descendants()
.find(|n| n.kind() == SyntaxKind::TABLE_CELL)
.expect("Should find TABLE_CELL node");
let _cell = TableCell::cast(cell_node).expect("Should cast to TableCell");
}
#[test]
fn grid_table_cast() {
let input = "+---+---+\n| A | B |\n+===+===+\n| 1 | 2 |\n+---+---+\n";
let tree = parse(input, None);
if let Some(table_node) = tree
.descendants()
.find(|n| n.kind() == SyntaxKind::GRID_TABLE)
{
let table = GridTable::cast(table_node).expect("Should cast to GridTable");
assert!(table.rows().next().is_some(), "Grid table should have rows");
let _ = table.caption();
}
}
#[test]
fn simple_table_cast() {
let input = " A B\n --- ---\n 1 2\n";
let tree = parse(input, None);
if let Some(table_node) = tree
.descendants()
.find(|n| n.kind() == SyntaxKind::SIMPLE_TABLE)
{
let table = SimpleTable::cast(table_node).expect("Should cast to SimpleTable");
assert!(
table.rows().next().is_some(),
"Simple table should have rows"
);
let _ = table.caption();
}
}
#[test]
fn multiline_table_cast() {
let input = "-----\nA B\n--- ---\n1 2\n-----\n";
let tree = parse(input, None);
if let Some(table_node) = tree
.descendants()
.find(|n| n.kind() == SyntaxKind::MULTILINE_TABLE)
{
let table = MultilineTable::cast(table_node).expect("Should cast to MultilineTable");
assert!(
table.rows().next().is_some(),
"Multiline table should have rows"
);
let _ = table.caption();
}
}
#[test]
fn table_caption_text() {
let input = "| A |\n|---|\n| 1 |\n\n: My Caption\n";
let tree = parse(input, None);
if let Some(caption_node) = tree
.descendants()
.find(|n| n.kind() == SyntaxKind::TABLE_CAPTION)
{
let caption = TableCaption::cast(caption_node).expect("Should cast to TableCaption");
let text = caption.text();
assert!(
!text.is_empty(),
"Caption should have text, got: '{}'",
text
);
}
}
#[test]
fn heading_cast_and_level() {
let input = "# Level 1\n";
let tree = parse(input, None);
let heading_node = tree
.descendants()
.find(|n| n.kind() == SyntaxKind::HEADING)
.expect("Should find HEADING node");
let heading = Heading::cast(heading_node).expect("Should cast to Heading");
let level = heading.level();
assert_eq!(level, 1, "Heading level should be 1");
let text = heading.text();
assert!(
text.contains("Level 1"),
"Heading text should contain 'Level 1'"
);
}
#[test]
fn heading_multiple_levels() {
for (input, expected_level) in [
("# H1\n", 1),
("## H2\n", 2),
("### H3\n", 3),
("#### H4\n", 4),
("##### H5\n", 5),
("###### H6\n", 6),
] {
let tree = parse(input, None);
let heading_node = tree
.descendants()
.find(|n| n.kind() == SyntaxKind::HEADING)
.expect("Should find HEADING node");
let heading = Heading::cast(heading_node).expect("Should cast to Heading");
assert_eq!(
heading.level(),
expected_level,
"Heading level should be {} for input '{}'",
expected_level,
input.trim()
);
}
}
#[test]
fn heading_with_inline_formatting() {
let input = "# Heading with *emphasis* and `code`\n";
let tree = parse(input, None);
let heading_node = tree
.descendants()
.find(|n| n.kind() == SyntaxKind::HEADING)
.expect("Should find HEADING node");
let heading = Heading::cast(heading_node).expect("Should cast to Heading");
let text = heading.text();
assert!(
text.contains("Heading with") && text.contains(" and "),
"Heading text should include direct text content (got: '{}')",
text
);
let full_text = heading.syntax().text().to_string();
assert!(
full_text.contains("*emphasis*") && full_text.contains("`code`"),
"Full heading should preserve formatting with content"
);
let has_emphasis = heading
.syntax()
.descendants()
.any(|n| n.kind() == SyntaxKind::EMPHASIS);
let has_code = heading
.syntax()
.descendants()
.any(|n| n.kind() == SyntaxKind::INLINE_CODE);
assert!(has_emphasis, "Heading should contain EMPHASIS node");
assert!(has_code, "Heading should contain CODE_SPAN node");
}
#[test]
fn heading_can_cast_rejects_wrong_kind() {
assert!(
!Heading::can_cast(SyntaxKind::PARAGRAPH),
"Heading should not accept PARAGRAPH kind"
);
assert!(
!Heading::can_cast(SyntaxKind::CODE_BLOCK),
"Heading should not accept CODE_BLOCK kind"
);
}
#[test]
fn astnode_can_cast_accepts_matching_kind() {
assert!(Link::can_cast(SyntaxKind::LINK));
assert!(ImageLink::can_cast(SyntaxKind::IMAGE_LINK));
assert!(PipeTable::can_cast(SyntaxKind::PIPE_TABLE));
assert!(Heading::can_cast(SyntaxKind::HEADING));
}
#[test]
fn astnode_cast_returns_none_for_wrong_node() {
let input = "# Heading\n";
let tree = parse(input, None);
let heading_node = tree
.descendants()
.find(|n| n.kind() == SyntaxKind::HEADING)
.expect("Should find HEADING node");
assert!(
Link::cast(heading_node.clone()).is_none(),
"Should not cast Heading node to Link"
);
assert!(
PipeTable::cast(heading_node).is_none(),
"Should not cast Heading node to PipeTable"
);
}
#[test]
fn astnode_syntax_returns_underlying_node() {
let input = "[link](url)\n";
let tree = parse(input, None);
let link_node = tree
.descendants()
.find(|n| n.kind() == SyntaxKind::LINK)
.expect("Should find LINK node");
let link = Link::cast(link_node.clone()).expect("Should cast to Link");
assert_eq!(
link.syntax().kind(),
SyntaxKind::LINK,
"syntax() should return the underlying LINK node"
);
assert_eq!(
link.syntax().text().to_string(),
link_node.text().to_string(),
"syntax() should preserve node text"
);
}
#[test]
fn support_child_finds_correct_child() {
let input = "[text](url)\n";
let tree = parse(input, None);
let link_node = tree
.descendants()
.find(|n| n.kind() == SyntaxKind::LINK)
.expect("Should find LINK node");
let link = Link::cast(link_node).expect("Should cast to Link");
assert!(link.text().is_some(), "Should find LINK_TEXT child");
assert!(link.dest().is_some(), "Should find LINK_DEST child");
}
#[test]
fn support_children_iterator() {
let input = "| A | B | C |\n|---|---|---|\n| 1 | 2 | 3 |\n";
let tree = parse(input, None);
if let Some(table_node) = tree
.descendants()
.find(|n| n.kind() == SyntaxKind::PIPE_TABLE)
{
let table = PipeTable::cast(table_node).expect("Should cast to PipeTable");
let mut rows = table.rows();
let first_row = rows.next();
assert!(first_row.is_some(), "Should find TABLE_ROW children");
if let Some(first_row) = first_row {
assert!(
first_row.cells().next().is_some(),
"Row should have TABLE_CELL children"
);
}
}
}
#[test]
fn heading_text_with_nested_emphasis() {
let input = "# Heading with *emphasis* and `code`\n";
let tree = parse(input, None);
let heading_node = tree
.descendants()
.find(|n| n.kind() == SyntaxKind::HEADING)
.expect("Should find HEADING node");
let heading = Heading::cast(heading_node).expect("Should cast to Heading");
let text = heading.text();
assert_eq!(
text, "Heading with emphasis and code",
"Heading text should extract text from nested inline elements"
);
}
#[test]
fn link_text_with_nested_emphasis() {
let input = "[text with *emphasis*](url)\n";
let tree = parse(input, None);
let link_text_node = tree
.descendants()
.find(|n| n.kind() == SyntaxKind::LINK_TEXT)
.expect("Should find LINK_TEXT node");
let link_text = LinkText::cast(link_text_node).expect("Should cast to LinkText");
let content = link_text.text_content();
assert_eq!(
content, "text with emphasis",
"Link text should extract text from nested inline elements"
);
}
#[test]
fn image_alt_with_nested_emphasis() {
let input = "\n";
let tree = parse(input, None);
let alt_node = tree
.descendants()
.find(|n| n.kind() == SyntaxKind::IMAGE_ALT)
.expect("Should find IMAGE_ALT node");
let alt = ImageAlt::cast(alt_node).expect("Should cast to ImageAlt");
let text = alt.text();
assert_eq!(
text, "alt with emphasis",
"Image alt should extract text from nested inline elements"
);
}
#[test]
fn table_caption_with_nested_emphasis() {
let input = "| A |\n|---|\n| 1 |\n\n: Caption with *emphasis*\n";
let tree = parse(input, None);
if let Some(caption_node) = tree
.descendants()
.find(|n| n.kind() == SyntaxKind::TABLE_CAPTION)
{
let caption = TableCaption::cast(caption_node).expect("Should cast to TableCaption");
let text = caption.text();
assert_eq!(
text, "Caption with emphasis",
"Table caption should extract text from nested inline elements"
);
}
}
#[test]
fn link_ref_label_extraction() {
let input = "[text][*emphasis* ref]\n\n[*emphasis* ref]: url\n";
let tree = parse(input, None);
if let Some(link_ref_node) = tree
.descendants()
.find(|n| n.kind() == SyntaxKind::LINK_REF)
{
let link_ref = LinkRef::cast(link_ref_node).expect("Should cast to LinkRef");
let label = link_ref.label();
println!("Link ref label: '{}'", label);
assert!(
label.contains("emphasis") || label.contains("*emphasis*"),
"Link reference label should contain the reference text, got: '{}'",
label
);
}
}
#[test]
fn empty_link_text() {
let input = "[](url)\n";
let tree = parse(input, None);
if let Some(link_node) = tree.descendants().find(|n| n.kind() == SyntaxKind::LINK) {
let link = Link::cast(link_node).expect("Should cast to Link");
if let Some(text) = link.text() {
let content = text.text_content();
assert!(
content.is_empty() || content.trim().is_empty(),
"Empty link should have empty text content"
);
}
}
}
#[test]
fn heading_without_text() {
let input = "#\n";
let tree = parse(input, None);
if let Some(heading_node) = tree.descendants().find(|n| n.kind() == SyntaxKind::HEADING) {
let heading = Heading::cast(heading_node).expect("Should cast to Heading");
assert_eq!(heading.level(), 1, "Should still recognize level");
let _ = heading.text();
}
}
#[test]
fn table_without_caption() {
let input = "| A | B |\n|---|---|\n| 1 | 2 |\n";
let tree = parse(input, None);
if let Some(table_node) = tree
.descendants()
.find(|n| n.kind() == SyntaxKind::PIPE_TABLE)
{
let table = PipeTable::cast(table_node).expect("Should cast to PipeTable");
assert!(
table.caption().is_none(),
"Table without caption should return None"
);
}
}