Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -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.

Expand Down
27 changes: 27 additions & 0 deletions knowledge/architecture/language-html.md
Original file line number Diff line number Diff line change
@@ -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).
2 changes: 1 addition & 1 deletion src/adapters/cli/format/help.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 <language> [--format json|block] [--json] [--pretty]\n\nOPTIONS:\n <language> One of c, cpp, css, fortran, go, markdown, python, or rust\n --format <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 <language> [--format json|block] [--json] [--pretty]\n\nOPTIONS:\n <language> One of c, cpp, css, fortran, go, html, markdown, python, or rust\n --format <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 {
Expand Down
22 changes: 19 additions & 3 deletions src/adapters/cli/tests/dispatch_materialize.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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"),
"<!doctype html><html><body><main>Welcome</main></body></html>\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(
[
Expand All @@ -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]
Expand Down
8 changes: 8 additions & 0 deletions src/adapters/cli/tests/graph.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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())
Expand Down
2 changes: 1 addition & 1 deletion src/adapters/mcp/tools.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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"])
);
}
}
19 changes: 18 additions & 1 deletion src/api/catalog.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand All @@ -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"));
}
}
1 change: 1 addition & 0 deletions src/api/lifecycle.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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"),
Expand Down
17 changes: 17 additions & 0 deletions src/parser/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(
"<!doctype html><html><head><style>.card { color: red; }</style></head><body><main>Welcome</main><script>boot();</script></body></html>",
&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(
Expand Down
2 changes: 2 additions & 0 deletions src/parser/tree_sitter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ fn grammar_language(profile: &LanguageProfile) -> Option<Language> {
("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,
Expand All @@ -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,
Expand Down
10 changes: 10 additions & 0 deletions src/profiles.rs
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,14 @@ pub(crate) fn built_in_profiles() -> Vec<LanguageProfile> {
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()],
Expand Down Expand Up @@ -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"),
Expand Down