rbtr-lang-markdown 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_markdown-2026.9.0.dev1/LICENSE +21 -0
- rbtr_lang_markdown-2026.9.0.dev1/PKG-INFO +79 -0
- rbtr_lang_markdown-2026.9.0.dev1/README.md +53 -0
- rbtr_lang_markdown-2026.9.0.dev1/pyproject.toml +95 -0
- rbtr_lang_markdown-2026.9.0.dev1/pyproject.toml.orig +84 -0
- rbtr_lang_markdown-2026.9.0.dev1/src/rbtr_lang_markdown/__init__.py +1 -0
- rbtr_lang_markdown-2026.9.0.dev1/src/rbtr_lang_markdown/injections.scm +4 -0
- rbtr_lang_markdown-2026.9.0.dev1/src/rbtr_lang_markdown/links.scm +1 -0
- rbtr_lang_markdown-2026.9.0.dev1/src/rbtr_lang_markdown/plugin.py +258 -0
- rbtr_lang_markdown-2026.9.0.dev1/src/rbtr_lang_markdown/py.typed +0 -0
- rbtr_lang_markdown-2026.9.0.dev1/src/rbtr_lang_markdown/sections.scm +3 -0
- rbtr_lang_markdown-2026.9.0.dev1/src/rbtr_lang_markdown/tests/__init__.py +0 -0
- rbtr_lang_markdown-2026.9.0.dev1/src/rbtr_lang_markdown/tests/__snapshots__/test_samples/test_edges_match_snapshot.json +8 -0
- rbtr_lang_markdown-2026.9.0.dev1/src/rbtr_lang_markdown/tests/__snapshots__/test_samples/test_extraction_matches_snapshot.json +572 -0
- rbtr_lang_markdown-2026.9.0.dev1/src/rbtr_lang_markdown/tests/cases_extraction.py +82 -0
- rbtr_lang_markdown-2026.9.0.dev1/src/rbtr_lang_markdown/tests/samples/markdown/api.md +23 -0
- rbtr_lang_markdown-2026.9.0.dev1/src/rbtr_lang_markdown/tests/samples/markdown/config.md +3 -0
- rbtr_lang_markdown-2026.9.0.dev1/src/rbtr_lang_markdown/tests/samples/markdown/embedding.md +59 -0
- rbtr_lang_markdown-2026.9.0.dev1/src/rbtr_lang_markdown/tests/samples/markdown/guides/setup.md +6 -0
- rbtr_lang_markdown-2026.9.0.dev1/src/rbtr_lang_markdown/tests/samples/markdown/guides/troubleshooting.md +7 -0
- rbtr_lang_markdown-2026.9.0.dev1/src/rbtr_lang_markdown/tests/samples/markdown/locales.md +3 -0
- rbtr_lang_markdown-2026.9.0.dev1/src/rbtr_lang_markdown/tests/samples/markdown/markdown.md +18 -0
- rbtr_lang_markdown-2026.9.0.dev1/src/rbtr_lang_markdown/tests/test_extraction.py +201 -0
- rbtr_lang_markdown-2026.9.0.dev1/src/rbtr_lang_markdown/tests/test_samples.py +86 -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,79 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: rbtr-lang-markdown
|
|
3
|
+
Version: 2026.9.0.dev1
|
|
4
|
+
Summary: rbtr — Markdown language plugin
|
|
5
|
+
Keywords: code-search,code-index,tree-sitter,static-analysis,semantic-search,developer-tools,markdown
|
|
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-markdown
|
|
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-markdown#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-markdown
|
|
28
|
+
|
|
29
|
+
Markdown support for [rbtr]. A **default** plugin — installed with
|
|
30
|
+
rbtr itself (`pip install rbtr`).
|
|
31
|
+
|
|
32
|
+
[rbtr]: https://github.com/janrito/rbtr/tree/main/packages/rbtr#readme
|
|
33
|
+
|
|
34
|
+
## What it ingests
|
|
35
|
+
|
|
36
|
+
A custom chunker (not a tree-sitter query): it splits a document by its
|
|
37
|
+
**heading hierarchy** — each section is a chunk, scoped by its ancestor
|
|
38
|
+
headings. Links and fenced code blocks are extracted on top.
|
|
39
|
+
|
|
40
|
+
- **Sections** — headings → doc sections; `scope` is the `::` chain of
|
|
41
|
+
enclosing headings. A headingless document falls back to plaintext chunks.
|
|
42
|
+
- **Links** — local `[text](path.md)` and `[text](path.md#fragment)` →
|
|
43
|
+
imports (the fragment becomes `names`). External and fragment-only links
|
|
44
|
+
are skipped.
|
|
45
|
+
- **Fenced code** — delegated to the block's language (see below).
|
|
46
|
+
|
|
47
|
+
## Chunks produced
|
|
48
|
+
|
|
49
|
+
```markdown
|
|
50
|
+
# Guide <!-- doc_section "Guide" -->
|
|
51
|
+
## Setup <!-- doc_section "Setup", scope "Guide" -->
|
|
52
|
+
See [the API](api.md#run). <!-- import, metadata {module: api.md, names: run} -->
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## Embedded / injected chunks
|
|
56
|
+
|
|
57
|
+
A fenced block delegates to its language, extracted at real line numbers —
|
|
58
|
+
and delegation **recurses**, so a block that itself embeds another language
|
|
59
|
+
resolves all the way down (markdown → html → js):
|
|
60
|
+
|
|
61
|
+
````markdown
|
|
62
|
+
```yaml
|
|
63
|
+
service: greeter <!-- doc_section "service" (yaml) -->
|
|
64
|
+
```
|
|
65
|
+
```html
|
|
66
|
+
<main> <!-- doc_section "main" (html) -->
|
|
67
|
+
<script>
|
|
68
|
+
function boot() {} <!-- function "boot" (javascript, nested) -->
|
|
69
|
+
</script>
|
|
70
|
+
</main>
|
|
71
|
+
```
|
|
72
|
+
````
|
|
73
|
+
|
|
74
|
+
## Grammar & dependencies
|
|
75
|
+
|
|
76
|
+
Uses the `tree-sitter-markdown` grammar. No runtime dependency on other
|
|
77
|
+
language plugins; the test suite dev-depends on the plugins its sample's
|
|
78
|
+
fenced blocks delegate to (bash, css, html, javascript, python, scss — plus
|
|
79
|
+
svelte/toml/yaml via core until those are packaged).
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# rbtr-lang-markdown
|
|
2
|
+
|
|
3
|
+
Markdown support for [rbtr]. A **default** plugin — installed with
|
|
4
|
+
rbtr itself (`pip install rbtr`).
|
|
5
|
+
|
|
6
|
+
[rbtr]: https://github.com/janrito/rbtr/tree/main/packages/rbtr#readme
|
|
7
|
+
|
|
8
|
+
## What it ingests
|
|
9
|
+
|
|
10
|
+
A custom chunker (not a tree-sitter query): it splits a document by its
|
|
11
|
+
**heading hierarchy** — each section is a chunk, scoped by its ancestor
|
|
12
|
+
headings. Links and fenced code blocks are extracted on top.
|
|
13
|
+
|
|
14
|
+
- **Sections** — headings → doc sections; `scope` is the `::` chain of
|
|
15
|
+
enclosing headings. A headingless document falls back to plaintext chunks.
|
|
16
|
+
- **Links** — local `[text](path.md)` and `[text](path.md#fragment)` →
|
|
17
|
+
imports (the fragment becomes `names`). External and fragment-only links
|
|
18
|
+
are skipped.
|
|
19
|
+
- **Fenced code** — delegated to the block's language (see below).
|
|
20
|
+
|
|
21
|
+
## Chunks produced
|
|
22
|
+
|
|
23
|
+
```markdown
|
|
24
|
+
# Guide <!-- doc_section "Guide" -->
|
|
25
|
+
## Setup <!-- doc_section "Setup", scope "Guide" -->
|
|
26
|
+
See [the API](api.md#run). <!-- import, metadata {module: api.md, names: run} -->
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Embedded / injected chunks
|
|
30
|
+
|
|
31
|
+
A fenced block delegates to its language, extracted at real line numbers —
|
|
32
|
+
and delegation **recurses**, so a block that itself embeds another language
|
|
33
|
+
resolves all the way down (markdown → html → js):
|
|
34
|
+
|
|
35
|
+
````markdown
|
|
36
|
+
```yaml
|
|
37
|
+
service: greeter <!-- doc_section "service" (yaml) -->
|
|
38
|
+
```
|
|
39
|
+
```html
|
|
40
|
+
<main> <!-- doc_section "main" (html) -->
|
|
41
|
+
<script>
|
|
42
|
+
function boot() {} <!-- function "boot" (javascript, nested) -->
|
|
43
|
+
</script>
|
|
44
|
+
</main>
|
|
45
|
+
```
|
|
46
|
+
````
|
|
47
|
+
|
|
48
|
+
## Grammar & dependencies
|
|
49
|
+
|
|
50
|
+
Uses the `tree-sitter-markdown` grammar. No runtime dependency on other
|
|
51
|
+
language plugins; the test suite dev-depends on the plugins its sample's
|
|
52
|
+
fenced blocks delegate to (bash, css, html, javascript, python, scss — plus
|
|
53
|
+
svelte/toml/yaml via core until those are packaged).
|
|
@@ -0,0 +1,95 @@
|
|
|
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_markdown"
|
|
7
|
+
|
|
8
|
+
[tool.uv.sources.rbtr]
|
|
9
|
+
workspace = true
|
|
10
|
+
|
|
11
|
+
[tool.uv.sources.rbtr-lang-bash]
|
|
12
|
+
workspace = true
|
|
13
|
+
|
|
14
|
+
[tool.uv.sources.rbtr-lang-css]
|
|
15
|
+
workspace = true
|
|
16
|
+
|
|
17
|
+
[tool.uv.sources.rbtr-lang-html]
|
|
18
|
+
workspace = true
|
|
19
|
+
|
|
20
|
+
[tool.uv.sources.rbtr-lang-javascript]
|
|
21
|
+
workspace = true
|
|
22
|
+
|
|
23
|
+
[tool.uv.sources.rbtr-lang-python]
|
|
24
|
+
workspace = true
|
|
25
|
+
|
|
26
|
+
[tool.uv.sources.rbtr-lang-scss]
|
|
27
|
+
workspace = true
|
|
28
|
+
|
|
29
|
+
[tool.uv.sources.rbtr-lang-svelte]
|
|
30
|
+
workspace = true
|
|
31
|
+
|
|
32
|
+
[tool.uv.sources.rbtr-lang-toml]
|
|
33
|
+
workspace = true
|
|
34
|
+
|
|
35
|
+
[tool.uv.sources.rbtr-lang-yaml]
|
|
36
|
+
workspace = true
|
|
37
|
+
|
|
38
|
+
[project]
|
|
39
|
+
name = "rbtr-lang-markdown"
|
|
40
|
+
version = "2026.9.0-dev1"
|
|
41
|
+
description = "rbtr — Markdown language plugin"
|
|
42
|
+
readme = "README.md"
|
|
43
|
+
license = "MIT"
|
|
44
|
+
license-files = ["LICENSE"]
|
|
45
|
+
keywords = [
|
|
46
|
+
"code-search",
|
|
47
|
+
"code-index",
|
|
48
|
+
"tree-sitter",
|
|
49
|
+
"static-analysis",
|
|
50
|
+
"semantic-search",
|
|
51
|
+
"developer-tools",
|
|
52
|
+
"markdown",
|
|
53
|
+
]
|
|
54
|
+
classifiers = [
|
|
55
|
+
"Development Status :: 4 - Beta",
|
|
56
|
+
"Intended Audience :: Developers",
|
|
57
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
58
|
+
"Programming Language :: Python :: 3.13",
|
|
59
|
+
"Topic :: Software Development :: Libraries",
|
|
60
|
+
"Topic :: Text Processing :: Indexing",
|
|
61
|
+
"Typing :: Typed",
|
|
62
|
+
]
|
|
63
|
+
requires-python = ">=3.13"
|
|
64
|
+
dependencies = [
|
|
65
|
+
"rbtr==2026.9.0-dev1",
|
|
66
|
+
"tree-sitter-markdown",
|
|
67
|
+
]
|
|
68
|
+
|
|
69
|
+
[[project.authors]]
|
|
70
|
+
name = "Alejandro Giacometti"
|
|
71
|
+
email = "alejandro.giacometti@gmail.com"
|
|
72
|
+
|
|
73
|
+
[project.urls]
|
|
74
|
+
Homepage = "https://github.com/janrito/rbtr"
|
|
75
|
+
Repository = "https://github.com/janrito/rbtr"
|
|
76
|
+
Documentation = "https://github.com/janrito/rbtr/tree/main/packages/rbtr-lang-markdown#readme"
|
|
77
|
+
Issues = "https://github.com/janrito/rbtr/issues"
|
|
78
|
+
Changelog = "https://github.com/janrito/rbtr/releases"
|
|
79
|
+
|
|
80
|
+
[project.entry-points."rbtr.languages"]
|
|
81
|
+
markdown = "rbtr_lang_markdown.plugin:markdown"
|
|
82
|
+
|
|
83
|
+
[dependency-groups]
|
|
84
|
+
dev = [
|
|
85
|
+
"rbtr[test]",
|
|
86
|
+
"rbtr-lang-bash",
|
|
87
|
+
"rbtr-lang-css",
|
|
88
|
+
"rbtr-lang-html",
|
|
89
|
+
"rbtr-lang-javascript",
|
|
90
|
+
"rbtr-lang-python",
|
|
91
|
+
"rbtr-lang-scss",
|
|
92
|
+
"rbtr-lang-svelte",
|
|
93
|
+
"rbtr-lang-toml",
|
|
94
|
+
"rbtr-lang-yaml",
|
|
95
|
+
]
|
|
@@ -0,0 +1,84 @@
|
|
|
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_markdown"
|
|
7
|
+
# No source-exclude: the plugin's tests, samples, and snapshots ship with
|
|
8
|
+
# the wheel (they are the language's golden record), and the .scm queries
|
|
9
|
+
# ship as package data.
|
|
10
|
+
|
|
11
|
+
[project]
|
|
12
|
+
name = "rbtr-lang-markdown"
|
|
13
|
+
version = "2026.9.0-dev1"
|
|
14
|
+
description = "rbtr — Markdown 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
|
+
"markdown",
|
|
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-markdown",
|
|
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-markdown#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
|
+
markdown = "rbtr_lang_markdown.plugin:markdown"
|
|
56
|
+
|
|
57
|
+
[tool.uv.sources]
|
|
58
|
+
rbtr = { workspace = true }
|
|
59
|
+
# The sample's fenced code blocks delegate to these (plus svelte/toml/yaml,
|
|
60
|
+
# which are still in core and come via `rbtr` until they are packaged — add
|
|
61
|
+
# them here when they are).
|
|
62
|
+
rbtr-lang-bash = { workspace = true }
|
|
63
|
+
rbtr-lang-css = { workspace = true }
|
|
64
|
+
rbtr-lang-html = { workspace = true }
|
|
65
|
+
rbtr-lang-javascript = { workspace = true }
|
|
66
|
+
rbtr-lang-python = { workspace = true }
|
|
67
|
+
rbtr-lang-scss = { workspace = true }
|
|
68
|
+
rbtr-lang-svelte = { workspace = true }
|
|
69
|
+
rbtr-lang-toml = { workspace = true }
|
|
70
|
+
rbtr-lang-yaml = { workspace = true }
|
|
71
|
+
|
|
72
|
+
[dependency-groups]
|
|
73
|
+
dev = [
|
|
74
|
+
"rbtr[test]",
|
|
75
|
+
"rbtr-lang-bash",
|
|
76
|
+
"rbtr-lang-css",
|
|
77
|
+
"rbtr-lang-html",
|
|
78
|
+
"rbtr-lang-javascript",
|
|
79
|
+
"rbtr-lang-python",
|
|
80
|
+
"rbtr-lang-scss",
|
|
81
|
+
"rbtr-lang-svelte",
|
|
82
|
+
"rbtr-lang-toml",
|
|
83
|
+
"rbtr-lang-yaml",
|
|
84
|
+
]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Markdown language plugin package."""
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
(inline_link (link_destination) @dest)
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
"""Markdown language plugin.
|
|
2
|
+
|
|
3
|
+
Splits Markdown by heading hierarchy using tree-sitter. Headed
|
|
4
|
+
sections produce one `doc_section` chunk per heading, containing
|
|
5
|
+
the heading and its direct content (excluding nested subsections).
|
|
6
|
+
Headingless documents fall back to `chunk_plaintext`.
|
|
7
|
+
|
|
8
|
+
Extracted chunks::
|
|
9
|
+
|
|
10
|
+
# Title → doc_section "Title", scope ""
|
|
11
|
+
Intro text. (content: heading + intro)
|
|
12
|
+
|
|
13
|
+
## Section A → doc_section "Section A", scope "Title"
|
|
14
|
+
Body A. (content: heading + body, excludes
|
|
15
|
+
child sections)
|
|
16
|
+
|
|
17
|
+
First paragraph. → raw_chunk (plaintext fallback)
|
|
18
|
+
Second paragraph. → raw_chunk
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
from collections.abc import Iterator
|
|
24
|
+
from typing import TYPE_CHECKING
|
|
25
|
+
|
|
26
|
+
from tree_sitter import Language, Parser, Query, QueryCursor
|
|
27
|
+
|
|
28
|
+
from rbtr.domain.models import Chunk, ChunkKind, ImportMeta
|
|
29
|
+
from rbtr.languages.chunks import chunk_plaintext, last_line
|
|
30
|
+
from rbtr.languages.registration import LanguageRegistration, load_query
|
|
31
|
+
|
|
32
|
+
if TYPE_CHECKING:
|
|
33
|
+
from tree_sitter import Node, Range
|
|
34
|
+
|
|
35
|
+
# URL schemes that indicate external links (not cross-references).
|
|
36
|
+
_EXTERNAL_SCHEMES = ("http://", "https://", "mailto:", "ftp://")
|
|
37
|
+
|
|
38
|
+
# Block-level query: captures headed sections.
|
|
39
|
+
_SECTION_QUERY = load_query(__package__, "sections")
|
|
40
|
+
|
|
41
|
+
# Injection query: a fenced code block delegates its content to the language
|
|
42
|
+
# named in the fence's info string (```python, ```sh). The info-string name
|
|
43
|
+
# is captured dynamically and resolved to a language id by the runner.
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
# ── Chunking ─────────────────────────────────────────────────────────
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _has_headings(root: Node) -> bool:
|
|
50
|
+
"""Return True if any section in the tree has an atx_heading."""
|
|
51
|
+
for child in root.children:
|
|
52
|
+
if child.type == "section":
|
|
53
|
+
if any(gc.type == "atx_heading" for gc in child.children):
|
|
54
|
+
return True
|
|
55
|
+
if _has_headings(child):
|
|
56
|
+
return True
|
|
57
|
+
return False
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _section_depth(node: Node) -> int:
|
|
61
|
+
"""Count ancestor nodes of the same type.
|
|
62
|
+
|
|
63
|
+
In the tree-sitter markdown grammar, sections nest:
|
|
64
|
+
a `## Sub` section is a child `section` node of the
|
|
65
|
+
`# Top` section. The depth (number of same-typed
|
|
66
|
+
ancestors) maps directly to the heading level minus
|
|
67
|
+
one: depth 0 = `#`, depth 1 = `##`, etc.
|
|
68
|
+
"""
|
|
69
|
+
depth = 0
|
|
70
|
+
ancestor = node.parent
|
|
71
|
+
while ancestor is not None:
|
|
72
|
+
if ancestor.type == node.type:
|
|
73
|
+
depth += 1
|
|
74
|
+
ancestor = ancestor.parent
|
|
75
|
+
return depth
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _section_own_content(node: Node, content_bytes: bytes) -> str:
|
|
79
|
+
"""Return the section's own text, excluding nested subsections.
|
|
80
|
+
|
|
81
|
+
A parent section's byte span covers its children. Trimming
|
|
82
|
+
at the first child of the same type gives us only the
|
|
83
|
+
heading and the prose that belongs directly to this section.
|
|
84
|
+
"""
|
|
85
|
+
end_byte = node.end_byte
|
|
86
|
+
for child in node.children:
|
|
87
|
+
if child.type == node.type:
|
|
88
|
+
end_byte = child.start_byte
|
|
89
|
+
break
|
|
90
|
+
return content_bytes[node.start_byte : end_byte].decode("utf-8", errors="replace").strip()
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _extract_sections(
|
|
94
|
+
content_bytes: bytes,
|
|
95
|
+
grammar: Language,
|
|
96
|
+
file_path: str,
|
|
97
|
+
blob_sha: str,
|
|
98
|
+
ranges: list[Range] | None,
|
|
99
|
+
) -> Iterator[Chunk]:
|
|
100
|
+
"""Run the section query and yield one chunk per headed section.
|
|
101
|
+
|
|
102
|
+
The tree-sitter markdown grammar represents headings as
|
|
103
|
+
nested `section` nodes. This function queries for all
|
|
104
|
+
sections that contain an `atx_heading` and produces one
|
|
105
|
+
`doc_section` chunk per match.
|
|
106
|
+
|
|
107
|
+
**Scope** is the parent heading chain, built by maintaining
|
|
108
|
+
a stack of heading names indexed by depth. When a section
|
|
109
|
+
at depth *d* is encountered, the stack is truncated to *d*
|
|
110
|
+
entries (popping deeper headings from a previous branch),
|
|
111
|
+
the current scope is read from the remaining stack, and
|
|
112
|
+
the new heading is pushed.
|
|
113
|
+
|
|
114
|
+
**Content** is trimmed to exclude nested subsections — see
|
|
115
|
+
`_section_own_content`.
|
|
116
|
+
"""
|
|
117
|
+
query = Query(grammar, _SECTION_QUERY)
|
|
118
|
+
parser = Parser(grammar)
|
|
119
|
+
if ranges is not None:
|
|
120
|
+
parser.included_ranges = ranges
|
|
121
|
+
tree = parser.parse(content_bytes)
|
|
122
|
+
matches = QueryCursor(query).matches(tree.root_node)
|
|
123
|
+
|
|
124
|
+
# Heading names indexed by depth. Tracks the current
|
|
125
|
+
# branch of the heading tree so we can reconstruct the
|
|
126
|
+
# scope ("Top::Mid") for each section.
|
|
127
|
+
scope_stack: list[str] = []
|
|
128
|
+
|
|
129
|
+
for _pattern_idx, capture_dict in matches:
|
|
130
|
+
section_nodes = capture_dict.get("doc_section", [])
|
|
131
|
+
name_nodes = capture_dict.get("_section_name", [])
|
|
132
|
+
if not section_nodes or not name_nodes:
|
|
133
|
+
continue
|
|
134
|
+
|
|
135
|
+
node = section_nodes[0]
|
|
136
|
+
name = name_nodes[0].text.decode("utf-8", errors="replace") if name_nodes[0].text else ""
|
|
137
|
+
|
|
138
|
+
# Maintain the scope stack: pop back to this section's
|
|
139
|
+
# depth, read scope, then push the current heading.
|
|
140
|
+
depth = _section_depth(node)
|
|
141
|
+
while len(scope_stack) > depth:
|
|
142
|
+
scope_stack.pop()
|
|
143
|
+
scope_segments = list(scope_stack)
|
|
144
|
+
scope_stack.append(name)
|
|
145
|
+
|
|
146
|
+
text = _section_own_content(node, content_bytes)
|
|
147
|
+
if not text:
|
|
148
|
+
continue
|
|
149
|
+
|
|
150
|
+
line_start = node.start_point[0] + 1
|
|
151
|
+
yield Chunk.model_validate(
|
|
152
|
+
{
|
|
153
|
+
"blob_sha": blob_sha,
|
|
154
|
+
"file_path": file_path,
|
|
155
|
+
"kind": ChunkKind.DOC_SECTION,
|
|
156
|
+
"name": name,
|
|
157
|
+
"scope": scope_segments,
|
|
158
|
+
"language": "markdown",
|
|
159
|
+
"content": text,
|
|
160
|
+
"line_start": line_start,
|
|
161
|
+
"line_end": last_line(node),
|
|
162
|
+
}
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def chunk_markdown(
|
|
167
|
+
file_path: str,
|
|
168
|
+
blob_sha: str,
|
|
169
|
+
content: str,
|
|
170
|
+
grammar: Language,
|
|
171
|
+
ranges: list[Range] | None = None,
|
|
172
|
+
) -> Iterator[Chunk]:
|
|
173
|
+
"""Split Markdown by heading hierarchy using tree-sitter."""
|
|
174
|
+
if not content.strip():
|
|
175
|
+
return
|
|
176
|
+
|
|
177
|
+
content_bytes = content.encode("utf-8")
|
|
178
|
+
parser = Parser(grammar)
|
|
179
|
+
if ranges is not None:
|
|
180
|
+
parser.included_ranges = ranges
|
|
181
|
+
tree = parser.parse(content_bytes)
|
|
182
|
+
|
|
183
|
+
if _has_headings(tree.root_node):
|
|
184
|
+
yield from _extract_sections(content_bytes, grammar, file_path, blob_sha, ranges)
|
|
185
|
+
else:
|
|
186
|
+
yield from chunk_plaintext(file_path, blob_sha, content)
|
|
187
|
+
|
|
188
|
+
# Extract local links as IMPORT chunks using the inline parser.
|
|
189
|
+
yield from _extract_links(content_bytes, file_path, blob_sha, ranges)
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
# ── Link extraction ──────────────────────────────────────────────────
|
|
193
|
+
|
|
194
|
+
_LINK_QUERY = load_query(__package__, "links")
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def _extract_links(
|
|
198
|
+
content_bytes: bytes,
|
|
199
|
+
file_path: str,
|
|
200
|
+
blob_sha: str,
|
|
201
|
+
ranges: list[Range] | None,
|
|
202
|
+
) -> Iterator[Chunk]:
|
|
203
|
+
"""Extract local links as IMPORT chunks using the inline parser.
|
|
204
|
+
|
|
205
|
+
Parses the full content with `tree_sitter_markdown.inline_language()`
|
|
206
|
+
and queries for `inline_link` nodes. External URLs and same-file
|
|
207
|
+
fragment-only links are skipped.
|
|
208
|
+
"""
|
|
209
|
+
import tree_sitter_markdown # deferred: heavy native lib
|
|
210
|
+
|
|
211
|
+
inline_lang = Language(tree_sitter_markdown.inline_language())
|
|
212
|
+
inline_parser = Parser(inline_lang)
|
|
213
|
+
if ranges is not None:
|
|
214
|
+
inline_parser.included_ranges = ranges
|
|
215
|
+
inline_tree = inline_parser.parse(content_bytes)
|
|
216
|
+
|
|
217
|
+
query = Query(inline_lang, _LINK_QUERY)
|
|
218
|
+
for _pattern_idx, captures in QueryCursor(query).matches(inline_tree.root_node):
|
|
219
|
+
for dest_node in captures.get("dest", []):
|
|
220
|
+
if dest_node.text is None:
|
|
221
|
+
continue
|
|
222
|
+
dest = dest_node.text.decode("utf-8", errors="replace")
|
|
223
|
+
|
|
224
|
+
# Skip external URLs and fragment-only anchors.
|
|
225
|
+
if any(dest.startswith(s) for s in _EXTERNAL_SCHEMES) or dest.startswith("#"):
|
|
226
|
+
continue
|
|
227
|
+
|
|
228
|
+
# Split path#fragment.
|
|
229
|
+
module = dest
|
|
230
|
+
names = ""
|
|
231
|
+
if "#" in dest:
|
|
232
|
+
module, names = dest.rsplit("#", 1)
|
|
233
|
+
|
|
234
|
+
yield Chunk(
|
|
235
|
+
blob_sha=blob_sha,
|
|
236
|
+
file_path=file_path,
|
|
237
|
+
kind=ChunkKind.IMPORT,
|
|
238
|
+
name=dest,
|
|
239
|
+
scope="",
|
|
240
|
+
content=dest,
|
|
241
|
+
metadata=ImportMeta(module=module, names=names),
|
|
242
|
+
line_start=dest_node.start_point[0] + 1,
|
|
243
|
+
line_end=last_line(dest_node),
|
|
244
|
+
)
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
# ── Plugin ───────────────────────────────────────────────────────────
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
markdown = LanguageRegistration(
|
|
251
|
+
id="markdown",
|
|
252
|
+
extensions=frozenset({".md"}),
|
|
253
|
+
grammar_module="tree_sitter_markdown",
|
|
254
|
+
injection_query=load_query(__package__, "injections"),
|
|
255
|
+
extraction_serial=5,
|
|
256
|
+
)
|
|
257
|
+
|
|
258
|
+
markdown.chunker(chunk_markdown)
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
[
|
|
2
|
+
"guides/setup.md::troubleshooting.md -> guides/troubleshooting.md::A step failed [documents]",
|
|
3
|
+
"guides/setup.md::troubleshooting.md -> guides/troubleshooting.md::Troubleshooting [documents]",
|
|
4
|
+
"markdown.md::api.md#format-greeting -> api.md::format-greeting [documents]",
|
|
5
|
+
"markdown.md::config.md -> config.md::Configuration [documents]",
|
|
6
|
+
"markdown.md::guides/setup.md -> guides/setup.md::Setup [documents]",
|
|
7
|
+
"markdown.md::locales.md -> locales.md::Locales [documents]"
|
|
8
|
+
]
|