use std::collections::HashMap;
use std::path::Path;
use mdbook_treesitter::{
Directive,
language::build_registry,
process_chapter,
query::{apply_strip, run_jq_query, run_treesitter_query},
};
const GEOMETRY_RS: &str = r#"/// A simple point in 2D space.
///
/// This struct is used throughout the geometry module.
pub struct Point {
pub x: f64,
pub y: f64,
}
/// A rectangle defined by its top-left corner, width, and height.
#[derive(Debug, Clone, PartialEq)]
pub struct Rectangle {
pub origin: Point,
pub width: f64,
pub height: f64,
}
/// Configuration for the geometry module.
#[derive(Debug)]
pub struct Config {
pub precision: u32,
}
"#;
const DOC_COMMENT: &str = r#"[
((line_comment)+ @doc_comment
.
(struct_item name: (type_identifier) @name))
((line_comment)+ @doc_comment
.
(attribute_item)
.
(struct_item name: (type_identifier) @name))
]"#;
const STRUCT: &str = r#"(struct_item name: (type_identifier) @name) @struct"#;
const STRUCT_FIELDS: &str = r#"
(struct_item
name: (type_identifier) @name
body: (field_declaration_list
(field_declaration) @field))
"#;
const COMMENT_STRIP: &str = r#"^///? ?"#;
const DOC_COMMENT_JQ: &str = r#"
.params.name as $target_name |
.children as $all |
([$all | to_entries[] |
select(
.value.type == "struct_item" and
(.value.children[]? | select(.type == "type_identifier") | .text) == $target_name
)
] | .[0].key) as $idx |
if $idx == null then error("struct not found")
else
([$all[0:$idx] | to_entries[] |
select(.value.type != "line_comment" and .value.type != "attribute_item")
] | if length > 0 then last.key else -1 end) as $last_gap |
[$all[($last_gap+1):$idx][] |
select(.type == "line_comment") | .text | rtrimstr("\n")] |
join("\n")
end
"#;
#[test]
fn parse_directive_file_only() {
let d = Directive::parse("../../foo.rs").unwrap();
assert_eq!(d.file_path, "../../foo.rs");
assert_eq!(d.query_name, None);
assert!(d.params.is_empty());
}
#[test]
fn parse_directive_with_query() {
let d = Directive::parse("foo.rs#doc_comment").unwrap();
assert_eq!(d.file_path, "foo.rs");
assert_eq!(d.query_name.as_deref(), Some("doc_comment"));
assert!(d.params.is_empty());
}
#[test]
fn parse_directive_with_query_and_param() {
let d = Directive::parse("foo.rs#doc_comment?name=Foo").unwrap();
assert_eq!(d.file_path, "foo.rs");
assert_eq!(d.query_name.as_deref(), Some("doc_comment"));
assert_eq!(d.params.get("name").map(|s| s.as_str()), Some("Foo"));
}
#[test]
fn parse_directive_with_multiple_params() {
let d = Directive::parse("foo.rs#struct?name=Bar&visibility=pub").unwrap();
assert_eq!(d.query_name.as_deref(), Some("struct"));
assert_eq!(d.params["name"], "Bar");
assert_eq!(d.params["visibility"], "pub");
}
#[test]
fn parse_directive_spaces_around_path() {
let d = Directive::parse(" foo.rs ").unwrap();
assert_eq!(d.file_path, "foo.rs");
}
fn rust_language() -> tree_sitter::Language {
tree_sitter::Language::new(tree_sitter_rust::LANGUAGE)
}
#[test]
fn ts_doc_comment_point() {
let mut params = HashMap::new();
params.insert("name".into(), "Point".into());
let result = run_treesitter_query(
&rust_language(),
GEOMETRY_RS,
DOC_COMMENT,
¶ms,
None,
None,
)
.unwrap();
assert!(
result.contains("A simple point in 2D space."),
"got: {result}"
);
assert!(result.contains("This struct is used"), "got: {result}");
assert!(!result.contains("rectangle"), "got: {result}");
assert!(
!result.contains("\n\n"),
"unexpected blank line in: {result}"
);
}
#[test]
fn ts_doc_comment_rectangle() {
let mut params = HashMap::new();
params.insert("name".into(), "Rectangle".into());
let result = run_treesitter_query(
&rust_language(),
GEOMETRY_RS,
DOC_COMMENT,
¶ms,
None,
None,
)
.unwrap();
assert!(result.contains("A rectangle defined"), "got: {result}");
assert!(!result.contains("Point"), "got: {result}");
assert!(
!result.contains("\n\n"),
"unexpected blank line in: {result}"
);
}
#[test]
fn ts_doc_comment_no_match_returns_error() {
let mut params = HashMap::new();
params.insert("name".into(), "NonExistent".into());
let err = run_treesitter_query(
&rust_language(),
GEOMETRY_RS,
DOC_COMMENT,
¶ms,
None,
None,
)
.unwrap_err();
assert!(err.to_string().contains("no results"), "got: {err}");
}
#[test]
fn ts_struct_rectangle_includes_body() {
let mut params = HashMap::new();
params.insert("name".into(), "Rectangle".into());
let result =
run_treesitter_query(&rust_language(), GEOMETRY_RS, STRUCT, ¶ms, None, None).unwrap();
assert!(result.contains("Rectangle"), "got: {result}");
assert!(result.contains("width"), "got: {result}");
}
#[test]
fn ts_struct_point() {
let mut params = HashMap::new();
params.insert("name".into(), "Point".into());
let result =
run_treesitter_query(&rust_language(), GEOMETRY_RS, STRUCT, ¶ms, None, None).unwrap();
assert!(result.contains("pub struct Point"), "got: {result}");
assert!(result.contains("pub x: f64"), "got: {result}");
}
#[test]
fn jq_doc_comment_point() {
let mut params = HashMap::new();
params.insert("name".into(), "Point".into());
let result = run_jq_query(&rust_language(), GEOMETRY_RS, DOC_COMMENT_JQ, ¶ms).unwrap();
assert!(
result.contains("A simple point in 2D space."),
"got: {result}"
);
assert!(!result.contains("rectangle"), "got: {result}");
assert!(
!result.contains("\n\n"),
"unexpected blank line in: {result}"
);
}
#[test]
fn jq_doc_comment_config() {
let mut params = HashMap::new();
params.insert("name".into(), "Config".into());
let result = run_jq_query(&rust_language(), GEOMETRY_RS, DOC_COMMENT_JQ, ¶ms).unwrap();
assert!(
result.contains("Configuration for the geometry module."),
"got: {result}"
);
assert!(!result.contains("simple point"), "got: {result}");
assert!(!result.contains("rectangle"), "got: {result}");
}
fn make_registry() -> HashMap<String, mdbook_treesitter::language::LanguageEntry> {
build_registry(&HashMap::new(), Path::new("/")).unwrap()
}
#[test]
fn process_chapter_whole_file() {
use tempfile::tempdir;
let dir = tempdir().unwrap();
let src_path = dir.path().join("geometry.rs");
std::fs::write(&src_path, GEOMETRY_RS).unwrap();
let registry = make_registry();
let content = "# Test\n\n```rust\n{{ #treesitter geometry.rs }}\n```\n";
let result = process_chapter(content, dir.path(), ®istry).unwrap();
assert!(result.contains("```rust"), "fence missing: {result}");
assert!(result.contains("pub struct Point"), "got: {result}");
assert!(
!result.contains("```rust\n```rust"),
"double fence: {result}"
);
}
#[test]
fn process_chapter_missing_file_returns_error() {
use tempfile::tempdir;
let dir = tempdir().unwrap();
let registry = make_registry();
let content = "{{ #treesitter nonexistent.rs }}\n";
let err = process_chapter(content, dir.path(), ®istry).unwrap_err();
assert!(
err.to_string().contains("nonexistent.rs") || err.to_string().contains("reading"),
"got: {err}"
);
}
#[test]
fn process_chapter_unknown_extension_returns_error() {
use tempfile::tempdir;
let dir = tempdir().unwrap();
std::fs::write(dir.path().join("data.xyz"), "hello").unwrap();
let registry = make_registry();
let content = "{{ #treesitter data.xyz }}\n";
let err = process_chapter(content, dir.path(), ®istry).unwrap_err();
assert!(
err.to_string().contains(".xyz") || err.to_string().contains("no language"),
"got: {err}"
);
}
#[test]
fn process_chapter_no_braces_spaces() {
use tempfile::tempdir;
let dir = tempdir().unwrap();
std::fs::write(dir.path().join("geometry.rs"), GEOMETRY_RS).unwrap();
let registry = make_registry();
let content = "{{#treesitter geometry.rs}}\n";
let result = process_chapter(content, dir.path(), ®istry).unwrap();
assert!(result.contains("pub struct Point"), "got: {result}");
assert!(!result.contains("```"), "unexpected fence: {result}");
}
#[test]
fn process_chapter_escaped_directive_is_not_expanded() {
use tempfile::tempdir;
let dir = tempdir().unwrap();
std::fs::write(dir.path().join("geometry.rs"), GEOMETRY_RS).unwrap();
let registry = make_registry();
let content = r"\{{ #treesitter geometry.rs#doc_comment?name=Point }}";
let result = process_chapter(content, dir.path(), ®istry).unwrap();
assert_eq!(
result.trim(),
"{{ #treesitter geometry.rs#doc_comment?name=Point }}"
);
}
#[test]
fn registry_has_builtin_languages() {
let registry = make_registry();
assert!(registry.contains_key("rs"), "missing Rust");
assert!(registry.contains_key("toml"), "missing TOML");
assert!(registry.contains_key("md"), "missing Markdown");
}
#[test]
fn registry_rust_has_no_builtin_queries() {
let registry = make_registry();
let rust = registry.get("rs").unwrap();
assert!(rust.queries.is_empty(), "expected no built-in queries");
}
#[test]
fn apply_strip_removes_comment_delimiters() {
let input = "/// A simple point in 2D space.\n///\n/// Multi-line.";
let result = apply_strip(input, COMMENT_STRIP).unwrap();
assert_eq!(result, "A simple point in 2D space.\n\nMulti-line.");
}
#[test]
fn apply_strip_invalid_regex_returns_error() {
let err = apply_strip("anything", "[invalid").unwrap_err();
assert!(
err.to_string().contains("invalid strip regex"),
"got: {err}"
);
}
#[test]
fn ts_comment_text_point_no_delimiters() {
let mut params = HashMap::new();
params.insert("name".into(), "Point".into());
let raw = run_treesitter_query(
&rust_language(),
GEOMETRY_RS,
DOC_COMMENT,
¶ms,
None,
None,
)
.unwrap();
let result = apply_strip(&raw, COMMENT_STRIP).unwrap();
assert!(
result.contains("A simple point in 2D space."),
"got: {result}"
);
assert!(!result.contains("///"), "delimiters not stripped: {result}");
}
const COLOR_RS: &str = r#"
/// The primary colors.
pub enum Color {
/// The color red.
Red,
/// The color green.
Green,
/// The color blue.
Blue,
}
"#;
const ENUM_VARIANT_DOC: &str = r#"[
((line_comment)+ @doc_comment
.
(enum_variant name: (identifier) @name))
((line_comment)+ @doc_comment
.
(attribute_item)+
.
(enum_variant name: (identifier) @name))
]"#;
#[test]
fn ts_template_enum_variant_list() {
let result = run_treesitter_query(
&rust_language(),
COLOR_RS,
ENUM_VARIANT_DOC,
&HashMap::new(),
Some(r"^///? ?"),
Some("- {name}: {doc_comment}"),
)
.unwrap();
assert!(result.contains("- Red: The color red."), "got: {result}");
assert!(
result.contains("- Green: The color green."),
"got: {result}"
);
assert!(result.contains("- Blue: The color blue."), "got: {result}");
assert_eq!(result.lines().count(), 3, "got: {result}");
}
#[test]
fn ts_struct_fields_point() {
let mut params = HashMap::new();
params.insert("name".into(), "Point".into());
let result = run_treesitter_query(
&rust_language(),
GEOMETRY_RS,
STRUCT_FIELDS,
¶ms,
None,
None,
)
.unwrap();
assert!(result.contains("pub x: f64"), "got: {result}");
assert!(result.contains("pub y: f64"), "got: {result}");
assert!(
!result.contains("pub struct"),
"struct header leaked: {result}"
);
assert!(!result.contains('{'), "brace leaked: {result}");
}
#[test]
fn ts_struct_fields_rectangle() {
let mut params = HashMap::new();
params.insert("name".into(), "Rectangle".into());
let result = run_treesitter_query(
&rust_language(),
GEOMETRY_RS,
STRUCT_FIELDS,
¶ms,
None,
None,
)
.unwrap();
assert!(result.contains("pub origin: Point"), "got: {result}");
assert!(result.contains("pub width: f64"), "got: {result}");
assert!(result.contains("pub height: f64"), "got: {result}");
assert!(
!result.contains("pub struct"),
"struct header leaked: {result}"
);
}