rbtr-lang-html 2026.7.0.dev0__py3-none-any.whl
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.
- rbtr_lang_html/__init__.py +1 -0
- rbtr_lang_html/html.scm +21 -0
- rbtr_lang_html/injections.scm +8 -0
- rbtr_lang_html/plugin.py +110 -0
- rbtr_lang_html/py.typed +0 -0
- rbtr_lang_html/tests/__init__.py +0 -0
- rbtr_lang_html/tests/__snapshots__/test_samples/test_edges_match_snapshot.json +4 -0
- rbtr_lang_html/tests/__snapshots__/test_samples/test_extraction_matches_snapshot.json +254 -0
- rbtr_lang_html/tests/cases_extraction.py +31 -0
- rbtr_lang_html/tests/samples/html/greeter.js +4 -0
- rbtr_lang_html/tests/samples/html/html.html +34 -0
- rbtr_lang_html/tests/samples/html/styles.css +3 -0
- rbtr_lang_html/tests/test_extraction.py +78 -0
- rbtr_lang_html/tests/test_samples.py +81 -0
- rbtr_lang_html-2026.7.0.dev0.dist-info/METADATA +8 -0
- rbtr_lang_html-2026.7.0.dev0.dist-info/RECORD +18 -0
- rbtr_lang_html-2026.7.0.dev0.dist-info/WHEEL +4 -0
- rbtr_lang_html-2026.7.0.dev0.dist-info/entry_points.txt +3 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""HTML language plugin package."""
|
rbtr_lang_html/html.scm
ADDED
|
@@ -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"))
|
rbtr_lang_html/plugin.py
ADDED
|
@@ -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.index.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 "<anonymous>"
|
|
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
|
rbtr_lang_html/py.typed
ADDED
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
[
|
|
2
|
+
{
|
|
3
|
+
"id": "0da3db16ee1c511c",
|
|
4
|
+
"blob_sha": "sha1",
|
|
5
|
+
"file_path": "greeter.js",
|
|
6
|
+
"kind": "comment",
|
|
7
|
+
"name": "<anonymous>",
|
|
8
|
+
"scope": "",
|
|
9
|
+
"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
|
+
},
|
|
20
|
+
{
|
|
21
|
+
"id": "7250027edb443428",
|
|
22
|
+
"blob_sha": "sha1",
|
|
23
|
+
"file_path": "greeter.js",
|
|
24
|
+
"kind": "function",
|
|
25
|
+
"name": "greet",
|
|
26
|
+
"scope": "",
|
|
27
|
+
"language": "javascript",
|
|
28
|
+
"content": "function greet(name) {\n return `Hello, ${name}`;\n}",
|
|
29
|
+
"line_start": 2,
|
|
30
|
+
"line_end": 4,
|
|
31
|
+
"metadata": {
|
|
32
|
+
"module": "",
|
|
33
|
+
"names": "",
|
|
34
|
+
"dots": "",
|
|
35
|
+
"language_hint": ""
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
"id": "a112d7329735ed9a",
|
|
40
|
+
"blob_sha": "sha1",
|
|
41
|
+
"file_path": "html.html",
|
|
42
|
+
"kind": "comment",
|
|
43
|
+
"name": "<anonymous>",
|
|
44
|
+
"scope": "",
|
|
45
|
+
"language": "html",
|
|
46
|
+
"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. -->",
|
|
47
|
+
"line_start": 2,
|
|
48
|
+
"line_end": 4,
|
|
49
|
+
"metadata": {
|
|
50
|
+
"module": "",
|
|
51
|
+
"names": "",
|
|
52
|
+
"dots": "",
|
|
53
|
+
"language_hint": ""
|
|
54
|
+
}
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
"id": "27068c2cfea9cd2c",
|
|
58
|
+
"blob_sha": "sha1",
|
|
59
|
+
"file_path": "html.html",
|
|
60
|
+
"kind": "doc_section",
|
|
61
|
+
"name": "head",
|
|
62
|
+
"scope": "",
|
|
63
|
+
"language": "html",
|
|
64
|
+
"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>",
|
|
65
|
+
"line_start": 6,
|
|
66
|
+
"line_end": 15,
|
|
67
|
+
"metadata": {
|
|
68
|
+
"module": "",
|
|
69
|
+
"names": "",
|
|
70
|
+
"dots": "",
|
|
71
|
+
"language_hint": ""
|
|
72
|
+
}
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
"id": "f28a9a9f2f8997fb",
|
|
76
|
+
"blob_sha": "sha1",
|
|
77
|
+
"file_path": "html.html",
|
|
78
|
+
"kind": "import",
|
|
79
|
+
"name": "<link rel=\"stylesheet\" href=\"styles.css\">",
|
|
80
|
+
"scope": "",
|
|
81
|
+
"language": "html",
|
|
82
|
+
"content": "<link rel=\"stylesheet\" href=\"styles.css\">\n ",
|
|
83
|
+
"line_start": 8,
|
|
84
|
+
"line_end": 9,
|
|
85
|
+
"metadata": {
|
|
86
|
+
"module": "styles.css",
|
|
87
|
+
"names": "",
|
|
88
|
+
"dots": "",
|
|
89
|
+
"language_hint": "css"
|
|
90
|
+
}
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
"id": "97b687eb6737629d",
|
|
94
|
+
"blob_sha": "sha1",
|
|
95
|
+
"file_path": "html.html",
|
|
96
|
+
"kind": "import",
|
|
97
|
+
"name": "<script src=\"greeter.js\"></script>",
|
|
98
|
+
"scope": "",
|
|
99
|
+
"language": "html",
|
|
100
|
+
"content": "<script src=\"greeter.js\"></script>",
|
|
101
|
+
"line_start": 9,
|
|
102
|
+
"line_end": 9,
|
|
103
|
+
"metadata": {
|
|
104
|
+
"module": "greeter.js",
|
|
105
|
+
"names": "",
|
|
106
|
+
"dots": "",
|
|
107
|
+
"language_hint": "javascript"
|
|
108
|
+
}
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
"id": "6913a3afc10a6ef7",
|
|
112
|
+
"blob_sha": "sha1",
|
|
113
|
+
"file_path": "html.html",
|
|
114
|
+
"kind": "doc_section",
|
|
115
|
+
"name": "body",
|
|
116
|
+
"scope": "",
|
|
117
|
+
"language": "html",
|
|
118
|
+
"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 — a greetings library.</small>\n </footer>\n <script>\n function init() {\n return greet(\"world\");\n }\n </script>\n </body>",
|
|
119
|
+
"line_start": 16,
|
|
120
|
+
"line_end": 33,
|
|
121
|
+
"metadata": {
|
|
122
|
+
"module": "",
|
|
123
|
+
"names": "",
|
|
124
|
+
"dots": "",
|
|
125
|
+
"language_hint": ""
|
|
126
|
+
}
|
|
127
|
+
},
|
|
128
|
+
{
|
|
129
|
+
"id": "e7560b4336313437",
|
|
130
|
+
"blob_sha": "sha1",
|
|
131
|
+
"file_path": "html.html",
|
|
132
|
+
"kind": "doc_section",
|
|
133
|
+
"name": "header",
|
|
134
|
+
"scope": "",
|
|
135
|
+
"language": "html",
|
|
136
|
+
"content": "<header class=\"page-header\">\n <h1>Greeter</h1>\n </header>",
|
|
137
|
+
"line_start": 17,
|
|
138
|
+
"line_end": 19,
|
|
139
|
+
"metadata": {
|
|
140
|
+
"module": "",
|
|
141
|
+
"names": "",
|
|
142
|
+
"dots": "",
|
|
143
|
+
"language_hint": ""
|
|
144
|
+
}
|
|
145
|
+
},
|
|
146
|
+
{
|
|
147
|
+
"id": "3c1680d2b75f7fa8",
|
|
148
|
+
"blob_sha": "sha1",
|
|
149
|
+
"file_path": "html.html",
|
|
150
|
+
"kind": "doc_section",
|
|
151
|
+
"name": "content",
|
|
152
|
+
"scope": "",
|
|
153
|
+
"language": "html",
|
|
154
|
+
"content": "<main id=\"content\">\n <section id=\"intro\">\n <p>Format greetings for named recipients.</p>\n </section>\n </main>",
|
|
155
|
+
"line_start": 20,
|
|
156
|
+
"line_end": 24,
|
|
157
|
+
"metadata": {
|
|
158
|
+
"module": "",
|
|
159
|
+
"names": "",
|
|
160
|
+
"dots": "",
|
|
161
|
+
"language_hint": ""
|
|
162
|
+
}
|
|
163
|
+
},
|
|
164
|
+
{
|
|
165
|
+
"id": "537c74674a50f01d",
|
|
166
|
+
"blob_sha": "sha1",
|
|
167
|
+
"file_path": "html.html",
|
|
168
|
+
"kind": "doc_section",
|
|
169
|
+
"name": "intro",
|
|
170
|
+
"scope": "",
|
|
171
|
+
"language": "html",
|
|
172
|
+
"content": "<section id=\"intro\">\n <p>Format greetings for named recipients.</p>\n </section>",
|
|
173
|
+
"line_start": 21,
|
|
174
|
+
"line_end": 23,
|
|
175
|
+
"metadata": {
|
|
176
|
+
"module": "",
|
|
177
|
+
"names": "",
|
|
178
|
+
"dots": "",
|
|
179
|
+
"language_hint": ""
|
|
180
|
+
}
|
|
181
|
+
},
|
|
182
|
+
{
|
|
183
|
+
"id": "766315b968053a92",
|
|
184
|
+
"blob_sha": "sha1",
|
|
185
|
+
"file_path": "html.html",
|
|
186
|
+
"kind": "doc_section",
|
|
187
|
+
"name": "footer",
|
|
188
|
+
"scope": "",
|
|
189
|
+
"language": "html",
|
|
190
|
+
"content": "<footer>\n <small>Greeter — a greetings library.</small>\n </footer>",
|
|
191
|
+
"line_start": 25,
|
|
192
|
+
"line_end": 27,
|
|
193
|
+
"metadata": {
|
|
194
|
+
"module": "",
|
|
195
|
+
"names": "",
|
|
196
|
+
"dots": "",
|
|
197
|
+
"language_hint": ""
|
|
198
|
+
}
|
|
199
|
+
},
|
|
200
|
+
{
|
|
201
|
+
"id": "ccfc3b2bb3faad2b",
|
|
202
|
+
"blob_sha": "sha1",
|
|
203
|
+
"file_path": "html.html",
|
|
204
|
+
"kind": "class",
|
|
205
|
+
"name": ".greeter",
|
|
206
|
+
"scope": "",
|
|
207
|
+
"language": "css",
|
|
208
|
+
"content": ".greeter {\n font-family: system-ui;\n }",
|
|
209
|
+
"line_start": 11,
|
|
210
|
+
"line_end": 13,
|
|
211
|
+
"metadata": {
|
|
212
|
+
"module": "",
|
|
213
|
+
"names": "",
|
|
214
|
+
"dots": "",
|
|
215
|
+
"language_hint": ""
|
|
216
|
+
}
|
|
217
|
+
},
|
|
218
|
+
{
|
|
219
|
+
"id": "3d6a94c70cb40c26",
|
|
220
|
+
"blob_sha": "sha1",
|
|
221
|
+
"file_path": "html.html",
|
|
222
|
+
"kind": "function",
|
|
223
|
+
"name": "init",
|
|
224
|
+
"scope": "",
|
|
225
|
+
"language": "javascript",
|
|
226
|
+
"content": "function init() {\n return greet(\"world\");\n }",
|
|
227
|
+
"line_start": 29,
|
|
228
|
+
"line_end": 31,
|
|
229
|
+
"metadata": {
|
|
230
|
+
"module": "",
|
|
231
|
+
"names": "",
|
|
232
|
+
"dots": "",
|
|
233
|
+
"language_hint": ""
|
|
234
|
+
}
|
|
235
|
+
},
|
|
236
|
+
{
|
|
237
|
+
"id": "6a463c4d63411e65",
|
|
238
|
+
"blob_sha": "sha1",
|
|
239
|
+
"file_path": "styles.css",
|
|
240
|
+
"kind": "class",
|
|
241
|
+
"name": ".greeter",
|
|
242
|
+
"scope": "",
|
|
243
|
+
"language": "css",
|
|
244
|
+
"content": ".greeter {\n color: #333;\n}",
|
|
245
|
+
"line_start": 1,
|
|
246
|
+
"line_end": 3,
|
|
247
|
+
"metadata": {
|
|
248
|
+
"module": "",
|
|
249
|
+
"names": "",
|
|
250
|
+
"dots": "",
|
|
251
|
+
"language_hint": ""
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
]
|
|
@@ -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,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 — 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,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.git import FileEntry
|
|
13
|
+
from rbtr.index.models import ChunkKind
|
|
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.git import FileEntry
|
|
17
|
+
from rbtr.index.models import Chunk, ChunkKind, Edge
|
|
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
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
rbtr_lang_html/__init__.py,sha256=Ifd5TOOkiyk4FVpH_H05Y_OaYUAdB_WJAWZccTw0ZTE,36
|
|
2
|
+
rbtr_lang_html/html.scm,sha256=pJD-czYLz_D7aZkNG1kKR5rSNcrYzta3gU-mvvaV4ls,733
|
|
3
|
+
rbtr_lang_html/injections.scm,sha256=8KIi5RGBArLNv3MZEGQQ5zWugGMRamCf6y4NiAnNXOs,316
|
|
4
|
+
rbtr_lang_html/plugin.py,sha256=TcGamk_MdfNaaOZSRDRIap9gNuSo8d7neAMyUPNKTMs,3946
|
|
5
|
+
rbtr_lang_html/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
6
|
+
rbtr_lang_html/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
7
|
+
rbtr_lang_html/tests/__snapshots__/test_samples/test_edges_match_snapshot.json,sha256=h3GTsS80PKJHjcVELR63vPsOfsCbNwUmp_xfgf17izk,183
|
|
8
|
+
rbtr_lang_html/tests/__snapshots__/test_samples/test_extraction_matches_snapshot.json,sha256=fS01IS0qS-uB2v8Yd8f9qx9nQhrYQW7kziBUUEyUfHg,6489
|
|
9
|
+
rbtr_lang_html/tests/cases_extraction.py,sha256=MK6vNe5kdEqAYYp6h8Wcb8npKs1tLwjdDNZoE35lDog,712
|
|
10
|
+
rbtr_lang_html/tests/samples/html/greeter.js,sha256=79U_oQoQr9Hrnheou3syf_Fc1nBXlSwvbLSJiO4h2Yk,83
|
|
11
|
+
rbtr_lang_html/tests/samples/html/html.html,sha256=Rul0PJdMeAXTOScCOyIKF9-HUWa9COaeDK0BDj1Z1jI,812
|
|
12
|
+
rbtr_lang_html/tests/samples/html/styles.css,sha256=8SA3GIIHSpPFdXHLF0mif0lUB8QvIr48kAfBPwNlA50,28
|
|
13
|
+
rbtr_lang_html/tests/test_extraction.py,sha256=l1xVVBBPOczgVLURHpMCxD_jokwTy1erL_mrruuIxBs,2869
|
|
14
|
+
rbtr_lang_html/tests/test_samples.py,sha256=ZqCMjx5dUE7p5XK2L6ruBX_vbt17pCHPFSLOrXXl168,2939
|
|
15
|
+
rbtr_lang_html-2026.7.0.dev0.dist-info/WHEEL,sha256=CoDSoyhtC_eO_tlxRYzsTraPv1fPJRXFx91k6ISeAvA,81
|
|
16
|
+
rbtr_lang_html-2026.7.0.dev0.dist-info/entry_points.txt,sha256=kDPcrHuXzWY9QOwVpNf7bqNZKzSf-HcsQ4OVPxUgEzc,52
|
|
17
|
+
rbtr_lang_html-2026.7.0.dev0.dist-info/METADATA,sha256=YKbPVi_R5HG_gz4BPSABxb2Y37WZjm8JYEQ60yT3Tlo,220
|
|
18
|
+
rbtr_lang_html-2026.7.0.dev0.dist-info/RECORD,,
|