use crate::parser::tests::cst::{ExpectedNode, assert_equivalent};
use crate::{SyntaxKind, parse};
#[test]
fn depth_one() {
assert_equivalent(
parse("- text\n"),
cst!(SOURCE_FILE {
GATHER {
GATHER_DASHES
MIXED_CONTENT {
TEXT
}
}
}),
);
}
#[test]
fn depth_two_adjacent() {
assert_equivalent(
parse("-- text\n"),
cst!(SOURCE_FILE {
GATHER {
GATHER_DASHES
MIXED_CONTENT {
TEXT
}
}
}),
);
}
#[test]
fn depth_two_spaced() {
assert_equivalent(
parse("- - text\n"),
cst!(SOURCE_FILE {
GATHER {
GATHER_DASHES
MIXED_CONTENT {
TEXT
}
}
}),
);
}
#[test]
fn depth_three() {
assert_equivalent(
parse("--- text\n"),
cst!(SOURCE_FILE {
GATHER {
GATHER_DASHES
MIXED_CONTENT {
TEXT
}
}
}),
);
}
#[test]
fn depth_three_spaced() {
assert_equivalent(
parse("- - - text\n"),
cst!(SOURCE_FILE {
GATHER {
GATHER_DASHES
MIXED_CONTENT {
TEXT
}
}
}),
);
}
#[test]
fn bare_single_dash() {
assert_equivalent(
parse("-\n"),
cst!(SOURCE_FILE {
GATHER {
GATHER_DASHES
}
}),
);
}
#[test]
fn bare_double_dash() {
assert_equivalent(
parse("--\n"),
cst!(SOURCE_FILE {
GATHER {
GATHER_DASHES
}
}),
);
}
#[test]
fn label_with_content() {
assert_equivalent(
parse("- (myLabel) Hello\n"),
cst!(SOURCE_FILE {
GATHER {
GATHER_DASHES
LABEL {
IDENTIFIER
}
MIXED_CONTENT {
TEXT
}
}
}),
);
}
#[test]
fn label_only() {
assert_equivalent(
parse("- (end)\n"),
cst!(SOURCE_FILE {
GATHER {
GATHER_DASHES
LABEL {
IDENTIFIER
}
}
}),
);
}
#[test]
fn label_with_divert() {
assert_equivalent(
parse("- (end) -> done\n"),
cst!(SOURCE_FILE {
GATHER {
GATHER_DASHES
LABEL {
IDENTIFIER
}
DIVERT_NODE {
SIMPLE_DIVERT {
DIVERT_TARGET_WITH_ARGS {
PATH
}
}
}
}
}),
);
}
#[test]
fn label_with_tags() {
assert_equivalent(
parse("- (end) Text #tag\n"),
cst!(SOURCE_FILE {
GATHER {
GATHER_DASHES
LABEL {
IDENTIFIER
}
MIXED_CONTENT {
TEXT
}
TAGS {
TAG
}
}
}),
);
}
#[test]
fn label_with_content_and_divert() {
assert_equivalent(
parse("- (lbl) Text -> next\n"),
cst!(SOURCE_FILE {
GATHER {
GATHER_DASHES
LABEL {
IDENTIFIER
}
MIXED_CONTENT {
TEXT
}
DIVERT_NODE {
SIMPLE_DIVERT {
DIVERT_TARGET_WITH_ARGS {
PATH
}
}
}
}
}),
);
}
#[test]
fn nested_with_label() {
assert_equivalent(
parse("-- (inner) Text\n"),
cst!(SOURCE_FILE {
GATHER {
GATHER_DASHES
LABEL {
IDENTIFIER
}
MIXED_CONTENT {
TEXT
}
}
}),
);
}
#[test]
fn plain_text() {
assert_equivalent(
parse("- Hello world\n"),
cst!(SOURCE_FILE {
GATHER {
GATHER_DASHES
MIXED_CONTENT {
TEXT
}
}
}),
);
}
#[test]
fn content_with_glue() {
assert_equivalent(
parse("- Hello<>world\n"),
cst!(SOURCE_FILE {
GATHER {
GATHER_DASHES
MIXED_CONTENT {
TEXT
GLUE_NODE
TEXT
}
}
}),
);
}
#[test]
fn content_with_escape() {
assert_equivalent(
parse("- Hello \\# not a tag\n"),
cst!(SOURCE_FILE {
GATHER {
GATHER_DASHES
MIXED_CONTENT {
TEXT
ESCAPE
TEXT
}
}
}),
);
}
#[test]
fn content_with_inline_logic() {
assert_equivalent(
parse("- Hello {x}\n"),
cst!(SOURCE_FILE {
GATHER {
GATHER_DASHES
MIXED_CONTENT {
TEXT
INLINE_LOGIC {
INNER_EXPRESSION {
PATH
}
}
}
}
}),
);
}
#[test]
fn content_with_inline_conditional() {
assert_equivalent(
parse("- {x: yes}\n"),
cst!(SOURCE_FILE {
GATHER {
GATHER_DASHES
MIXED_CONTENT {
INLINE_LOGIC {
CONDITIONAL_WITH_EXPR {
PATH
INLINE_BRANCHES_COND {
BRANCH_CONTENT {
TEXT
}
}
}
}
}
}
}),
);
}
#[test]
fn simple_divert() {
assert_equivalent(
parse("- -> knot\n"),
cst!(SOURCE_FILE {
GATHER {
GATHER_DASHES
DIVERT_NODE {
SIMPLE_DIVERT {
DIVERT_TARGET_WITH_ARGS {
PATH
}
}
}
}
}),
);
}
#[test]
fn divert_to_done() {
assert_equivalent(
parse("- -> DONE\n"),
cst!(SOURCE_FILE {
GATHER {
GATHER_DASHES
DIVERT_NODE {
SIMPLE_DIVERT {
DIVERT_TARGET_WITH_ARGS
}
}
}
}),
);
}
#[test]
fn divert_to_end() {
assert_equivalent(
parse("- -> END\n"),
cst!(SOURCE_FILE {
GATHER {
GATHER_DASHES
DIVERT_NODE {
SIMPLE_DIVERT {
DIVERT_TARGET_WITH_ARGS
}
}
}
}),
);
}
#[test]
fn divert_dotted_path() {
assert_equivalent(
parse("- -> knot.stitch\n"),
cst!(SOURCE_FILE {
GATHER {
GATHER_DASHES
DIVERT_NODE {
SIMPLE_DIVERT {
DIVERT_TARGET_WITH_ARGS {
PATH
}
}
}
}
}),
);
}
#[test]
fn divert_with_args() {
assert_equivalent(
parse("- -> func(x)\n"),
cst!(SOURCE_FILE {
GATHER {
GATHER_DASHES
DIVERT_NODE {
SIMPLE_DIVERT {
DIVERT_TARGET_WITH_ARGS {
PATH
ARG_LIST {
PATH
}
}
}
}
}
}),
);
}
#[test]
fn divert_chain() {
assert_equivalent(
parse("- -> a -> b\n"),
cst!(SOURCE_FILE {
GATHER {
GATHER_DASHES
DIVERT_NODE {
SIMPLE_DIVERT {
DIVERT_TARGET_WITH_ARGS {
PATH
}
DIVERT_TARGET_WITH_ARGS {
PATH
}
}
}
}
}),
);
}
#[test]
fn tunnel_call() {
assert_equivalent(
parse("- -> target ->\n"),
cst!(SOURCE_FILE {
GATHER {
GATHER_DASHES
DIVERT_NODE {
TUNNEL_CALL_NODE {
DIVERT_TARGET_WITH_ARGS {
PATH
}
}
}
}
}),
);
}
#[test]
fn tunnel_onwards() {
assert_equivalent(
parse("- ->->\n"),
cst!(SOURCE_FILE {
GATHER {
GATHER_DASHES
DIVERT_NODE {
TUNNEL_ONWARDS_NODE
}
}
}),
);
}
#[test]
fn thread_start() {
assert_equivalent(
parse("- <- background\n"),
cst!(SOURCE_FILE {
GATHER {
GATHER_DASHES
DIVERT_NODE {
THREAD_START {
PATH
}
}
}
}),
);
}
#[test]
fn content_then_divert() {
assert_equivalent(
parse("- Gathered -> next\n"),
cst!(SOURCE_FILE {
GATHER {
GATHER_DASHES
MIXED_CONTENT {
TEXT
}
DIVERT_NODE {
SIMPLE_DIVERT {
DIVERT_TARGET_WITH_ARGS {
PATH
}
}
}
}
}),
);
}
#[test]
fn single_tag() {
assert_equivalent(
parse("- Text #tag1\n"),
cst!(SOURCE_FILE {
GATHER {
GATHER_DASHES
MIXED_CONTENT {
TEXT
}
TAGS {
TAG
}
}
}),
);
}
#[test]
fn multiple_tags() {
assert_equivalent(
parse("- Text #tag1 #tag2\n"),
cst!(SOURCE_FILE {
GATHER {
GATHER_DASHES
MIXED_CONTENT {
TEXT
}
TAGS {
TAG
TAG
}
}
}),
);
}
#[test]
fn tag_after_divert() {
assert_equivalent(
parse("- -> done #tag\n"),
cst!(SOURCE_FILE {
GATHER {
GATHER_DASHES
DIVERT_NODE {
SIMPLE_DIVERT {
DIVERT_TARGET_WITH_ARGS {
PATH
}
}
}
TAGS {
TAG
}
}
}),
);
}
#[test]
fn tag_only() {
assert_equivalent(
parse("- #tag\n"),
cst!(SOURCE_FILE {
GATHER {
GATHER_DASHES
TAGS {
TAG
}
}
}),
);
}
#[test]
fn full_gather() {
assert_equivalent(
parse("- (lbl) Text -> next #tag\n"),
cst!(SOURCE_FILE {
GATHER {
GATHER_DASHES
LABEL {
IDENTIFIER
}
MIXED_CONTENT {
TEXT
}
DIVERT_NODE {
SIMPLE_DIVERT {
DIVERT_TARGET_WITH_ARGS {
PATH
}
}
}
TAGS {
TAG
}
}
}),
);
}
#[test]
fn nested_full() {
assert_equivalent(
parse("-- (lbl) Text -> next #tag\n"),
cst!(SOURCE_FILE {
GATHER {
GATHER_DASHES
LABEL {
IDENTIFIER
}
MIXED_CONTENT {
TEXT
}
DIVERT_NODE {
SIMPLE_DIVERT {
DIVERT_TARGET_WITH_ARGS {
PATH
}
}
}
TAGS {
TAG
}
}
}),
);
}
#[test]
fn label_divert_tags_no_content() {
assert_equivalent(
parse("- (end) -> done #tag\n"),
cst!(SOURCE_FILE {
GATHER {
GATHER_DASHES
LABEL {
IDENTIFIER
}
DIVERT_NODE {
SIMPLE_DIVERT {
DIVERT_TARGET_WITH_ARGS {
PATH
}
}
}
TAGS {
TAG
}
}
}),
);
}
#[test]
fn content_divert_tags() {
assert_equivalent(
parse("- Hello -> knot #tag1 #tag2\n"),
cst!(SOURCE_FILE {
GATHER {
GATHER_DASHES
MIXED_CONTENT {
TEXT
}
DIVERT_NODE {
SIMPLE_DIVERT {
DIVERT_TARGET_WITH_ARGS {
PATH
}
}
}
TAGS {
TAG
TAG
}
}
}),
);
}
fn assert_gather_invariants(src: &str) {
let p = parse(src);
assert!(
p.errors().is_empty(),
"unexpected errors in {src:?}: {:?}",
p.errors()
);
for node in p.syntax().descendants() {
if node.kind() != SyntaxKind::GATHER {
continue;
}
let dash_count = node
.children()
.filter(|c| c.kind() == SyntaxKind::GATHER_DASHES)
.count();
assert_eq!(
dash_count, 1,
"GATHER should have exactly 1 GATHER_DASHES in {src:?}, found {dash_count}"
);
let first = node
.children()
.next()
.expect("GATHER must have at least one child");
assert_eq!(
first.kind(),
SyntaxKind::GATHER_DASHES,
"first child of GATHER must be GATHER_DASHES in {src:?}, found {:?}",
first.kind()
);
let label_end = node
.children()
.find(|c| c.kind() == SyntaxKind::LABEL)
.map(|c| c.text_range().end());
let content_range = node
.children()
.find(|c| c.kind() == SyntaxKind::MIXED_CONTENT)
.map(|c| c.text_range());
let divert_range = node
.children()
.find(|c| c.kind() == SyntaxKind::DIVERT_NODE)
.map(|c| c.text_range());
let tags_start = node
.children()
.find(|c| c.kind() == SyntaxKind::TAGS)
.map(|c| c.text_range().start());
if let (Some(le), Some(cr)) = (label_end, content_range) {
assert!(
le <= cr.start(),
"LABEL (ends {le:?}) should precede MIXED_CONTENT (starts {:?}) in {src:?}",
cr.start()
);
}
if let (Some(cr), Some(dr)) = (content_range, divert_range) {
assert!(
cr.end() <= dr.start(),
"MIXED_CONTENT (ends {:?}) should precede DIVERT_NODE (starts {:?}) in {src:?}",
cr.end(),
dr.start()
);
}
if let (Some(dr), Some(ts)) = (divert_range, tags_start) {
assert!(
dr.end() <= ts,
"DIVERT_NODE (ends {:?}) should precede TAGS (starts {ts:?}) in {src:?}",
dr.end()
);
}
}
}
#[test]
fn invariants_bare() {
assert_gather_invariants("-\n");
}
#[test]
fn invariants_with_content() {
assert_gather_invariants("- text\n");
}
#[test]
fn invariants_with_label() {
assert_gather_invariants("- (lbl) text\n");
}
#[test]
fn invariants_with_divert() {
assert_gather_invariants("- -> done\n");
}
#[test]
fn invariants_with_tags() {
assert_gather_invariants("- text #tag\n");
}
#[test]
fn invariants_full_combination() {
assert_gather_invariants("- (lbl) Text -> next #tag\n");
}
#[test]
fn invariants_nested() {
assert_gather_invariants("-- (lbl) Text -> next #tag\n");
}
#[test]
fn gather_has_gather_dashes() {
let p = parse("- text\n");
let has = p
.syntax()
.descendants()
.any(|n| n.kind() == SyntaxKind::GATHER_DASHES);
assert!(has, "gather must have GATHER_DASHES");
}
#[test]
fn bare_gather_no_mixed_content() {
let p = parse("-\n");
let gather = p
.syntax()
.descendants()
.find(|n| n.kind() == SyntaxKind::GATHER)
.expect("expected GATHER");
let has = gather
.children()
.any(|c| c.kind() == SyntaxKind::MIXED_CONTENT);
assert!(!has, "bare gather must not have MIXED_CONTENT");
}
#[test]
fn bare_gather_no_label() {
let p = parse("-\n");
let gather = p
.syntax()
.descendants()
.find(|n| n.kind() == SyntaxKind::GATHER)
.expect("expected GATHER");
let has = gather.children().any(|c| c.kind() == SyntaxKind::LABEL);
assert!(!has, "bare gather must not have LABEL");
}
#[test]
fn bare_gather_no_divert() {
let p = parse("-\n");
let gather = p
.syntax()
.descendants()
.find(|n| n.kind() == SyntaxKind::GATHER)
.expect("expected GATHER");
let has = gather
.children()
.any(|c| c.kind() == SyntaxKind::DIVERT_NODE);
assert!(!has, "bare gather must not have DIVERT_NODE");
}
#[test]
fn bare_gather_no_tags() {
let p = parse("-\n");
let gather = p
.syntax()
.descendants()
.find(|n| n.kind() == SyntaxKind::GATHER)
.expect("expected GATHER");
let has = gather.children().any(|c| c.kind() == SyntaxKind::TAGS);
assert!(!has, "bare gather must not have TAGS");
}
#[test]
fn gather_not_content_line() {
let p = parse("- text\n");
let root = p.syntax();
let has_gather = root.descendants().any(|n| n.kind() == SyntaxKind::GATHER);
let has_content_line = root
.children()
.any(|c| c.kind() == SyntaxKind::CONTENT_LINE);
assert!(has_gather, "expected GATHER node");
assert!(!has_content_line, "gather should not produce CONTENT_LINE");
}
#[test]
fn label_not_content() {
let p = parse("- (lbl) Hello\n");
let gather = p
.syntax()
.descendants()
.find(|n| n.kind() == SyntaxKind::GATHER)
.expect("expected GATHER");
let has_label = gather.children().any(|c| c.kind() == SyntaxKind::LABEL);
assert!(has_label, "labeled gather must have LABEL child");
}
#[test]
fn gather_with_inline_choice() {
assert_equivalent(
parse("- * hello\n"),
cst!(SOURCE_FILE {
GATHER {
GATHER_DASHES
CHOICE {
CHOICE_BULLETS
CHOICE_START_CONTENT {
TEXT
}
}
}
}),
);
}
#[test]
fn gather_with_inline_sticky_choice() {
assert_equivalent(
parse("- + sticky\n"),
cst!(SOURCE_FILE {
GATHER {
GATHER_DASHES
CHOICE {
CHOICE_BULLETS
CHOICE_START_CONTENT {
TEXT
}
}
}
}),
);
}
#[test]
fn labeled_gather_with_inline_choice() {
assert_equivalent(
parse("- (lbl) * hello\n"),
cst!(SOURCE_FILE {
GATHER {
GATHER_DASHES
LABEL {
IDENTIFIER
}
CHOICE {
CHOICE_BULLETS
CHOICE_START_CONTENT {
TEXT
}
}
}
}),
);
}
#[test]
fn gather_with_inline_choice_bracket_divert() {
assert_equivalent(
parse("- * [bracket]inner -> target\n"),
cst!(SOURCE_FILE {
GATHER {
GATHER_DASHES
CHOICE {
CHOICE_BULLETS
CHOICE_BRACKET_CONTENT {
TEXT
}
CHOICE_INNER_CONTENT {
TEXT
}
DIVERT_NODE {
SIMPLE_DIVERT {
DIVERT_TARGET_WITH_ARGS {
PATH
}
}
}
}
}
}),
);
}
#[test]
fn gather_at_eof_no_newline() {
let src = "-";
let p = parse(src);
assert_eq!(src, p.syntax().text().to_string(), "lossless round-trip");
}
#[test]
fn empty_parens_not_label() {
let p = parse("- () text\n");
let has_label = p
.syntax()
.descendants()
.any(|n| n.kind() == SyntaxKind::LABEL);
assert!(!has_label, "empty parens should not produce a LABEL node");
}