sphinx-codelinks 0.1.2__tar.gz → 1.0.0__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 (27) hide show
  1. {sphinx_codelinks-0.1.2 → sphinx_codelinks-1.0.0}/PKG-INFO +10 -4
  2. {sphinx_codelinks-0.1.2 → sphinx_codelinks-1.0.0}/README.md +4 -3
  3. {sphinx_codelinks-0.1.2 → sphinx_codelinks-1.0.0}/pyproject.toml +27 -4
  4. {sphinx_codelinks-0.1.2 → sphinx_codelinks-1.0.0}/src/sphinx_codelinks/__init__.py +1 -1
  5. sphinx_codelinks-1.0.0/src/sphinx_codelinks/analyse/analyse.py +379 -0
  6. sphinx_codelinks-1.0.0/src/sphinx_codelinks/analyse/models.py +86 -0
  7. sphinx_codelinks-0.1.2/src/sphinx_codelinks/virtual_docs/utils.py → sphinx_codelinks-1.0.0/src/sphinx_codelinks/analyse/oneline_parser.py +14 -11
  8. sphinx_codelinks-1.0.0/src/sphinx_codelinks/analyse/projects.py +84 -0
  9. sphinx_codelinks-1.0.0/src/sphinx_codelinks/analyse/utils.py +280 -0
  10. sphinx_codelinks-1.0.0/src/sphinx_codelinks/cmd.py +315 -0
  11. sphinx_codelinks-1.0.0/src/sphinx_codelinks/config.py +856 -0
  12. sphinx_codelinks-1.0.0/src/sphinx_codelinks/logger.py +94 -0
  13. sphinx_codelinks-1.0.0/src/sphinx_codelinks/needextend_write.py +218 -0
  14. {sphinx_codelinks-0.1.2/src/sphinx_codelinks/source_discovery → sphinx_codelinks-1.0.0/src/sphinx_codelinks/source_discover}/config.py +31 -8
  15. {sphinx_codelinks-0.1.2/src/sphinx_codelinks/source_discovery → sphinx_codelinks-1.0.0/src/sphinx_codelinks/source_discover}/source_discover.py +25 -28
  16. {sphinx_codelinks-0.1.2 → sphinx_codelinks-1.0.0}/src/sphinx_codelinks/sphinx_extension/directives/src_trace.py +92 -134
  17. {sphinx_codelinks-0.1.2 → sphinx_codelinks-1.0.0}/src/sphinx_codelinks/sphinx_extension/source_tracing.py +21 -41
  18. sphinx_codelinks-0.1.2/src/sphinx_codelinks/README.md +0 -46
  19. sphinx_codelinks-0.1.2/src/sphinx_codelinks/cmd.py +0 -212
  20. sphinx_codelinks-0.1.2/src/sphinx_codelinks/sphinx_extension/config.py +0 -324
  21. sphinx_codelinks-0.1.2/src/sphinx_codelinks/virtual_docs/config.py +0 -256
  22. sphinx_codelinks-0.1.2/src/sphinx_codelinks/virtual_docs/ubt_models.py +0 -131
  23. sphinx_codelinks-0.1.2/src/sphinx_codelinks/virtual_docs/virtual_docs.py +0 -238
  24. {sphinx_codelinks-0.1.2 → sphinx_codelinks-1.0.0}/LICENSE +0 -0
  25. {sphinx_codelinks-0.1.2 → sphinx_codelinks-1.0.0}/src/sphinx_codelinks/sphinx_extension/debug.py +0 -0
  26. {sphinx_codelinks-0.1.2 → sphinx_codelinks-1.0.0}/src/sphinx_codelinks/sphinx_extension/html_wrapper.py +0 -0
  27. {sphinx_codelinks-0.1.2 → sphinx_codelinks-1.0.0}/src/sphinx_codelinks/sphinx_extension/ub_sct.css +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sphinx-codelinks
3
- Version: 0.1.2
3
+ Version: 1.0.0
4
4
  Summary: Fast Source Code Traceability for Sphinx-Needs
5
5
  Author-email: team useblocks <info@useblocks.com>
6
6
  Maintainer-email: Marco Heinemann <marco.heinemann@useblocks.com>, Jui-Wen Chen <jui-wen.chen@useblocks.com>
@@ -10,12 +10,17 @@ License-File: LICENSE
10
10
  Requires-Dist: comment-parser>=1.2.4
11
11
  Requires-Dist: gitignore-parser>=0.1.11
12
12
  Requires-Dist: typer>=0.16.0
13
+ Requires-Dist: click < 8.2
13
14
  Requires-Dist: jsonschema
14
15
  Requires-Dist: sphinx>=7.4,<9
15
16
  Requires-Dist: sphinx-needs>=4.2.0
16
17
  Requires-Dist: jinja2
17
18
  Requires-Dist: pygments
18
19
  Requires-Dist: docutils
20
+ Requires-Dist: tree-sitter-cpp>=0.23.4
21
+ Requires-Dist: giturlparse>=0.12.0
22
+ Requires-Dist: tree-sitter-python>=0.23.6
23
+ Requires-Dist: tree-sitter~=0.25.1
19
24
 
20
25
  # Sphinx CodeLinks
21
26
 
@@ -24,7 +29,7 @@ A Sphinx extension for discovering, linking, and documenting source code across
24
29
  ## Features
25
30
 
26
31
  - **Source Discovery**: Automatically discover source files in your project
27
- - **Virtual Documentation**: Generate documentation from code without modifying source files
32
+ - **Analyse**: Parse source codes and extract specified markers with their metadata
28
33
  - **Code Linking**: Create intelligent links between code elements
29
34
  - **Sphinx Integration**: Seamless integration with existing Sphinx documentation
30
35
 
@@ -38,6 +43,7 @@ Add to your `conf.py`:
38
43
 
39
44
  ```python
40
45
  extensions = ['sphinx_needs', 'sphinx_codelinks']
46
+ src_trace_config_from_toml = "codelinks.toml"
41
47
  ```
42
48
 
43
49
  ## Documentation
@@ -46,8 +52,8 @@ Full documentation: https://codelinks.useblocks.com
46
52
 
47
53
  ## Components
48
54
 
49
- - **Source Discovery** ([`src/sphinx_codelinks/source_discovery`](src/sphinx_codelinks/source_discovery)): Code analysis and discovery
50
- - **Virtual Docs** ([`src/sphinx_codelinks/virtual_docs`](src/sphinx_codelinks/virtual_docs)): Documentation generation
55
+ - **Source Discovery** ([`src/sphinx_codelinks/source_discover`](src/sphinx_codelinks/source_discover)): Code analysis and discovery
56
+ - **Analyse** ([`src/sphinx_codelinks/analyse`](src/sphinx_codelinks/analyse)): Documentation generation
51
57
  - **Sphinx Extension** ([`src/sphinx_codelinks/sphinx_extension`](src/sphinx_codelinks/sphinx_extension)): Sphinx integration
52
58
  - **Command Line** ([`src/sphinx_codelinks/cmd.py`](src/sphinx_codelinks/cmd.py)): CLI interface
53
59
 
@@ -5,7 +5,7 @@ A Sphinx extension for discovering, linking, and documenting source code across
5
5
  ## Features
6
6
 
7
7
  - **Source Discovery**: Automatically discover source files in your project
8
- - **Virtual Documentation**: Generate documentation from code without modifying source files
8
+ - **Analyse**: Parse source codes and extract specified markers with their metadata
9
9
  - **Code Linking**: Create intelligent links between code elements
10
10
  - **Sphinx Integration**: Seamless integration with existing Sphinx documentation
11
11
 
@@ -19,6 +19,7 @@ Add to your `conf.py`:
19
19
 
20
20
  ```python
21
21
  extensions = ['sphinx_needs', 'sphinx_codelinks']
22
+ src_trace_config_from_toml = "codelinks.toml"
22
23
  ```
23
24
 
24
25
  ## Documentation
@@ -27,8 +28,8 @@ Full documentation: https://codelinks.useblocks.com
27
28
 
28
29
  ## Components
29
30
 
30
- - **Source Discovery** ([`src/sphinx_codelinks/source_discovery`](src/sphinx_codelinks/source_discovery)): Code analysis and discovery
31
- - **Virtual Docs** ([`src/sphinx_codelinks/virtual_docs`](src/sphinx_codelinks/virtual_docs)): Documentation generation
31
+ - **Source Discovery** ([`src/sphinx_codelinks/source_discover`](src/sphinx_codelinks/source_discover)): Code analysis and discovery
32
+ - **Analyse** ([`src/sphinx_codelinks/analyse`](src/sphinx_codelinks/analyse)): Documentation generation
32
33
  - **Sphinx Extension** ([`src/sphinx_codelinks/sphinx_extension`](src/sphinx_codelinks/sphinx_extension)): Sphinx integration
33
34
  - **Command Line** ([`src/sphinx_codelinks/cmd.py`](src/sphinx_codelinks/cmd.py)): CLI interface
34
35
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "sphinx-codelinks"
3
- version = "0.1.2"
3
+ version = "1.0.0"
4
4
  description = "Fast Source Code Traceability for Sphinx-Needs"
5
5
  authors = [{ name = "team useblocks", email = "info@useblocks.com" }]
6
6
  maintainers = [
@@ -14,13 +14,20 @@ dependencies = [
14
14
  "comment-parser>=1.2.4",
15
15
  "gitignore-parser>=0.1.11",
16
16
  "typer>=0.16.0",
17
+ "click < 8.2", # click 8.2.* produces empty errors if no args are given
17
18
  "jsonschema",
18
19
  "sphinx>=7.4,<9",
19
- "sphinx-needs>=4.2.0",
20
- # unconstrained versions, to be pinned by user or Sphinx
20
+ "sphinx-needs>=4.2.0", # unconstrained versions, to be pinned by user or Sphinx
21
21
  "jinja2",
22
22
  "pygments",
23
- "docutils", # constrained by user or Sphinx
23
+ "docutils", # constrained by user or Sphinx
24
+ "tree-sitter-cpp>=0.23.4",
25
+ "giturlparse>=0.12.0",
26
+ "tree-sitter-python>=0.23.6",
27
+ # Compiling issue on manylinux aarch64 image, so install from git instead of Pypi
28
+ # https://github.com/tree-sitter/py-tree-sitter/issues/386#issuecomment-3101430799
29
+ "tree-sitter~=0.25.1",
30
+
24
31
  ]
25
32
 
26
33
  [build-system]
@@ -53,6 +60,7 @@ dev-dependencies = [
53
60
  "toml>=0.10.2",
54
61
  "sphinx-code-tabs>=0.5.5",
55
62
  "sphinxcontrib-typer>=0.5.1",
63
+ "sphinxcontrib-video>=0.4.1",
56
64
  ]
57
65
 
58
66
  [project.scripts]
@@ -68,6 +76,21 @@ codelinks = "sphinx_codelinks.cmd:app"
68
76
  "docs:rm" = "rm -rf docs/_build/html"
69
77
  "docs" = "sphinx-build -nW --keep-going -b html -T -c docs docs/source docs/_build/html"
70
78
  "docs:clean" = { chain = ["docs:rm", "docs"] }
79
+ # needextend demo
80
+ "analyse" = "codelinks analyse tests/data/configs/minimum_config.toml"
81
+ "analyse:rm" = "rm -rf output/marked_content.json"
82
+ "write" = "codelinks write rst output/marked_content.json --outpath tests/data/needextend_demo/needextend.rst"
83
+ "write:rm" = "rm -rf tests/data/needextend_demo/needextend.rst"
84
+ "demo:rm" = "rm -rf tests/data/needextend_demo/_build"
85
+ "demo:build" = "sphinx-build -nW --keep-going -b html -T -c tests/data/needextend_demo tests/data/needextend_demo tests/data/needextend_demo/_build/html"
86
+ "demo:clean" = { chain = [
87
+ "demo:rm",
88
+ "analyse:rm",
89
+ "write:rm",
90
+ "analyse",
91
+ "write",
92
+ "demo:build",
93
+ ] }
71
94
 
72
95
  [tool.ruff.lint]
73
96
  extend-select = [
@@ -1,4 +1,4 @@
1
- """CodeLinks source code analyzer"""
1
+ """CodeLinks source code analyser"""
2
2
 
3
3
  from sphinx_codelinks.sphinx_extension.source_tracing import setup
4
4
 
@@ -0,0 +1,379 @@
1
+ from collections.abc import Generator
2
+ from dataclasses import dataclass
3
+ import json
4
+ import logging
5
+ from pathlib import Path
6
+ from typing import Any, TypedDict
7
+
8
+ from tree_sitter import Node as TreeSitterNode
9
+
10
+ from sphinx_codelinks.analyse import utils
11
+ from sphinx_codelinks.analyse.models import (
12
+ MarkedContentType,
13
+ MarkedRst,
14
+ NeedIdRefs,
15
+ OneLineNeed,
16
+ SourceComment,
17
+ SourceFile,
18
+ SourceMap,
19
+ )
20
+ from sphinx_codelinks.analyse.oneline_parser import (
21
+ OnelineParserInvalidWarning,
22
+ oneline_parser,
23
+ )
24
+ from sphinx_codelinks.config import (
25
+ UNIX_NEWLINE,
26
+ OneLineCommentStyle,
27
+ SourceAnalyseConfig,
28
+ )
29
+
30
+ # initialize logger
31
+ logger = logging.getLogger(__name__)
32
+ logger.setLevel(logging.INFO)
33
+ # log to the console
34
+ console = logging.StreamHandler()
35
+ console.setLevel(logging.INFO)
36
+ logger.addHandler(console)
37
+
38
+
39
+ class AnalyseWarningType(TypedDict):
40
+ file_path: str
41
+ lineno: int
42
+ msg: str
43
+ type: str
44
+ sub_type: str
45
+
46
+
47
+ @dataclass
48
+ class AnalyseWarning:
49
+ file_path: str
50
+ lineno: int
51
+ msg: str
52
+ type: str
53
+ sub_type: str
54
+
55
+
56
+ class SourceAnalyse:
57
+ def __init__(
58
+ self,
59
+ analyse_config: SourceAnalyseConfig,
60
+ ) -> None:
61
+ self.analyse_config = analyse_config
62
+ self.src_files: list[SourceFile] = []
63
+ self.src_comments: list[SourceComment] = []
64
+ self.need_id_refs: list[NeedIdRefs] = []
65
+ self.oneline_needs: list[OneLineNeed] = []
66
+ self.marked_rst: list[MarkedRst] = []
67
+ self.all_marked_content: list[NeedIdRefs | OneLineNeed | MarkedRst] = []
68
+ self.git_root: Path | None = utils.locate_git_root(self.analyse_config.src_dir)
69
+ self.git_remote_url: str | None = (
70
+ utils.get_remote_url(self.git_root) if self.git_root else None
71
+ )
72
+ self.git_commit_rev: str | None = (
73
+ utils.get_current_rev(self.git_root) if self.git_root else None
74
+ )
75
+ self.project_path: Path = (
76
+ self.git_root if self.git_root else self.analyse_config.src_dir
77
+ )
78
+ self.oneline_warnings: list[AnalyseWarning] = []
79
+
80
+ def get_src_strings(self) -> Generator[tuple[Path, bytes], Any, None]: # type: ignore[explicit-any]
81
+ """Load source files and extract their content."""
82
+ for src_path in self.analyse_config.src_files:
83
+ if not utils.is_text_file(src_path):
84
+ continue
85
+ with src_path.open("r", encoding="utf-8", newline="") as f:
86
+ # Normalize all line endings to Unix LF
87
+ text = f.read()
88
+ text = text.replace("\r\n", "\n").replace("\r", "\n")
89
+ yield src_path, text.encode("utf-8")
90
+
91
+ def create_src_objects(self) -> None:
92
+ parser, query = utils.init_tree_sitter(self.analyse_config.comment_type)
93
+
94
+ for src_path, src_string in self.get_src_strings():
95
+ comments: list[TreeSitterNode] | None = utils.extract_comments(
96
+ src_string, parser, query
97
+ )
98
+ if not comments:
99
+ continue
100
+ src_comments: list[SourceComment] = [
101
+ SourceComment(node) for node in comments
102
+ ]
103
+
104
+ src_file = SourceFile(src_path.absolute())
105
+ src_file.add_comments(src_comments)
106
+ self.src_files.append(src_file)
107
+ self.src_comments.extend(src_comments)
108
+
109
+ logger.info(f"Source files loaded: {len(self.src_files)}")
110
+ logger.info(f"Source comments extracted: {len(self.src_comments)}")
111
+
112
+ def extract_marker(
113
+ self,
114
+ text: str,
115
+ ) -> Generator[tuple[str, list[str], int, int, int], None, None]:
116
+ lines = text.splitlines()
117
+ row_offset = 0
118
+ for line in lines:
119
+ for marker in self.analyse_config.need_id_refs_config.markers:
120
+ marker_idx = line.find(marker)
121
+ if marker_idx == -1:
122
+ continue
123
+ markered_text = line[marker_idx + len(marker) :].strip()
124
+ need_ids = markered_text.replace(",", " ").split()
125
+ start_column = marker_idx + len(marker)
126
+ end_column = start_column + len(markered_text)
127
+ yield marker, need_ids, row_offset, start_column, end_column
128
+ row_offset += 1
129
+
130
+ def extract_anchors(
131
+ self,
132
+ text: str,
133
+ filepath: Path,
134
+ tagged_scope: TreeSitterNode | None,
135
+ src_comment: SourceComment,
136
+ ) -> list[NeedIdRefs]:
137
+ """Extract need-ids-refs from a comment."""
138
+ anchors: list[NeedIdRefs] = []
139
+ for (
140
+ marker,
141
+ need_ids,
142
+ row_offset,
143
+ start_column,
144
+ end_column,
145
+ ) in self.extract_marker(text):
146
+ lineno = src_comment.node.start_point.row + row_offset + 1
147
+ remote_url = self.git_remote_url
148
+ if self.git_remote_url and self.git_commit_rev:
149
+ remote_url = utils.form_https_url(
150
+ self.git_remote_url,
151
+ self.git_commit_rev,
152
+ self.project_path,
153
+ filepath,
154
+ lineno,
155
+ )
156
+ source_map: SourceMap = {
157
+ "start": {
158
+ "row": lineno - 1,
159
+ "column": start_column,
160
+ },
161
+ "end": {
162
+ "row": lineno - 1,
163
+ "column": end_column,
164
+ },
165
+ }
166
+ anchors.append(
167
+ NeedIdRefs(
168
+ filepath,
169
+ remote_url,
170
+ source_map,
171
+ src_comment,
172
+ tagged_scope,
173
+ need_ids,
174
+ marker,
175
+ )
176
+ )
177
+ return anchors
178
+
179
+ def extract_oneline_need(
180
+ self,
181
+ text: str,
182
+ src_comment: SourceComment,
183
+ oneline_comment_style: OneLineCommentStyle,
184
+ ) -> Generator[tuple[dict[str, str | list[str] | int], int]]:
185
+ lines = text.splitlines(keepends=True)
186
+ row_offset = 0
187
+ if len(lines) == 1:
188
+ # single line comment has no newline char in the extracted comment
189
+ lines[0] = f"{lines[0]}{UNIX_NEWLINE}"
190
+
191
+ for line in lines:
192
+ resolved = oneline_parser(line, oneline_comment_style)
193
+ if not resolved:
194
+ row_offset += 1
195
+ continue
196
+ if isinstance(resolved, OnelineParserInvalidWarning):
197
+ if not src_comment.source_file:
198
+ row_offset += 1
199
+ continue
200
+ self.oneline_warnings.append(
201
+ AnalyseWarning(
202
+ str(src_comment.source_file.filepath),
203
+ src_comment.node.start_point.row + row_offset + 1,
204
+ resolved.msg,
205
+ MarkedContentType.need,
206
+ resolved.sub_type.value,
207
+ )
208
+ )
209
+ row_offset += 1
210
+ continue
211
+ yield resolved, row_offset
212
+ row_offset += 1
213
+
214
+ def extract_oneline_needs(
215
+ self,
216
+ text: str,
217
+ filepath: Path,
218
+ tagged_scope: TreeSitterNode | None,
219
+ src_comment: SourceComment,
220
+ oneline_comment_style: OneLineCommentStyle,
221
+ ) -> list[OneLineNeed]:
222
+ row_offset = 0
223
+ oneline_needs = []
224
+ for resolved, row_offset in self.extract_oneline_need(
225
+ text, src_comment, oneline_comment_style
226
+ ):
227
+ lineno = src_comment.node.start_point.row + row_offset + 1
228
+ remote_url = self.git_remote_url
229
+ if self.git_remote_url and self.git_commit_rev:
230
+ remote_url = utils.form_https_url(
231
+ self.git_remote_url,
232
+ self.git_commit_rev,
233
+ self.project_path,
234
+ filepath,
235
+ lineno,
236
+ )
237
+ source_map: SourceMap = {
238
+ "start": {
239
+ "row": lineno - 1,
240
+ "column": resolved["start_column"], # type: ignore[typeddict-item] # dynamic keys
241
+ },
242
+ "end": {
243
+ "row": lineno - 1,
244
+ "column": resolved["end_column"], # type: ignore[typeddict-item] # dynamic keys
245
+ },
246
+ }
247
+ del resolved["start_column"]
248
+ del resolved["end_column"]
249
+ oneline_needs.append(
250
+ OneLineNeed(
251
+ filepath,
252
+ remote_url,
253
+ source_map,
254
+ src_comment,
255
+ tagged_scope,
256
+ resolved, # type: ignore[arg-type] # int arguments were deleted
257
+ )
258
+ )
259
+ return oneline_needs
260
+
261
+ def extract_marked_rst(
262
+ self,
263
+ text: str,
264
+ filepath: Path,
265
+ tagged_scope: TreeSitterNode | None,
266
+ src_comment: SourceComment,
267
+ ) -> MarkedRst | None:
268
+ """Extract marked rst from a comment.
269
+
270
+ Presumably, only one marked rst text in a comment.
271
+ """
272
+ extracted_rst = utils.extract_rst(
273
+ text,
274
+ self.analyse_config.marked_rst_config.start_sequence,
275
+ self.analyse_config.marked_rst_config.end_sequence,
276
+ )
277
+ if not extracted_rst:
278
+ return None
279
+ if UNIX_NEWLINE in extracted_rst["rst_text"]:
280
+ rst_text = utils.remove_leading_sequences(extracted_rst["rst_text"], ["*"])
281
+ else:
282
+ rst_text = extracted_rst["rst_text"]
283
+ lineno = src_comment.node.start_point.row + extracted_rst["row_offset"] + 1
284
+ remote_url = self.git_remote_url
285
+ if self.git_remote_url and self.git_commit_rev:
286
+ remote_url = utils.form_https_url(
287
+ self.git_remote_url,
288
+ self.git_commit_rev,
289
+ self.project_path,
290
+ filepath,
291
+ lineno,
292
+ )
293
+ source_map: SourceMap = {
294
+ "start": {
295
+ "row": lineno - 1,
296
+ "column": extracted_rst["start_idx"],
297
+ },
298
+ "end": {
299
+ "row": lineno - 1,
300
+ "column": extracted_rst["end_idx"],
301
+ },
302
+ }
303
+ return MarkedRst(
304
+ filepath,
305
+ remote_url,
306
+ source_map,
307
+ src_comment,
308
+ tagged_scope,
309
+ rst_text,
310
+ )
311
+
312
+ def extract_marked_content(self) -> None:
313
+ for src_comment in self.src_comments:
314
+ text = (
315
+ src_comment.node.text.decode("utf-8") if src_comment.node.text else None
316
+ )
317
+ if not text:
318
+ continue
319
+ filepath = (
320
+ src_comment.source_file.filepath if src_comment.source_file else None
321
+ )
322
+ if not filepath:
323
+ continue
324
+ tagged_scope: TreeSitterNode | None = utils.find_associated_scope(
325
+ src_comment.node
326
+ )
327
+ if self.analyse_config.get_need_id_refs:
328
+ anchors = self.extract_anchors(
329
+ text, filepath, tagged_scope, src_comment
330
+ )
331
+ self.need_id_refs.extend(anchors)
332
+
333
+ if self.analyse_config.get_oneline_needs:
334
+ oneline_needs = self.extract_oneline_needs(
335
+ text,
336
+ filepath,
337
+ tagged_scope,
338
+ src_comment,
339
+ self.analyse_config.oneline_comment_style,
340
+ )
341
+ self.oneline_needs.extend(oneline_needs)
342
+ if self.analyse_config.get_rst:
343
+ marked_rst = self.extract_marked_rst(
344
+ text, filepath, tagged_scope, src_comment
345
+ )
346
+ if marked_rst:
347
+ self.marked_rst.append(marked_rst)
348
+
349
+ if self.analyse_config.get_need_id_refs:
350
+ logger.info(f"Need-id-refs extracted: {len(self.need_id_refs)}")
351
+ if self.analyse_config.get_oneline_needs:
352
+ logger.info(f"Oneline needs extracted: {len(self.oneline_needs)}")
353
+ if self.analyse_config.get_rst:
354
+ logger.info(f"Marked rst extracted: {len(self.marked_rst)}")
355
+
356
+ def merge_marked_content(self) -> None:
357
+ self.all_marked_content.extend(self.need_id_refs)
358
+ self.oneline_needs.sort(key=lambda x: x.source_map["start"]["row"])
359
+ self.all_marked_content.extend(self.oneline_needs)
360
+ self.all_marked_content.extend(self.marked_rst)
361
+ self.all_marked_content.sort(
362
+ key=lambda x: (x.filepath, x.source_map["start"]["row"])
363
+ )
364
+
365
+ def dump_marked_content(self, outdir: Path) -> None:
366
+ output_path = outdir / "marked_content.json"
367
+ if not output_path.parent.exists():
368
+ output_path.parent.mkdir(parents=True)
369
+ to_dump = [
370
+ marked_content.to_dict() for marked_content in self.all_marked_content
371
+ ]
372
+ with output_path.open("w") as f:
373
+ json.dump(to_dump, f)
374
+ logger.info(f"Marked content dumped to {output_path}")
375
+
376
+ def run(self) -> None:
377
+ self.create_src_objects()
378
+ self.extract_marked_content()
379
+ self.merge_marked_content()
@@ -0,0 +1,86 @@
1
+ from dataclasses import dataclass, field
2
+ from enum import Enum
3
+ from pathlib import Path
4
+ from typing import TypedDict
5
+
6
+ from tree_sitter import Node as TreeSitterNode
7
+
8
+
9
+ class MarkedContentType(str, Enum):
10
+ need = "need"
11
+ need_id_refs = "need-id-refs"
12
+ rst = "rst"
13
+
14
+
15
+ class SourceComment:
16
+ def __init__(self, node: TreeSitterNode) -> None:
17
+ self.node: TreeSitterNode = node
18
+ self.source_file: SourceFile | None = None
19
+
20
+
21
+ class SourceFile:
22
+ def __init__(self, filepath: Path) -> None:
23
+ self.filepath: Path = filepath
24
+ self.src_comments: list[SourceComment] = []
25
+
26
+ def add_comment(self, comment: SourceComment) -> None:
27
+ self.src_comments.append(comment)
28
+ comment.source_file = self
29
+
30
+ def add_comments(self, comments: list[SourceComment]) -> None:
31
+ for comment in comments:
32
+ self.add_comment(comment)
33
+
34
+
35
+ class Position(TypedDict):
36
+ row: int
37
+ column: int
38
+
39
+
40
+ class SourceMap(TypedDict):
41
+ start: Position
42
+ end: Position
43
+
44
+
45
+ @dataclass
46
+ class Metadata:
47
+ filepath: Path
48
+ remote_url: str | None
49
+ source_map: SourceMap
50
+ source_comment: SourceComment
51
+ tagged_scope: TreeSitterNode | None
52
+ type: MarkedContentType
53
+
54
+ def to_dict(self) -> dict[str, str | int | list[str]]:
55
+ obj = self.__dict__.copy()
56
+ obj["filepath"] = str(self.filepath)
57
+ obj["tagged_scope"] = (
58
+ str(self.tagged_scope.text.decode("utf-8"))
59
+ if self.tagged_scope and self.tagged_scope.text
60
+ else None
61
+ )
62
+ obj["type"] = self.type.value
63
+ del obj["source_comment"]
64
+ return obj
65
+
66
+ def add_src_comment(self, src_comment: SourceComment) -> None:
67
+ self.source_comment = src_comment
68
+
69
+
70
+ @dataclass
71
+ class NeedIdRefs(Metadata):
72
+ need_ids: list[str]
73
+ marker: str
74
+ type: MarkedContentType = field(init=False, default=MarkedContentType.need_id_refs)
75
+
76
+
77
+ @dataclass
78
+ class OneLineNeed(Metadata):
79
+ need: dict[str, str | list[str]]
80
+ type: MarkedContentType = field(init=False, default=MarkedContentType.need)
81
+
82
+
83
+ @dataclass
84
+ class MarkedRst(Metadata):
85
+ rst: str
86
+ type: MarkedContentType = field(init=False, default=MarkedContentType.rst)