sphinx-data2table 0.1.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,35 @@
1
+ """Sphinx Data2Table Extension.
2
+
3
+ Renders TOML, YAML, or JSON data as HTML/LaTeX tables with Markdown cell parsing.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ from typing import TYPE_CHECKING, Any
9
+
10
+ from sphinx_data2table.directive import DataTableDirective
11
+
12
+ if TYPE_CHECKING:
13
+ from sphinx.application import Sphinx
14
+
15
+ __version__ = "0.1.0"
16
+
17
+
18
+ def setup(app: Sphinx) -> dict[str, Any]:
19
+ """Registers the data-table and datatable directives with the Sphinx application.
20
+
21
+ Args:
22
+ app: The active Sphinx application instance.
23
+
24
+ Returns:
25
+ A dictionary containing metadata about the extension, including
26
+ version and parallel read/write safety flags.
27
+ """
28
+ app.add_directive("data-table", DataTableDirective)
29
+ app.add_directive("datatable", DataTableDirective)
30
+
31
+ return {
32
+ "version": __version__,
33
+ "parallel_read_safe": True,
34
+ "parallel_write_safe": True,
35
+ }
@@ -0,0 +1,333 @@
1
+ """Directive implementation for rendering TOML, YAML, and JSON tables."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import contextlib
6
+ import json
7
+ import os
8
+ import textwrap
9
+ import tomllib
10
+ from typing import Any
11
+
12
+ import yaml
13
+ from docutils import nodes
14
+ from docutils.parsers.rst import Directive, directives
15
+ from docutils.statemachine import StringList
16
+
17
+
18
+ class DataTableDirective(Directive):
19
+ """Sphinx/Docutils directive to render TOML, YAML, or JSON data as tables.
20
+
21
+ This directive parses raw inline TOML/YAML/JSON text or external data files
22
+ into structured data and constructs docutils table nodes. Each cell's text is
23
+ parsed via nested_parse to support Markdown inline and block elements.
24
+ """
25
+
26
+ has_content = True
27
+ required_arguments = 0
28
+ optional_arguments = 0
29
+ final_argument_whitespace = False
30
+ option_spec = {
31
+ "file": directives.path,
32
+ "format": directives.unchanged,
33
+ "headers": directives.unchanged,
34
+ }
35
+
36
+ def run(self) -> list[nodes.Node]:
37
+ """Main execution method invoked by docutils when parsing directive.
38
+
39
+ Returns:
40
+ A list containing constructed docutils table node or error nodes.
41
+ """
42
+ env = (
43
+ self.state.document.settings.env
44
+ if hasattr(self.state.document.settings, "env")
45
+ else None
46
+ )
47
+
48
+ # 1. Obtain raw data content
49
+ file_path = self.options.get("file")
50
+ raw_text = ""
51
+
52
+ if file_path:
53
+ if env:
54
+ # Resolve relative path to sphinx doc source directory
55
+ rel_path, abs_path = env.relfn2path(file_path)
56
+ env.note_dependency(rel_path)
57
+ else:
58
+ abs_path = os.path.abspath(file_path)
59
+
60
+ try:
61
+ with open(abs_path, encoding="utf-8") as f:
62
+ raw_text = f.read()
63
+ except OSError as err:
64
+ return [
65
+ self.state_machine.reporter.error(
66
+ f"data-table: Could not read file '{file_path}': {err}",
67
+ line=self.lineno,
68
+ )
69
+ ]
70
+ elif self.content:
71
+ raw_text = textwrap.dedent("\n".join(self.content)).strip()
72
+ else:
73
+ return [
74
+ self.state_machine.reporter.error(
75
+ "data-table: Neither content nor ':file:' option provided.",
76
+ line=self.lineno,
77
+ )
78
+ ]
79
+
80
+ # 2. Determine format (yaml / toml / json)
81
+ fmt = self.options.get("format", "auto").lower()
82
+
83
+ if fmt == "auto":
84
+ if file_path:
85
+ ext = os.path.splitext(file_path)[1].lower()
86
+ if ext in (".toml",):
87
+ fmt = "toml"
88
+ elif ext in (".yaml", ".yml"):
89
+ fmt = "yaml"
90
+ elif ext in (".json",):
91
+ fmt = "json"
92
+ if fmt == "auto":
93
+ fmt = self._guess_format(raw_text)
94
+
95
+ # 3. Parse data
96
+ data, parse_err = self._parse_data(raw_text, fmt)
97
+ if parse_err:
98
+ return [
99
+ self.state_machine.reporter.error(
100
+ f"data-table: Failed to parse {fmt.upper()} data: {parse_err}",
101
+ line=self.lineno,
102
+ )
103
+ ]
104
+
105
+ rows_data = self._normalize_rows(data)
106
+ if not rows_data:
107
+ return [
108
+ self.state_machine.reporter.warning(
109
+ "data-table: Data is empty or invalid format.",
110
+ line=self.lineno,
111
+ )
112
+ ]
113
+
114
+ # 4. Determine Headers
115
+ headers_opt = self.options.get("headers")
116
+ if headers_opt:
117
+ headers = [h.strip() for h in headers_opt.split(",") if h.strip()]
118
+ else:
119
+ # Collect all unique keys across rows while preserving order
120
+ headers = []
121
+ for row in rows_data:
122
+ if isinstance(row, dict):
123
+ for k in row.keys():
124
+ if k not in headers:
125
+ headers.append(k)
126
+
127
+ if not headers:
128
+ return [
129
+ self.state_machine.reporter.error(
130
+ "data-table: Could not determine headers from data.",
131
+ line=self.lineno,
132
+ )
133
+ ]
134
+
135
+ # 5. Build Docutils Table Node
136
+ table_node = self._build_table_node(headers, rows_data)
137
+ return [table_node]
138
+
139
+ def _guess_format(self, text: str) -> str:
140
+ """Guesses whether raw text content is JSON, TOML, or YAML.
141
+
142
+ Args:
143
+ text: The raw string content of the data.
144
+
145
+ Returns:
146
+ The string 'json', 'toml', or 'yaml'. Defaults to 'yaml' if ambiguous.
147
+ """
148
+ # Try JSON first
149
+ with contextlib.suppress(Exception):
150
+ parsed_json = json.loads(text)
151
+ if isinstance(parsed_json, (list, dict)):
152
+ return "json"
153
+
154
+ # Try TOML
155
+ with contextlib.suppress(Exception):
156
+ parsed_toml = tomllib.loads(text)
157
+ if isinstance(parsed_toml, dict) and parsed_toml:
158
+ return "toml"
159
+
160
+ # Try YAML
161
+ with contextlib.suppress(Exception):
162
+ parsed_yaml = yaml.safe_load(text)
163
+ if isinstance(parsed_yaml, (list, dict)):
164
+ return "yaml"
165
+
166
+ return "yaml"
167
+
168
+ def _parse_data(self, text: str, fmt: str) -> tuple[Any, str | None]:
169
+ """Parses raw text data using the specified format parser.
170
+
171
+ Args:
172
+ text: Raw data text string.
173
+ fmt: Format string ('json', 'toml', or 'yaml').
174
+
175
+ Returns:
176
+ A tuple containing (parsed_object, error_message_or_None).
177
+ """
178
+ if fmt == "json":
179
+ try:
180
+ return json.loads(text), None
181
+ except Exception as e:
182
+ return None, str(e)
183
+ elif fmt == "toml":
184
+ try:
185
+ return tomllib.loads(text), None
186
+ except Exception as e:
187
+ return None, str(e)
188
+ elif fmt == "yaml":
189
+ try:
190
+ return yaml.safe_load(text), None
191
+ except Exception as e:
192
+ return None, str(e)
193
+ else:
194
+ return None, f"Unsupported format '{fmt}'. Use 'json', 'yaml', or 'toml'."
195
+
196
+ def _normalize_rows(self, data: Any) -> list[dict[str, Any]]:
197
+ """Normalizes parsed data into a list of row dictionaries.
198
+
199
+ Args:
200
+ data: Parsed data resulting from JSON, TOML, or YAML parser.
201
+
202
+ Returns:
203
+ A list of dictionary objects representing table rows.
204
+ """
205
+ if isinstance(data, list):
206
+ return [item for item in data if isinstance(item, dict)]
207
+ elif isinstance(data, dict):
208
+ # If root is a dict containing a list of dicts (e.g. {"items": [...]})
209
+ for val in data.values():
210
+ if isinstance(val, list) and all(isinstance(x, dict) for x in val):
211
+ return val
212
+ # Or a single row dict
213
+ return [data]
214
+ return []
215
+
216
+ def _build_table_node(
217
+ self, headers: list[str], rows_data: list[dict[str, Any]]
218
+ ) -> nodes.table:
219
+ """Constructs docutils table nodes and parses Markdown for each cell.
220
+
221
+ Args:
222
+ headers: A list of column header names.
223
+ rows_data: A list of row dictionary objects containing cell contents.
224
+
225
+ Returns:
226
+ A docutils.nodes.table instance containing header and body rows.
227
+ """
228
+ table = nodes.table()
229
+ table["classes"] += ["datatable"]
230
+ tgroup = nodes.tgroup(cols=len(headers))
231
+ table += tgroup
232
+
233
+ for _ in headers:
234
+ tgroup += nodes.colspec(colwidth=1)
235
+
236
+ # Header Row
237
+ thead = nodes.thead()
238
+ tgroup += thead
239
+ header_row = nodes.row()
240
+ thead += header_row
241
+
242
+ for header_text in headers:
243
+ entry = nodes.entry()
244
+ self._parse_cell_markdown(str(header_text), entry)
245
+ header_row += entry
246
+
247
+ # Body Rows
248
+ tbody = nodes.tbody()
249
+ tgroup += tbody
250
+
251
+ for row_dict in rows_data:
252
+ row_node = nodes.row()
253
+ tbody += row_node
254
+ for h in headers:
255
+ entry = nodes.entry()
256
+ cell_value = row_dict.get(h, "")
257
+ if cell_value is None:
258
+ cell_value = ""
259
+ self._parse_cell_markdown(str(cell_value), entry)
260
+ row_node += entry
261
+
262
+ return table
263
+
264
+ def _parse_cell_markdown(self, content_str: str, entry_node: nodes.entry) -> None:
265
+ """Parses cell string as Markdown/reST AST into docutils nodes in entry_node.
266
+
267
+ Args:
268
+ content_str: The raw text content of the table cell.
269
+ entry_node: Target docutils.nodes.entry node to append child nodes into.
270
+ """
271
+ dedented_str = textwrap.dedent(content_str).strip()
272
+ if not dedented_str:
273
+ return
274
+
275
+ lines = dedented_str.splitlines()
276
+ string_list = StringList(lines, source="datatable_cell")
277
+
278
+ # Create temporary container node to hold nested parsed nodes
279
+ container = nodes.Element()
280
+ self.state.nested_parse(string_list, 0, container)
281
+
282
+ # Post-process container nodes to replace in-cell newlines with latex-safe
283
+ self._replace_cell_newlines(container)
284
+
285
+ # Transfer children from container to entry_node
286
+ for child in container.children:
287
+ entry_node += child
288
+
289
+ def _replace_cell_newlines(self, node: nodes.Node) -> None:
290
+ """Recursively replaces in-cell line breaks with raw break nodes.
291
+
292
+ WORKAROUND:
293
+ Sphinx's default LaTeXTranslator turns in-cell line break nodes into
294
+ '\\\\', which LaTeX interprets as a table row separator.
295
+
296
+ Args:
297
+ node: The docutils AST node to process recursively.
298
+ """
299
+ new_children: list[nodes.Node] = []
300
+ modified = False
301
+
302
+ for child in list(node.children):
303
+ if (
304
+ isinstance(child, nodes.raw)
305
+ and child.get("format") == "latex"
306
+ and child.astext().strip() in ("\\\\", r"\\")
307
+ ):
308
+ modified = True
309
+ latex_break = nodes.raw("", r"\newline ", format="latex")
310
+ latex_break.parent = node
311
+ new_children.append(latex_break)
312
+ elif isinstance(child, nodes.Text) and "\n" in child:
313
+ modified = True
314
+ parts = str(child).split("\n")
315
+ for i, part in enumerate(parts):
316
+ clean_part = part.rstrip()
317
+ if clean_part:
318
+ t = nodes.Text(clean_part)
319
+ t.parent = node
320
+ new_children.append(t)
321
+ if i < len(parts) - 1:
322
+ latex_break = nodes.raw("", r"\newline ", format="latex")
323
+ html_break = nodes.raw("", "<br/>", format="html")
324
+ latex_break.parent = node
325
+ html_break.parent = node
326
+ new_children.extend([latex_break, html_break])
327
+ else:
328
+ self._replace_cell_newlines(child)
329
+ child.parent = node
330
+ new_children.append(child)
331
+
332
+ if modified:
333
+ node.children = new_children
@@ -0,0 +1,12 @@
1
+ Metadata-Version: 2.4
2
+ Name: sphinx-data2table
3
+ Version: 0.1.1
4
+ Summary: Sphinx data-table extension for TOML, YAML, and JSON with Markdown cell parsing
5
+ Author: jagarikokamukamu
6
+ License-Expression: MIT
7
+ Requires-Python: >=3.12
8
+ Description-Content-Type: text/markdown
9
+ License-File: LICENSE
10
+ Requires-Dist: pyyaml>=6.0.3
11
+ Requires-Dist: sphinx>=8.0
12
+ Dynamic: license-file
@@ -0,0 +1,7 @@
1
+ sphinx_data2table/__init__.py,sha256=RKatWID8p2r1j8EFCPGYHqfEsN-NZaI72ww_XJb8D0Q,916
2
+ sphinx_data2table/directive.py,sha256=Abs0f86-BKadBbLhrPEuRbjfeJi3ui8ktYGS6bNoyII,11492
3
+ sphinx_data2table-0.1.1.dist-info/licenses/LICENSE,sha256=fH3Y9McptuMlBOJaKKnH0MWywtXzzXru8txKerv5S0s,1072
4
+ sphinx_data2table-0.1.1.dist-info/METADATA,sha256=fw2nncuUVTlZt59iYD6uEH1giWc_iISIqxhZqO1oJok,363
5
+ sphinx_data2table-0.1.1.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
6
+ sphinx_data2table-0.1.1.dist-info/top_level.txt,sha256=iDTbSqriAxdWeBFybs0MreaEBfZ3cBVUEZItg2zZ40w,18
7
+ sphinx_data2table-0.1.1.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (83.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 jagarikokamukamu
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 @@
1
+ sphinx_data2table