diff --git a/Cargo.lock b/Cargo.lock index 9f06420..acc0ce1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -205,6 +205,7 @@ dependencies = [ "tree-sitter-css", "tree-sitter-fortran", "tree-sitter-go", + "tree-sitter-html", "tree-sitter-python", "tree-sitter-rust", "walkdir", @@ -1591,6 +1592,16 @@ dependencies = [ "tree-sitter-language", ] +[[package]] +name = "tree-sitter-html" +version = "0.23.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "261b708e5d92061ede329babaaa427b819329a9d427a1d710abb0f67bbef63ee" +dependencies = [ + "cc", + "tree-sitter-language", +] + [[package]] name = "tree-sitter-language" version = "0.1.7" diff --git a/Cargo.toml b/Cargo.toml index 2f084e0..dc64b53 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -47,6 +47,7 @@ tree-sitter-cpp = "0.23.4" tree-sitter-css = "0.25.0" tree-sitter-fortran = "0.6.0" tree-sitter-go = "0.25.0" +tree-sitter-html = "0.23.2" tree-sitter-python = "0.25.0" tree-sitter-rust = "0.24.2" walkdir = "2" diff --git a/README.md b/README.md index 8f7648b..2437f75 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # codebaseGraph `codebaseGraph` turns a local source repository into a searchable code graph for -AI coding agents. It indexes Python, Rust, Go, C, C++, Fortran, CSS, Markdown, +AI coding agents. It indexes Python, Rust, Go, C, C++, Fortran, CSS, HTML, Markdown, and MDX, then exposes compact context, schema information, query helpers, and bounded read-only graph queries through a native CLI and MCP server. diff --git a/knowledge/architecture/language-html.md b/knowledge/architecture/language-html.md new file mode 100644 index 0000000..93ce18b --- /dev/null +++ b/knowledge/architecture/language-html.md @@ -0,0 +1,27 @@ +--- +description: Built-in HTML discovery, grammar, syntax-catalog, and conservative graph-normalization contract. +resource: repository-architecture +tags: +- architecture +- html +- language-support +- parser +- tree-sitter +timestamp: 2026-08-25 +title: HTML Parsing Contract +type: architecture +--- +# HTML Parsing Contract + +The Graph Runtime indexes HTML with a built-in Tree-sitter profile while preserving the distinction between raw markup syntax and higher-level application components. + +## Public contract + +- `html` recognizes `.html` and `.htm` using `tree_sitter_html@0.23.2`. +- The key is advertised by the CLI syntax catalog and MCP `graph_syntax` schema. +- Ordered raw syntax includes documents, doctypes, elements, tags, scripts, styles, attributes, and text. +- HTML elements are not promoted to ontology `Component` nodes because markup alone does not establish framework or component identity. + +Embedded JavaScript and CSS injection parsing is outside this contract; standalone files are handled by their own language profiles. + +Related: [Graph Runtime](./graph-runtime.md) and [Repository Ownership Map](./repository-map.md). \ No newline at end of file diff --git a/src/adapters/cli/format/help.rs b/src/adapters/cli/format/help.rs index a981b65..3e098e2 100644 --- a/src/adapters/cli/format/help.rs +++ b/src/adapters/cli/format/help.rs @@ -43,7 +43,7 @@ pub(in crate::adapters::cli) fn graph_schema_help() -> &'static str { } pub(in crate::adapters::cli) fn graph_syntax_help() -> &'static str { - "codebase-graph syntax\n\nUSAGE:\n codebase-graph syntax [--format json|block] [--json] [--pretty]\n\nOPTIONS:\n One of c, cpp, css, fortran, go, markdown, python, or rust\n --format block or json; defaults to block\n --json Emit compact JSON output\n --pretty Pretty-print JSON output" + "codebase-graph syntax\n\nUSAGE:\n codebase-graph syntax [--format json|block] [--json] [--pretty]\n\nOPTIONS:\n One of c, cpp, css, fortran, go, html, markdown, python, or rust\n --format block or json; defaults to block\n --json Emit compact JSON output\n --pretty Pretty-print JSON output" } pub(in crate::adapters::cli) fn graph_query_helpers_help() -> &'static str { diff --git a/src/adapters/cli/tests/dispatch_materialize.rs b/src/adapters/cli/tests/dispatch_materialize.rs index 3e499ee..68ccc62 100644 --- a/src/adapters/cli/tests/dispatch_materialize.rs +++ b/src/adapters/cli/tests/dispatch_materialize.rs @@ -276,9 +276,26 @@ fn materialize_css_source_root_without_profiles() { ".card { color: red; width: calc(100% - 1rem); }\n", ) .unwrap(); + assert_materializes_language(&root, "styles.css", "css"); + let _ = fs::remove_dir_all(root); +} + +#[test] +fn materialize_html_source_root_without_profiles() { + let root = unique_temp_dir("codebase-graph-html-source-root"); + fs::create_dir_all(&root).unwrap(); + fs::write( + root.join("index.html"), + "
Welcome
\n", + ) + .unwrap(); + assert_materializes_language(&root, "index.html", "html"); + let _ = fs::remove_dir_all(root); +} + +fn assert_materializes_language(root: &Path, path: &str, language: &str) { let db_path = root.join(".codebaseGraph").join("graph.ldb"); let manifest_path = root.join(".codebaseGraph").join("manifest.json"); - let mut output = Vec::new(); run( [ @@ -303,8 +320,7 @@ fn materialize_css_source_root_without_profiles() { assert_eq!(value["database_written"], true); let manifest: serde_json::Value = serde_json::from_str(&fs::read_to_string(&manifest_path).unwrap()).unwrap(); - assert_eq!(manifest["files"]["styles.css"]["language"], "css"); - let _ = fs::remove_dir_all(root); + assert_eq!(manifest["files"][path]["language"], language); } #[test] diff --git a/src/adapters/cli/tests/graph.rs b/src/adapters/cli/tests/graph.rs index 2368fe9..e5f91ec 100644 --- a/src/adapters/cli/tests/graph.rs +++ b/src/adapters/cli/tests/graph.rs @@ -40,6 +40,14 @@ fn graph_syntax_outputs_language_catalog_and_validates_language() { .as_array() .is_some_and(|nodes| nodes.iter().any(|node| node["type"] == "rule_set"))); + let mut html_output = Vec::new(); + run(["syntax", "html", "--json"], &mut html_output).unwrap(); + let html: serde_json::Value = serde_json::from_slice(&html_output).unwrap(); + assert_eq!(html["language"], "html"); + assert!(html["node_types"] + .as_array() + .is_some_and(|nodes| nodes.iter().any(|node| node["type"] == "element"))); + let error = run(["syntax", "custom"], &mut Vec::new()).unwrap_err(); assert!(error.contains("Unknown syntax language")); assert!(run(["syntax"], &mut Vec::new()) diff --git a/src/adapters/mcp/tools.rs b/src/adapters/mcp/tools.rs index 7962797..6d8eed3 100644 --- a/src/adapters/mcp/tools.rs +++ b/src/adapters/mcp/tools.rs @@ -228,7 +228,7 @@ mod tests { assert_eq!(syntax["inputSchema"]["required"], json!(["language"])); assert_eq!( syntax["inputSchema"]["properties"]["language"]["enum"], - json!(["c", "cpp", "css", "fortran", "go", "markdown", "python", "rust"]) + json!(["c", "cpp", "css", "fortran", "go", "html", "markdown", "python", "rust"]) ); } } diff --git a/src/api/catalog.rs b/src/api/catalog.rs index 1468725..d5f50fd 100644 --- a/src/api/catalog.rs +++ b/src/api/catalog.rs @@ -196,6 +196,23 @@ mod tests { .is_some_and(Vec::is_empty)); } + #[test] + fn syntax_catalog_describes_html_document_nodes() { + let mut payload = load_catalog("syntax").expect("syntax catalog index should load"); + filter_catalog("syntax", &mut payload, Some("html")) + .expect("HTML syntax catalog should load"); + + assert_eq!(payload["grammar_version"], "tree_sitter_html@0.23.2"); + let nodes = payload["node_types"] + .as_array() + .expect("node types should be an array"); + assert!(nodes.iter().any(|node| node["type"] == "document")); + assert!(nodes.iter().any(|node| node["type"] == "element")); + assert!(payload["capture_mappings"] + .as_array() + .is_some_and(Vec::is_empty)); + } + #[test] fn syntax_catalog_rejects_missing_and_unsupported_languages() { let mut payload = load_catalog("syntax").expect("syntax catalog index should load"); @@ -205,6 +222,6 @@ mod tests { ); assert!(filter_catalog("syntax", &mut payload, Some("custom")) .unwrap_err() - .contains("Valid languages: c, cpp, css, fortran, go, markdown, python, rust")); + .contains("Valid languages: c, cpp, css, fortran, go, html, markdown, python, rust")); } } diff --git a/src/api/lifecycle.rs b/src/api/lifecycle.rs index 9476553..15a5d78 100644 --- a/src/api/lifecycle.rs +++ b/src/api/lifecycle.rs @@ -722,6 +722,7 @@ fn language_for_path(path: &Path) -> Option<&'static str> { Some("rs") => Some("rust"), Some("go") => Some("go"), Some("css") => Some("css"), + Some("html") | Some("htm") => Some("html"), Some("c") | Some("h") => Some("c"), Some("cc") | Some("cpp") | Some("cxx") | Some("hpp") | Some("hh") => Some("cpp"), Some("f") | Some("f90") | Some("f95") | Some("for") => Some("fortran"), diff --git a/src/parser/mod.rs b/src/parser/mod.rs index b4d852d..aa1b2c5 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -147,6 +147,23 @@ mod tests { assert!(marked_captures(&output.root).is_empty()); } + #[test] + fn html_tree_sitter_parser_preserves_document_structure() { + let output = parse_source( + "
Welcome
", + &profile("html"), + ) + .expect("HTML parsing should succeed"); + + assert_eq!(output.root.node_type, "document"); + assert!(output.diagnostics.is_empty()); + assert!(contains_node_type(&output.root, "doctype")); + assert!(contains_node_type(&output.root, "element")); + assert!(contains_node_type(&output.root, "style_element")); + assert!(contains_node_type(&output.root, "script_element")); + assert!(marked_captures(&output.root).is_empty()); + } + #[test] fn c_tree_sitter_parser_marks_profile_captures() { let output = parse_source( diff --git a/src/parser/tree_sitter.rs b/src/parser/tree_sitter.rs index 77ef3ba..444b0cb 100644 --- a/src/parser/tree_sitter.rs +++ b/src/parser/tree_sitter.rs @@ -50,6 +50,7 @@ fn grammar_language(profile: &LanguageProfile) -> Option { ("tree_sitter_css", _) | (_, "css") => Some(tree_sitter_css::LANGUAGE.into()), ("tree_sitter_fortran", _) | (_, "fortran") => Some(tree_sitter_fortran::LANGUAGE.into()), ("tree_sitter_go", _) | (_, "go") => Some(tree_sitter_go::LANGUAGE.into()), + ("tree_sitter_html", _) | (_, "html") => Some(tree_sitter_html::LANGUAGE.into()), ("tree_sitter_python", _) | (_, "python") => Some(tree_sitter_python::LANGUAGE.into()), ("tree_sitter_rust", _) | (_, "rust") => Some(tree_sitter_rust::LANGUAGE.into()), _ => None, @@ -63,6 +64,7 @@ pub(super) fn grammar_node_types(profile: &LanguageProfile) -> Option<&'static s ("tree_sitter_css", _) | (_, "css") => Some(tree_sitter_css::NODE_TYPES), ("tree_sitter_fortran", _) | (_, "fortran") => Some(tree_sitter_fortran::NODE_TYPES), ("tree_sitter_go", _) | (_, "go") => Some(tree_sitter_go::NODE_TYPES), + ("tree_sitter_html", _) | (_, "html") => Some(tree_sitter_html::NODE_TYPES), ("tree_sitter_python", _) | (_, "python") => Some(tree_sitter_python::NODE_TYPES), ("tree_sitter_rust", _) | (_, "rust") => Some(tree_sitter_rust::NODE_TYPES), _ => None, diff --git a/src/profiles.rs b/src/profiles.rs index 0813694..94f3c8e 100644 --- a/src/profiles.rs +++ b/src/profiles.rs @@ -93,6 +93,14 @@ pub(crate) fn built_in_profiles() -> Vec { root_node_types: vec!["stylesheet".to_string()], capture_mappings: Vec::new(), }, + LanguageProfile { + language: "html".to_string(), + suffixes: vec![".html".to_string(), ".htm".to_string()], + grammar_package: "tree_sitter_html".to_string(), + grammar_version: "tree_sitter_html@0.23.2".to_string(), + root_node_types: vec!["document".to_string()], + capture_mappings: Vec::new(), + }, LanguageProfile { language: "rust".to_string(), suffixes: vec![".rs".to_string()], @@ -239,6 +247,8 @@ mod tests { ("README.md", "markdown"), ("README.mdx", "markdown"), ("styles.css", "css"), + ("index.html", "html"), + ("legacy.htm", "html"), ("src/lib.rs", "rust"), ("main.go", "go"), ("service.c", "c"),