use super::*;
fn first_node(root: &SyntaxNode, kind: SyntaxKind) -> SyntaxNode {
let found = root.descendants().find(|n| n.kind() == kind);
assert!(found.is_some(), "no {kind:?} node in tree");
found.expect("asserted present just above")
}
fn tag_texts(node: &SyntaxNode) -> Vec<String> {
node.children()
.filter(|n| n.kind() == SyntaxKind::TAG)
.map(|n| n.text().to_string().trim().to_owned())
.collect()
}
#[test]
fn scene_heading_with_slug_and_tags_parses_in_the_ruled_line_order() {
let src = "INT. MARKET SQUARE - NIGHT [market] #tense #act1\n";
let p = assert_lossless(src);
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
let heading = first_node(&p.syntax(), SyntaxKind::SCENE_HEADING);
let heading = ast::SceneHeading::cast(heading).expect("SCENE_HEADING casts");
assert_eq!(
heading.title().expect("title").text(),
"INT. MARKET SQUARE - NIGHT"
);
assert_eq!(
heading
.slug()
.expect("slug")
.name_token()
.expect("slug ident")
.text(),
"market"
);
assert_eq!(
heading
.tags()
.map(|t| t.to_string().trim().to_owned())
.collect::<Vec<_>>(),
vec!["#tense".to_owned(), "#act1".to_owned()]
);
}
#[test]
fn ext_prefix_and_a_slugless_heading_are_both_recognized() {
let src = "EXT. COLD ALLEY - CONTINUOUS\n";
let p = assert_lossless(src);
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
let heading = ast::SceneHeading::cast(first_node(&p.syntax(), SyntaxKind::SCENE_HEADING))
.expect("SCENE_HEADING");
assert_eq!(
heading.title().expect("title").text(),
"EXT. COLD ALLEY - CONTINUOUS"
);
assert!(
heading.slug().is_none(),
"no explicit slug: the address is inferred from the title (spec 3.3)"
);
}
#[test]
fn a_bracket_in_the_middle_of_a_title_is_not_a_slug() {
let src = "INT. VAULT [b7] - NIGHT\n";
let p = assert_lossless(src);
let heading = ast::SceneHeading::cast(first_node(&p.syntax(), SyntaxKind::SCENE_HEADING))
.expect("SCENE_HEADING");
assert!(heading.slug().is_none());
assert_eq!(
heading.title().expect("title").text(),
"INT. VAULT [b7] - NIGHT"
);
}
#[test]
fn a_scene_title_with_an_escaped_hash_does_not_end_the_title_early() {
let src = "INT. MARKET \\#3\n";
let p = assert_lossless(src);
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
let heading = ast::SceneHeading::cast(first_node(&p.syntax(), SyntaxKind::SCENE_HEADING))
.expect("SCENE_HEADING");
assert_eq!(
heading.title().expect("title").text(),
"INT. MARKET #3",
"an escaped `#` must not end the title early, and (issue #2045)
`SceneTitle::text()` now strips the recognized escape's backslash,
parity with `markup::escape`'s stripping in ordinary content"
);
assert!(
!has_node_kind(&p.syntax(), SyntaxKind::TAG),
"the escaped `#` must not be reparsed as a trailing TAG"
);
}
#[test]
fn a_scene_titles_text_accessor_strips_a_recognized_open_brace_escapes_backslash() {
let src = "INT. MARKET \\{3\n";
let p = assert_lossless(src);
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
let heading = ast::SceneHeading::cast(first_node(&p.syntax(), SyntaxKind::SCENE_HEADING))
.expect("SCENE_HEADING");
assert_eq!(heading.title().expect("title").text(), "INT. MARKET {3");
}
#[test]
fn int_stays_an_ordinary_identifier_away_from_item_position() {
let src = "var INT = 1\n";
let p = assert_lossless(src);
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
assert!(!has_node_kind(&p.syntax(), SyntaxKind::SCENE_HEADING));
assert!(has_node_kind(&p.syntax(), SyntaxKind::VAR_DECL));
}
#[test]
fn a_heading_scopes_the_lines_below_it_without_braces() {
let src = "INT. A [a]\nOne.\nTwo.\n";
let p = assert_lossless(src);
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
let stitch = ast::SceneStitch::cast(first_node(&p.syntax(), SyntaxKind::SCENE_STITCH))
.expect("SCENE_STITCH");
let body = stitch.body().expect("SCENE_BODY");
let lines: Vec<_> = body
.items()
.filter(|n| n.kind() == SyntaxKind::CONTENT_LINE)
.collect();
assert_eq!(lines.len(), 2, "both lines belong to the heading's body");
}
#[test]
fn consecutive_headings_are_flat_siblings_never_nested() {
let src = "INT. A [a]\nOne.\nEXT. B [b]\nTwo.\n";
let p = assert_lossless(src);
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
let root = p.syntax();
let stitches: Vec<_> = root
.children()
.filter(|n| n.kind() == SyntaxKind::SCENE_STITCH)
.collect();
assert_eq!(stitches.len(), 2, "two sibling stitches under SOURCE_FILE");
for stitch in &stitches {
let nested = stitch
.descendants()
.filter(|n| n.kind() == SyntaxKind::SCENE_STITCH)
.count();
assert_eq!(nested, 1, "a scene stitch never contains another");
}
}
#[test]
fn a_doc_comment_above_the_second_heading_keeps_flat_siblings() {
let src = "INT. A [a]\nOne.\n/// doc\nEXT. B [b]\nTwo.\n";
let p = assert_lossless(src);
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
let root = p.syntax();
let stitches: Vec<_> = root
.children()
.filter(|n| n.kind() == SyntaxKind::SCENE_STITCH)
.collect();
assert_eq!(stitches.len(), 2, "two sibling stitches under SOURCE_FILE");
for stitch in &stitches {
let nested = stitch
.descendants()
.filter(|n| n.kind() == SyntaxKind::SCENE_STITCH)
.count();
assert_eq!(nested, 1, "a scene stitch never contains another");
}
let second = ast::SceneStitch::cast(stitches[1].clone()).expect("SCENE_STITCH");
assert!(
second.doc().is_some(),
"the `///` run attaches to the second (documented) heading, not the first"
);
}
#[test]
fn a_heading_body_ends_at_the_enclosing_close() {
let src = "flow f() {\n INT. A [a]\n Inside.\n}\nAfter.\n";
let p = assert_lossless(src);
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
let stitch = first_node(&p.syntax(), SyntaxKind::SCENE_STITCH);
assert!(
!stitch.text().to_string().contains("After."),
"the scene body stops at the enclosing `}}`: {}",
stitch.text()
);
let block = first_node(&p.syntax(), SyntaxKind::BLOCK);
assert!(block.text().to_string().contains("Inside."));
}
#[test]
fn deeper_nesting_uses_the_general_flow_spelling_inside_a_scene_body() {
let src = "INT. A [a]\nOne.\nflow inner() {\n Two.\n}\n";
let p = assert_lossless(src);
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
let stitch = first_node(&p.syntax(), SyntaxKind::SCENE_STITCH);
assert!(
stitch
.descendants()
.any(|n| n.kind() == SyntaxKind::FLOW_DECL),
"the nested flow belongs to the scene's header-scoped body"
);
}
#[test]
fn a_doc_comment_above_a_heading_attaches_to_the_stitch() {
let src = "/// The market, after curfew.\nINT. A [a]\nOne.\n";
let p = assert_lossless(src);
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
let stitch = ast::SceneStitch::cast(first_node(&p.syntax(), SyntaxKind::SCENE_STITCH))
.expect("SCENE_STITCH");
assert!(
stitch.doc().is_some(),
"the `///` run wraps as the stitch's doc"
);
}
#[test]
fn a_block_cue_carries_its_extension_on_the_tag_channel() {
let src = "@VENDOR #(v.o.)\nYou shouldn't be here.\n";
let p = assert_lossless(src);
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
let cue = first_node(&p.syntax(), SyntaxKind::CUE);
assert_eq!(tag_texts(&cue), vec!["#(v.o.)".to_owned()]);
let cue = ast::Cue::cast(cue).expect("CUE");
assert_eq!(cue.name().expect("name").text(), "VENDOR");
}
#[test]
fn a_multi_word_cue_name_is_one_name() {
let src = "@MARKET VENDOR\nHello.\n";
let p = assert_lossless(src);
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
let cue = ast::Cue::cast(first_node(&p.syntax(), SyntaxKind::CUE)).expect("CUE");
assert_eq!(cue.name().expect("name").text(), "MARKET VENDOR");
}
#[test]
fn a_balanced_brace_in_a_cue_name_does_not_swallow_the_enclosing_blocks_own_closer() {
let src = "flow f() {\n @NAME {gold} coins.\n}\n";
let p = assert_lossless(src);
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
let decl =
ast::FlowDecl::cast(first_node(&p.syntax(), SyntaxKind::FLOW_DECL)).expect("FLOW_DECL");
assert!(
decl.body().is_some(),
"the cue name's balanced brace must not swallow the flow's own body closer"
);
let cue = ast::Cue::cast(first_node(&p.syntax(), SyntaxKind::CUE)).expect("CUE");
assert_eq!(cue.name().expect("name").text(), "NAME {gold} coins.");
}
#[test]
fn an_unbalanced_open_brace_in_a_cue_name_eats_the_enclosing_blocks_own_closer() {
let src = "flow f() { @NAME { }\n";
let p = assert_lossless(src);
assert!(
!p.errors().is_empty(),
"expected the unbalanced `{{` to consume the flow's own closer and error, got: {:?}",
p.errors()
);
}
#[test]
fn an_unbalanced_open_brace_in_a_cue_name_with_a_colon_inside_it_eats_the_enclosing_blocks_own_closer()
{
let src = "flow f() { @NAME {x: y }\n";
let p = assert_lossless(src);
assert!(
!p.errors().is_empty(),
"expected the unbalanced `{{` to swallow the `:` and the flow's own closer, got: {:?}",
p.errors()
);
}
#[test]
fn a_cue_name_with_an_escaped_open_brace_does_not_swallow_the_enclosing_blocks_own_closer() {
let src = "flow f() { @NAME \\{ }\n";
let p = assert_lossless(src);
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
assert_eq!(count_node_kind(&p.syntax(), SyntaxKind::FLOW_DECL), 1);
}
#[test]
fn a_cue_name_with_an_escaped_backslash_before_a_real_brace_counts_the_brace() {
let src = "flow f() { @NAME \\\\{ } coins. }\n";
let p = assert_lossless(src);
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
assert_eq!(count_node_kind(&p.syntax(), SyntaxKind::FLOW_DECL), 1);
}
#[test]
fn a_cue_name_with_a_colon_inside_braces_does_not_terminate_the_name() {
let src = "flow f() { @NAME {a:b} }\n";
let p = assert_lossless(src);
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
assert_eq!(count_node_kind(&p.syntax(), SyntaxKind::FLOW_DECL), 1);
}
#[test]
fn a_cue_name_with_an_escaped_hash_does_not_end_the_name_early() {
let src = "@NAME \\#not a tag\nHello.\n";
let p = assert_lossless(src);
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
let cue = ast::Cue::cast(first_node(&p.syntax(), SyntaxKind::CUE)).expect("CUE");
assert_eq!(
cue.name().expect("name").text(),
"NAME #not a tag",
"an escaped `#` must not end the name early, and (issue #2045)
`CueName::text()` now strips the recognized escape's backslash,
parity with `markup::escape`'s stripping in ordinary content"
);
assert!(
!has_node_kind(&p.syntax(), SyntaxKind::TAG),
"the escaped `#` must not be reparsed as a trailing TAG"
);
}
#[test]
fn a_hash_inside_an_open_brace_still_ends_a_cue_name_early() {
let src = "@NAME {a#b} c.\nHello.\n";
let p = assert_lossless(src);
assert!(
!p.errors().is_empty(),
"expected the open `{{` to be abandoned at the HASH boundary and \
the later `}}` to become a stray top-level token, got: {:?}",
p.errors()
);
let cue = ast::Cue::cast(first_node(&p.syntax(), SyntaxKind::CUE)).expect("CUE");
assert_eq!(
cue.name().expect("name").text(),
"NAME {a",
"the name ends at the unescaped HASH regardless of the still-open `{{`"
);
assert_eq!(
count_node_kind(&p.syntax(), SyntaxKind::TAG),
1,
"the HASH starts its own sibling TAG rather than continuing the name"
);
}
#[test]
fn a_cue_names_own_unescaped_closing_brace_remains_the_terminator_even_when_preceded_by_a_backslash()
{
let src = "flow f() { @NAME \\{a\\} c. }\n";
let p = assert_lossless(src);
assert!(
!p.errors().is_empty(),
"expected the name's own `\\}}` to swallow the flow's closer early \
and leave a stray top-level `}}`, got: {:?}",
p.errors()
);
let name = p
.syntax()
.descendants()
.find(|n| n.kind() == SyntaxKind::CUE_NAME)
.expect("CUE_NAME");
assert_eq!(
name.text(),
"NAME \\{a\\",
"the raw CST node — not `ast::CueName::text()`, which (issue #2045) \
strips the recognized `\\{{` escape's backslash — stops the instant \
it meets the `}}` from `\\}}`, backslash and all, exactly as it \
would for an unescaped `}}` at depth zero"
);
}
#[test]
fn a_cue_names_text_accessor_strips_a_recognized_open_brace_escapes_backslash() {
let src = "@NAME \\{not a brace\nHello.\n";
let p = assert_lossless(src);
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
let cue = ast::Cue::cast(first_node(&p.syntax(), SyntaxKind::CUE)).expect("CUE");
assert_eq!(cue.name().expect("name").text(), "NAME {not a brace");
}
#[test]
fn a_cue_immediately_followed_by_the_enclosing_blocks_own_closer_still_stops_there() {
let src = "flow f() { @NAME }\n";
let p = assert_lossless(src);
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
assert_eq!(count_node_kind(&p.syntax(), SyntaxKind::FLOW_DECL), 1);
}
#[test]
fn a_cue_name_containing_a_balanced_alternation_brace_does_not_end_early() {
let src = "flow f() {\n @NAME {gold|silver} coins.\n}\n";
let p = assert_lossless(src);
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
assert_eq!(count_node_kind(&p.syntax(), SyntaxKind::FLOW_DECL), 1);
}
#[test]
fn the_compact_cue_fuses_a_name_and_one_dialogue_line() {
let src = "@KID: Says who?\n";
let p = assert_lossless(src);
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
let compact = ast::CompactCue::cast(first_node(&p.syntax(), SyntaxKind::COMPACT_CUE))
.expect("COMPACT_CUE");
assert_eq!(compact.name().expect("name").text(), "KID");
assert_eq!(
compact.line().expect("fused line").to_string().trim(),
"Says who?"
);
assert!(
!has_node_kind(&p.syntax(), SyntaxKind::CUE),
"the compact form is its own shape, not a CUE plus a line"
);
}
#[test]
fn a_compact_cue_line_keeps_interpolation_and_trailing_tags() {
let src = "@KID: I have {gold} coins. #beat\n";
let p = assert_lossless(src);
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
let compact = first_node(&p.syntax(), SyntaxKind::COMPACT_CUE);
assert!(has_node_kind(&compact, SyntaxKind::INTERPOLATION));
assert!(has_node_kind(&compact, SyntaxKind::TAG));
}
#[test]
fn a_bang_name_line_parses_as_a_bang_dispatch() {
let src = "!radio TAC-2: All units report in.\n";
let p = assert_lossless(src);
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
let dispatch = ast::BangDispatch::cast(first_node(&p.syntax(), SyntaxKind::BANG_DISPATCH))
.expect("BANG_DISPATCH");
assert_eq!(dispatch.name().expect("name").text(), "radio");
assert_eq!(
dispatch.line().expect("fused remainder").to_string().trim(),
"TAC-2: All units report in."
);
}
#[test]
fn a_bang_dispatch_line_keeps_interpolation_and_trailing_tags() {
let src = "!radio I have {gold} coins. #beat\n";
let p = assert_lossless(src);
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
let dispatch = first_node(&p.syntax(), SyntaxKind::BANG_DISPATCH);
assert!(has_node_kind(&dispatch, SyntaxKind::INTERPOLATION));
assert!(has_node_kind(&dispatch, SyntaxKind::TAG));
}
#[test]
fn a_bang_not_immediately_followed_by_an_ident_is_still_plain_text() {
let src = "flow f() {\n ! Wait, listen.\n}\n";
let p = assert_lossless(src);
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
assert!(!has_node_kind(&p.syntax(), SyntaxKind::BANG_DISPATCH));
assert!(has_node_kind(&p.syntax(), SyntaxKind::CONTENT_LINE));
}
#[test]
fn an_escaped_bang_composes_with_bang_dispatch_and_stays_plain_text() {
let src = "flow f() {\n \\!radio still just prose.\n}\n";
let p = assert_lossless(src);
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
assert!(
!has_node_kind(&p.syntax(), SyntaxKind::BANG_DISPATCH),
"an escaped `\\!` must never open a BANG_DISPATCH"
);
assert!(has_node_kind(&p.syntax(), SyntaxKind::ESCAPE));
}
#[test]
fn a_lone_at_in_prose_is_still_plain_text() {
let src = "flow f() {\n @ home tomorrow\n}\n";
let p = assert_lossless(src);
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
assert!(!has_node_kind(&p.syntax(), SyntaxKind::CUE));
assert!(!has_node_kind(&p.syntax(), SyntaxKind::COMPACT_CUE));
assert!(has_node_kind(&p.syntax(), SyntaxKind::CONTENT_LINE));
}
#[test]
fn an_annotation_line_is_not_a_cue() {
let src = "@[effects(pure)]\nflow f() {\n Hi.\n}\n";
let p = assert_lossless(src);
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
assert!(!has_node_kind(&p.syntax(), SyntaxKind::CUE));
assert!(has_node_kind(&p.syntax(), SyntaxKind::ANNOTATION_LINE));
}
#[test]
fn a_parenthetical_after_a_cue_is_a_delivery_line() {
let src = "@VENDOR\n(hushed)\nYou shouldn't be here.\n";
let p = assert_lossless(src);
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
let paren = ast::Parenthetical::cast(first_node(&p.syntax(), SyntaxKind::PARENTHETICAL))
.expect("PARENTHETICAL");
assert_eq!(paren.text(), "hushed");
}
#[test]
fn a_parenthetical_after_that_cues_dialogue_is_still_a_delivery_line() {
let src = "@VENDOR\nYou shouldn't be here.\n(muttering)\nNot after dark.\n";
let p = assert_lossless(src);
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
assert!(has_node_kind(&p.syntax(), SyntaxKind::PARENTHETICAL));
}
#[test]
fn a_blank_line_breaks_the_chain_so_a_label_line_stays_a_label() {
let src = "@VENDOR\nYou shouldn't be here.\n\n(loop_back)\n";
let p = assert_lossless(src);
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
assert!(!has_node_kind(&p.syntax(), SyntaxKind::PARENTHETICAL));
assert!(has_node_kind(&p.syntax(), SyntaxKind::LABEL));
}
#[test]
fn a_bare_label_line_outside_any_chain_is_untouched() {
let src = "flow f() {\n (look_around)\n You look around.\n}\n";
let p = assert_lossless(src);
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
assert!(!has_node_kind(&p.syntax(), SyntaxKind::PARENTHETICAL));
assert!(has_node_kind(&p.syntax(), SyntaxKind::LABEL));
}
#[test]
fn a_parenthetical_must_fill_its_whole_line() {
let src = "@VENDOR\n(spot_here) I trudge on.\n";
let p = assert_lossless(src);
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
assert!(!has_node_kind(&p.syntax(), SyntaxKind::PARENTHETICAL));
assert!(has_node_kind(&p.syntax(), SyntaxKind::LABEL));
}
#[test]
fn a_braced_body_starts_a_fresh_chain() {
let src = "@VENDOR\nflow inner() {\n (loop_back)\n}\n";
let p = assert_lossless(src);
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
assert!(!has_node_kind(&p.syntax(), SyntaxKind::PARENTHETICAL));
assert!(has_node_kind(&p.syntax(), SyntaxKind::LABEL));
}
#[test]
fn a_scene_heading_breaks_the_chain() {
let src = "@VENDOR\nHello.\nINT. A [a]\n(loop_back)\n";
let p = assert_lossless(src);
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
assert!(!has_node_kind(&p.syntax(), SyntaxKind::PARENTHETICAL));
assert!(has_node_kind(&p.syntax(), SyntaxKind::LABEL));
}
#[test]
fn a_multi_word_parenthetical_carries_trailing_tags() {
let src = "@VENDOR\n(under his breath) #beat\nHello.\n";
let p = assert_lossless(src);
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
let paren = first_node(&p.syntax(), SyntaxKind::PARENTHETICAL);
assert_eq!(tag_texts(&paren), vec!["#beat".to_owned()]);
assert_eq!(
ast::Parenthetical::cast(paren)
.expect("PARENTHETICAL")
.text(),
"under his breath"
);
}
#[test]
fn a_flow_header_carries_trailing_tags_before_its_body() {
let src = "flow market(gold) #act1 #tense {\n Hi.\n}\n";
let p = assert_lossless(src);
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
let decl =
ast::FlowDecl::cast(first_node(&p.syntax(), SyntaxKind::FLOW_DECL)).expect("FLOW_DECL");
assert_eq!(
decl.tags()
.map(|t| t.to_string().trim().to_owned())
.collect::<Vec<_>>(),
vec!["#act1".to_owned(), "#tense".to_owned()]
);
assert!(
decl.body().is_some(),
"the tag text must not swallow the body brace"
);
}
#[test]
fn a_paren_less_flow_header_carries_tags_too() {
let src = "flow market #act1 {\n Hi.\n}\n";
let p = assert_lossless(src);
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
let decl =
ast::FlowDecl::cast(first_node(&p.syntax(), SyntaxKind::FLOW_DECL)).expect("FLOW_DECL");
assert_eq!(decl.tags().count(), 1);
assert!(decl.body().is_some());
}
#[test]
fn header_tags_do_not_swallow_a_body_dialect_selector() {
let src = "flow market #act1 ~{\n let x = 1;\n}\n";
let p = assert_lossless(src);
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
assert!(has_node_kind(&p.syntax(), SyntaxKind::STMT_BLOCK));
}
#[test]
fn a_flow_word_starting_a_prose_line_is_still_prose() {
let src = "flow f() {\n flow gently #1 and the river bends.\n}\n";
let p = assert_lossless(src);
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
let inner = p
.syntax()
.descendants()
.filter(|n| n.kind() == SyntaxKind::FLOW_DECL)
.count();
assert_eq!(inner, 1, "the prose line must not parse as a second flow");
}
#[test]
fn a_prose_line_with_an_interpolation_brace_is_still_prose() {
let src = "flow f() {\n flow gently #1 and {gold} coins.\n The river bends.\n}\n";
let p = assert_lossless(src);
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
let inner = p
.syntax()
.descendants()
.filter(|n| n.kind() == SyntaxKind::FLOW_DECL)
.count();
assert_eq!(inner, 1, "the prose line must not parse as a second flow");
}
#[test]
fn a_prose_line_with_an_alternation_brace_is_still_prose() {
let src = "flow f() {\n flow gently #1 and {river|stream} bends.\n}\n";
let p = assert_lossless(src);
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
let inner = p
.syntax()
.descendants()
.filter(|n| n.kind() == SyntaxKind::FLOW_DECL)
.count();
assert_eq!(inner, 1, "the prose line must not parse as a second flow");
}
#[test]
fn the_complement_pass_page_parses_cleanly() {
let src = "\
INT. MARKET SQUARE - NIGHT [market] #act1
The square is empty. A single lantern gutters against the dark.
@VENDOR #(v.o.)
(hushed)
You shouldn't be here after dark. The gates closed an hour ago.
@KID: Says who?
@VENDOR: The curfew, kid. That.
{?
* \"I was just leaving.\"[] I muttered, backing away.
* [Slip into the alley] -> alley_escape
}
The bell tolls again.
-> alley_escape
EXT. COLD ALLEY - CONTINUOUS [alley_escape]
Cold brick. Distant bells.
-> DONE
";
let p = assert_lossless(src);
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
let root = p.syntax();
assert_eq!(
root.children()
.filter(|n| n.kind() == SyntaxKind::SCENE_STITCH)
.count(),
2
);
assert!(has_node_kind(&root, SyntaxKind::CUE));
assert!(has_node_kind(&root, SyntaxKind::COMPACT_CUE));
assert!(has_node_kind(&root, SyntaxKind::PARENTHETICAL));
assert!(has_node_kind(&root, SyntaxKind::SCENE_SLUG));
assert!(has_node_kind(&root, SyntaxKind::CHOICE_POINT));
}