brink-syntax 0.0.16

Syntax types and parser for inkle's ink narrative scripting language
Documentation
mod cst;

use super::{check, check_lossless};
use crate::parse;

#[test]
fn include_basic() {
    check("INCLUDE story.ink\n");
}

#[test]
fn include_path_with_slashes() {
    check("INCLUDE path/to/story.ink\n");
}

#[test]
fn external_no_params() {
    check("EXTERNAL myFunc()\n");
}

#[test]
fn external_with_params() {
    check("EXTERNAL myFunc(a, b, c)\n");
}

/// Ink keywords are contextual — an external may be named after an operator
/// keyword (e.g. `has`, the `Has` list operator). C# `IdentifierWithMetadata`
/// imposes no reserved-word check, so these are valid names in this position.
#[test]
fn external_keyword_name() {
    check("EXTERNAL has(item)\n");
}

#[test]
fn external_keyword_name_with_doc() {
    check("/// doc\nEXTERNAL has(item)\n");
}

#[test]
fn external_keyword_params() {
    check("EXTERNAL myFunc(and, or, mod)\n");
}

#[test]
fn var_declaration() {
    check("VAR x = 5\n");
}

#[test]
fn const_declaration() {
    check("CONST PI = 3\n");
}

#[test]
fn list_simple() {
    check("LIST colors = red, green, blue\n");
}

#[test]
fn list_with_values() {
    check("LIST items = (sword = 1), shield, (potion = 3)\n");
}

/// Ink keywords are contextual — `or`, `and`, `not`, `mod`, `has`, `hasnt`,
/// `true`, `false`, `else`, `done`, `end`, `ref`, etc. may all appear as
/// list member names.
#[test]
fn list_keyword_member_off() {
    check("LIST items = or, and, not\n");
}

#[test]
fn list_keyword_member_on() {
    check("LIST items = (or), (and), (not)\n");
}

#[test]
fn list_mixed_keyword_members() {
    check("LIST items = (true), false, (mod = 1), has\n");
}

/// Exact corpus pattern from Jonkeevy `FUNC_NameGenerator.ink`
#[test]
fn list_corpus_midsyll_demonic() {
    check("LIST Midsyll_Demonic = (ng), (ik), (yek), (roth), (och), (ra), (or), (gor)\n");
}

#[test]
fn include_bare_emits_error() {
    let p = parse("INCLUDE\n");
    assert_eq!(
        "INCLUDE\n",
        p.syntax().text().to_string(),
        "lossless round-trip"
    );
    assert!(
        p.errors().iter().any(|e| e.message.contains("file path")),
        "expected error about missing file path, got: {:?}",
        p.errors()
    );
}

#[test]
fn insta_include() {
    let p = parse("INCLUDE story.ink\n");
    insta::assert_snapshot!(format!("{:#?}", p.syntax()));
}

#[test]
fn insta_external() {
    let p = parse("EXTERNAL myFunc(a, b)\n");
    insta::assert_snapshot!(format!("{:#?}", p.syntax()));
}

#[test]
fn insta_list() {
    let p = parse("LIST items = (sword = 1), shield\n");
    insta::assert_snapshot!(format!("{:#?}", p.syntax()));
}

// ── TM-4b struct declarations (docs/typed-mode-spec.md §6) ────────────

#[test]
fn struct_decl_single_line() {
    check("STRUCT Point = #{x: float, y: float}\n");
}

#[test]
fn struct_decl_multiline() {
    check("STRUCT Point = #{\n    x: float,\n    y: float,\n}\n");
}

#[test]
fn struct_decl_trailing_comma_single_line() {
    check("STRUCT Point = #{x: float, y: float,}\n");
}

#[test]
fn struct_decl_empty_body() {
    check("STRUCT Empty = #{}\n");
}

#[test]
fn struct_decl_single_field() {
    check("STRUCT Wrapper = #{value: int}\n");
}

#[test]
fn struct_decl_generic_field_type() {
    check("STRUCT Bag = #{items: Array<int>}\n");
}

/// `STRUCT` is a contextual keyword — a plain identifier everywhere else,
/// same discipline as T1b's `if`/`while`/`for` (docs/t1b-surface-spec.md
/// §2). An existing knot named `STRUCT` must parse completely unaffected.
#[test]
fn struct_as_ordinary_knot_name_is_unaffected() {
    check("=== STRUCT ===\nHello.\n-> DONE\n");
}

/// Bare `STRUCT` with no `= #{` lookahead is not a struct declaration at
/// all — falls through to ordinary content parsing, same as any other word.
#[test]
fn struct_word_without_decl_shape_is_content() {
    check_lossless("STRUCT is just a word here.\n");
}

#[test]
fn insta_struct_decl_multiline() {
    let p = parse("STRUCT Point = #{\n    x: float,\n    y: float,\n}\n");
    insta::assert_snapshot!(format!("{:#?}", p.syntax()));
}