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 +58 -0
- typsphinx/builder.py +322 -0
- typsphinx/pdf.py +192 -0
- typsphinx/template_engine.py +396 -0
- typsphinx/templates/base.typ +81 -0
- typsphinx/translator.py +1583 -0
- typsphinx/writer.py +144 -0
- typsphinx-0.3.0.dist-info/METADATA +355 -0
- typsphinx-0.3.0.dist-info/RECORD +13 -0
- typsphinx-0.3.0.dist-info/WHEEL +5 -0
- typsphinx-0.3.0.dist-info/entry_points.txt +3 -0
- typsphinx-0.3.0.dist-info/licenses/LICENSE +21 -0
- typsphinx-0.3.0.dist-info/top_level.txt +1 -0
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}"
|