Skip to main content

brink_syntax/
lib.rs

1//! Syntax types and parser for inkle's ink narrative scripting language.
2
3pub mod ast;
4pub mod lexer;
5pub mod parser;
6pub mod segment;
7pub mod syntax_kind;
8
9pub use lexer::lex;
10pub use parser::{Parse, ParseError, parse, parse_with_cache};
11pub use segment::{Segment, SegmentKind, segment_file};
12pub use syntax_kind::{InkLanguage, SyntaxElement, SyntaxKind, SyntaxNode, SyntaxToken};
13
14impl Parse {
15    /// Returns the typed root AST node.
16    #[must_use]
17    #[expect(
18        clippy::expect_used,
19        reason = "parse() always produces SOURCE_FILE root"
20    )]
21    pub fn tree(&self) -> ast::SourceFile {
22        use ast::AstNode as _;
23        ast::SourceFile::cast(self.syntax()).expect("parse always produces a SOURCE_FILE root")
24    }
25}
26
27/// Extract the file paths of all `INCLUDE` directives from an ink source.
28///
29/// Performs a full parse and walks the resulting AST for `IncludeStmt`
30/// nodes, returning each include's raw filename (whitespace-trimmed).
31/// Includes nested in commented-out blocks are correctly excluded by
32/// the parser.
33///
34/// Useful for tools that need to discover an ink project's transitive
35/// file graph without doing full HIR lowering — for example, the bevy
36/// `.ink` asset loader walks the include graph asynchronously and feeds
37/// the resulting source cache into the synchronous compiler.
38#[must_use]
39pub fn extract_includes(source: &str) -> Vec<String> {
40    parse(source)
41        .tree()
42        .includes()
43        .filter_map(|inc| inc.file_path())
44        .map(|fp| fp.text().trim().to_string())
45        .filter(|s| !s.is_empty())
46        .collect()
47}
48
49#[cfg(test)]
50mod extract_includes_tests {
51    use super::*;
52
53    #[test]
54    fn extracts_top_level_includes() {
55        let src = "INCLUDE helper.ink\nINCLUDE other.ink\n";
56        assert_eq!(extract_includes(src), vec!["helper.ink", "other.ink"]);
57    }
58
59    #[test]
60    fn empty_source_returns_empty() {
61        assert!(extract_includes("").is_empty());
62    }
63
64    #[test]
65    fn no_includes_returns_empty() {
66        assert!(extract_includes("=== knot ===\nhello\n").is_empty());
67    }
68
69    #[test]
70    fn ignores_commented_out_include() {
71        let src = "// INCLUDE commented.ink\nINCLUDE real.ink\n";
72        assert_eq!(extract_includes(src), vec!["real.ink"]);
73    }
74}