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.
typsphinx/__init__.py ADDED
@@ -0,0 +1,58 @@
1
+ """
2
+ Sphinx Typst Extension
3
+ =======================
4
+
5
+ A Sphinx extension that provides Typst output format support.
6
+
7
+ This extension allows you to generate Typst documents from reStructuredText
8
+ sources using Sphinx, which can then be compiled to PDF using the Typst compiler.
9
+
10
+ :copyright: Copyright 2024 by Sphinx Typst Contributors
11
+ :license: MIT, see LICENSE for details.
12
+ """
13
+
14
+ __version__ = "0.3.0"
15
+ __author__ = "Sphinx Typst Contributors"
16
+
17
+ from typing import Any, Dict
18
+
19
+ from sphinx.application import Sphinx
20
+
21
+ from typsphinx.builder import TypstBuilder, TypstPDFBuilder
22
+
23
+
24
+ def setup(app: Sphinx) -> Dict[str, Any]:
25
+ """
26
+ Sphinx extension setup function.
27
+
28
+ This function will be called by Sphinx to register the extension.
29
+
30
+ Args:
31
+ app: The Sphinx application instance
32
+
33
+ Returns:
34
+ Extension metadata dictionary
35
+ """
36
+ app.add_builder(TypstBuilder)
37
+ app.add_builder(TypstPDFBuilder)
38
+
39
+ # Register configuration values
40
+ app.add_config_value("typst_documents", [], "html", [list])
41
+ app.add_config_value("typst_template", None, "html", [str, type(None)])
42
+ app.add_config_value("typst_template_mapping", None, "html", [dict, type(None)])
43
+ app.add_config_value("typst_toctree_defaults", None, "html", [dict, type(None)])
44
+ app.add_config_value("typst_use_mitex", True, "html", [bool])
45
+ app.add_config_value("typst_elements", {}, "html", [dict])
46
+ # Task 13.4: Other configuration options (Requirement 8.6)
47
+ app.add_config_value("typst_package", None, "html", [str, type(None)])
48
+ app.add_config_value("typst_package_imports", None, "html", [list, type(None)])
49
+ app.add_config_value("typst_template_function", None, "html", [str, type(None)])
50
+ # Task 13.4: Output directory and debug mode
51
+ app.add_config_value("typst_output_dir", "_build/typst", "html", [str])
52
+ app.add_config_value("typst_debug", False, "html", [bool])
53
+
54
+ return {
55
+ "version": __version__,
56
+ "parallel_read_safe": True,
57
+ "parallel_write_safe": True,
58
+ }
typsphinx/builder.py ADDED
@@ -0,0 +1,322 @@
1
+ """
2
+ Typst builder for Sphinx.
3
+
4
+ This module implements the TypstBuilder class, which is responsible for
5
+ building Typst output from Sphinx documentation.
6
+ """
7
+
8
+ from collections.abc import Iterator
9
+ from os import path
10
+ from typing import Optional, Set
11
+
12
+ from docutils import nodes
13
+ from sphinx.builders import Builder
14
+ from sphinx.util import logging
15
+ from sphinx.util.osutil import ensuredir
16
+
17
+ from typsphinx.pdf import compile_typst_to_pdf
18
+ from typsphinx.writer import TypstWriter
19
+
20
+ logger = logging.getLogger(__name__)
21
+
22
+
23
+ class TypstBuilder(Builder):
24
+ """
25
+ Builder class for Typst output format.
26
+
27
+ This builder converts Sphinx documentation to Typst markup files (.typ),
28
+ which can then be compiled to PDF using the Typst compiler.
29
+ """
30
+
31
+ name = "typst"
32
+ format = "typst"
33
+ out_suffix = ".typ"
34
+ allow_parallel = True
35
+
36
+ def init(self) -> None:
37
+ """
38
+ Initialize the builder.
39
+
40
+ This method is called once at the beginning of the build process.
41
+ """
42
+ pass
43
+
44
+ def get_outdated_docs(self) -> Iterator[str]:
45
+ """
46
+ Return an iterator of document names that need to be rebuilt.
47
+
48
+ For now, we rebuild all documents on every build.
49
+
50
+ Returns:
51
+ Iterator of document names that are outdated
52
+ """
53
+ for docname in self.env.found_docs:
54
+ yield docname
55
+
56
+ def get_target_uri(self, docname: str, typ: Optional[str] = None) -> str:
57
+ """
58
+ Return the target URI for a document.
59
+
60
+ Args:
61
+ docname: Name of the document
62
+ typ: Type of the target (not used for Typst builder)
63
+
64
+ Returns:
65
+ Target URI string
66
+ """
67
+ return docname + self.out_suffix
68
+
69
+ def prepare_writing(self, docnames: Set[str]) -> None:
70
+ """
71
+ Prepare for writing the documents.
72
+
73
+ This method is called before writing begins.
74
+ Writes the template file to the output directory for master documents to import.
75
+
76
+ Args:
77
+ docnames: Set of document names to be written
78
+ """
79
+ # Create the writer instance
80
+ self.writer = TypstWriter(self)
81
+
82
+ # Write template file for master documents to import
83
+ self._write_template_file()
84
+
85
+ def write(
86
+ self,
87
+ build_docnames: Optional[Set[str]],
88
+ updated_docnames: Set[str],
89
+ method: str = "update",
90
+ ) -> None:
91
+ """
92
+ Override write() to preserve toctree nodes.
93
+
94
+ By default, Sphinx's Builder.write() calls env.get_and_resolve_doctree()
95
+ which expands toctree nodes into compact_paragraph with links.
96
+ For Typst, we need the original toctree nodes to generate #include() directives.
97
+
98
+ This method uses env.get_doctree() instead to preserve toctree nodes.
99
+
100
+ Args:
101
+ build_docnames: Document names to build (None = all)
102
+ updated_docnames: Document names that were updated
103
+ method: Build method ('update' or 'all')
104
+ """
105
+ if build_docnames is None or build_docnames == ["__all__"]:
106
+ # build_all
107
+ build_docnames = self.env.found_docs
108
+ if method == "update":
109
+ # build updated and specified
110
+ docnames = set(build_docnames) | set(updated_docnames)
111
+ else:
112
+ # build all
113
+ docnames = set(build_docnames)
114
+
115
+ logger.info("preparing documents... ", nonl=True)
116
+ self.prepare_writing(docnames)
117
+ logger.info("done")
118
+
119
+ # Write individual documents
120
+ warnings_count = 0
121
+ for docname in sorted(docnames):
122
+ # Use env.get_doctree() instead of env.get_and_resolve_doctree()
123
+ # to preserve toctree nodes (Requirement 13.2)
124
+ doctree = self.env.get_doctree(docname)
125
+ self.env.apply_post_transforms(doctree, docname)
126
+
127
+ # Log progress
128
+ logger.info(f"writing output... [{docname}]", nonl=True)
129
+
130
+ # Write the document
131
+ self.write_doc(docname, doctree)
132
+
133
+ logger.info(" done")
134
+
135
+ def write_doc(self, docname: str, doctree: nodes.document) -> None:
136
+ """
137
+ Write a document.
138
+
139
+ This method is called for each document that needs to be written.
140
+
141
+ Requirement 13.1: 各 reStructuredText ファイルに対応する独立した
142
+ .typ ファイルを生成する
143
+
144
+ Requirement 13.12: ソースディレクトリ構造を保持して出力する
145
+
146
+ Args:
147
+ docname: Name of the document
148
+ doctree: Document tree to be written
149
+ """
150
+ # Get the output file path
151
+ destination = path.join(self.outdir, docname + self.out_suffix)
152
+
153
+ # Ensure the directory for this specific file exists
154
+ # This handles nested paths like "chapter1/section"
155
+ dest_dir = path.dirname(destination)
156
+ ensuredir(dest_dir)
157
+
158
+ # Set current docname for template application logic
159
+ self.current_docname = docname
160
+
161
+ # Set the document on the writer
162
+ self.writer.document = doctree
163
+
164
+ # Translate the document to Typst markup
165
+ self.writer.translate()
166
+
167
+ # Save the output to the file
168
+ with open(destination, "w", encoding="utf-8") as f:
169
+ f.write(self.writer.output)
170
+
171
+ def _write_template_file(self) -> None:
172
+ """
173
+ Write the template file to the output directory.
174
+
175
+ This writes a separate template.typ file that master documents can import.
176
+ Only writes if a template is configured (not using Typst Universe packages).
177
+ """
178
+ from typsphinx.template_engine import TemplateEngine
179
+
180
+ config = self.config
181
+
182
+ # Get template configuration
183
+ template_path = getattr(config, "typst_template", None)
184
+ if template_path:
185
+ # Resolve relative path from source directory
186
+ import os
187
+
188
+ template_path = os.path.join(self.srcdir, template_path)
189
+
190
+ # Skip if using Typst Universe package (no separate template file needed)
191
+ typst_package = getattr(config, "typst_package", None)
192
+ if typst_package:
193
+ return
194
+
195
+ # Create template engine
196
+ template_engine = TemplateEngine(
197
+ template_path=template_path,
198
+ search_paths=[self.srcdir],
199
+ parameter_mapping=getattr(config, "typst_template_mapping", None),
200
+ typst_package=typst_package,
201
+ typst_template_function=getattr(config, "typst_template_function", None),
202
+ typst_package_imports=getattr(config, "typst_package_imports", None),
203
+ )
204
+
205
+ # Get template content
206
+ template_content = template_engine.get_template_content()
207
+
208
+ # Write template file
209
+ template_file_path = path.join(self.outdir, "_template.typ")
210
+ with open(template_file_path, "w", encoding="utf-8") as f:
211
+ f.write(template_content)
212
+
213
+ logger.info(f"Template written to {template_file_path}")
214
+
215
+ def finish(self) -> None:
216
+ """
217
+ Finish the build process.
218
+
219
+ This method is called once after all documents have been written.
220
+ """
221
+ pass
222
+
223
+
224
+ class TypstPDFBuilder(TypstBuilder):
225
+ """
226
+ Builder class for generating PDF output directly from Typst.
227
+
228
+ This builder extends TypstBuilder to compile generated .typ files
229
+ to PDF using the typst-py package.
230
+
231
+ Requirement 9.3: TypstPDFBuilder extends TypstBuilder
232
+ Requirement 9.4: Generate PDF from Typst markup
233
+ """
234
+
235
+ name = "typstpdf"
236
+ format = "pdf"
237
+ out_suffix = ".pdf"
238
+
239
+ def write_doc(self, docname: str, doctree: nodes.document) -> None:
240
+ """
241
+ Write a document as both .typ and .pdf.
242
+
243
+ Override to generate .typ file (not .pdf) during the write phase.
244
+ The .pdf will be generated in finish() by compiling the .typ file.
245
+
246
+ Args:
247
+ docname: Name of the document
248
+ doctree: Document tree to be written
249
+ """
250
+ # Generate .typ file (not .pdf)
251
+ typ_destination = path.join(self.outdir, docname + ".typ")
252
+
253
+ # Ensure the directory exists
254
+ dest_dir = path.dirname(typ_destination)
255
+ ensuredir(dest_dir)
256
+
257
+ # Set current docname for template application logic
258
+ self.current_docname = docname
259
+
260
+ # Set the document on the writer
261
+ self.writer.document = doctree
262
+
263
+ # Translate the document to Typst markup
264
+ self.writer.translate()
265
+
266
+ # Save the .typ file
267
+ with open(typ_destination, "w", encoding="utf-8") as f:
268
+ f.write(self.writer.output)
269
+
270
+ def finish(self) -> None:
271
+ """
272
+ Finish the build process by compiling Typst files to PDF.
273
+
274
+ After the parent TypstBuilder has generated .typ files,
275
+ this method compiles them to PDF using typst-py.
276
+
277
+ Only master documents (defined in typst_documents) are compiled to PDF.
278
+ Included documents are not compiled individually.
279
+
280
+ Requirement 9.2: Execute Typst compilation within Python
281
+ Requirement 9.4: Generate PDF from Typst markup
282
+ """
283
+ # First, call parent finish() to complete .typ generation
284
+ super().finish()
285
+
286
+ # Get master documents from typst_documents config
287
+ typst_documents = getattr(self.config, "typst_documents", [])
288
+
289
+ if not typst_documents:
290
+ logger.warning(
291
+ "No documents defined in typst_documents. Nothing to compile."
292
+ )
293
+ return
294
+
295
+ logger.info(f"Compiling {len(typst_documents)} master document(s) to PDF...")
296
+
297
+ for doc_tuple in typst_documents:
298
+ # doc_tuple format: (sourcename, targetname, title, author)
299
+ docname = doc_tuple[0]
300
+ typ_file = path.join(self.outdir, docname + ".typ")
301
+
302
+ if not path.exists(typ_file):
303
+ logger.warning(f"Master document not found: {typ_file}")
304
+ continue
305
+
306
+ try:
307
+ # Read Typst content
308
+ with open(typ_file, encoding="utf-8") as f:
309
+ typst_content = f.read()
310
+
311
+ # Compile to PDF
312
+ pdf_bytes = compile_typst_to_pdf(typst_content, root_dir=self.outdir)
313
+
314
+ # Write PDF file
315
+ pdf_file = path.join(self.outdir, docname + ".pdf")
316
+ with open(pdf_file, "wb") as f:
317
+ f.write(pdf_bytes)
318
+
319
+ logger.info(f"Generated PDF: {pdf_file}")
320
+
321
+ except Exception as e:
322
+ logger.error(f"Failed to compile {typ_file}: {e}")
typsphinx/pdf.py ADDED
@@ -0,0 +1,192 @@
1
+ """
2
+ PDF generation utilities using typst-py.
3
+
4
+ This module provides functionality for generating PDFs from Typst markup
5
+ using the typst Python package (Requirement 9).
6
+ """
7
+
8
+ import logging
9
+ import os
10
+ import tempfile
11
+ from typing import Optional
12
+
13
+ logger = logging.getLogger(__name__)
14
+
15
+
16
+ class TypstCompilationError(Exception):
17
+ """
18
+ Exception raised when Typst compilation fails.
19
+
20
+ This exception provides detailed information about compilation errors,
21
+ including the original error from typst-py and contextual information.
22
+
23
+ Attributes:
24
+ message: Human-readable error message
25
+ typst_error: Original error from typst compiler
26
+ source_location: Location information if available
27
+
28
+ Requirement 10.3: Error detection and handling
29
+ Requirement 10.4: Error message parsing and user display
30
+ """
31
+
32
+ def __init__(
33
+ self,
34
+ message: str,
35
+ typst_error: Optional[Exception] = None,
36
+ source_location: Optional[str] = None,
37
+ ):
38
+ """
39
+ Initialize TypstCompilationError.
40
+
41
+ Args:
42
+ message: Human-readable error description
43
+ typst_error: Original exception from typst compiler
44
+ source_location: Source file location information
45
+ """
46
+ self.message = message
47
+ self.typst_error = typst_error
48
+ self.source_location = source_location
49
+
50
+ # Build full error message
51
+ full_message = f"Typst compilation failed: {message}"
52
+ if source_location:
53
+ full_message += f"\nLocation: {source_location}"
54
+ if typst_error:
55
+ full_message += f"\nDetails: {str(typst_error)}"
56
+
57
+ super().__init__(full_message)
58
+
59
+
60
+ def check_typst_available() -> None:
61
+ """
62
+ Check if typst package is available.
63
+
64
+ Raises:
65
+ ImportError: If typst package is not installed
66
+
67
+ Requirement 9.1: Typst compiler functionality as dependency
68
+ Requirement 9.7: Automatic availability of Typst compiler
69
+ """
70
+ try:
71
+ import typst # noqa: F401
72
+ except ImportError as e:
73
+ raise ImportError(
74
+ "typst package not found. Please install it:\n"
75
+ " pip install typst\n"
76
+ "Or install typsphinx with PDF support:\n"
77
+ " pip install typsphinx[pdf]"
78
+ ) from e
79
+
80
+
81
+ def get_typst_version() -> str:
82
+ """
83
+ Get the version of the typst package.
84
+
85
+ Returns:
86
+ Version string (e.g., "0.13.7")
87
+
88
+ Requirement 9.7: Version information for Typst compiler
89
+ """
90
+ try:
91
+ import typst
92
+
93
+ # Try to get version from __version__ attribute
94
+ if hasattr(typst, "__version__"):
95
+ return typst.__version__
96
+
97
+ # Try to get from package metadata
98
+ try:
99
+ from importlib.metadata import version
100
+
101
+ return version("typst")
102
+ except Exception:
103
+ pass
104
+
105
+ # Fallback
106
+ return "unknown"
107
+ except ImportError:
108
+ return "not installed"
109
+
110
+
111
+ def compile_typst_to_pdf(typst_content: str, root_dir: Optional[str] = None) -> bytes:
112
+ """
113
+ Compile Typst content to PDF bytes.
114
+
115
+ Args:
116
+ typst_content: Typst markup content
117
+ root_dir: Root directory for resolving includes and images
118
+
119
+ Returns:
120
+ PDF content as bytes
121
+
122
+ Raises:
123
+ ImportError: If typst package not available
124
+ TypstCompilationError: If compilation fails
125
+
126
+ Requirement 9.2: Execute Typst compilation within Python environment
127
+ Requirement 9.4: Generate PDF from Typst markup
128
+ Requirement 10.3: Error detection and handling
129
+ """
130
+ check_typst_available()
131
+
132
+ import typst
133
+
134
+ # Create a temporary file for the Typst content
135
+ # typst.compile() requires a file path, not string content
136
+ temp_file = None
137
+ try:
138
+ # Create temporary file in root_dir if specified, otherwise use system temp
139
+ temp_dir = root_dir if root_dir else None
140
+
141
+ with tempfile.NamedTemporaryFile(
142
+ mode="w", suffix=".typ", dir=temp_dir, delete=False, encoding="utf-8"
143
+ ) as f:
144
+ f.write(typst_content)
145
+ temp_file = f.name
146
+
147
+ # Compile Typst file to PDF
148
+ # The typst.compile() function takes a file path and returns PDF bytes
149
+ try:
150
+ pdf_bytes = typst.compile(temp_file, root=root_dir)
151
+ return pdf_bytes
152
+ except Exception as typst_error:
153
+ # Parse and wrap the error with more context
154
+ error_msg = _parse_typst_error(typst_error)
155
+ source_loc = temp_file if temp_file else "unknown"
156
+
157
+ logger.error(f"Typst compilation failed at {source_loc}: {error_msg}")
158
+
159
+ raise TypstCompilationError(
160
+ message=error_msg, typst_error=typst_error, source_location=source_loc
161
+ ) from typst_error
162
+
163
+ finally:
164
+ # Clean up temporary file
165
+ if temp_file and os.path.exists(temp_file):
166
+ try:
167
+ os.unlink(temp_file)
168
+ except Exception:
169
+ pass # Ignore cleanup errors
170
+
171
+
172
+ def _parse_typst_error(error: Exception) -> str:
173
+ """
174
+ Parse Typst compiler error to extract useful information.
175
+
176
+ Args:
177
+ error: Original exception from typst compiler
178
+
179
+ Returns:
180
+ Human-readable error message
181
+
182
+ Requirement 10.4: Error message parsing
183
+ """
184
+ error_str = str(error)
185
+
186
+ # Extract meaningful information from error
187
+ # Typst errors often contain detailed information
188
+ if not error_str:
189
+ return f"{type(error).__name__}"
190
+
191
+ # Return the error string with type information
192
+ return f"{type(error).__name__}: {error_str}"