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.
- rbtr_lang_html-2026.9.0.dev1/LICENSE +21 -0
- rbtr_lang_html-2026.9.0.dev1/PKG-INFO +72 -0
- rbtr_lang_html-2026.9.0.dev1/README.md +46 -0
- rbtr_lang_html-2026.9.0.dev1/pyproject.toml +67 -0
- rbtr_lang_html-2026.9.0.dev1/pyproject.toml.orig +66 -0
- rbtr_lang_html-2026.9.0.dev1/src/rbtr_lang_html/__init__.py +1 -0
- rbtr_lang_html-2026.9.0.dev1/src/rbtr_lang_html/html.scm +21 -0
- rbtr_lang_html-2026.9.0.dev1/src/rbtr_lang_html/injections.scm +8 -0
- rbtr_lang_html-2026.9.0.dev1/src/rbtr_lang_html/plugin.py +110 -0
- rbtr_lang_html-2026.9.0.dev1/src/rbtr_lang_html/py.typed +0 -0
- rbtr_lang_html-2026.9.0.dev1/src/rbtr_lang_html/tests/__init__.py +0 -0
- rbtr_lang_html-2026.9.0.dev1/src/rbtr_lang_html/tests/__snapshots__/test_samples/test_edges_match_snapshot.json +5 -0
- rbtr_lang_html-2026.9.0.dev1/src/rbtr_lang_html/tests/__snapshots__/test_samples/test_extraction_matches_snapshot.json +382 -0
- rbtr_lang_html-2026.9.0.dev1/src/rbtr_lang_html/tests/cases_extraction.py +31 -0
- rbtr_lang_html-2026.9.0.dev1/src/rbtr_lang_html/tests/samples/html/greeter.js +4 -0
- rbtr_lang_html-2026.9.0.dev1/src/rbtr_lang_html/tests/samples/html/html.html +34 -0
- rbtr_lang_html-2026.9.0.dev1/src/rbtr_lang_html/tests/samples/html/pages/about.html +10 -0
- rbtr_lang_html-2026.9.0.dev1/src/rbtr_lang_html/tests/samples/html/pages/about.js +4 -0
- rbtr_lang_html-2026.9.0.dev1/src/rbtr_lang_html/tests/samples/html/styles.css +3 -0
- rbtr_lang_html-2026.9.0.dev1/src/rbtr_lang_html/tests/test_extraction.py +78 -0
- 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
|
|
File without changes
|
|
File without changes
|
|
@@ -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 — 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 — 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,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.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
|