typsphinx 0.3.0__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,396 @@
1
+ """
2
+ Template engine for Typst document generation.
3
+
4
+ This module implements template loading, parameter mapping, and rendering
5
+ for Typst documents (Requirement 8).
6
+ """
7
+
8
+ import logging
9
+ from pathlib import Path
10
+ from typing import Any, Dict, List, Optional
11
+
12
+ logger = logging.getLogger(__name__)
13
+
14
+
15
+ class TemplateEngine:
16
+ """
17
+ Manages Typst templates for document generation.
18
+
19
+ Responsibilities:
20
+ - Load default or custom Typst templates
21
+ - Search templates in multiple directories with priority
22
+ - Provide fallback to default template when custom template not found
23
+ - Map Sphinx metadata to template parameters
24
+ - Render final Typst document with template and content
25
+
26
+ Requirement 8.1: Default Typst template included in package
27
+ Requirement 8.2: Support custom template specification
28
+ Requirement 8.7: Priority search in user project directory
29
+ Requirement 8.9: Fallback to default template with warning
30
+ """
31
+
32
+ # Standard mapping from Sphinx metadata to template parameters
33
+ # Requirement 8.3: Sphinx metadata passed to template
34
+ # Requirement 8.5: Standard metadata name transformation
35
+ DEFAULT_PARAMETER_MAPPING = {
36
+ "project": "title",
37
+ "author": "authors",
38
+ "release": "date",
39
+ }
40
+
41
+ def __init__(
42
+ self,
43
+ template_path: Optional[str] = None,
44
+ template_name: Optional[str] = None,
45
+ search_paths: Optional[List[str]] = None,
46
+ parameter_mapping: Optional[Dict[str, str]] = None,
47
+ typst_package: Optional[str] = None,
48
+ typst_template_function: Optional[str] = None,
49
+ typst_package_imports: Optional[List[str]] = None,
50
+ ):
51
+ """
52
+ Initialize TemplateEngine.
53
+
54
+ Args:
55
+ template_path: Absolute path to template file (highest priority)
56
+ template_name: Template filename to search in search_paths
57
+ search_paths: List of directories to search for templates (priority order)
58
+ parameter_mapping: Custom mapping from Sphinx metadata to template parameters
59
+ (Requirement 8.4: different parameter names)
60
+ typst_package: Typst Universe package specification (e.g., "@preview/charged-ieee:0.1.0")
61
+ (Requirement 8.6: external template packages)
62
+ typst_template_function: Template function name from package
63
+ typst_package_imports: Specific items to import from package
64
+ """
65
+ self.template_path = template_path
66
+ self.template_name = template_name or "base.typ"
67
+ self.search_paths = search_paths or []
68
+ self.parameter_mapping = (
69
+ parameter_mapping or self.DEFAULT_PARAMETER_MAPPING.copy()
70
+ )
71
+ self.typst_package = typst_package
72
+ self.typst_template_function = typst_template_function
73
+ self.typst_package_imports = typst_package_imports or []
74
+
75
+ def get_default_template_path(self) -> str:
76
+ """
77
+ Get the path to the default template bundled with the package.
78
+
79
+ Returns:
80
+ Absolute path to default template file
81
+ """
82
+ # Template is located in sphinxcontrib/typst/templates/base.typ
83
+ package_dir = Path(__file__).parent
84
+ template_dir = package_dir / "templates"
85
+ default_template = template_dir / "base.typ"
86
+
87
+ return str(default_template)
88
+
89
+ def load_template(self) -> str:
90
+ """
91
+ Load Typst template with priority order:
92
+ 1. Explicit template_path if provided
93
+ 2. Search for template_name in search_paths (first match wins)
94
+ 3. Default template bundled with package
95
+
96
+ Returns:
97
+ Template content as string
98
+
99
+ Requirement 8.1: Load default template
100
+ Requirement 8.2: Load custom template
101
+ Requirement 8.7: Search in user project directory
102
+ Requirement 8.9: Fallback to default with warning
103
+ """
104
+ template_content = None
105
+
106
+ # Priority 1: Explicit template path
107
+ if self.template_path:
108
+ template_content = self._try_load_file(self.template_path)
109
+ if template_content is None:
110
+ logger.warning(
111
+ f"Custom template not found: {self.template_path}. "
112
+ f"Falling back to default template."
113
+ )
114
+
115
+ # Priority 2: Search in search_paths
116
+ if template_content is None and self.search_paths:
117
+ for search_dir in self.search_paths:
118
+ candidate_path = Path(search_dir) / self.template_name
119
+ template_content = self._try_load_file(str(candidate_path))
120
+ if template_content is not None:
121
+ logger.debug(f"Loaded template from: {candidate_path}")
122
+ break
123
+
124
+ # Priority 3: Default template
125
+ if template_content is None:
126
+ default_path = self.get_default_template_path()
127
+ template_content = self._try_load_file(default_path)
128
+
129
+ if template_content is None:
130
+ # This should never happen if package is properly installed
131
+ raise FileNotFoundError(
132
+ f"Default template not found at: {default_path}. "
133
+ f"Package installation may be corrupted."
134
+ )
135
+
136
+ return template_content
137
+
138
+ def map_parameters(self, sphinx_metadata: Dict[str, Any]) -> Dict[str, Any]:
139
+ """
140
+ Map Sphinx metadata to template parameters.
141
+
142
+ Args:
143
+ sphinx_metadata: Dictionary of Sphinx configuration metadata
144
+ (project, author, release, etc.)
145
+
146
+ Returns:
147
+ Dictionary of template parameters ready to pass to template
148
+
149
+ Requirement 8.3: Pass Sphinx metadata to template
150
+ Requirement 8.4: Support different parameter names
151
+ Requirement 8.5: Standard metadata name transformation
152
+ Requirement 8.8: Convert to arrays and complex structures
153
+ """
154
+ params = {}
155
+
156
+ # Apply mapping
157
+ for sphinx_key, template_key in self.parameter_mapping.items():
158
+ if sphinx_key in sphinx_metadata:
159
+ value = sphinx_metadata[sphinx_key]
160
+
161
+ # Special handling for authors (both standard "authors" and custom names)
162
+ if sphinx_key == "author" or template_key in ("authors", "doc_authors"):
163
+ value = self._convert_to_authors_tuple(value)
164
+
165
+ params[template_key] = value
166
+
167
+ # Provide default values for missing required parameters
168
+ if "title" not in params:
169
+ params["title"] = ""
170
+ if "authors" not in params:
171
+ params["authors"] = ()
172
+ if "date" not in params:
173
+ params["date"] = None
174
+
175
+ return params
176
+
177
+ def generate_package_import(self) -> str:
178
+ """
179
+ Generate Typst package import statement.
180
+
181
+ Returns:
182
+ Import statement string, or empty string if no package specified
183
+
184
+ Requirement 8.6: Typst Universe external template packages
185
+ """
186
+ if not self.typst_package:
187
+ return ""
188
+
189
+ # Generate import statement
190
+ if self.typst_package_imports:
191
+ # Import specific items: #import "@package:version": item1, item2
192
+ items = ", ".join(self.typst_package_imports)
193
+ return f'#import "{self.typst_package}": {items}'
194
+ elif self.typst_template_function:
195
+ # Import template function: #import "@package:version": template_func
196
+ return f'#import "{self.typst_package}": {self.typst_template_function}'
197
+ else:
198
+ # Import entire module: #import "@package:version"
199
+ return f'#import "{self.typst_package}"'
200
+
201
+ def extract_toctree_options(self, doctree: Any) -> Dict[str, Any]:
202
+ """
203
+ Extract toctree options from doctree for template parameters.
204
+
205
+ Args:
206
+ doctree: Docutils document tree
207
+
208
+ Returns:
209
+ Dictionary of toctree options for template
210
+
211
+ Requirement 8.12: toctree options passed as template parameters
212
+ Requirement 8.13: template reflects toctree options in #outline()
213
+ Requirement 13.8: #outline() managed at template level
214
+ Requirement 13.9: toctree options mapped to template parameters
215
+ """
216
+ from sphinx import addnodes
217
+
218
+ # Try to find toctree node in doctree
219
+ toctree_nodes = list(doctree.traverse(addnodes.toctree))
220
+
221
+ if not toctree_nodes:
222
+ # No toctree found - return empty dict
223
+ return {}
224
+
225
+ # Use first toctree node found
226
+ toctree = toctree_nodes[0]
227
+
228
+ # Extract options with defaults
229
+ # Note: Sphinx toctree numbered can be False, True, or int
230
+ # Convert to bool for Typst (0 means False, positive means True)
231
+ numbered_value = toctree.get("numbered", False)
232
+ if isinstance(numbered_value, int):
233
+ numbered_value = numbered_value > 0
234
+
235
+ # Note: Sphinx toctree maxdepth can be -1 (unlimited)
236
+ # Typst outline() requires positive depth or none
237
+ # Convert -1 to none for unlimited depth
238
+ maxdepth_value = toctree.get("maxdepth", 2)
239
+ if maxdepth_value == -1:
240
+ maxdepth_value = None
241
+
242
+ return {
243
+ "toctree_maxdepth": maxdepth_value,
244
+ "toctree_numbered": numbered_value,
245
+ "toctree_caption": toctree.get("caption", ""),
246
+ }
247
+
248
+ def get_template_content(self) -> str:
249
+ """
250
+ Get the template content for writing to a separate file.
251
+
252
+ Returns:
253
+ Template content as string
254
+
255
+ This is used when templates are written as separate files
256
+ instead of being inlined in the main document.
257
+ """
258
+ template = self.load_template()
259
+ return template
260
+
261
+ def render(
262
+ self, params: Dict[str, Any], body: str, template_file: str = None
263
+ ) -> str:
264
+ """
265
+ Render final Typst document with template and body.
266
+
267
+ Args:
268
+ params: Template parameters (title, authors, etc.)
269
+ body: Document body content (Typst markup)
270
+ template_file: Path to template file for import (relative to output dir).
271
+ If None, template is inlined (old behavior).
272
+ If specified, template is imported from file.
273
+
274
+ Returns:
275
+ Complete Typst document string
276
+
277
+ Requirement 8.2: Use custom template
278
+ Requirement 8.10: Pass document settings to template
279
+ Requirement 8.14: #outline() in template, not body
280
+ """
281
+ # Build output parts
282
+ output_parts = []
283
+
284
+ # Add package import if using Typst Universe package
285
+ package_import = self.generate_package_import()
286
+ if package_import:
287
+ output_parts.append(package_import)
288
+ output_parts.append("") # Blank line
289
+
290
+ if template_file:
291
+ # Import essential packages (needed for content, not just template)
292
+ output_parts.append("// Essential package imports")
293
+ output_parts.append('#import "@preview/codly:1.3.0": *')
294
+ output_parts.append('#import "@preview/codly-languages:0.1.1": *')
295
+ output_parts.append('#import "@preview/mitex:0.2.4": mi, mitex')
296
+ output_parts.append('#import "@preview/gentle-clues:1.2.0": *')
297
+ output_parts.append("") # Blank line
298
+
299
+ # Import template from separate file
300
+ template_func = self.typst_template_function or "project"
301
+ output_parts.append(f'#import "{template_file}": {template_func}')
302
+ output_parts.append("") # Blank line
303
+ else:
304
+ # Load template inline (old behavior)
305
+ # For external packages, we skip loading the template
306
+ if not self.typst_package:
307
+ template = self.load_template()
308
+ output_parts.append(template)
309
+ output_parts.append("") # Blank line
310
+
311
+ # Generate #show statement with template function call
312
+ template_func = self.typst_template_function or "project"
313
+ output_parts.append(f"#show: {template_func}.with(")
314
+
315
+ # Format parameters
316
+ for key, value in params.items():
317
+ formatted_value = self._format_typst_value(value)
318
+ output_parts.append(f" {key}: {formatted_value},")
319
+
320
+ output_parts.append(")")
321
+ output_parts.append("") # Blank line
322
+
323
+ # Add body content
324
+ output_parts.append(body)
325
+
326
+ return "\n".join(output_parts)
327
+
328
+ def _format_typst_value(self, value: Any) -> str:
329
+ """
330
+ Format Python value as Typst value.
331
+
332
+ Args:
333
+ value: Python value (str, int, bool, tuple, etc.)
334
+
335
+ Returns:
336
+ Typst-formatted value string
337
+ """
338
+ if value is None:
339
+ return "none"
340
+ elif isinstance(value, bool):
341
+ # Typst uses lowercase true/false
342
+ return "true" if value else "false"
343
+ elif isinstance(value, str):
344
+ # Escape quotes and backslashes
345
+ escaped = value.replace("\\", "\\\\").replace('"', '\\"')
346
+ return f'"{escaped}"'
347
+ elif isinstance(value, (int, float)):
348
+ return str(value)
349
+ elif isinstance(value, (list, tuple)):
350
+ # Format as Typst array/tuple
351
+ formatted_items = [self._format_typst_value(item) for item in value]
352
+ return f'({", ".join(formatted_items)},)' if formatted_items else "()"
353
+ elif isinstance(value, dict):
354
+ # Format as Typst dictionary (not commonly used in templates)
355
+ items = [f"{k}: {self._format_typst_value(v)}" for k, v in value.items()]
356
+ return f'({", ".join(items)})'
357
+ else:
358
+ # Default: convert to string and quote
359
+ return f'"{str(value)}"'
360
+
361
+ def _convert_to_authors_tuple(self, author_value: Any) -> tuple:
362
+ """
363
+ Convert author metadata to tuple of authors.
364
+
365
+ Args:
366
+ author_value: Author metadata (string or list)
367
+
368
+ Returns:
369
+ Tuple of author names
370
+
371
+ Requirement 8.8: Convert to arrays and complex structures
372
+ """
373
+ if isinstance(author_value, (list, tuple)):
374
+ return tuple(author_value)
375
+ elif isinstance(author_value, str):
376
+ # Split comma-separated authors
377
+ authors = [a.strip() for a in author_value.split(",")]
378
+ return tuple(authors)
379
+ else:
380
+ return (str(author_value),)
381
+
382
+ def _try_load_file(self, file_path: str) -> Optional[str]:
383
+ """
384
+ Try to load content from file.
385
+
386
+ Args:
387
+ file_path: Path to file
388
+
389
+ Returns:
390
+ File content as string, or None if file doesn't exist or can't be read
391
+ """
392
+ try:
393
+ with open(file_path, encoding="utf-8") as f:
394
+ return f.read()
395
+ except (FileNotFoundError, OSError):
396
+ return None
@@ -0,0 +1,81 @@
1
+ // Default Typst template for sphinx-typst
2
+ // Requirement 8.1: Default template bundled with package
3
+ // Requirement 8.11: Include #outline() in template (not in body)
4
+ // Requirement 7.4: codly package integration for code highlighting
5
+
6
+ // Import codly for code highlighting (Task 4.2.1)
7
+ // Design 3.5: codly is mandatory for all code blocks
8
+ #import "@preview/codly:1.3.0": *
9
+ #import "@preview/codly-languages:0.1.1": *
10
+
11
+ // Import mitex for LaTeX math support (Task 6.1)
12
+ // Design 3.3: mitex for LaTeX math compatibility
13
+ // Requirement 4.1: mitex package integration
14
+ #import "@preview/mitex:0.2.4": *
15
+
16
+ // Import gentle-clues for admonitions (Task 3.4)
17
+ // Design 3.6: gentle-clues for admonition display
18
+ // Requirement 2.8-2.10: Admonition conversion to gentle-clues
19
+ #import "@preview/gentle-clues:1.2.0": *
20
+
21
+ // Initialize codly
22
+ #show: codly-init.with()
23
+
24
+ // Configure codly with codly-languages for comprehensive language support
25
+ #codly(languages: codly-languages)
26
+
27
+ #let project(
28
+ title: "",
29
+ authors: (),
30
+ date: none,
31
+ toctree_maxdepth: 2,
32
+ toctree_numbered: false,
33
+ toctree_caption: "Contents",
34
+ papersize: "a4",
35
+ fontsize: 11pt,
36
+ body
37
+ ) = {
38
+ // Document metadata
39
+ set document(title: title, author: authors)
40
+
41
+ // Page setup
42
+ set page(
43
+ paper: papersize,
44
+ numbering: "1",
45
+ number-align: center
46
+ )
47
+
48
+ // Text setup
49
+ set text(size: fontsize, lang: "en")
50
+
51
+ // Heading setup
52
+ set heading(numbering: "1.1")
53
+
54
+ // Title page
55
+ align(center)[
56
+ #text(2em, weight: "bold")[#title]
57
+ #v(1em)
58
+ #text(1.2em)[#authors.join(", ")]
59
+ #v(0.5em)
60
+ #date
61
+ ]
62
+
63
+ pagebreak()
64
+
65
+ // Table of Contents
66
+ // Requirement 13.8: #outline() managed at template level, not in body
67
+ // Requirement 8.12, 8.13: toctree options mapped to #outline() parameters
68
+ if toctree_caption != "" [
69
+ #heading(outlined: false)[#toctree_caption]
70
+ ]
71
+ outline(
72
+ depth: toctree_maxdepth,
73
+ indent: auto
74
+ )
75
+
76
+ pagebreak()
77
+
78
+ // Document body
79
+ // Requirement 13: body contains #include() directives from toctree
80
+ body
81
+ }