typsphinx 0.4.2__tar.gz → 0.4.3__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 (47) hide show
  1. {typsphinx-0.4.2 → typsphinx-0.4.3}/PKG-INFO +5 -5
  2. {typsphinx-0.4.2 → typsphinx-0.4.3}/README.md +1 -1
  3. {typsphinx-0.4.2 → typsphinx-0.4.3}/pyproject.toml +4 -4
  4. {typsphinx-0.4.2 → typsphinx-0.4.3}/tests/test_inline_references.py +151 -0
  5. typsphinx-0.4.3/tests/test_template_assets.py +403 -0
  6. {typsphinx-0.4.2 → typsphinx-0.4.3}/typsphinx/__init__.py +4 -2
  7. {typsphinx-0.4.2 → typsphinx-0.4.3}/typsphinx/builder.py +176 -1
  8. {typsphinx-0.4.2 → typsphinx-0.4.3}/typsphinx/translator.py +23 -0
  9. {typsphinx-0.4.2 → typsphinx-0.4.3}/typsphinx.egg-info/PKG-INFO +5 -5
  10. {typsphinx-0.4.2 → typsphinx-0.4.3}/typsphinx.egg-info/SOURCES.txt +1 -0
  11. {typsphinx-0.4.2 → typsphinx-0.4.3}/typsphinx.egg-info/requires.txt +1 -1
  12. {typsphinx-0.4.2 → typsphinx-0.4.3}/LICENSE +0 -0
  13. {typsphinx-0.4.2 → typsphinx-0.4.3}/setup.cfg +0 -0
  14. {typsphinx-0.4.2 → typsphinx-0.4.3}/tests/test_admonitions.py +0 -0
  15. {typsphinx-0.4.2 → typsphinx-0.4.3}/tests/test_builder.py +0 -0
  16. {typsphinx-0.4.2 → typsphinx-0.4.3}/tests/test_builder_requirement13.py +0 -0
  17. {typsphinx-0.4.2 → typsphinx-0.4.3}/tests/test_config.py +0 -0
  18. {typsphinx-0.4.2 → typsphinx-0.4.3}/tests/test_config_other_options.py +0 -0
  19. {typsphinx-0.4.2 → typsphinx-0.4.3}/tests/test_config_template_mapping.py +0 -0
  20. {typsphinx-0.4.2 → typsphinx-0.4.3}/tests/test_config_toctree_defaults.py +0 -0
  21. {typsphinx-0.4.2 → typsphinx-0.4.3}/tests/test_documentation_configuration.py +0 -0
  22. {typsphinx-0.4.2 → typsphinx-0.4.3}/tests/test_documentation_installation.py +0 -0
  23. {typsphinx-0.4.2 → typsphinx-0.4.3}/tests/test_documentation_usage.py +0 -0
  24. {typsphinx-0.4.2 → typsphinx-0.4.3}/tests/test_entry_points.py +0 -0
  25. {typsphinx-0.4.2 → typsphinx-0.4.3}/tests/test_examples_basic.py +0 -0
  26. {typsphinx-0.4.2 → typsphinx-0.4.3}/tests/test_extension.py +0 -0
  27. {typsphinx-0.4.2 → typsphinx-0.4.3}/tests/test_integration_advanced.py +0 -0
  28. {typsphinx-0.4.2 → typsphinx-0.4.3}/tests/test_integration_basic.py +0 -0
  29. {typsphinx-0.4.2 → typsphinx-0.4.3}/tests/test_integration_multi_doc.py +0 -0
  30. {typsphinx-0.4.2 → typsphinx-0.4.3}/tests/test_integration_nested_toctree.py +0 -0
  31. {typsphinx-0.4.2 → typsphinx-0.4.3}/tests/test_math_fallback.py +0 -0
  32. {typsphinx-0.4.2 → typsphinx-0.4.3}/tests/test_math_mitex.py +0 -0
  33. {typsphinx-0.4.2 → typsphinx-0.4.3}/tests/test_math_native.py +0 -0
  34. {typsphinx-0.4.2 → typsphinx-0.4.3}/tests/test_nested_toctree_paths.py +0 -0
  35. {typsphinx-0.4.2 → typsphinx-0.4.3}/tests/test_pdf_generation.py +0 -0
  36. {typsphinx-0.4.2 → typsphinx-0.4.3}/tests/test_template_codly.py +0 -0
  37. {typsphinx-0.4.2 → typsphinx-0.4.3}/tests/test_template_engine.py +0 -0
  38. {typsphinx-0.4.2 → typsphinx-0.4.3}/tests/test_template_mitex.py +0 -0
  39. {typsphinx-0.4.2 → typsphinx-0.4.3}/tests/test_toctree_requirement13.py +0 -0
  40. {typsphinx-0.4.2 → typsphinx-0.4.3}/tests/test_translator.py +0 -0
  41. {typsphinx-0.4.2 → typsphinx-0.4.3}/typsphinx/pdf.py +0 -0
  42. {typsphinx-0.4.2 → typsphinx-0.4.3}/typsphinx/template_engine.py +0 -0
  43. {typsphinx-0.4.2 → typsphinx-0.4.3}/typsphinx/templates/base.typ +0 -0
  44. {typsphinx-0.4.2 → typsphinx-0.4.3}/typsphinx/writer.py +0 -0
  45. {typsphinx-0.4.2 → typsphinx-0.4.3}/typsphinx.egg-info/dependency_links.txt +0 -0
  46. {typsphinx-0.4.2 → typsphinx-0.4.3}/typsphinx.egg-info/entry_points.txt +0 -0
  47. {typsphinx-0.4.2 → typsphinx-0.4.3}/typsphinx.egg-info/top_level.txt +0 -0
@@ -1,15 +1,15 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: typsphinx
3
- Version: 0.4.2
3
+ Version: 0.4.3
4
4
  Summary: Sphinx extension for Typst output
5
- Author-email: Sphinx Typst Contributors <noreply@example.com>
5
+ Author-email: YuSabo <yusabo90002@gmail.com>
6
6
  License-Expression: MIT
7
7
  Project-URL: Homepage, https://github.com/YuSabo90002/typsphinx
8
8
  Project-URL: Documentation, https://github.com/YuSabo90002/typsphinx#readme
9
9
  Project-URL: Repository, https://github.com/YuSabo90002/typsphinx
10
10
  Project-URL: Issues, https://github.com/YuSabo90002/typsphinx/issues
11
11
  Keywords: sphinx,typst,documentation,pdf
12
- Classifier: Development Status :: 4 - Beta
12
+ Classifier: Development Status :: 5 - Production/Stable
13
13
  Classifier: Framework :: Sphinx :: Extension
14
14
  Classifier: Intended Audience :: Developers
15
15
  Classifier: Programming Language :: Python :: 3
@@ -24,7 +24,7 @@ Description-Content-Type: text/markdown
24
24
  License-File: LICENSE
25
25
  Requires-Dist: sphinx>=5.0
26
26
  Requires-Dist: docutils>=0.18
27
- Requires-Dist: typst>=0.11.1
27
+ Requires-Dist: typst>=0.14.1
28
28
  Provides-Extra: dev
29
29
  Requires-Dist: pytest>=7.0; extra == "dev"
30
30
  Requires-Dist: pytest-cov>=4.0; extra == "dev"
@@ -84,7 +84,7 @@ typsphinx is a Sphinx extension that enables generating Typst documents from reS
84
84
 
85
85
  ## Installation
86
86
 
87
- ### From PyPI (Beta Release)
87
+ ### From PyPI
88
88
 
89
89
  ```bash
90
90
  pip install typsphinx
@@ -39,7 +39,7 @@ typsphinx is a Sphinx extension that enables generating Typst documents from reS
39
39
 
40
40
  ## Installation
41
41
 
42
- ### From PyPI (Beta Release)
42
+ ### From PyPI
43
43
 
44
44
  ```bash
45
45
  pip install typsphinx
@@ -4,17 +4,17 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "typsphinx"
7
- version = "0.4.2"
7
+ version = "0.4.3"
8
8
  description = "Sphinx extension for Typst output"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.9"
11
11
  license = "MIT"
12
12
  authors = [
13
- {name = "Sphinx Typst Contributors", email = "noreply@example.com"}
13
+ {name = "YuSabo", email = "yusabo90002@gmail.com"}
14
14
  ]
15
15
  keywords = ["sphinx", "typst", "documentation", "pdf"]
16
16
  classifiers = [
17
- "Development Status :: 4 - Beta",
17
+ "Development Status :: 5 - Production/Stable",
18
18
  "Framework :: Sphinx :: Extension",
19
19
  "Intended Audience :: Developers",
20
20
  "Programming Language :: Python :: 3",
@@ -29,7 +29,7 @@ classifiers = [
29
29
  dependencies = [
30
30
  "sphinx>=5.0",
31
31
  "docutils>=0.18",
32
- "typst>=0.11.1",
32
+ "typst>=0.14.1",
33
33
  ]
34
34
 
35
35
  [project.optional-dependencies]
@@ -186,3 +186,154 @@ class TestInlineReferenceConversion:
186
186
  output = translator.astext()
187
187
  # Literal should use raw() function in unified code mode
188
188
  assert 'raw("code_reference")' in output
189
+
190
+
191
+ class TestEmptyURLHandling:
192
+ """Test empty URL handling in references (Typst 0.14+ compatibility)."""
193
+
194
+ def test_empty_refuri_skips_link_wrapper(self, temp_sphinx_app: SphinxTestApp):
195
+ """Test that empty refuri skips link() generation."""
196
+ ref = nodes.reference()
197
+ ref["refuri"] = "" # Empty URL
198
+ ref += nodes.Text("broken reference")
199
+
200
+ doc = create_document()
201
+ para = nodes.paragraph()
202
+ para += ref
203
+ doc += para
204
+
205
+ writer = TypstWriter(temp_sphinx_app.builder)
206
+ writer.document = doc
207
+ translator = TypstTranslator(doc, temp_sphinx_app.builder)
208
+ doc.walkabout(translator)
209
+
210
+ output = translator.astext()
211
+ # Should NOT generate link()
212
+ assert 'link("")' not in output
213
+ assert 'link("", ' not in output
214
+ # Should render content as plain text
215
+ assert "broken reference" in output
216
+
217
+ def test_empty_refuri_renders_content_as_text(self, temp_sphinx_app: SphinxTestApp):
218
+ """Test that content is rendered as plain text when refuri is empty."""
219
+ ref = nodes.reference()
220
+ ref["refuri"] = ""
221
+ ref += nodes.Text("nonexistent-section")
222
+
223
+ doc = create_document()
224
+ para = nodes.paragraph()
225
+ para += ref
226
+ doc += para
227
+
228
+ writer = TypstWriter(temp_sphinx_app.builder)
229
+ writer.document = doc
230
+ translator = TypstTranslator(doc, temp_sphinx_app.builder)
231
+ doc.walkabout(translator)
232
+
233
+ output = translator.astext()
234
+ # Content should be present as text
235
+ assert "nonexistent-section" in output
236
+ # No link wrapper
237
+ assert "link(" not in output
238
+
239
+ def test_empty_refuri_emits_warning(self, temp_sphinx_app: SphinxTestApp):
240
+ """Test that warning is emitted for empty refuri."""
241
+ ref = nodes.reference()
242
+ ref["refuri"] = ""
243
+ ref += nodes.Text("broken-link")
244
+
245
+ doc = create_document()
246
+ para = nodes.paragraph()
247
+ para += ref
248
+ doc += para
249
+
250
+ writer = TypstWriter(temp_sphinx_app.builder)
251
+ writer.document = doc
252
+ translator = TypstTranslator(doc, temp_sphinx_app.builder)
253
+
254
+ # Capture warnings
255
+ import io
256
+ from contextlib import redirect_stderr
257
+
258
+ stderr_capture = io.StringIO()
259
+ with redirect_stderr(stderr_capture):
260
+ doc.walkabout(translator)
261
+
262
+ # Sphinx logger output goes to stderr in test context
263
+ # We just verify the code runs without error and produces expected output
264
+ output = translator.astext()
265
+ assert "broken-link" in output
266
+ assert 'link("")' not in output
267
+
268
+ def test_valid_refuri_unchanged(self, temp_sphinx_app: SphinxTestApp):
269
+ """Test that valid refuri generates link() as before (regression test)."""
270
+ ref = nodes.reference()
271
+ ref["refuri"] = "https://python.org"
272
+ ref += nodes.Text("Python")
273
+
274
+ doc = create_document()
275
+ para = nodes.paragraph()
276
+ para += ref
277
+ doc += para
278
+
279
+ writer = TypstWriter(temp_sphinx_app.builder)
280
+ writer.document = doc
281
+ translator = TypstTranslator(doc, temp_sphinx_app.builder)
282
+ doc.walkabout(translator)
283
+
284
+ output = translator.astext()
285
+ # Should generate link()
286
+ assert 'link("https://python.org"' in output
287
+ assert "Python" in output
288
+
289
+ def test_internal_reference_with_hash(self, temp_sphinx_app: SphinxTestApp):
290
+ """Test that internal references (starting with #) work correctly."""
291
+ ref = nodes.reference()
292
+ ref["refuri"] = "#section-label"
293
+ ref += nodes.Text("See section")
294
+
295
+ doc = create_document()
296
+ para = nodes.paragraph()
297
+ para += ref
298
+ doc += para
299
+
300
+ writer = TypstWriter(temp_sphinx_app.builder)
301
+ writer.document = doc
302
+ translator = TypstTranslator(doc, temp_sphinx_app.builder)
303
+ doc.walkabout(translator)
304
+
305
+ output = translator.astext()
306
+ # Should generate link(<label>, ...)
307
+ assert "link(<section-label>" in output
308
+ assert "See section" in output
309
+
310
+ def test_multiple_empty_urls(self, temp_sphinx_app: SphinxTestApp):
311
+ """Test that multiple empty URLs are handled correctly."""
312
+ # First empty reference
313
+ ref1 = nodes.reference()
314
+ ref1["refuri"] = ""
315
+ ref1 += nodes.Text("ref1")
316
+
317
+ # Second empty reference
318
+ ref2 = nodes.reference()
319
+ ref2["refuri"] = ""
320
+ ref2 += nodes.Text("ref2")
321
+
322
+ doc = create_document()
323
+ para = nodes.paragraph()
324
+ para += ref1
325
+ para += nodes.Text(" and ")
326
+ para += ref2
327
+ doc += para
328
+
329
+ writer = TypstWriter(temp_sphinx_app.builder)
330
+ writer.document = doc
331
+ translator = TypstTranslator(doc, temp_sphinx_app.builder)
332
+ doc.walkabout(translator)
333
+
334
+ output = translator.astext()
335
+ # Both should be rendered as text
336
+ assert "ref1" in output
337
+ assert "ref2" in output
338
+ # No link wrappers
339
+ assert 'link("")' not in output
@@ -0,0 +1,403 @@
1
+ """
2
+ Tests for template asset copying functionality (Issue #75).
3
+
4
+ This module tests the copy_template_assets() method and related functionality
5
+ for automatically copying assets (fonts, images, logos) referenced by custom templates.
6
+ """
7
+
8
+ import pytest
9
+ from sphinx.testing.util import SphinxTestApp
10
+
11
+
12
+ @pytest.fixture
13
+ def temp_app_with_template(tmp_path):
14
+ """
15
+ Create a temporary Sphinx app with a custom template and assets.
16
+
17
+ Directory structure:
18
+ source/
19
+ index.rst
20
+ _templates/
21
+ template.typ
22
+ logo.png
23
+ assets/
24
+ font.otf
25
+ icon.svg
26
+ """
27
+ srcdir = tmp_path / "source"
28
+ srcdir.mkdir()
29
+
30
+ # Create index.rst
31
+ (srcdir / "index.rst").write_text(
32
+ "Test Document\n" "=============\n" "\n" "This is a test document.\n"
33
+ )
34
+
35
+ # Create template directory and files
36
+ template_dir = srcdir / "_templates"
37
+ template_dir.mkdir()
38
+
39
+ # Create template file
40
+ (template_dir / "template.typ").write_text(
41
+ '#import "logo.png"\n'
42
+ '#set text(font: "assets/font.otf")\n'
43
+ '#image("assets/icon.svg")\n'
44
+ )
45
+
46
+ # Create asset files
47
+ (template_dir / "logo.png").write_bytes(b"fake png data")
48
+
49
+ assets_dir = template_dir / "assets"
50
+ assets_dir.mkdir()
51
+ (assets_dir / "font.otf").write_bytes(b"fake font data")
52
+ (assets_dir / "icon.svg").write_text("<svg></svg>")
53
+
54
+ # Create conf.py
55
+ (srcdir / "conf.py").write_text(
56
+ "project = 'Test'\n"
57
+ "extensions = ['typsphinx']\n"
58
+ "typst_template = '_templates/template.typ'\n"
59
+ "typst_documents = [('index', 'index', 'Test', 'Author')]\n"
60
+ )
61
+
62
+ outdir = tmp_path / "build"
63
+
64
+ app = SphinxTestApp(buildername="typst", srcdir=srcdir, builddir=outdir)
65
+
66
+ return app, srcdir, outdir
67
+
68
+
69
+ def test_copy_template_assets_automatic_directory_copy(temp_app_with_template):
70
+ """
71
+ Test automatic directory copy (default behavior).
72
+
73
+ When typst_template_assets is None, all files in the template directory
74
+ should be automatically copied (except .typ files).
75
+ """
76
+ app, srcdir, outdir = temp_app_with_template
77
+
78
+ # Build the project
79
+ app.build()
80
+
81
+ # Check that assets were copied to output directory
82
+ typst_outdir = outdir / "typst"
83
+ template_out_dir = typst_outdir / "_templates"
84
+
85
+ assert (template_out_dir / "logo.png").exists()
86
+ assert (template_out_dir / "assets" / "font.otf").exists()
87
+ assert (template_out_dir / "assets" / "icon.svg").exists()
88
+
89
+ # Verify file contents match
90
+ assert (template_out_dir / "logo.png").read_bytes() == b"fake png data"
91
+ assert (template_out_dir / "assets" / "font.otf").read_bytes() == b"fake font data"
92
+ assert (template_out_dir / "assets" / "icon.svg").read_text() == "<svg></svg>"
93
+
94
+
95
+ def test_copy_template_assets_explicit_list(tmp_path):
96
+ """
97
+ Test explicit asset list specification.
98
+
99
+ When typst_template_assets is configured, only specified assets should be copied.
100
+ """
101
+ srcdir = tmp_path / "source"
102
+ srcdir.mkdir()
103
+
104
+ # Create index.rst
105
+ (srcdir / "index.rst").write_text(
106
+ "Test Document\n" "=============\n" "\n" "This is a test document.\n"
107
+ )
108
+
109
+ # Create template directory and files
110
+ template_dir = srcdir / "_templates"
111
+ template_dir.mkdir()
112
+
113
+ (template_dir / "template.typ").write_text('#image("logo.png")')
114
+ (template_dir / "logo.png").write_bytes(b"logo data")
115
+ (template_dir / "unused.png").write_bytes(b"unused data")
116
+
117
+ # Create conf.py with explicit asset list
118
+ (srcdir / "conf.py").write_text(
119
+ "project = 'Test'\n"
120
+ "extensions = ['typsphinx']\n"
121
+ "typst_template = '_templates/template.typ'\n"
122
+ "typst_template_assets = ['_templates/logo.png']\n"
123
+ "typst_documents = [('index', 'index', 'Test', 'Author')]\n"
124
+ )
125
+
126
+ outdir = tmp_path / "build"
127
+
128
+ app = SphinxTestApp(buildername="typst", srcdir=srcdir, builddir=outdir)
129
+
130
+ # Build the project
131
+ app.build()
132
+
133
+ # Check that only specified asset was copied
134
+ typst_outdir = outdir / "typst"
135
+ template_out_dir = typst_outdir / "_templates"
136
+
137
+ assert (template_out_dir / "logo.png").exists()
138
+ assert not (template_out_dir / "unused.png").exists()
139
+
140
+
141
+ def test_copy_template_assets_glob_pattern(tmp_path):
142
+ """
143
+ Test glob pattern support in asset list.
144
+
145
+ Patterns like "*.png" should match multiple files.
146
+ """
147
+ srcdir = tmp_path / "source"
148
+ srcdir.mkdir()
149
+
150
+ # Create index.rst
151
+ (srcdir / "index.rst").write_text(
152
+ "Test Document\n" "=============\n" "\n" "This is a test document.\n"
153
+ )
154
+
155
+ # Create template directory and files
156
+ template_dir = srcdir / "_templates"
157
+ template_dir.mkdir()
158
+
159
+ (template_dir / "template.typ").write_text('#image("logo.png")')
160
+ (template_dir / "logo.png").write_bytes(b"logo data")
161
+ (template_dir / "icon.png").write_bytes(b"icon data")
162
+ (template_dir / "readme.txt").write_text("readme")
163
+
164
+ # Create conf.py with glob pattern
165
+ (srcdir / "conf.py").write_text(
166
+ "project = 'Test'\n"
167
+ "extensions = ['typsphinx']\n"
168
+ "typst_template = '_templates/template.typ'\n"
169
+ "typst_template_assets = ['_templates/*.png']\n"
170
+ "typst_documents = [('index', 'index', 'Test', 'Author')]\n"
171
+ )
172
+
173
+ outdir = tmp_path / "build"
174
+
175
+ app = SphinxTestApp(buildername="typst", srcdir=srcdir, builddir=outdir)
176
+
177
+ # Build the project
178
+ app.build()
179
+
180
+ # Check that all PNG files were copied, but not TXT
181
+ typst_outdir = outdir / "typst"
182
+ template_out_dir = typst_outdir / "_templates"
183
+
184
+ assert (template_out_dir / "logo.png").exists()
185
+ assert (template_out_dir / "icon.png").exists()
186
+ assert not (template_out_dir / "readme.txt").exists()
187
+
188
+
189
+ def test_copy_template_assets_empty_list_disables(tmp_path):
190
+ """
191
+ Test that empty list disables automatic copying.
192
+
193
+ typst_template_assets = [] should disable all automatic asset copying.
194
+ """
195
+ srcdir = tmp_path / "source"
196
+ srcdir.mkdir()
197
+
198
+ # Create index.rst
199
+ (srcdir / "index.rst").write_text(
200
+ "Test Document\n" "=============\n" "\n" "This is a test document.\n"
201
+ )
202
+
203
+ # Create template directory and files
204
+ template_dir = srcdir / "_templates"
205
+ template_dir.mkdir()
206
+
207
+ (template_dir / "template.typ").write_text('#image("logo.png")')
208
+ (template_dir / "logo.png").write_bytes(b"logo data")
209
+
210
+ # Create conf.py with empty asset list
211
+ (srcdir / "conf.py").write_text(
212
+ "project = 'Test'\n"
213
+ "extensions = ['typsphinx']\n"
214
+ "typst_template = '_templates/template.typ'\n"
215
+ "typst_template_assets = []\n"
216
+ "typst_documents = [('index', 'index', 'Test', 'Author')]\n"
217
+ )
218
+
219
+ outdir = tmp_path / "build"
220
+
221
+ app = SphinxTestApp(buildername="typst", srcdir=srcdir, builddir=outdir)
222
+
223
+ # Build the project
224
+ app.build()
225
+
226
+ # Check that assets were NOT copied
227
+ typst_outdir = outdir / "typst"
228
+ template_out_dir = typst_outdir / "_templates"
229
+
230
+ # Template file should exist (copied by _write_template_file)
231
+ assert (typst_outdir / "_template.typ").exists()
232
+
233
+ # But logo.png should NOT be copied
234
+ assert not (template_out_dir / "logo.png").exists()
235
+
236
+
237
+ def test_copy_template_assets_no_template(tmp_path):
238
+ """
239
+ Test that no assets are copied when no template is configured.
240
+
241
+ This ensures backward compatibility with projects not using templates.
242
+ """
243
+ srcdir = tmp_path / "source"
244
+ srcdir.mkdir()
245
+
246
+ # Create index.rst
247
+ (srcdir / "index.rst").write_text(
248
+ "Test Document\n" "=============\n" "\n" "This is a test document.\n"
249
+ )
250
+
251
+ # Create conf.py WITHOUT template configuration
252
+ (srcdir / "conf.py").write_text(
253
+ "project = 'Test'\n"
254
+ "extensions = ['typsphinx']\n"
255
+ "typst_documents = [('index', 'index', 'Test', 'Author')]\n"
256
+ )
257
+
258
+ outdir = tmp_path / "build"
259
+
260
+ app = SphinxTestApp(buildername="typst", srcdir=srcdir, builddir=outdir)
261
+
262
+ # Build should succeed without errors
263
+ app.build()
264
+
265
+ # No template directory should be created
266
+ typst_outdir = outdir / "typst"
267
+ template_out_dir = typst_outdir / "_templates"
268
+
269
+ assert not template_out_dir.exists()
270
+
271
+
272
+ def test_copy_template_assets_with_typst_package(tmp_path):
273
+ """
274
+ Test that assets are NOT copied when using Typst Universe packages.
275
+
276
+ Typst Universe packages handle assets automatically, so we should skip copying.
277
+ """
278
+ srcdir = tmp_path / "source"
279
+ srcdir.mkdir()
280
+
281
+ # Create index.rst
282
+ (srcdir / "index.rst").write_text(
283
+ "Test Document\n" "=============\n" "\n" "This is a test document.\n"
284
+ )
285
+
286
+ # Create template directory (even though using package)
287
+ template_dir = srcdir / "_templates"
288
+ template_dir.mkdir()
289
+ (template_dir / "logo.png").write_bytes(b"logo data")
290
+
291
+ # Create conf.py with Typst package (takes precedence over template)
292
+ (srcdir / "conf.py").write_text(
293
+ "project = 'Test'\n"
294
+ "extensions = ['typsphinx']\n"
295
+ "typst_package = '@preview/charged-ieee:0.1.0'\n"
296
+ "typst_template = '_templates/template.typ'\n"
297
+ "typst_documents = [('index', 'index', 'Test', 'Author')]\n"
298
+ )
299
+
300
+ outdir = tmp_path / "build"
301
+
302
+ app = SphinxTestApp(buildername="typst", srcdir=srcdir, builddir=outdir)
303
+
304
+ # Build the project
305
+ app.build()
306
+
307
+ # Check that assets were NOT copied (package handles them)
308
+ typst_outdir = outdir / "typst"
309
+ template_out_dir = typst_outdir / "_templates"
310
+
311
+ assert not (template_out_dir / "logo.png").exists()
312
+
313
+
314
+ def test_copy_template_assets_missing_source(tmp_path, caplog):
315
+ """
316
+ Test graceful handling of missing source files.
317
+
318
+ Should log warning but not fail the build.
319
+ """
320
+ srcdir = tmp_path / "source"
321
+ srcdir.mkdir()
322
+
323
+ # Create index.rst
324
+ (srcdir / "index.rst").write_text(
325
+ "Test Document\n" "=============\n" "\n" "This is a test document.\n"
326
+ )
327
+
328
+ # Create template directory
329
+ template_dir = srcdir / "_templates"
330
+ template_dir.mkdir()
331
+
332
+ (template_dir / "template.typ").write_text('#image("logo.png")')
333
+ # Note: logo.png does NOT exist
334
+
335
+ # Create conf.py with explicit asset that doesn't exist
336
+ (srcdir / "conf.py").write_text(
337
+ "project = 'Test'\n"
338
+ "extensions = ['typsphinx']\n"
339
+ "typst_template = '_templates/template.typ'\n"
340
+ "typst_template_assets = ['_templates/logo.png']\n"
341
+ "typst_documents = [('index', 'index', 'Test', 'Author')]\n"
342
+ )
343
+
344
+ outdir = tmp_path / "build"
345
+
346
+ app = SphinxTestApp(buildername="typst", srcdir=srcdir, builddir=outdir)
347
+
348
+ # Build should succeed despite missing file
349
+ app.build()
350
+
351
+ # Check that warning was logged in Sphinx's warning stream
352
+ warnings = app._warning.getvalue()
353
+ assert "Template asset not found" in warnings
354
+
355
+
356
+ def test_copy_template_assets_typstpdf_builder(tmp_path):
357
+ """
358
+ Test that asset copying works with TypstPDFBuilder.
359
+
360
+ Assets should be copied before PDF compilation.
361
+ """
362
+ srcdir = tmp_path / "source"
363
+ srcdir.mkdir()
364
+
365
+ # Create index.rst
366
+ (srcdir / "index.rst").write_text(
367
+ "Test Document\n" "=============\n" "\n" "This is a test document.\n"
368
+ )
369
+
370
+ # Create template directory and files
371
+ template_dir = srcdir / "_templates"
372
+ template_dir.mkdir()
373
+
374
+ (template_dir / "template.typ").write_text('#image("logo.png")')
375
+ (template_dir / "logo.png").write_bytes(b"logo data")
376
+
377
+ # Create conf.py
378
+ (srcdir / "conf.py").write_text(
379
+ "project = 'Test'\n"
380
+ "extensions = ['typsphinx']\n"
381
+ "typst_template = '_templates/template.typ'\n"
382
+ "typst_documents = [('index', 'index', 'Test', 'Author')]\n"
383
+ )
384
+
385
+ outdir = tmp_path / "build"
386
+
387
+ app = SphinxTestApp(
388
+ buildername="typstpdf", srcdir=srcdir, builddir=outdir # Use PDF builder
389
+ )
390
+
391
+ # Build the project
392
+ # Note: This may fail at PDF compilation if typst-py is not properly set up,
393
+ # but asset copying should still happen
394
+ try:
395
+ app.build()
396
+ except Exception:
397
+ pass # Ignore PDF compilation errors
398
+
399
+ # Check that assets were copied before PDF compilation
400
+ typstpdf_outdir = outdir / "typstpdf"
401
+ template_out_dir = typstpdf_outdir / "_templates"
402
+
403
+ assert (template_out_dir / "logo.png").exists()
@@ -11,8 +11,8 @@ sources using Sphinx, which can then be compiled to PDF using the Typst compiler
11
11
  :license: MIT, see LICENSE for details.
12
12
  """
13
13
 
14
- __version__ = "0.4.1"
15
- __author__ = "Sphinx Typst Contributors"
14
+ __version__ = "0.4.3"
15
+ __author__ = "YuSabo"
16
16
 
17
17
  from typing import Any, Dict
18
18
 
@@ -55,6 +55,8 @@ def setup(app: Sphinx) -> Dict[str, Any]:
55
55
  # Task 13.4: Output directory and debug mode
56
56
  app.add_config_value("typst_output_dir", "_build/typst", "html", [str])
57
57
  app.add_config_value("typst_debug", False, "html", [bool])
58
+ # Issue #75: Template asset support
59
+ app.add_config_value("typst_template_assets", None, "html", [list, type(None)])
58
60
 
59
61
  return {
60
62
  "version": __version__,
@@ -280,14 +280,189 @@ class TypstBuilder(Builder):
280
280
  except Exception as e:
281
281
  logger.warning(f"Failed to copy image {imguri}: {e}")
282
282
 
283
+ def copy_template_assets(self) -> None:
284
+ """
285
+ Copy template-associated assets to the output directory.
286
+
287
+ When using custom Typst templates via typst_template configuration,
288
+ this method copies assets (fonts, images, logos, etc.) referenced by
289
+ the template to the output directory.
290
+
291
+ Behavior:
292
+ - If typst_template_assets is configured, copies only specified files/directories
293
+ - If typst_template_assets is None (default), automatically copies entire template directory
294
+ - If typst_template_assets is empty list, disables automatic copying
295
+ - Skips .typ files to avoid duplicating template file (already handled by _write_template_file)
296
+
297
+ This follows the same pattern as copy_image_files() from Issue #38.
298
+ """
299
+
300
+ # Early return if no custom template is configured
301
+ template_path = getattr(self.config, "typst_template", None)
302
+ if not template_path:
303
+ return # No custom template
304
+
305
+ # Early return if using Typst Universe package (assets handled by Typst compiler)
306
+ typst_package = getattr(self.config, "typst_package", None)
307
+ if typst_package:
308
+ return
309
+
310
+ # Get template assets configuration
311
+ template_assets = getattr(self.config, "typst_template_assets", None)
312
+
313
+ # Check if explicitly disabled (empty list)
314
+ if template_assets is not None and len(template_assets) == 0:
315
+ logger.debug("Template asset copying disabled (empty list)")
316
+ return
317
+
318
+ logger.info("Copying template assets...")
319
+
320
+ if template_assets:
321
+ # Option 2: Explicit asset list
322
+ self._copy_explicit_assets(template_assets)
323
+ else:
324
+ # Option 1: Automatic directory copy
325
+ self._copy_template_directory(template_path)
326
+
327
+ def _copy_template_directory(self, template_path: str) -> None:
328
+ """
329
+ Copy entire template directory to output (default behavior).
330
+
331
+ Automatically copies all files in the template directory,
332
+ excluding .typ files (which are handled separately).
333
+
334
+ Args:
335
+ template_path: Path to template file relative to source directory
336
+ """
337
+ import os
338
+
339
+ # Get template directory path
340
+ template_dir = path.dirname(template_path)
341
+ if not template_dir:
342
+ # Template is in root directory, no assets to copy
343
+ return
344
+
345
+ # Resolve absolute paths
346
+ src_dir = path.join(self.srcdir, template_dir)
347
+ dest_dir = path.join(self.outdir, template_dir)
348
+
349
+ # Check if template directory exists
350
+ if not path.exists(src_dir):
351
+ logger.warning(f"Template directory not found: {src_dir}")
352
+ return
353
+
354
+ # Track copied files for logging
355
+ copied_count = 0
356
+
357
+ # Walk through directory and copy all files except .typ
358
+ for root, _dirs, files in os.walk(src_dir):
359
+ for file in files:
360
+ # Skip .typ files (already handled by _write_template_file)
361
+ if file.endswith(".typ"):
362
+ continue
363
+
364
+ # Get source and destination paths
365
+ src_file = path.join(root, file)
366
+ rel_path = path.relpath(src_file, src_dir)
367
+ dest_file = path.join(dest_dir, rel_path)
368
+
369
+ # Ensure destination directory exists
370
+ ensuredir(path.dirname(dest_file))
371
+
372
+ # Copy the file
373
+ try:
374
+ shutil.copy2(src_file, dest_file)
375
+ logger.debug(f"Copied template asset: {rel_path}")
376
+ copied_count += 1
377
+ except Exception as e:
378
+ logger.warning(f"Failed to copy template asset {rel_path}: {e}")
379
+
380
+ if copied_count > 0:
381
+ logger.info(f"Copied {copied_count} template asset(s) from {template_dir}/")
382
+
383
+ def _copy_explicit_assets(self, assets: list) -> None:
384
+ """
385
+ Copy explicitly specified assets.
386
+
387
+ Supports individual files, directories, and glob patterns.
388
+
389
+ Args:
390
+ assets: List of asset paths (relative to source directory)
391
+ May include glob patterns like "*.png" or "fonts/*.otf"
392
+ """
393
+ import glob
394
+
395
+ copied_count = 0
396
+
397
+ for asset_pattern in assets:
398
+ # Resolve absolute pattern path
399
+ abs_pattern = path.join(self.srcdir, asset_pattern)
400
+
401
+ # Check if pattern contains wildcards
402
+ if "*" in asset_pattern or "?" in asset_pattern:
403
+ # Expand glob pattern
404
+ matches = glob.glob(abs_pattern, recursive=True)
405
+ if not matches:
406
+ logger.warning(f"No files matched pattern: {asset_pattern}")
407
+ continue
408
+
409
+ for match in matches:
410
+ if self._copy_single_asset(match, asset_pattern):
411
+ copied_count += 1
412
+ else:
413
+ # Single file or directory
414
+ if self._copy_single_asset(abs_pattern, asset_pattern):
415
+ copied_count += 1
416
+
417
+ if copied_count > 0:
418
+ logger.info(f"Copied {copied_count} explicitly specified template asset(s)")
419
+
420
+ def _copy_single_asset(self, src_path: str, original_pattern: str) -> bool:
421
+ """
422
+ Copy a single asset file or directory.
423
+
424
+ Args:
425
+ src_path: Absolute source path
426
+ original_pattern: Original pattern from configuration (for error messages)
427
+
428
+ Returns:
429
+ True if successfully copied, False otherwise
430
+ """
431
+
432
+ # Check if source exists
433
+ if not path.exists(src_path):
434
+ logger.warning(f"Template asset not found: {original_pattern}")
435
+ return False
436
+
437
+ # Calculate relative path from source directory
438
+ rel_path = path.relpath(src_path, self.srcdir)
439
+ dest_path = path.join(self.outdir, rel_path)
440
+
441
+ try:
442
+ if path.isdir(src_path):
443
+ # Copy directory recursively
444
+ # Use copytree with dirs_exist_ok for Python 3.8+
445
+ shutil.copytree(src_path, dest_path, dirs_exist_ok=True)
446
+ logger.debug(f"Copied template asset directory: {rel_path}/")
447
+ else:
448
+ # Copy single file
449
+ ensuredir(path.dirname(dest_path))
450
+ shutil.copy2(src_path, dest_path)
451
+ logger.debug(f"Copied template asset: {rel_path}")
452
+ return True
453
+ except Exception as e:
454
+ logger.warning(f"Failed to copy template asset {rel_path}: {e}")
455
+ return False
456
+
283
457
  def finish(self) -> None:
284
458
  """
285
459
  Finish the build process.
286
460
 
287
461
  This method is called once after all documents have been written.
288
- Copies image files to the output directory.
462
+ Copies image files and template assets to the output directory.
289
463
  """
290
464
  self.copy_image_files()
465
+ self.copy_template_assets()
291
466
 
292
467
 
293
468
  class TypstPDFBuilder(TypstBuilder):
@@ -1930,6 +1930,19 @@ class TypstTranslator(SphinxTranslator):
1930
1930
  # Get the reference URI
1931
1931
  refuri = node.get("refuri", "")
1932
1932
 
1933
+ # Handle empty URLs (Typst 0.14+ rejects empty URLs)
1934
+ # This can occur with unresolved references, broken cross-references,
1935
+ # or malformed reStructuredText. Instead of generating invalid link("", ...),
1936
+ # we skip the link wrapper and render content as plain text.
1937
+ if not refuri:
1938
+ logger.warning(
1939
+ f"Reference node has empty URL. "
1940
+ f"Link will be rendered as plain text. "
1941
+ f"Check for broken references in source: {node.astext()}"
1942
+ )
1943
+ self._skip_link_wrapper = True
1944
+ return
1945
+
1933
1946
  # Determine if we need # prefix (in markup mode)
1934
1947
  prefix = "#" if self._in_markup_mode else ""
1935
1948
 
@@ -1961,6 +1974,16 @@ class TypstTranslator(SphinxTranslator):
1961
1974
  Args:
1962
1975
  node: The reference node
1963
1976
  """
1977
+ # Skip link wrapper closing if we skipped it in visit
1978
+ if getattr(self, "_skip_link_wrapper", False):
1979
+ self._skip_link_wrapper = False
1980
+ # Restore list item separator state if needed
1981
+ if hasattr(self, "_reference_was_list_item_needs_separator"):
1982
+ if self.in_list_item:
1983
+ self.list_item_needs_separator = True
1984
+ delattr(self, "_reference_was_list_item_needs_separator")
1985
+ return
1986
+
1964
1987
  # Close the link function
1965
1988
  self.add_text(")")
1966
1989
 
@@ -1,15 +1,15 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: typsphinx
3
- Version: 0.4.2
3
+ Version: 0.4.3
4
4
  Summary: Sphinx extension for Typst output
5
- Author-email: Sphinx Typst Contributors <noreply@example.com>
5
+ Author-email: YuSabo <yusabo90002@gmail.com>
6
6
  License-Expression: MIT
7
7
  Project-URL: Homepage, https://github.com/YuSabo90002/typsphinx
8
8
  Project-URL: Documentation, https://github.com/YuSabo90002/typsphinx#readme
9
9
  Project-URL: Repository, https://github.com/YuSabo90002/typsphinx
10
10
  Project-URL: Issues, https://github.com/YuSabo90002/typsphinx/issues
11
11
  Keywords: sphinx,typst,documentation,pdf
12
- Classifier: Development Status :: 4 - Beta
12
+ Classifier: Development Status :: 5 - Production/Stable
13
13
  Classifier: Framework :: Sphinx :: Extension
14
14
  Classifier: Intended Audience :: Developers
15
15
  Classifier: Programming Language :: Python :: 3
@@ -24,7 +24,7 @@ Description-Content-Type: text/markdown
24
24
  License-File: LICENSE
25
25
  Requires-Dist: sphinx>=5.0
26
26
  Requires-Dist: docutils>=0.18
27
- Requires-Dist: typst>=0.11.1
27
+ Requires-Dist: typst>=0.14.1
28
28
  Provides-Extra: dev
29
29
  Requires-Dist: pytest>=7.0; extra == "dev"
30
30
  Requires-Dist: pytest-cov>=4.0; extra == "dev"
@@ -84,7 +84,7 @@ typsphinx is a Sphinx extension that enables generating Typst documents from reS
84
84
 
85
85
  ## Installation
86
86
 
87
- ### From PyPI (Beta Release)
87
+ ### From PyPI
88
88
 
89
89
  ```bash
90
90
  pip install typsphinx
@@ -24,6 +24,7 @@ tests/test_math_mitex.py
24
24
  tests/test_math_native.py
25
25
  tests/test_nested_toctree_paths.py
26
26
  tests/test_pdf_generation.py
27
+ tests/test_template_assets.py
27
28
  tests/test_template_codly.py
28
29
  tests/test_template_engine.py
29
30
  tests/test_template_mitex.py
@@ -1,6 +1,6 @@
1
1
  sphinx>=5.0
2
2
  docutils>=0.18
3
- typst>=0.11.1
3
+ typst>=0.14.1
4
4
 
5
5
  [dev]
6
6
  pytest>=7.0
File without changes
File without changes
File without changes
File without changes