rbtr-lang-html 2026.9.0.dev1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (21) hide show
  1. rbtr_lang_html-2026.9.0.dev1/LICENSE +21 -0
  2. rbtr_lang_html-2026.9.0.dev1/PKG-INFO +72 -0
  3. rbtr_lang_html-2026.9.0.dev1/README.md +46 -0
  4. rbtr_lang_html-2026.9.0.dev1/pyproject.toml +67 -0
  5. rbtr_lang_html-2026.9.0.dev1/pyproject.toml.orig +66 -0
  6. rbtr_lang_html-2026.9.0.dev1/src/rbtr_lang_html/__init__.py +1 -0
  7. rbtr_lang_html-2026.9.0.dev1/src/rbtr_lang_html/html.scm +21 -0
  8. rbtr_lang_html-2026.9.0.dev1/src/rbtr_lang_html/injections.scm +8 -0
  9. rbtr_lang_html-2026.9.0.dev1/src/rbtr_lang_html/plugin.py +110 -0
  10. rbtr_lang_html-2026.9.0.dev1/src/rbtr_lang_html/py.typed +0 -0
  11. rbtr_lang_html-2026.9.0.dev1/src/rbtr_lang_html/tests/__init__.py +0 -0
  12. rbtr_lang_html-2026.9.0.dev1/src/rbtr_lang_html/tests/__snapshots__/test_samples/test_edges_match_snapshot.json +5 -0
  13. rbtr_lang_html-2026.9.0.dev1/src/rbtr_lang_html/tests/__snapshots__/test_samples/test_extraction_matches_snapshot.json +382 -0
  14. rbtr_lang_html-2026.9.0.dev1/src/rbtr_lang_html/tests/cases_extraction.py +31 -0
  15. rbtr_lang_html-2026.9.0.dev1/src/rbtr_lang_html/tests/samples/html/greeter.js +4 -0
  16. rbtr_lang_html-2026.9.0.dev1/src/rbtr_lang_html/tests/samples/html/html.html +34 -0
  17. rbtr_lang_html-2026.9.0.dev1/src/rbtr_lang_html/tests/samples/html/pages/about.html +10 -0
  18. rbtr_lang_html-2026.9.0.dev1/src/rbtr_lang_html/tests/samples/html/pages/about.js +4 -0
  19. rbtr_lang_html-2026.9.0.dev1/src/rbtr_lang_html/tests/samples/html/styles.css +3 -0
  20. rbtr_lang_html-2026.9.0.dev1/src/rbtr_lang_html/tests/test_extraction.py +78 -0
  21. rbtr_lang_html-2026.9.0.dev1/src/rbtr_lang_html/tests/test_samples.py +81 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Alejandro Giacometti
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,72 @@
1
+ Metadata-Version: 2.4
2
+ Name: rbtr-lang-html
3
+ Version: 2026.9.0.dev1
4
+ Summary: rbtr — HTML language plugin
5
+ Keywords: code-search,code-index,tree-sitter,static-analysis,semantic-search,developer-tools,html
6
+ Author: Alejandro Giacometti
7
+ Author-email: Alejandro Giacometti <alejandro.giacometti@gmail.com>
8
+ License-Expression: MIT
9
+ License-File: LICENSE
10
+ Classifier: Development Status :: 4 - Beta
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: Programming Language :: Python :: 3 :: Only
13
+ Classifier: Programming Language :: Python :: 3.13
14
+ Classifier: Topic :: Software Development :: Libraries
15
+ Classifier: Topic :: Text Processing :: Indexing
16
+ Classifier: Typing :: Typed
17
+ Requires-Dist: rbtr==2026.9.0.dev1
18
+ Requires-Dist: tree-sitter-html
19
+ Requires-Python: >=3.13
20
+ Project-URL: Homepage, https://github.com/janrito/rbtr
21
+ Project-URL: Repository, https://github.com/janrito/rbtr
22
+ Project-URL: Documentation, https://github.com/janrito/rbtr/tree/main/packages/rbtr-lang-html#readme
23
+ Project-URL: Issues, https://github.com/janrito/rbtr/issues
24
+ Project-URL: Changelog, https://github.com/janrito/rbtr/releases
25
+ Description-Content-Type: text/markdown
26
+
27
+ # rbtr-lang-html
28
+
29
+ HTML support for [rbtr]. A **default** plugin — installed with rbtr
30
+ itself (`pip install rbtr`).
31
+
32
+ [rbtr]: https://github.com/janrito/rbtr/tree/main/packages/rbtr#readme
33
+
34
+ ## What it ingests
35
+
36
+ - **Sections** — major `<body>` elements (`<main>`, `<article>`, `<nav>`, …)
37
+ → doc sections, named by their `id` (else the tag name).
38
+ - **Imports** — `<script src>` and `<link href>` → import chunks carrying the
39
+ referenced path and a language hint, so edges link to the target file.
40
+
41
+ ## Chunks produced
42
+
43
+ ```html
44
+ <main id="content">…</main> <!-- doc_section "content" -->
45
+ <nav>…</nav> <!-- doc_section "nav" -->
46
+ <script src="app.js"></script> <!-- import {module: app.js, hint: javascript} -->
47
+ <link rel="stylesheet" href="a.css"> <!-- import {module: a.css, hint: css} -->
48
+ ```
49
+
50
+ ## Embedded / injected chunks
51
+
52
+ HTML embeds other languages: an inline `<script>` delegates to JavaScript and
53
+ an inline `<style>` to CSS, so their chunks extract at real line numbers
54
+ within the page, each carrying its own language.
55
+
56
+ ```html
57
+ <main id="app"> <!-- doc_section "app" (html) -->
58
+ <script>
59
+ function boot() { start(); } <!-- function "boot" (javascript) -->
60
+ </script>
61
+ <style>
62
+ .hero { color: crimson; } <!-- class ".hero" (css) -->
63
+ </style>
64
+ </main>
65
+ ```
66
+
67
+ ## Grammar & dependencies
68
+
69
+ Uses the `tree-sitter-html` grammar. No runtime dependency on other language
70
+ plugins; the test suite dev-depends on `rbtr-lang-javascript` and
71
+ `rbtr-lang-css` so the sample's linked and injected js/css extract and the
72
+ cross-file edges snapshot.
@@ -0,0 +1,46 @@
1
+ # rbtr-lang-html
2
+
3
+ HTML support for [rbtr]. A **default** plugin — installed with rbtr
4
+ itself (`pip install rbtr`).
5
+
6
+ [rbtr]: https://github.com/janrito/rbtr/tree/main/packages/rbtr#readme
7
+
8
+ ## What it ingests
9
+
10
+ - **Sections** — major `<body>` elements (`<main>`, `<article>`, `<nav>`, …)
11
+ → doc sections, named by their `id` (else the tag name).
12
+ - **Imports** — `<script src>` and `<link href>` → import chunks carrying the
13
+ referenced path and a language hint, so edges link to the target file.
14
+
15
+ ## Chunks produced
16
+
17
+ ```html
18
+ <main id="content">…</main> <!-- doc_section "content" -->
19
+ <nav>…</nav> <!-- doc_section "nav" -->
20
+ <script src="app.js"></script> <!-- import {module: app.js, hint: javascript} -->
21
+ <link rel="stylesheet" href="a.css"> <!-- import {module: a.css, hint: css} -->
22
+ ```
23
+
24
+ ## Embedded / injected chunks
25
+
26
+ HTML embeds other languages: an inline `<script>` delegates to JavaScript and
27
+ an inline `<style>` to CSS, so their chunks extract at real line numbers
28
+ within the page, each carrying its own language.
29
+
30
+ ```html
31
+ <main id="app"> <!-- doc_section "app" (html) -->
32
+ <script>
33
+ function boot() { start(); } <!-- function "boot" (javascript) -->
34
+ </script>
35
+ <style>
36
+ .hero { color: crimson; } <!-- class ".hero" (css) -->
37
+ </style>
38
+ </main>
39
+ ```
40
+
41
+ ## Grammar & dependencies
42
+
43
+ Uses the `tree-sitter-html` grammar. No runtime dependency on other language
44
+ plugins; the test suite dev-depends on `rbtr-lang-javascript` and
45
+ `rbtr-lang-css` so the sample's linked and injected js/css extract and the
46
+ cross-file edges snapshot.
@@ -0,0 +1,67 @@
1
+ [build-system]
2
+ requires = ["uv_build>=0.11.26,<1"]
3
+ build-backend = "uv_build"
4
+
5
+ [tool.uv.build-backend]
6
+ module-name = "rbtr_lang_html"
7
+
8
+ [tool.uv.sources.rbtr]
9
+ workspace = true
10
+
11
+ [tool.uv.sources.rbtr-lang-css]
12
+ workspace = true
13
+
14
+ [tool.uv.sources.rbtr-lang-javascript]
15
+ workspace = true
16
+
17
+ [project]
18
+ name = "rbtr-lang-html"
19
+ version = "2026.9.0-dev1"
20
+ description = "rbtr — HTML language plugin"
21
+ readme = "README.md"
22
+ license = "MIT"
23
+ license-files = ["LICENSE"]
24
+ keywords = [
25
+ "code-search",
26
+ "code-index",
27
+ "tree-sitter",
28
+ "static-analysis",
29
+ "semantic-search",
30
+ "developer-tools",
31
+ "html",
32
+ ]
33
+ classifiers = [
34
+ "Development Status :: 4 - Beta",
35
+ "Intended Audience :: Developers",
36
+ "Programming Language :: Python :: 3 :: Only",
37
+ "Programming Language :: Python :: 3.13",
38
+ "Topic :: Software Development :: Libraries",
39
+ "Topic :: Text Processing :: Indexing",
40
+ "Typing :: Typed",
41
+ ]
42
+ requires-python = ">=3.13"
43
+ dependencies = [
44
+ "rbtr==2026.9.0-dev1",
45
+ "tree-sitter-html",
46
+ ]
47
+
48
+ [[project.authors]]
49
+ name = "Alejandro Giacometti"
50
+ email = "alejandro.giacometti@gmail.com"
51
+
52
+ [project.urls]
53
+ Homepage = "https://github.com/janrito/rbtr"
54
+ Repository = "https://github.com/janrito/rbtr"
55
+ Documentation = "https://github.com/janrito/rbtr/tree/main/packages/rbtr-lang-html#readme"
56
+ Issues = "https://github.com/janrito/rbtr/issues"
57
+ Changelog = "https://github.com/janrito/rbtr/releases"
58
+
59
+ [project.entry-points."rbtr.languages"]
60
+ html = "rbtr_lang_html.plugin:html"
61
+
62
+ [dependency-groups]
63
+ dev = [
64
+ "rbtr[test]",
65
+ "rbtr-lang-css",
66
+ "rbtr-lang-javascript",
67
+ ]
@@ -0,0 +1,66 @@
1
+ [build-system]
2
+ requires = ["uv_build>=0.11.26,<1"]
3
+ build-backend = "uv_build"
4
+
5
+ [tool.uv.build-backend]
6
+ module-name = "rbtr_lang_html"
7
+ # No source-exclude: the plugin's tests, samples, and snapshots ship
8
+ # with the wheel (they are the language's golden record), and the .scm
9
+ # queries ship as package data.
10
+
11
+ [project]
12
+ name = "rbtr-lang-html"
13
+ version = "2026.9.0-dev1"
14
+ description = "rbtr — HTML language plugin"
15
+ readme = "README.md"
16
+ license = "MIT"
17
+ license-files = ["LICENSE"]
18
+ authors = [
19
+ { name = "Alejandro Giacometti", email = "alejandro.giacometti@gmail.com" },
20
+ ]
21
+ keywords = [
22
+ "code-search",
23
+ "code-index",
24
+ "tree-sitter",
25
+ "static-analysis",
26
+ "semantic-search",
27
+ "developer-tools",
28
+ "html",
29
+ ]
30
+ classifiers = [
31
+ "Development Status :: 4 - Beta",
32
+ "Intended Audience :: Developers",
33
+ "Programming Language :: Python :: 3 :: Only",
34
+ "Programming Language :: Python :: 3.13",
35
+ "Topic :: Software Development :: Libraries",
36
+ "Topic :: Text Processing :: Indexing",
37
+ "Typing :: Typed",
38
+ ]
39
+ requires-python = ">=3.13"
40
+ dependencies = [
41
+ "rbtr==2026.9.0-dev1",
42
+ "tree-sitter-html",
43
+ ]
44
+
45
+ [project.urls]
46
+ Homepage = "https://github.com/janrito/rbtr"
47
+ Repository = "https://github.com/janrito/rbtr"
48
+ Documentation = "https://github.com/janrito/rbtr/tree/main/packages/rbtr-lang-html#readme"
49
+ Issues = "https://github.com/janrito/rbtr/issues"
50
+ Changelog = "https://github.com/janrito/rbtr/releases"
51
+
52
+ [project.entry-points."rbtr.languages"]
53
+ # The value resolves to the language's `LanguageRegistration` (named by its
54
+ # id) — see the rbtr ARCHITECTURE "External plugins" note.
55
+ html = "rbtr_lang_html.plugin:html"
56
+
57
+ [tool.uv.sources]
58
+ rbtr = { workspace = true }
59
+ # The html sample embeds JavaScript and CSS (inline <script>/<style> and a
60
+ # linked .js/.css), so the sample test needs those plugins to extract the
61
+ # injected/linked chunks and snapshot the cross edges.
62
+ rbtr-lang-css = { workspace = true }
63
+ rbtr-lang-javascript = { workspace = true }
64
+
65
+ [dependency-groups]
66
+ dev = ["rbtr[test]", "rbtr-lang-css", "rbtr-lang-javascript"]
@@ -0,0 +1 @@
1
+ """HTML language plugin package."""
@@ -0,0 +1,21 @@
1
+ ; Comments (`<!-- -->`).
2
+ (comment) @comment
3
+
4
+ (element (start_tag (tag_name) @_tag)
5
+ (#any-of? @_tag "head" "body" "article" "aside" "nav" "section" "header" "main" "footer" "figure" "form" "dialog" "details" "table")) @doc_section
6
+
7
+ (script_element
8
+ (start_tag
9
+ (attribute (attribute_name) @_a
10
+ (quoted_attribute_value (attribute_value) @_import_module))
11
+ (#eq? @_a "src"))) @import
12
+
13
+ (element
14
+ [(start_tag (tag_name) @_lt
15
+ (attribute (attribute_name) @_h
16
+ (quoted_attribute_value (attribute_value) @_import_module)))
17
+ (self_closing_tag (tag_name) @_lt
18
+ (attribute (attribute_name) @_h
19
+ (quoted_attribute_value (attribute_value) @_import_module)))]
20
+ (#eq? @_lt "link")
21
+ (#eq? @_h "href")) @import
@@ -0,0 +1,8 @@
1
+ ; Inline <script>/<style> delegate to JavaScript/CSS. External
2
+ ; <script src>/<link href> have no raw_text, so they become imports instead.
3
+
4
+ ((script_element (raw_text) @injection.content)
5
+ (#set! injection.language "javascript"))
6
+
7
+ ((style_element (raw_text) @injection.content)
8
+ (#set! injection.language "css"))
@@ -0,0 +1,110 @@
1
+ """HTML language plugin.
2
+
3
+ Extracts semantic and structural elements as doc sections and cross-language
4
+ import references, via a tree-sitter query.
5
+
6
+ Extracted chunks::
7
+
8
+ <main id="content">…</main> → doc_section "content" (named by id)
9
+ <nav>…</nav> → doc_section "nav" (named by tag)
10
+ <script src="app.js"> → import {module: "app.js", hint javascript}
11
+ <link href="styles.css"> → import {module: "styles.css", hint css}
12
+
13
+ Element chunks cover `head`, `body`, sectioning content, landmarks, and
14
+ self-contained units; nested elements are each extracted (overlapping, like a
15
+ class and its methods). A file with none of these is left to the engine's
16
+ presence handling. Inline `<script>`/`<style>` delegate to JavaScript/CSS via
17
+ `injection_query`; external `<script src>`/`<link href>` become import edges.
18
+ """
19
+
20
+ from __future__ import annotations
21
+
22
+ from typing import TYPE_CHECKING
23
+
24
+ from rbtr.domain.models import ImportMeta
25
+ from rbtr.languages.registration import (
26
+ ImportResolver,
27
+ LanguageRegistration,
28
+ NameResolver,
29
+ QueryExtraction,
30
+ load_query,
31
+ )
32
+
33
+ if TYPE_CHECKING:
34
+ from tree_sitter import Node
35
+
36
+ # Elements that hold meaning of their own: the document containers, HTML5
37
+ # sectioning content and landmarks, and self-contained units. Named by their
38
+ # `id` when present, else by tag.
39
+
40
+
41
+ # Inline <script>/<style> delegate to JavaScript/CSS. External <script src>/
42
+ # <link href> have no raw_text, so they are skipped here and become imports.
43
+
44
+
45
+ def _get_attr(start_tag: Node, attr_name: str) -> str | None:
46
+ """Extract an attribute value from a start_tag node."""
47
+ for child in start_tag.children:
48
+ if child.type != "attribute":
49
+ continue
50
+ name_node = None
51
+ value_node = None
52
+ for gc in child.children:
53
+ if gc.type == "attribute_name" and gc.text:
54
+ name_node = gc
55
+ elif gc.type == "quoted_attribute_value":
56
+ for vg in gc.children:
57
+ if vg.type == "attribute_value" and vg.text:
58
+ value_node = vg
59
+ if (
60
+ name_node is not None
61
+ and value_node is not None
62
+ and name_node.text == attr_name.encode()
63
+ ):
64
+ text = value_node.text
65
+ return text.decode("utf-8", errors="replace") if text else None
66
+ return None
67
+
68
+
69
+ # ── Plugin ───────────────────────────────────────────────────────────
70
+
71
+
72
+ html = LanguageRegistration(
73
+ id="html",
74
+ extensions=frozenset({".html", ".htm"}),
75
+ grammar_module="tree_sitter_html",
76
+ extraction=QueryExtraction(
77
+ query=load_query(__package__, "html"),
78
+ ),
79
+ injection_query=load_query(__package__, "injections"),
80
+ import_targets=frozenset({"javascript", "typescript", "css"}),
81
+ extraction_serial=3,
82
+ )
83
+
84
+
85
+ @html.name_extractor
86
+ def _element_name(
87
+ resolver: NameResolver, capture_name: str, node: Node, captures: dict[str, list[Node]]
88
+ ) -> str:
89
+ """Name a semantic element by its `id`, else its tag; others by default."""
90
+ if capture_name != "doc_section":
91
+ return resolver(capture_name, node, captures)
92
+ start_tag = next((c for c in node.children if c.type == "start_tag"), None)
93
+ if start_tag is not None:
94
+ elem_id = _get_attr(start_tag, "id")
95
+ if elem_id:
96
+ return elem_id
97
+ tag_nodes = captures.get("_tag")
98
+ if tag_nodes and tag_nodes[0].text:
99
+ return tag_nodes[0].text.decode()
100
+ return ""
101
+
102
+
103
+ @html.import_extractor
104
+ def _import_meta(
105
+ resolver: ImportResolver, node: Node, captures: dict[str, list[Node]]
106
+ ) -> ImportMeta:
107
+ """Import metadata with a language hint from the element kind."""
108
+ meta = resolver(node, captures)
109
+ meta.language_hint = "javascript" if node.type == "script_element" else "css"
110
+ return meta
@@ -0,0 +1,5 @@
1
+ [
2
+ "html.html::<link rel=\"stylesheet\" href=\"styles.css\"> -> styles.css::.greeter [imports]",
3
+ "html.html::<script src=\"greeter.js\"></script> -> greeter.js::greet [imports]",
4
+ "pages/about.html::<script src=\"about.js\"></script> -> pages/about.js::describe [imports]"
5
+ ]
@@ -0,0 +1,382 @@
1
+ [
2
+ {
3
+ "blob_sha": "sha1",
4
+ "file_path": "greeter.js",
5
+ "kind": "comment",
6
+ "name": "",
7
+ "scope": "",
8
+ "language": "javascript",
9
+ "file_language": "javascript",
10
+ "content": "// Client-side greeter.",
11
+ "line_start": 1,
12
+ "line_end": 1,
13
+ "metadata": {
14
+ "module": "",
15
+ "names": "",
16
+ "dots": "",
17
+ "language_hint": ""
18
+ },
19
+ "id": "1086b120f17a34b9"
20
+ },
21
+ {
22
+ "blob_sha": "sha1",
23
+ "file_path": "greeter.js",
24
+ "kind": "function",
25
+ "name": "greet",
26
+ "scope": "",
27
+ "language": "javascript",
28
+ "file_language": "javascript",
29
+ "content": "function greet(name) {\n return `Hello, ${name}`;\n}",
30
+ "line_start": 2,
31
+ "line_end": 4,
32
+ "metadata": {
33
+ "module": "",
34
+ "names": "",
35
+ "dots": "",
36
+ "language_hint": ""
37
+ },
38
+ "id": "0622b12891ac9cdc"
39
+ },
40
+ {
41
+ "blob_sha": "sha1",
42
+ "file_path": "html.html",
43
+ "kind": "comment",
44
+ "name": "",
45
+ "scope": "",
46
+ "language": "html",
47
+ "file_language": "html",
48
+ "content": "<!-- Greeter page. The HTML plugin extracts <script src>/<link href>\n as imports (with a language hint) and major <body> elements as\n doc sections. -->",
49
+ "line_start": 2,
50
+ "line_end": 4,
51
+ "metadata": {
52
+ "module": "",
53
+ "names": "",
54
+ "dots": "",
55
+ "language_hint": ""
56
+ },
57
+ "id": "df0cb4650d419a7c"
58
+ },
59
+ {
60
+ "blob_sha": "sha1",
61
+ "file_path": "html.html",
62
+ "kind": "doc_section",
63
+ "name": "head",
64
+ "scope": "",
65
+ "language": "html",
66
+ "file_language": "html",
67
+ "content": "<head>\n <title>Greeter</title>\n <link rel=\"stylesheet\" href=\"styles.css\">\n <script src=\"greeter.js\"></script>\n <style>\n .greeter {\n font-family: system-ui;\n }\n </style>\n </head>",
68
+ "line_start": 6,
69
+ "line_end": 15,
70
+ "metadata": {
71
+ "module": "",
72
+ "names": "",
73
+ "dots": "",
74
+ "language_hint": ""
75
+ },
76
+ "id": "8b6caffda7ad3b29"
77
+ },
78
+ {
79
+ "blob_sha": "sha1",
80
+ "file_path": "html.html",
81
+ "kind": "import",
82
+ "name": "<link rel=\"stylesheet\" href=\"styles.css\">",
83
+ "scope": "",
84
+ "language": "html",
85
+ "file_language": "html",
86
+ "content": "<link rel=\"stylesheet\" href=\"styles.css\">\n ",
87
+ "line_start": 8,
88
+ "line_end": 9,
89
+ "metadata": {
90
+ "module": "styles.css",
91
+ "names": "",
92
+ "dots": "",
93
+ "language_hint": "css"
94
+ },
95
+ "id": "74784d330d51707b"
96
+ },
97
+ {
98
+ "blob_sha": "sha1",
99
+ "file_path": "html.html",
100
+ "kind": "import",
101
+ "name": "<script src=\"greeter.js\"></script>",
102
+ "scope": "",
103
+ "language": "html",
104
+ "file_language": "html",
105
+ "content": "<script src=\"greeter.js\"></script>",
106
+ "line_start": 9,
107
+ "line_end": 9,
108
+ "metadata": {
109
+ "module": "greeter.js",
110
+ "names": "",
111
+ "dots": "",
112
+ "language_hint": "javascript"
113
+ },
114
+ "id": "da346ac121464c2b"
115
+ },
116
+ {
117
+ "blob_sha": "sha1",
118
+ "file_path": "html.html",
119
+ "kind": "doc_section",
120
+ "name": "body",
121
+ "scope": "",
122
+ "language": "html",
123
+ "file_language": "html",
124
+ "content": "<body>\n <header class=\"page-header\">\n <h1>Greeter</h1>\n </header>\n <main id=\"content\">\n <section id=\"intro\">\n <p>Format greetings for named recipients.</p>\n </section>\n </main>\n <footer>\n <small>Greeter &mdash; a greetings library.</small>\n </footer>\n <script>\n function init() {\n return greet(\"world\");\n }\n </script>\n </body>",
125
+ "line_start": 16,
126
+ "line_end": 33,
127
+ "metadata": {
128
+ "module": "",
129
+ "names": "",
130
+ "dots": "",
131
+ "language_hint": ""
132
+ },
133
+ "id": "8c9ed79d9778bcb8"
134
+ },
135
+ {
136
+ "blob_sha": "sha1",
137
+ "file_path": "html.html",
138
+ "kind": "doc_section",
139
+ "name": "header",
140
+ "scope": "",
141
+ "language": "html",
142
+ "file_language": "html",
143
+ "content": "<header class=\"page-header\">\n <h1>Greeter</h1>\n </header>",
144
+ "line_start": 17,
145
+ "line_end": 19,
146
+ "metadata": {
147
+ "module": "",
148
+ "names": "",
149
+ "dots": "",
150
+ "language_hint": ""
151
+ },
152
+ "id": "be2a2586545ab335"
153
+ },
154
+ {
155
+ "blob_sha": "sha1",
156
+ "file_path": "html.html",
157
+ "kind": "doc_section",
158
+ "name": "content",
159
+ "scope": "",
160
+ "language": "html",
161
+ "file_language": "html",
162
+ "content": "<main id=\"content\">\n <section id=\"intro\">\n <p>Format greetings for named recipients.</p>\n </section>\n </main>",
163
+ "line_start": 20,
164
+ "line_end": 24,
165
+ "metadata": {
166
+ "module": "",
167
+ "names": "",
168
+ "dots": "",
169
+ "language_hint": ""
170
+ },
171
+ "id": "a55b5c4e1e5c6f38"
172
+ },
173
+ {
174
+ "blob_sha": "sha1",
175
+ "file_path": "html.html",
176
+ "kind": "doc_section",
177
+ "name": "intro",
178
+ "scope": "",
179
+ "language": "html",
180
+ "file_language": "html",
181
+ "content": "<section id=\"intro\">\n <p>Format greetings for named recipients.</p>\n </section>",
182
+ "line_start": 21,
183
+ "line_end": 23,
184
+ "metadata": {
185
+ "module": "",
186
+ "names": "",
187
+ "dots": "",
188
+ "language_hint": ""
189
+ },
190
+ "id": "2633ecb762f41b95"
191
+ },
192
+ {
193
+ "blob_sha": "sha1",
194
+ "file_path": "html.html",
195
+ "kind": "doc_section",
196
+ "name": "footer",
197
+ "scope": "",
198
+ "language": "html",
199
+ "file_language": "html",
200
+ "content": "<footer>\n <small>Greeter &mdash; a greetings library.</small>\n </footer>",
201
+ "line_start": 25,
202
+ "line_end": 27,
203
+ "metadata": {
204
+ "module": "",
205
+ "names": "",
206
+ "dots": "",
207
+ "language_hint": ""
208
+ },
209
+ "id": "b036defcfc351337"
210
+ },
211
+ {
212
+ "blob_sha": "sha1",
213
+ "file_path": "html.html",
214
+ "kind": "class",
215
+ "name": ".greeter",
216
+ "scope": "",
217
+ "language": "css",
218
+ "file_language": "html",
219
+ "content": ".greeter {\n font-family: system-ui;\n }",
220
+ "line_start": 11,
221
+ "line_end": 13,
222
+ "metadata": {
223
+ "module": "",
224
+ "names": "",
225
+ "dots": "",
226
+ "language_hint": ""
227
+ },
228
+ "id": "722491d8d6786030"
229
+ },
230
+ {
231
+ "blob_sha": "sha1",
232
+ "file_path": "html.html",
233
+ "kind": "function",
234
+ "name": "init",
235
+ "scope": "",
236
+ "language": "javascript",
237
+ "file_language": "html",
238
+ "content": "function init() {\n return greet(\"world\");\n }",
239
+ "line_start": 29,
240
+ "line_end": 31,
241
+ "metadata": {
242
+ "module": "",
243
+ "names": "",
244
+ "dots": "",
245
+ "language_hint": ""
246
+ },
247
+ "id": "ab46b1ed9ad7e266"
248
+ },
249
+ {
250
+ "blob_sha": "sha1",
251
+ "file_path": "pages/about.html",
252
+ "kind": "comment",
253
+ "name": "",
254
+ "scope": "",
255
+ "language": "html",
256
+ "file_language": "html",
257
+ "content": "<!-- Linked from html.html. Its script src is a bare sibling, which only\n resolves relative to this directory. -->",
258
+ "line_start": 1,
259
+ "line_end": 2,
260
+ "metadata": {
261
+ "module": "",
262
+ "names": "",
263
+ "dots": "",
264
+ "language_hint": ""
265
+ },
266
+ "id": "685e10d015c11d95"
267
+ },
268
+ {
269
+ "blob_sha": "sha1",
270
+ "file_path": "pages/about.html",
271
+ "kind": "doc_section",
272
+ "name": "head",
273
+ "scope": "",
274
+ "language": "html",
275
+ "file_language": "html",
276
+ "content": "<head>\n <script src=\"about.js\"></script>\n </head>",
277
+ "line_start": 4,
278
+ "line_end": 6,
279
+ "metadata": {
280
+ "module": "",
281
+ "names": "",
282
+ "dots": "",
283
+ "language_hint": ""
284
+ },
285
+ "id": "3b7525e08faf54be"
286
+ },
287
+ {
288
+ "blob_sha": "sha1",
289
+ "file_path": "pages/about.html",
290
+ "kind": "import",
291
+ "name": "<script src=\"about.js\"></script>",
292
+ "scope": "",
293
+ "language": "html",
294
+ "file_language": "html",
295
+ "content": "<script src=\"about.js\"></script>",
296
+ "line_start": 5,
297
+ "line_end": 5,
298
+ "metadata": {
299
+ "module": "about.js",
300
+ "names": "",
301
+ "dots": "",
302
+ "language_hint": "javascript"
303
+ },
304
+ "id": "869b48c31de20a5d"
305
+ },
306
+ {
307
+ "blob_sha": "sha1",
308
+ "file_path": "pages/about.html",
309
+ "kind": "doc_section",
310
+ "name": "body",
311
+ "scope": "",
312
+ "language": "html",
313
+ "file_language": "html",
314
+ "content": "<body>\n <h1>About</h1>\n </body>",
315
+ "line_start": 7,
316
+ "line_end": 9,
317
+ "metadata": {
318
+ "module": "",
319
+ "names": "",
320
+ "dots": "",
321
+ "language_hint": ""
322
+ },
323
+ "id": "8c81895e570fa47d"
324
+ },
325
+ {
326
+ "blob_sha": "sha1",
327
+ "file_path": "pages/about.js",
328
+ "kind": "comment",
329
+ "name": "",
330
+ "scope": "",
331
+ "language": "javascript",
332
+ "file_language": "javascript",
333
+ "content": "// Referenced from pages/about.html as a bare sibling.",
334
+ "line_start": 1,
335
+ "line_end": 1,
336
+ "metadata": {
337
+ "module": "",
338
+ "names": "",
339
+ "dots": "",
340
+ "language_hint": ""
341
+ },
342
+ "id": "1086b120f17a34b9"
343
+ },
344
+ {
345
+ "blob_sha": "sha1",
346
+ "file_path": "pages/about.js",
347
+ "kind": "function",
348
+ "name": "describe",
349
+ "scope": "",
350
+ "language": "javascript",
351
+ "file_language": "javascript",
352
+ "content": "function describe() {\n return \"about\";\n}",
353
+ "line_start": 2,
354
+ "line_end": 4,
355
+ "metadata": {
356
+ "module": "",
357
+ "names": "",
358
+ "dots": "",
359
+ "language_hint": ""
360
+ },
361
+ "id": "e8f56e596019719c"
362
+ },
363
+ {
364
+ "blob_sha": "sha1",
365
+ "file_path": "styles.css",
366
+ "kind": "class",
367
+ "name": ".greeter",
368
+ "scope": "",
369
+ "language": "css",
370
+ "file_language": "css",
371
+ "content": ".greeter {\n color: #333;\n}",
372
+ "line_start": 1,
373
+ "line_end": 3,
374
+ "metadata": {
375
+ "module": "",
376
+ "names": "",
377
+ "dots": "",
378
+ "language_hint": ""
379
+ },
380
+ "id": "6f956de079e5ad55"
381
+ }
382
+ ]
@@ -0,0 +1,31 @@
1
+ """HTML extraction test cases."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pytest_cases import case
6
+
7
+ type SymbolCase = tuple[str, str, list[tuple[str, str, str]]]
8
+
9
+
10
+ @case(tags=["symbol"])
11
+ def case_html_semantic_elements() -> SymbolCase:
12
+ """HTML names an element by its id (even beside a class), else its tag."""
13
+ src = """\
14
+ <html>
15
+ <body>
16
+ <main id="content"><p>Hello</p></main>
17
+ <article class="post" id="first">Text</article>
18
+ <nav>Links</nav>
19
+ </body>
20
+ </html>
21
+ """
22
+ return (
23
+ "html",
24
+ src,
25
+ [
26
+ ("doc_section", "body", ""),
27
+ ("doc_section", "content", ""),
28
+ ("doc_section", "first", ""),
29
+ ("doc_section", "nav", ""),
30
+ ],
31
+ )
@@ -0,0 +1,4 @@
1
+ // Client-side greeter.
2
+ export function greet(name) {
3
+ return `Hello, ${name}`;
4
+ }
@@ -0,0 +1,34 @@
1
+ <!DOCTYPE html>
2
+ <!-- Greeter page. The HTML plugin extracts <script src>/<link href>
3
+ as imports (with a language hint) and major <body> elements as
4
+ doc sections. -->
5
+ <html lang="en">
6
+ <head>
7
+ <title>Greeter</title>
8
+ <link rel="stylesheet" href="styles.css">
9
+ <script src="greeter.js"></script>
10
+ <style>
11
+ .greeter {
12
+ font-family: system-ui;
13
+ }
14
+ </style>
15
+ </head>
16
+ <body>
17
+ <header class="page-header">
18
+ <h1>Greeter</h1>
19
+ </header>
20
+ <main id="content">
21
+ <section id="intro">
22
+ <p>Format greetings for named recipients.</p>
23
+ </section>
24
+ </main>
25
+ <footer>
26
+ <small>Greeter &mdash; a greetings library.</small>
27
+ </footer>
28
+ <script>
29
+ function init() {
30
+ return greet("world");
31
+ }
32
+ </script>
33
+ </body>
34
+ </html>
@@ -0,0 +1,10 @@
1
+ <!-- Linked from html.html. Its script src is a bare sibling, which only
2
+ resolves relative to this directory. -->
3
+ <html>
4
+ <head>
5
+ <script src="about.js"></script>
6
+ </head>
7
+ <body>
8
+ <h1>About</h1>
9
+ </body>
10
+ </html>
@@ -0,0 +1,4 @@
1
+ // Referenced from pages/about.html as a bare sibling.
2
+ export function describe() {
3
+ return "about";
4
+ }
@@ -0,0 +1,3 @@
1
+ .greeter {
2
+ color: #333;
3
+ }
@@ -0,0 +1,78 @@
1
+ """HTML extraction tests.
2
+
3
+ The symbol case (`cases_extraction.py`) drives the shared check; the functions
4
+ below pin HTML's element naming and its `<script src>` / `<link href>` import
5
+ and non-semantic-presence edge behaviour.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from pytest_cases import parametrize_with_cases
11
+
12
+ from rbtr.domain.models import ChunkKind
13
+ from rbtr.git import FileEntry
14
+ from rbtr.languages.extract import extract_file
15
+
16
+
17
+ @parametrize_with_cases("lang, source, expected", cases=".cases_extraction", has_tag="symbol")
18
+ def test_extracts_expected_symbols(lang: str, source: str, expected: list) -> None:
19
+ """Each expected (kind, name, scope) tuple appears in the output."""
20
+ chunks = extract_file(FileEntry("input", "sha1", source.encode()), lang)
21
+ symbols = [(c.kind, c.name, c.scope) for c in chunks]
22
+ for exp in expected:
23
+ assert exp in symbols, f"expected {exp} not found in {symbols}"
24
+
25
+
26
+ def test_html_non_semantic_only_emits_presence() -> None:
27
+ """HTML with no head/body/semantic element yields only a presence chunk.
28
+
29
+ A non-semantic wrapper (`<div>`) is not elevated, so no searchable
30
+ doc_section is produced; the engine appends one content-less host chunk.
31
+ """
32
+ chunks = extract_file(FileEntry("input", "sha1", b"<div>hello</div>"), "html")
33
+ assert [c.kind for c in chunks if c.content] == []
34
+ assert [c.language for c in chunks] == ["html"]
35
+
36
+
37
+ def test_html_script_src_produces_import() -> None:
38
+ """HTML <script src> produces an import chunk."""
39
+ src = """\
40
+ <html>
41
+ <head><script src="app.js"></script></head>
42
+ <body><p>hello</p></body>
43
+ </html>
44
+ """
45
+ chunks = extract_file(FileEntry("input", "sha1", src.encode()), "html")
46
+ imports = [c for c in chunks if c.kind == ChunkKind.IMPORT]
47
+ assert len(imports) == 1
48
+ assert imports[0].metadata.module == "app.js"
49
+ assert imports[0].metadata.language_hint == "javascript"
50
+
51
+
52
+ def test_html_link_href_produces_import() -> None:
53
+ """HTML <link href> produces an import chunk."""
54
+ src = """\
55
+ <html>
56
+ <head><link rel="stylesheet" href="styles.css"></head>
57
+ <body><p>hello</p></body>
58
+ </html>
59
+ """
60
+ chunks = extract_file(FileEntry("input", "sha1", src.encode()), "html")
61
+ imports = [c for c in chunks if c.kind == ChunkKind.IMPORT]
62
+ assert len(imports) == 1
63
+ assert imports[0].metadata.module == "styles.css"
64
+ assert imports[0].metadata.language_hint == "css"
65
+
66
+
67
+ def test_html_self_closing_link_produces_import() -> None:
68
+ """An XHTML-style self-closing `<link ... />` produces an import."""
69
+ src = """\
70
+ <html>
71
+ <head><link rel="stylesheet" href="styles.css" /></head>
72
+ <body><p>hello</p></body>
73
+ </html>
74
+ """
75
+ chunks = extract_file(FileEntry("input", "sha1", src.encode()), "html")
76
+ imports = [c for c in chunks if c.kind == ChunkKind.IMPORT]
77
+ assert len(imports) == 1
78
+ assert imports[0].metadata.module == "styles.css"
@@ -0,0 +1,81 @@
1
+ """HTML sample extraction: the `samples/html/` project through the real pipeline.
2
+
3
+ The sample links a `.js` and a `.css` and embeds inline `<script>`/`<style>`,
4
+ so the javascript and css plugins (dev dependencies) extract the linked and
5
+ injected chunks, and the edge snapshot captures the cross-file links.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from pathlib import Path
11
+ from typing import TYPE_CHECKING
12
+
13
+ import pytest
14
+ from tree_sitter import Parser
15
+
16
+ from rbtr.domain.models import Chunk, ChunkKind, Edge
17
+ from rbtr.git import FileEntry
18
+ from rbtr.languages.edges import build_resolution_map, infer_import_edges
19
+ from rbtr.languages.extract import extract_file
20
+ from rbtr.languages.manager import get_manager
21
+ from rbtr.testing import render_edges
22
+
23
+ if TYPE_CHECKING:
24
+ from syrupy.assertion import SnapshotAssertion
25
+
26
+
27
+ @pytest.fixture
28
+ def project() -> list[tuple[str, str]]:
29
+ """The `(relative path, text)` files of the `samples/html/` project."""
30
+ root = Path(__file__).parent / "samples" / "html"
31
+ return [
32
+ (str(p.relative_to(root)), p.read_text()) for p in sorted(root.rglob("*")) if p.is_file()
33
+ ]
34
+
35
+
36
+ @pytest.fixture
37
+ def chunks(project: list[tuple[str, str]]) -> list[Chunk]:
38
+ """Chunks from every project file, each via the real `extract_file`.
39
+
40
+ A file is extracted as its own detected language, so the project spans
41
+ html, javascript, and css.
42
+ """
43
+ manager = get_manager()
44
+ out: list[Chunk] = []
45
+ for path, text in project:
46
+ lang = manager.detect_language(path) or "html"
47
+ out.extend(extract_file(FileEntry(path, "sha1", text.encode()), lang))
48
+ return out
49
+
50
+
51
+ @pytest.fixture
52
+ def edges(project: list[tuple[str, str]], chunks: list[Chunk]) -> list[Edge]:
53
+ """Import edges inferred across the project's files."""
54
+ manager = get_manager()
55
+ repo_files = {path for path, _ in project}
56
+ return infer_import_edges(chunks, repo_files, build_resolution_map(manager))
57
+
58
+
59
+ def test_emits_expected_kinds(chunks: list[Chunk]) -> None:
60
+ """The sample exercises HTML's import, doc-section, and (injected) function chunks."""
61
+ kinds = {c.kind for c in chunks}
62
+ assert {ChunkKind.IMPORT, ChunkKind.DOC_SECTION, ChunkKind.FUNCTION, ChunkKind.COMMENT} <= kinds
63
+
64
+
65
+ def test_parses_cleanly(project: list[tuple[str, str]]) -> None:
66
+ """Every project file is valid source — no tree-sitter ERROR/MISSING nodes."""
67
+ manager = get_manager()
68
+ for path, text in project:
69
+ grammar = manager.grammar(manager.detect_language(path) or "html")
70
+ assert grammar is not None
71
+ assert not Parser(grammar).parse(text.encode()).root_node.has_error, path
72
+
73
+
74
+ def test_extraction_matches_snapshot(chunks: list[Chunk], snapshot_json: SnapshotAssertion) -> None:
75
+ assert chunks == snapshot_json
76
+
77
+
78
+ def test_edges_match_snapshot(
79
+ chunks: list[Chunk], edges: list[Edge], snapshot_json: SnapshotAssertion
80
+ ) -> None:
81
+ assert render_edges(edges, chunks) == snapshot_json