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.
Files changed (24) hide show
  1. rbtr_lang_markdown-2026.9.0.dev1/LICENSE +21 -0
  2. rbtr_lang_markdown-2026.9.0.dev1/PKG-INFO +79 -0
  3. rbtr_lang_markdown-2026.9.0.dev1/README.md +53 -0
  4. rbtr_lang_markdown-2026.9.0.dev1/pyproject.toml +95 -0
  5. rbtr_lang_markdown-2026.9.0.dev1/pyproject.toml.orig +84 -0
  6. rbtr_lang_markdown-2026.9.0.dev1/src/rbtr_lang_markdown/__init__.py +1 -0
  7. rbtr_lang_markdown-2026.9.0.dev1/src/rbtr_lang_markdown/injections.scm +4 -0
  8. rbtr_lang_markdown-2026.9.0.dev1/src/rbtr_lang_markdown/links.scm +1 -0
  9. rbtr_lang_markdown-2026.9.0.dev1/src/rbtr_lang_markdown/plugin.py +258 -0
  10. rbtr_lang_markdown-2026.9.0.dev1/src/rbtr_lang_markdown/py.typed +0 -0
  11. rbtr_lang_markdown-2026.9.0.dev1/src/rbtr_lang_markdown/sections.scm +3 -0
  12. rbtr_lang_markdown-2026.9.0.dev1/src/rbtr_lang_markdown/tests/__init__.py +0 -0
  13. rbtr_lang_markdown-2026.9.0.dev1/src/rbtr_lang_markdown/tests/__snapshots__/test_samples/test_edges_match_snapshot.json +8 -0
  14. rbtr_lang_markdown-2026.9.0.dev1/src/rbtr_lang_markdown/tests/__snapshots__/test_samples/test_extraction_matches_snapshot.json +572 -0
  15. rbtr_lang_markdown-2026.9.0.dev1/src/rbtr_lang_markdown/tests/cases_extraction.py +82 -0
  16. rbtr_lang_markdown-2026.9.0.dev1/src/rbtr_lang_markdown/tests/samples/markdown/api.md +23 -0
  17. rbtr_lang_markdown-2026.9.0.dev1/src/rbtr_lang_markdown/tests/samples/markdown/config.md +3 -0
  18. rbtr_lang_markdown-2026.9.0.dev1/src/rbtr_lang_markdown/tests/samples/markdown/embedding.md +59 -0
  19. rbtr_lang_markdown-2026.9.0.dev1/src/rbtr_lang_markdown/tests/samples/markdown/guides/setup.md +6 -0
  20. rbtr_lang_markdown-2026.9.0.dev1/src/rbtr_lang_markdown/tests/samples/markdown/guides/troubleshooting.md +7 -0
  21. rbtr_lang_markdown-2026.9.0.dev1/src/rbtr_lang_markdown/tests/samples/markdown/locales.md +3 -0
  22. rbtr_lang_markdown-2026.9.0.dev1/src/rbtr_lang_markdown/tests/samples/markdown/markdown.md +18 -0
  23. rbtr_lang_markdown-2026.9.0.dev1/src/rbtr_lang_markdown/tests/test_extraction.py +201 -0
  24. 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,4 @@
1
+ ; Fenced code blocks: the info string names the embedded language.
2
+ (fenced_code_block
3
+ (info_string (language) @injection.language)
4
+ (code_fence_content) @injection.content)
@@ -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)
@@ -0,0 +1,3 @@
1
+ (section
2
+ (atx_heading
3
+ (inline) @_section_name)) @doc_section
@@ -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
+ ]