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