mod cst;
use super::check;
use crate::{SyntaxKind, parse};
#[test]
fn plain_text() {
check("Hello, world!\n");
}
#[test]
fn multi_word_text() {
check("The quick brown fox\n");
}
#[test]
fn text_with_punctuation() {
check("Hello! How are you?\n");
}
#[test]
fn content_then_divert() {
check("Hello -> knot\n");
}
#[test]
fn content_with_escape() {
check("Hello \\# not a tag\n");
}
#[test]
fn content_with_glue() {
check("Hello<>world\n");
}
#[test]
fn content_with_inline_logic() {
check("Hello {name}\n");
}
#[test]
fn content_divert_tags() {
check("Hello -> knot #tag1\n");
}
#[test]
fn bare_divert_line() {
check("-> knot\n");
}
#[test]
fn content_multiple_elements() {
check("Hello <>world {name} -> knot #tag\n");
}
#[test]
fn multiple_glue_operators() {
check("a<>b<>c\n");
}
#[test]
fn escape_backslash() {
check("Hello \\\\ world\n");
}
#[test]
fn escape_open_brace() {
check("Hello \\{ world\n");
}
#[test]
fn text_at_eof_no_newline() {
check("Hello");
}
#[test]
fn content_with_line_comment() {
check("Hello // comment\n");
}
#[test]
fn consecutive_content_lines() {
check("Line one.\nLine two.\n");
}
#[test]
fn glue_between_text() {
check("first<>second\n");
}
#[test]
fn multiple_escapes() {
check("\\# and \\{ and \\|\n");
}
#[test]
fn content_with_block_comment() {
check("Hello /*comment*/ world\n");
}
#[test]
fn glue_at_start() {
check("<>continued\n");
}
#[test]
fn glue_at_end_before_newline() {
check("text<>\n");
}
#[test]
fn insta_content_with_escape() {
let p = parse("Hello \\# not a tag\n");
insta::assert_snapshot!(format!("{:#?}", p.syntax()));
}
#[test]
fn insta_content_multiple_elements() {
let p = parse("Hello <>world {name} -> knot #tag\n");
insta::assert_snapshot!(format!("{:#?}", p.syntax()));
}
#[test]
fn insta_plain_text() {
let p = parse("Hello, world!\n");
insta::assert_snapshot!(format!("{:#?}", p.syntax()));
}
#[test]
fn insta_glue_between_text() {
let p = parse("a<>b\n");
insta::assert_snapshot!(format!("{:#?}", p.syntax()));
}
#[test]
fn insta_multiple_escapes() {
let p = parse("\\# \\{ \\\\\n");
insta::assert_snapshot!(format!("{:#?}", p.syntax()));
}
#[test]
fn insta_content_divert_tags() {
let p = parse("Hello -> knot #tag\n");
insta::assert_snapshot!(format!("{:#?}", p.syntax()));
}
#[test]
fn plain_text_has_mixed_content() {
let p = parse("Hello\n");
let root = p.syntax();
let has_mixed = root
.descendants()
.any(|n| n.kind() == SyntaxKind::MIXED_CONTENT);
let has_text = root.descendants().any(|n| n.kind() == SyntaxKind::TEXT);
let has_glue = root
.descendants()
.any(|n| n.kind() == SyntaxKind::GLUE_NODE);
let has_escape = root.descendants().any(|n| n.kind() == SyntaxKind::ESCAPE);
let has_divert = root
.descendants()
.any(|n| n.kind() == SyntaxKind::DIVERT_NODE);
assert!(has_mixed, "plain text should have MIXED_CONTENT");
assert!(has_text, "plain text should have TEXT");
assert!(!has_glue, "plain text should not have GLUE_NODE");
assert!(!has_escape, "plain text should not have ESCAPE");
assert!(!has_divert, "plain text should not have DIVERT_NODE");
}
#[test]
fn glue_produces_glue_node() {
let p = parse("a<>b\n");
let root = p.syntax();
let has_glue = root
.descendants()
.any(|n| n.kind() == SyntaxKind::GLUE_NODE);
let has_escape = root.descendants().any(|n| n.kind() == SyntaxKind::ESCAPE);
let has_divert = root
.descendants()
.any(|n| n.kind() == SyntaxKind::DIVERT_NODE);
assert!(has_glue, "glue line should have GLUE_NODE");
assert!(!has_escape, "glue line should not have ESCAPE");
assert!(!has_divert, "glue line should not have DIVERT_NODE");
}
#[test]
fn escape_produces_escape() {
let p = parse("\\# tag\n");
let root = p.syntax();
let has_escape = root.descendants().any(|n| n.kind() == SyntaxKind::ESCAPE);
let has_glue = root
.descendants()
.any(|n| n.kind() == SyntaxKind::GLUE_NODE);
let has_divert = root
.descendants()
.any(|n| n.kind() == SyntaxKind::DIVERT_NODE);
assert!(has_escape, "escape line should have ESCAPE");
assert!(!has_glue, "escape line should not have GLUE_NODE");
assert!(!has_divert, "escape line should not have DIVERT_NODE");
}
#[test]
fn bare_divert_no_mixed_content() {
let p = parse("-> knot\n");
let root = p.syntax();
let has_divert = root
.descendants()
.any(|n| n.kind() == SyntaxKind::DIVERT_NODE);
let content_line = root
.descendants()
.find(|n| n.kind() == SyntaxKind::CONTENT_LINE)
.expect("expected CONTENT_LINE");
let has_mixed_child = content_line
.children()
.any(|c| c.kind() == SyntaxKind::MIXED_CONTENT);
assert!(has_divert, "bare divert should have DIVERT_NODE");
assert!(
!has_mixed_child,
"bare divert CONTENT_LINE should not have MIXED_CONTENT child"
);
}
#[test]
fn content_with_divert_has_both() {
let p = parse("Hello -> knot\n");
let root = p.syntax();
let has_mixed = root
.descendants()
.any(|n| n.kind() == SyntaxKind::MIXED_CONTENT);
let has_divert = root
.descendants()
.any(|n| n.kind() == SyntaxKind::DIVERT_NODE);
let has_glue = root
.descendants()
.any(|n| n.kind() == SyntaxKind::GLUE_NODE);
assert!(has_mixed, "content + divert should have MIXED_CONTENT");
assert!(has_divert, "content + divert should have DIVERT_NODE");
assert!(!has_glue, "content + divert should not have GLUE_NODE");
}
#[test]
fn tags_line_not_content_line() {
let p = parse("#tag\n");
let root = p.syntax();
let has_tag_line = root.descendants().any(|n| n.kind() == SyntaxKind::TAG_LINE);
let has_content_line = root
.descendants()
.any(|n| n.kind() == SyntaxKind::CONTENT_LINE);
assert!(has_tag_line, "hash-only line should produce TAG_LINE");
assert!(
!has_content_line,
"hash-only line should not produce CONTENT_LINE"
);
}
#[test]
fn empty_line_not_content_line() {
let p = parse("\n");
let root = p.syntax();
let has_empty = root
.descendants()
.any(|n| n.kind() == SyntaxKind::EMPTY_LINE);
let has_content_line = root
.descendants()
.any(|n| n.kind() == SyntaxKind::CONTENT_LINE);
assert!(has_empty, "bare newline should produce EMPTY_LINE");
assert!(
!has_content_line,
"bare newline should not produce CONTENT_LINE"
);
}
#[test]
fn logic_line_not_content_line() {
let p = parse("~ x = 5\n");
let root = p.syntax();
let has_logic = root
.descendants()
.any(|n| n.kind() == SyntaxKind::LOGIC_LINE);
let has_content_line = root
.descendants()
.any(|n| n.kind() == SyntaxKind::CONTENT_LINE);
assert!(has_logic, "tilde line should produce LOGIC_LINE");
assert!(
!has_content_line,
"tilde line should not produce CONTENT_LINE"
);
}
#[test]
fn block_comment_mid_line_stays_one_content_line() {
let p = parse("Hello /* note */ world\n");
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
let content_lines: Vec<_> = p
.syntax()
.descendants()
.filter(|n| n.kind() == SyntaxKind::CONTENT_LINE)
.collect();
assert_eq!(
content_lines.len(),
1,
"expected exactly one CONTENT_LINE, got {content_lines:?}"
);
let comment_inside_content_line = content_lines[0]
.descendants_with_tokens()
.any(|c| c.kind() == SyntaxKind::BLOCK_COMMENT);
assert!(
comment_inside_content_line,
"BLOCK_COMMENT should be nested inside the CONTENT_LINE"
);
}
#[test]
fn block_comment_elision_preserves_surrounding_whitespace() {
let p = parse("Hello /* note */ world\n");
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
let text_nodes: String = p
.syntax()
.descendants()
.filter(|n| n.kind() == SyntaxKind::TEXT)
.map(|n| n.text().to_string())
.collect();
assert_eq!(
text_nodes, "Hello world",
"comment span alone should be elided, both adjoining spaces kept \
(double space), matching inklecate's own semantics"
);
}
#[test]
fn block_comment_mid_line_matches_astrochili_corpus_case() {
let src = "Before comment ... /* A comment */ ... and after.\n";
let p = parse(src);
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
assert_eq!(src, p.syntax().text().to_string(), "lossless round-trip");
let content_lines = p
.syntax()
.descendants()
.filter(|n| n.kind() == SyntaxKind::CONTENT_LINE)
.count();
assert_eq!(content_lines, 1, "expected exactly one CONTENT_LINE");
let text_nodes: String = p
.syntax()
.descendants()
.filter(|n| n.kind() == SyntaxKind::TEXT)
.map(|n| n.text().to_string())
.collect();
assert_eq!(
text_nodes, "Before comment ... ... and after.",
"must match inklecate's compiled output byte-for-byte (double \
space — both spaces surrounding the elided comment survive)"
);
}
#[test]
fn block_comment_between_interpolations_preserves_whitespace() {
let p = parse("Hello {a} /* c */ {b}\n");
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
let content_lines = p
.syntax()
.descendants()
.filter(|n| n.kind() == SyntaxKind::CONTENT_LINE)
.count();
assert_eq!(content_lines, 1, "expected exactly one CONTENT_LINE");
let text_nodes: String = p
.syntax()
.descendants()
.filter(|n| n.kind() == SyntaxKind::TEXT)
.map(|n| n.text().to_string())
.collect();
assert_eq!(
text_nodes, "Hello ",
"both spaces around the elided comment must survive"
);
}
#[test]
fn line_comment_mid_line_unaffected_by_fix() {
let src = "Hello // note\n";
let p = parse(src);
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
assert_eq!(src, p.syntax().text().to_string(), "lossless round-trip");
let content_lines = p
.syntax()
.descendants()
.filter(|n| n.kind() == SyntaxKind::CONTENT_LINE)
.count();
assert_eq!(content_lines, 1, "expected exactly one CONTENT_LINE");
let text_nodes: String = p
.syntax()
.descendants()
.filter(|n| n.kind() == SyntaxKind::TEXT)
.map(|n| n.text().to_string())
.collect();
assert_eq!(text_nodes, "Hello ", "text before the line comment only");
}
#[test]
fn block_comment_at_line_start_unchanged() {
let src = "/* c */ Hello\n";
check(src);
let p = parse(src);
let content_lines: Vec<_> = p
.syntax()
.descendants()
.filter(|n| n.kind() == SyntaxKind::CONTENT_LINE)
.collect();
assert_eq!(content_lines.len(), 1, "expected exactly one CONTENT_LINE");
let comment_inside_content_line = content_lines[0]
.descendants_with_tokens()
.any(|c| c.kind() == SyntaxKind::BLOCK_COMMENT);
assert!(
!comment_inside_content_line,
"a line-leading comment stays hoisted above CONTENT_LINE, unchanged by this fix"
);
}
#[test]
fn stray_r_brace_in_content_does_not_hang() {
let src = "Hello } world\n";
let p = parse(src);
assert_eq!(src, p.syntax().text().to_string(), "lossless round-trip");
assert!(
!p.errors().is_empty(),
"stray `{{}}` should still be diagnosed"
);
}
#[test]
fn stray_pipe_in_content_does_not_hang() {
let src = "Hello | world\n";
let p = parse(src);
assert_eq!(src, p.syntax().text().to_string(), "lossless round-trip");
assert!(
!p.errors().is_empty(),
"stray `|` should still be diagnosed"
);
}
#[test]
fn choice_not_content_line() {
let p = parse("* Hello\n");
let root = p.syntax();
let has_choice = root.descendants().any(|n| n.kind() == SyntaxKind::CHOICE);
let has_content_line = root
.descendants()
.any(|n| n.kind() == SyntaxKind::CONTENT_LINE);
assert!(has_choice, "star line should produce CHOICE");
assert!(
!has_content_line,
"star line should not produce CONTENT_LINE"
);
}