typsphinx 0.6.2__tar.gz → 0.6.4__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.
- {typsphinx-0.6.2 → typsphinx-0.6.4}/PKG-INFO +16 -15
- {typsphinx-0.6.2 → typsphinx-0.6.4}/README.md +14 -13
- {typsphinx-0.6.2 → typsphinx-0.6.4}/pyproject.toml +2 -2
- typsphinx-0.6.4/tests/test_no_stale_github_io_links.py +151 -0
- {typsphinx-0.6.2 → typsphinx-0.6.4}/tests/test_pdf_render_gate.py +328 -0
- {typsphinx-0.6.2 → typsphinx-0.6.4}/tests/test_preview_version_sync.py +44 -1
- typsphinx-0.6.4/tests/test_readthedocs_config.py +359 -0
- {typsphinx-0.6.2 → typsphinx-0.6.4}/tests/test_template_engine.py +428 -0
- {typsphinx-0.6.2 → typsphinx-0.6.4}/tests/test_translator.py +309 -0
- typsphinx-0.6.4/tests/test_typst_elements_pass_through_gate.py +422 -0
- typsphinx-0.6.4/tests/test_typst_lang_gate.py +860 -0
- {typsphinx-0.6.2 → typsphinx-0.6.4}/typsphinx/__init__.py +0 -1
- {typsphinx-0.6.2 → typsphinx-0.6.4}/typsphinx/template_engine.py +267 -24
- {typsphinx-0.6.2 → typsphinx-0.6.4}/typsphinx/templates/base.typ +2 -1
- {typsphinx-0.6.2 → typsphinx-0.6.4}/typsphinx/translator.py +167 -25
- {typsphinx-0.6.2 → typsphinx-0.6.4}/typsphinx/writer.py +51 -6
- {typsphinx-0.6.2 → typsphinx-0.6.4}/typsphinx.egg-info/PKG-INFO +16 -15
- {typsphinx-0.6.2 → typsphinx-0.6.4}/typsphinx.egg-info/SOURCES.txt +4 -4
- typsphinx-0.6.2/tests/test_config_toctree_defaults.py +0 -245
- typsphinx-0.6.2/tests/test_documentation_configuration.py +0 -156
- typsphinx-0.6.2/tests/test_documentation_installation.py +0 -143
- typsphinx-0.6.2/tests/test_documentation_usage.py +0 -153
- {typsphinx-0.6.2 → typsphinx-0.6.4}/LICENSE +0 -0
- {typsphinx-0.6.2 → typsphinx-0.6.4}/setup.cfg +0 -0
- {typsphinx-0.6.2 → typsphinx-0.6.4}/tests/test_abbr_pep_separator_render_gate.py +0 -0
- {typsphinx-0.6.2 → typsphinx-0.6.4}/tests/test_admonitions.py +0 -0
- {typsphinx-0.6.2 → typsphinx-0.6.4}/tests/test_builder.py +0 -0
- {typsphinx-0.6.2 → typsphinx-0.6.4}/tests/test_builder_output_stem.py +0 -0
- {typsphinx-0.6.2 → typsphinx-0.6.4}/tests/test_builder_requirement13.py +0 -0
- {typsphinx-0.6.2 → typsphinx-0.6.4}/tests/test_codly_caption_listitem_leak_render_gate.py +0 -0
- {typsphinx-0.6.2 → typsphinx-0.6.4}/tests/test_config.py +0 -0
- {typsphinx-0.6.2 → typsphinx-0.6.4}/tests/test_config_other_options.py +0 -0
- {typsphinx-0.6.2 → typsphinx-0.6.4}/tests/test_config_template_mapping.py +0 -0
- {typsphinx-0.6.2 → typsphinx-0.6.4}/tests/test_confval_field_body_render_gate.py +0 -0
- {typsphinx-0.6.2 → typsphinx-0.6.4}/tests/test_confval_field_spacing_render_gate.py +0 -0
- {typsphinx-0.6.2 → typsphinx-0.6.4}/tests/test_corpus_gate.py +0 -0
- {typsphinx-0.6.2 → typsphinx-0.6.4}/tests/test_cross_doc_label_namespace_render_gate.py +0 -0
- {typsphinx-0.6.2 → typsphinx-0.6.4}/tests/test_deflist_definition_multiblock_render_gate.py +0 -0
- {typsphinx-0.6.2 → typsphinx-0.6.4}/tests/test_deflist_nested_definition_render_gate.py +0 -0
- {typsphinx-0.6.2 → typsphinx-0.6.4}/tests/test_deflist_term_concat_render_gate.py +0 -0
- {typsphinx-0.6.2 → typsphinx-0.6.4}/tests/test_deflist_term_inline_children_gate.py +0 -0
- {typsphinx-0.6.2 → typsphinx-0.6.4}/tests/test_desc_bodyless_concat_render_gate.py +0 -0
- {typsphinx-0.6.2 → typsphinx-0.6.4}/tests/test_desc_container_propagated_target_render_gate.py +0 -0
- {typsphinx-0.6.2 → typsphinx-0.6.4}/tests/test_desc_sig_space_render_gate.py +0 -0
- {typsphinx-0.6.2 → typsphinx-0.6.4}/tests/test_desc_signature_anchor_render_gate.py +0 -0
- {typsphinx-0.6.2 → typsphinx-0.6.4}/tests/test_desc_signature_concat_render_gate.py +0 -0
- {typsphinx-0.6.2 → typsphinx-0.6.4}/tests/test_duplicate_include_label_render_gate.py +0 -0
- {typsphinx-0.6.2 → typsphinx-0.6.4}/tests/test_entry_points.py +0 -0
- {typsphinx-0.6.2 → typsphinx-0.6.4}/tests/test_epigraph_render_gate.py +0 -0
- {typsphinx-0.6.2 → typsphinx-0.6.4}/tests/test_examples_basic.py +0 -0
- {typsphinx-0.6.2 → typsphinx-0.6.4}/tests/test_examples_charged_ieee_gate.py +0 -0
- {typsphinx-0.6.2 → typsphinx-0.6.4}/tests/test_extension.py +0 -0
- {typsphinx-0.6.2 → typsphinx-0.6.4}/tests/test_external_link_style_render_gate.py +0 -0
- {typsphinx-0.6.2 → typsphinx-0.6.4}/tests/test_field_list_in_list_item_render_gate.py +0 -0
- {typsphinx-0.6.2 → typsphinx-0.6.4}/tests/test_footnotes.py +0 -0
- {typsphinx-0.6.2 → typsphinx-0.6.4}/tests/test_glob_image_render_gate.py +0 -0
- {typsphinx-0.6.2 → typsphinx-0.6.4}/tests/test_inline_literal_overflow_render_gate.py +0 -0
- {typsphinx-0.6.2 → typsphinx-0.6.4}/tests/test_inline_references.py +0 -0
- {typsphinx-0.6.2 → typsphinx-0.6.4}/tests/test_integration_advanced.py +0 -0
- {typsphinx-0.6.2 → typsphinx-0.6.4}/tests/test_integration_basic.py +0 -0
- {typsphinx-0.6.2 → typsphinx-0.6.4}/tests/test_integration_multi_doc.py +0 -0
- {typsphinx-0.6.2 → typsphinx-0.6.4}/tests/test_integration_nested_toctree.py +0 -0
- {typsphinx-0.6.2 → typsphinx-0.6.4}/tests/test_label_at_char_render_gate.py +0 -0
- {typsphinx-0.6.2 → typsphinx-0.6.4}/tests/test_line_blocks.py +0 -0
- {typsphinx-0.6.2 → typsphinx-0.6.4}/tests/test_list_item_nested_block_render_gate.py +0 -0
- {typsphinx-0.6.2 → typsphinx-0.6.4}/tests/test_math_fallback.py +0 -0
- {typsphinx-0.6.2 → typsphinx-0.6.4}/tests/test_math_mitex.py +0 -0
- {typsphinx-0.6.2 → typsphinx-0.6.4}/tests/test_math_native.py +0 -0
- {typsphinx-0.6.2 → typsphinx-0.6.4}/tests/test_missing_and_malformed_master_gate.py +0 -0
- {typsphinx-0.6.2 → typsphinx-0.6.4}/tests/test_nested_master_render_gate.py +0 -0
- {typsphinx-0.6.2 → typsphinx-0.6.4}/tests/test_nested_toctree_paths.py +0 -0
- {typsphinx-0.6.2 → typsphinx-0.6.4}/tests/test_package_only_config_gate.py +0 -0
- {typsphinx-0.6.2 → typsphinx-0.6.4}/tests/test_package_template_routing.py +0 -0
- {typsphinx-0.6.2 → typsphinx-0.6.4}/tests/test_paragraph_concat_render_gate.py +0 -0
- {typsphinx-0.6.2 → typsphinx-0.6.4}/tests/test_paragraph_propagated_target_render_gate.py +0 -0
- {typsphinx-0.6.2 → typsphinx-0.6.4}/tests/test_paragraph_soft_newline_render_gate.py +0 -0
- {typsphinx-0.6.2 → typsphinx-0.6.4}/tests/test_pdf_generation.py +0 -0
- {typsphinx-0.6.2 → typsphinx-0.6.4}/tests/test_preview_smoke_gate.py +0 -0
- {typsphinx-0.6.2 → typsphinx-0.6.4}/tests/test_readme_version_sync.py +0 -0
- {typsphinx-0.6.2 → typsphinx-0.6.4}/tests/test_ref_target_nested_list_render_gate.py +0 -0
- {typsphinx-0.6.2 → typsphinx-0.6.4}/tests/test_rubric_option_concat_render_gate.py +0 -0
- {typsphinx-0.6.2 → typsphinx-0.6.4}/tests/test_rubric_propagated_target_render_gate.py +0 -0
- {typsphinx-0.6.2 → typsphinx-0.6.4}/tests/test_static_asset_copy_gate.py +0 -0
- {typsphinx-0.6.2 → typsphinx-0.6.4}/tests/test_substitution_definition_render_gate.py +0 -0
- {typsphinx-0.6.2 → typsphinx-0.6.4}/tests/test_table_in_list_item_render_gate.py +0 -0
- {typsphinx-0.6.2 → typsphinx-0.6.4}/tests/test_target_label_render_gate.py +0 -0
- {typsphinx-0.6.2 → typsphinx-0.6.4}/tests/test_target_name_render_gate.py +0 -0
- {typsphinx-0.6.2 → typsphinx-0.6.4}/tests/test_template_assets.py +0 -0
- {typsphinx-0.6.2 → typsphinx-0.6.4}/tests/test_template_codly.py +0 -0
- {typsphinx-0.6.2 → typsphinx-0.6.4}/tests/test_template_import_path.py +0 -0
- {typsphinx-0.6.2 → typsphinx-0.6.4}/tests/test_template_mitex.py +0 -0
- {typsphinx-0.6.2 → typsphinx-0.6.4}/tests/test_toctree_requirement13.py +0 -0
- {typsphinx-0.6.2 → typsphinx-0.6.4}/tests/test_topics.py +0 -0
- {typsphinx-0.6.2 → typsphinx-0.6.4}/tests/test_typst_string_escape_gate.py +0 -0
- {typsphinx-0.6.2 → typsphinx-0.6.4}/tests/test_wide_table_render_gate.py +0 -0
- {typsphinx-0.6.2 → typsphinx-0.6.4}/tests/test_xref_orphan_degrade_render_gate.py +0 -0
- {typsphinx-0.6.2 → typsphinx-0.6.4}/typsphinx/builder.py +0 -0
- {typsphinx-0.6.2 → typsphinx-0.6.4}/typsphinx/pdf.py +0 -0
- {typsphinx-0.6.2 → typsphinx-0.6.4}/typsphinx.egg-info/dependency_links.txt +0 -0
- {typsphinx-0.6.2 → typsphinx-0.6.4}/typsphinx.egg-info/entry_points.txt +0 -0
- {typsphinx-0.6.2 → typsphinx-0.6.4}/typsphinx.egg-info/requires.txt +0 -0
- {typsphinx-0.6.2 → typsphinx-0.6.4}/typsphinx.egg-info/top_level.txt +0 -0
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: typsphinx
|
|
3
|
-
Version: 0.6.
|
|
3
|
+
Version: 0.6.4
|
|
4
4
|
Summary: Sphinx extension for Typst output
|
|
5
5
|
Author-email: YuSabo <yusabo90002@gmail.com>
|
|
6
6
|
License-Expression: MIT
|
|
7
7
|
Project-URL: Homepage, https://github.com/YuSabo90002/typsphinx
|
|
8
|
-
Project-URL: Documentation, https://
|
|
8
|
+
Project-URL: Documentation, https://typsphinx.readthedocs.io/
|
|
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
|
|
@@ -49,11 +49,11 @@ Dynamic: license-file
|
|
|
49
49
|
[](https://pypi.org/project/typsphinx/)
|
|
50
50
|
[](https://opensource.org/licenses/MIT)
|
|
51
51
|
[](https://github.com/psf/black)
|
|
52
|
-
[](https://typsphinx.readthedocs.io/)
|
|
53
53
|
|
|
54
54
|
Sphinx extension for Typst output format support.
|
|
55
55
|
|
|
56
|
-
**[📖 Documentation](https://
|
|
56
|
+
**[📖 Documentation](https://typsphinx.readthedocs.io/)** | **[🐛 Issue Tracker](https://github.com/YuSabo90002/typsphinx/issues)** | **[📦 PyPI](https://pypi.org/project/typsphinx/)**
|
|
57
57
|
|
|
58
58
|
## Overview
|
|
59
59
|
|
|
@@ -249,7 +249,6 @@ Below are the main configuration options. This is not the complete set — see [
|
|
|
249
249
|
- `typst_template`: Custom template path
|
|
250
250
|
- `typst_elements`: Template parameters (paper size, fonts, etc.)
|
|
251
251
|
- `typst_template_mapping`: Sphinx metadata to template parameter mapping
|
|
252
|
-
- `typst_toctree_defaults`: Default toctree options
|
|
253
252
|
|
|
254
253
|
## Development
|
|
255
254
|
|
|
@@ -309,17 +308,19 @@ typsphinx/
|
|
|
309
308
|
|
|
310
309
|
## Documentation
|
|
311
310
|
|
|
312
|
-
**📖 Full documentation is available at [
|
|
311
|
+
**📖 Full documentation is available at [typsphinx.readthedocs.io](https://typsphinx.readthedocs.io/)**
|
|
312
|
+
|
|
313
|
+
日本語ドキュメントは [typsphinx.readthedocs.io/ja/latest/](https://typsphinx.readthedocs.io/ja/latest/) にあります。
|
|
313
314
|
|
|
314
315
|
Quick links:
|
|
315
316
|
|
|
316
|
-
- [Installation Guide](https://
|
|
317
|
-
- [Quick Start](https://
|
|
318
|
-
- [User Guide](https://
|
|
319
|
-
- [Configuration Reference](https://
|
|
320
|
-
- [Examples](https://
|
|
321
|
-
- [API Reference](https://
|
|
322
|
-
- [Contributing Guide](https://
|
|
317
|
+
- [Installation Guide](https://typsphinx.readthedocs.io/en/latest/installation.html)
|
|
318
|
+
- [Quick Start](https://typsphinx.readthedocs.io/en/latest/quickstart.html)
|
|
319
|
+
- [User Guide](https://typsphinx.readthedocs.io/en/latest/user_guide/)
|
|
320
|
+
- [Configuration Reference](https://typsphinx.readthedocs.io/en/latest/user_guide/configuration.html)
|
|
321
|
+
- [Examples](https://typsphinx.readthedocs.io/en/latest/examples/)
|
|
322
|
+
- [API Reference](https://typsphinx.readthedocs.io/en/latest/api/)
|
|
323
|
+
- [Contributing Guide](https://typsphinx.readthedocs.io/en/latest/contributing.html)
|
|
323
324
|
|
|
324
325
|
## Contributing
|
|
325
326
|
|
|
@@ -349,7 +350,7 @@ MIT License - see [LICENSE](LICENSE) file for details.
|
|
|
349
350
|
- Integrates [mitex](https://github.com/mitex-rs/mitex) for LaTeX math
|
|
350
351
|
- Uses [codly](https://typst.app/universe/package/codly) for code highlighting
|
|
351
352
|
- Uses [gentle-clues](https://typst.app/universe/package/gentle-clues) for admonitions
|
|
352
|
-
- Developed with [Claude Code](https://claude.
|
|
353
|
+
- Developed with [Claude Code](https://claude.com/product/claude-code) and [GSD](https://github.com/open-gsd/gsd-core) (spec-driven development for AI coding agents)
|
|
353
354
|
|
|
354
355
|
## Version History
|
|
355
356
|
|
|
@@ -357,5 +358,5 @@ See [CHANGELOG.md](CHANGELOG.md) for detailed version history.
|
|
|
357
358
|
|
|
358
359
|
---
|
|
359
360
|
|
|
360
|
-
**Status**: Stable (v0.6.
|
|
361
|
+
**Status**: Stable (v0.6.4) - Production ready
|
|
361
362
|
**Python**: 3.12+ | **Sphinx**: 9.1+ | **Typst**: 0.15+
|
|
@@ -5,11 +5,11 @@
|
|
|
5
5
|
[](https://pypi.org/project/typsphinx/)
|
|
6
6
|
[](https://opensource.org/licenses/MIT)
|
|
7
7
|
[](https://github.com/psf/black)
|
|
8
|
-
[](https://typsphinx.readthedocs.io/)
|
|
9
9
|
|
|
10
10
|
Sphinx extension for Typst output format support.
|
|
11
11
|
|
|
12
|
-
**[📖 Documentation](https://
|
|
12
|
+
**[📖 Documentation](https://typsphinx.readthedocs.io/)** | **[🐛 Issue Tracker](https://github.com/YuSabo90002/typsphinx/issues)** | **[📦 PyPI](https://pypi.org/project/typsphinx/)**
|
|
13
13
|
|
|
14
14
|
## Overview
|
|
15
15
|
|
|
@@ -205,7 +205,6 @@ Below are the main configuration options. This is not the complete set — see [
|
|
|
205
205
|
- `typst_template`: Custom template path
|
|
206
206
|
- `typst_elements`: Template parameters (paper size, fonts, etc.)
|
|
207
207
|
- `typst_template_mapping`: Sphinx metadata to template parameter mapping
|
|
208
|
-
- `typst_toctree_defaults`: Default toctree options
|
|
209
208
|
|
|
210
209
|
## Development
|
|
211
210
|
|
|
@@ -265,17 +264,19 @@ typsphinx/
|
|
|
265
264
|
|
|
266
265
|
## Documentation
|
|
267
266
|
|
|
268
|
-
**📖 Full documentation is available at [
|
|
267
|
+
**📖 Full documentation is available at [typsphinx.readthedocs.io](https://typsphinx.readthedocs.io/)**
|
|
268
|
+
|
|
269
|
+
日本語ドキュメントは [typsphinx.readthedocs.io/ja/latest/](https://typsphinx.readthedocs.io/ja/latest/) にあります。
|
|
269
270
|
|
|
270
271
|
Quick links:
|
|
271
272
|
|
|
272
|
-
- [Installation Guide](https://
|
|
273
|
-
- [Quick Start](https://
|
|
274
|
-
- [User Guide](https://
|
|
275
|
-
- [Configuration Reference](https://
|
|
276
|
-
- [Examples](https://
|
|
277
|
-
- [API Reference](https://
|
|
278
|
-
- [Contributing Guide](https://
|
|
273
|
+
- [Installation Guide](https://typsphinx.readthedocs.io/en/latest/installation.html)
|
|
274
|
+
- [Quick Start](https://typsphinx.readthedocs.io/en/latest/quickstart.html)
|
|
275
|
+
- [User Guide](https://typsphinx.readthedocs.io/en/latest/user_guide/)
|
|
276
|
+
- [Configuration Reference](https://typsphinx.readthedocs.io/en/latest/user_guide/configuration.html)
|
|
277
|
+
- [Examples](https://typsphinx.readthedocs.io/en/latest/examples/)
|
|
278
|
+
- [API Reference](https://typsphinx.readthedocs.io/en/latest/api/)
|
|
279
|
+
- [Contributing Guide](https://typsphinx.readthedocs.io/en/latest/contributing.html)
|
|
279
280
|
|
|
280
281
|
## Contributing
|
|
281
282
|
|
|
@@ -305,7 +306,7 @@ MIT License - see [LICENSE](LICENSE) file for details.
|
|
|
305
306
|
- Integrates [mitex](https://github.com/mitex-rs/mitex) for LaTeX math
|
|
306
307
|
- Uses [codly](https://typst.app/universe/package/codly) for code highlighting
|
|
307
308
|
- Uses [gentle-clues](https://typst.app/universe/package/gentle-clues) for admonitions
|
|
308
|
-
- Developed with [Claude Code](https://claude.
|
|
309
|
+
- Developed with [Claude Code](https://claude.com/product/claude-code) and [GSD](https://github.com/open-gsd/gsd-core) (spec-driven development for AI coding agents)
|
|
309
310
|
|
|
310
311
|
## Version History
|
|
311
312
|
|
|
@@ -313,5 +314,5 @@ See [CHANGELOG.md](CHANGELOG.md) for detailed version history.
|
|
|
313
314
|
|
|
314
315
|
---
|
|
315
316
|
|
|
316
|
-
**Status**: Stable (v0.6.
|
|
317
|
+
**Status**: Stable (v0.6.4) - Production ready
|
|
317
318
|
**Python**: 3.12+ | **Sphinx**: 9.1+ | **Typst**: 0.15+
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "typsphinx"
|
|
7
|
-
version = "0.6.
|
|
7
|
+
version = "0.6.4"
|
|
8
8
|
description = "Sphinx extension for Typst output"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.12"
|
|
@@ -53,7 +53,7 @@ docs = [
|
|
|
53
53
|
|
|
54
54
|
[project.urls]
|
|
55
55
|
Homepage = "https://github.com/YuSabo90002/typsphinx"
|
|
56
|
-
Documentation = "https://
|
|
56
|
+
Documentation = "https://typsphinx.readthedocs.io/"
|
|
57
57
|
Repository = "https://github.com/YuSabo90002/typsphinx"
|
|
58
58
|
Issues = "https://github.com/YuSabo90002/typsphinx/issues"
|
|
59
59
|
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Regression guard for the retired-host -> Read the Docs URL rewrite (DOC-09).
|
|
3
|
+
|
|
4
|
+
Phase 31 rewrote every published documentation URL in README.md and
|
|
5
|
+
pyproject.toml: they used to point at a GitHub Pages host that had been
|
|
6
|
+
retired. Seven of README.md's deep links pointed at paths that no longer
|
|
7
|
+
resolved on that retired host and returned HTTP 404 for months; an external
|
|
8
|
+
user hit one of them and filed Issue #119. This module locks the rewrite
|
|
9
|
+
behind four hermetic invariants so a future edit that reintroduces the
|
|
10
|
+
retired host, drops or reorders a deep link, or version-pins a top-level
|
|
11
|
+
link fails a `pytest` run immediately instead of silently regressing a
|
|
12
|
+
second time.
|
|
13
|
+
|
|
14
|
+
All four tests parse raw file text (and `pyproject.toml` via `tomllib`) --
|
|
15
|
+
never package metadata -- and make no network calls. The real-HTTP checks
|
|
16
|
+
(curl sweeps, CI Link Check) are a separate instrument; see
|
|
17
|
+
`.planning/phases/31-published-url-cutover-repo-wide-link-guard/`.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
import re
|
|
21
|
+
import tomllib
|
|
22
|
+
from pathlib import Path
|
|
23
|
+
|
|
24
|
+
REPO_ROOT = Path(__file__).resolve().parents[1]
|
|
25
|
+
README_PATH = REPO_ROOT / "README.md"
|
|
26
|
+
PYPROJECT_PATH = REPO_ROOT / "pyproject.toml"
|
|
27
|
+
|
|
28
|
+
# The retired documentation host, built from two concatenated fragments
|
|
29
|
+
# split across the dot. This phase's success criteria require that a fresh
|
|
30
|
+
# repo-wide grep for the retired host find matches only in CHANGELOG.md. If
|
|
31
|
+
# this module spelled the host as one literal, it would itself be a
|
|
32
|
+
# permanent match, making that success criterion unverifiable without a
|
|
33
|
+
# permanent exclusion list. Splitting the literal keeps the repo-wide grep
|
|
34
|
+
# clean and honest.
|
|
35
|
+
_RETIRED_HOST_PREFIX = "yusabo90002.github"
|
|
36
|
+
_RETIRED_HOST_SUFFIX = "io"
|
|
37
|
+
_RETIRED_HOST = f"{_RETIRED_HOST_PREFIX}.{_RETIRED_HOST_SUFFIX}"
|
|
38
|
+
|
|
39
|
+
# Matches any URL on the Read the Docs documentation host.
|
|
40
|
+
_READTHEDOCS_URL_RE = re.compile(r"https://typsphinx\.readthedocs\.io/[^\s)\]]*")
|
|
41
|
+
|
|
42
|
+
# Matches a README deep link's suffix -- everything after `/en/latest/`.
|
|
43
|
+
_DEEP_LINK_RE = re.compile(r"https://typsphinx\.readthedocs\.io/en/latest/([^\s)\]]+)")
|
|
44
|
+
|
|
45
|
+
# A language segment (`/en/` or `/ja/`) anywhere in a URL -- used to
|
|
46
|
+
# partition Read the Docs URLs into "deep" (language-scoped) vs. top-level.
|
|
47
|
+
_LANGUAGE_SEGMENT_RE = re.compile(r"/(en|ja)/")
|
|
48
|
+
|
|
49
|
+
# A version-like path segment (`/latest/`, `/stable/`, `/v1.2.3/`, ...)
|
|
50
|
+
# appearing anywhere in a URL, independent of whether it is preceded by a
|
|
51
|
+
# language segment. Used to catch a top-level link that got version-pinned
|
|
52
|
+
# without also picking up a language prefix.
|
|
53
|
+
_VERSION_LIKE_SEGMENT_RE = re.compile(r"/(latest|stable|v?\d+\.\d+(?:\.\d+)?)(?:/|$)")
|
|
54
|
+
|
|
55
|
+
# The seven quick-link deep-link suffixes, in the order they must appear in
|
|
56
|
+
# README.md's "## Documentation" section. A dropped, reordered, or renamed
|
|
57
|
+
# suffix must fail this test, not just a wrong host.
|
|
58
|
+
_EXPECTED_DEEP_LINK_SUFFIXES = (
|
|
59
|
+
"installation.html",
|
|
60
|
+
"quickstart.html",
|
|
61
|
+
"user_guide/",
|
|
62
|
+
"user_guide/configuration.html",
|
|
63
|
+
"examples/",
|
|
64
|
+
"api/",
|
|
65
|
+
"contributing.html",
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _read_readme_text() -> str:
|
|
70
|
+
return README_PATH.read_text(encoding="utf-8")
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def test_readme_documentation_links_point_at_readthedocs():
|
|
74
|
+
"""README.md must contain no URL on the retired documentation host.
|
|
75
|
+
|
|
76
|
+
Guards against a vacuous pass: asserts README.md actually contains at
|
|
77
|
+
least one Read the Docs URL before asserting the retired host is
|
|
78
|
+
absent, so an accidental deletion of the whole Documentation section
|
|
79
|
+
would not silently satisfy this test.
|
|
80
|
+
"""
|
|
81
|
+
text = _read_readme_text()
|
|
82
|
+
readthedocs_urls = _READTHEDOCS_URL_RE.findall(text)
|
|
83
|
+
assert readthedocs_urls, (
|
|
84
|
+
"README.md contains no typsphinx.readthedocs.io URLs at all -- has "
|
|
85
|
+
"the Documentation section been removed?"
|
|
86
|
+
)
|
|
87
|
+
assert _RETIRED_HOST not in text, (
|
|
88
|
+
f"README.md still references the retired documentation host "
|
|
89
|
+
f"({_RETIRED_HOST}) -- Phase 31 rewrote every link to Read the "
|
|
90
|
+
"Docs; see 31-03-SUMMARY.md."
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def test_readme_deep_links_carry_language_version_prefix():
|
|
95
|
+
"""README.md must have exactly 7 `/en/latest/` deep links, in order.
|
|
96
|
+
|
|
97
|
+
Asserts both the count and the exact suffixes so a dropped, reordered,
|
|
98
|
+
or silently-retargeted link is caught -- not just a wrong host.
|
|
99
|
+
"""
|
|
100
|
+
text = _read_readme_text()
|
|
101
|
+
deep_links = _DEEP_LINK_RE.findall(text)
|
|
102
|
+
assert len(deep_links) == 7, (
|
|
103
|
+
f"expected exactly 7 README deep links under /en/latest/, found "
|
|
104
|
+
f"{len(deep_links)}: {deep_links}"
|
|
105
|
+
)
|
|
106
|
+
assert deep_links == list(_EXPECTED_DEEP_LINK_SUFFIXES), (
|
|
107
|
+
"README.md's /en/latest/ deep links do not match the expected "
|
|
108
|
+
f"suffixes in order. Expected {_EXPECTED_DEEP_LINK_SUFFIXES!r}, "
|
|
109
|
+
f"found {tuple(deep_links)!r}."
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def test_readme_top_level_links_carry_no_version_segment():
|
|
114
|
+
"""Top-level README Read the Docs links must stay version-less.
|
|
115
|
+
|
|
116
|
+
D-11: the badge click-through, header link, and Documentation section
|
|
117
|
+
lead sentence deliberately carry no `/en/` or `/ja/` segment, so Read
|
|
118
|
+
the Docs' root redirect follows the project's Default Version --
|
|
119
|
+
Phase 33's `latest` -> `stable` flip reaches these links without
|
|
120
|
+
another edit. A future "fix" that pins them to an explicit version
|
|
121
|
+
would undo that design, not tidy it.
|
|
122
|
+
"""
|
|
123
|
+
text = _read_readme_text()
|
|
124
|
+
all_urls = _READTHEDOCS_URL_RE.findall(text)
|
|
125
|
+
top_level_urls = [url for url in all_urls if not _LANGUAGE_SEGMENT_RE.search(url)]
|
|
126
|
+
assert top_level_urls, (
|
|
127
|
+
"found no version-less typsphinx.readthedocs.io URLs in README.md "
|
|
128
|
+
"-- expected the badge click-through, header link, and section "
|
|
129
|
+
"lead sentence to all be bare-root links (D-11)."
|
|
130
|
+
)
|
|
131
|
+
version_pinned = [
|
|
132
|
+
url for url in top_level_urls if _VERSION_LIKE_SEGMENT_RE.search(url)
|
|
133
|
+
]
|
|
134
|
+
assert not version_pinned, (
|
|
135
|
+
"top-level README links must stay version-less so Read the Docs' "
|
|
136
|
+
"Default Version flip (Phase 33) propagates without a re-edit "
|
|
137
|
+
f"(D-11); found a version-like segment on: {version_pinned}"
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def test_pyproject_documentation_url_points_at_readthedocs():
|
|
142
|
+
"""pyproject.toml's `project.urls.Documentation` must address Read the Docs."""
|
|
143
|
+
with open(PYPROJECT_PATH, "rb") as f:
|
|
144
|
+
data = tomllib.load(f)
|
|
145
|
+
documentation_url = data["project"]["urls"]["Documentation"]
|
|
146
|
+
assert "typsphinx.readthedocs.io" in documentation_url, (
|
|
147
|
+
f"pyproject.toml's project.urls.Documentation is "
|
|
148
|
+
f"{documentation_url!r}, which does not address "
|
|
149
|
+
"typsphinx.readthedocs.io -- PyPI's release metadata would "
|
|
150
|
+
"advertise a stale documentation link."
|
|
151
|
+
)
|
|
@@ -126,6 +126,12 @@ def table_width_render_gate_dir(fixtures_dir):
|
|
|
126
126
|
return fixtures_dir / "table_width_render_gate"
|
|
127
127
|
|
|
128
128
|
|
|
129
|
+
@pytest.fixture
|
|
130
|
+
def captioned_table_render_gate_dir(fixtures_dir):
|
|
131
|
+
"""Return the path to the captioned_table_render_gate fixture project."""
|
|
132
|
+
return fixtures_dir / "captioned_table_render_gate"
|
|
133
|
+
|
|
134
|
+
|
|
129
135
|
@pytest.fixture
|
|
130
136
|
def temp_build_dir(tmp_path):
|
|
131
137
|
"""Provide a temporary directory for build output."""
|
|
@@ -2333,3 +2339,325 @@ class TestTableWidthRenderGate:
|
|
|
2333
2339
|
"rendered PDF text -- table-width markup/code-mode "
|
|
2334
2340
|
"regression"
|
|
2335
2341
|
)
|
|
2342
|
+
|
|
2343
|
+
|
|
2344
|
+
# ---------------------------------------------------------------------------
|
|
2345
|
+
# Phase 25 (TBL-01, TBL-02, GATE-01): extend the render-gate pattern to prove
|
|
2346
|
+
# the Plan 25-01 depart_table figure-wrap + single <label> fix renders
|
|
2347
|
+
# correctly through a real sphinx-build -> typst.compile() -> pypdf
|
|
2348
|
+
# round-trip -- two consecutive captioned tables (the stale-buffer proof:
|
|
2349
|
+
# 25-RESEARCH.md Verified Mechanism 2), a caption+:width: composition case,
|
|
2350
|
+
# a :numref:/:ref:-resolves case, and a captioned csv-table + list-table
|
|
2351
|
+
# case (D-05). ``TestCaptionedTablePreFixBasisFailureProof`` below closes
|
|
2352
|
+
# D-06's durable fail-pre-fix red->green requirement by reconstructing the
|
|
2353
|
+
# two pre-fix defect shapes from first principles now that Plan 25-01's fix
|
|
2354
|
+
# has replaced the buggy code -- the double-anchor collision (Critical
|
|
2355
|
+
# Pitfall 3, 25-RESEARCH.md) and the stale-buffer caption loss (Verified
|
|
2356
|
+
# Mechanism 2) -- mirroring the ``TestPreFixBasisFailureProof`` convention
|
|
2357
|
+
# already established in tests/test_package_only_config_gate.py.
|
|
2358
|
+
# ---------------------------------------------------------------------------
|
|
2359
|
+
|
|
2360
|
+
# Sentinel tokens for the captioned-table render gate -- distinctive,
|
|
2361
|
+
# ALLCAPS-no-space words chosen so `full_text.count(...)` can prove each
|
|
2362
|
+
# caption reached the compiled PDF exactly once (guarding against both the
|
|
2363
|
+
# stray-heading bug -- Verified Mechanism 1 -- and the 2nd-table
|
|
2364
|
+
# stale-buffer caption-loss bug -- Verified Mechanism 2). Must match the
|
|
2365
|
+
# sentinel tokens embedded in captioned_table_render_gate/index.rst.
|
|
2366
|
+
TBLCAP_FIRST_SENTINEL = "TBLCAPFIRSTSENTINEL"
|
|
2367
|
+
TBLCAP_SECOND_SENTINEL = "TBLCAPSECONDSENTINEL"
|
|
2368
|
+
TBLCAP_WIDTH_SENTINEL = "TBLCAPWIDTHSENTINEL"
|
|
2369
|
+
TBLCAP_CSV_SENTINEL = "TBLCAPCSVSENTINEL"
|
|
2370
|
+
TBLCAP_LIST_SENTINEL = "TBLCAPLISTSENTINEL"
|
|
2371
|
+
|
|
2372
|
+
|
|
2373
|
+
class _CaptionedTableRenderGateArtifacts:
|
|
2374
|
+
"""
|
|
2375
|
+
Plain container for the one real-compile artifact shared by every
|
|
2376
|
+
``TestCaptionedTableRenderGate`` test method: the emitted Typst source
|
|
2377
|
+
text and the pypdf-extracted PDF text. A plain class (not a dict/tuple)
|
|
2378
|
+
so each thin test method below can read ``.typ_source``/``.full_text``
|
|
2379
|
+
by name.
|
|
2380
|
+
"""
|
|
2381
|
+
|
|
2382
|
+
def __init__(self, typ_source: str, full_text: str) -> None:
|
|
2383
|
+
self.typ_source = typ_source
|
|
2384
|
+
self.full_text = full_text
|
|
2385
|
+
|
|
2386
|
+
|
|
2387
|
+
@pytest.fixture(scope="class")
|
|
2388
|
+
def captioned_table_render_gate_artifacts(tmp_path_factory):
|
|
2389
|
+
"""
|
|
2390
|
+
Build + real-compile the captioned_table_render_gate fixture ONCE per
|
|
2391
|
+
class and return both the emitted Typst source text and the
|
|
2392
|
+
pypdf-extracted PDF text.
|
|
2393
|
+
|
|
2394
|
+
Class-scoped so the four thin test methods below each assert a
|
|
2395
|
+
disjoint slice of the SAME real-compile artifact rather than
|
|
2396
|
+
re-running sphinx-build/typst.compile() four times (mirrors the
|
|
2397
|
+
``topic_line_block_render_gate_pdf_text`` class-scoped fixture pattern
|
|
2398
|
+
above). Depends only on ``tmp_path_factory`` (session-scoped-
|
|
2399
|
+
compatible) -- not on the narrower, function-scoped
|
|
2400
|
+
``captioned_table_render_gate_dir``/``fixtures_dir`` fixtures, to avoid
|
|
2401
|
+
a pytest ScopeMismatch (a class-scoped fixture may not depend on a
|
|
2402
|
+
narrower, function-scoped one).
|
|
2403
|
+
"""
|
|
2404
|
+
source_dir = Path(__file__).parent / "fixtures" / "captioned_table_render_gate"
|
|
2405
|
+
build_dir = tmp_path_factory.mktemp("captioned_table_gate") / "_build"
|
|
2406
|
+
|
|
2407
|
+
result = _run_sphinx_build_typst(source_dir, build_dir)
|
|
2408
|
+
assert (
|
|
2409
|
+
result.returncode == 0
|
|
2410
|
+
), f"sphinx-build failed:\nstdout: {result.stdout}\nstderr: {result.stderr}"
|
|
2411
|
+
|
|
2412
|
+
index_typ = build_dir / "index.typ"
|
|
2413
|
+
assert index_typ.exists(), "index.typ was not generated"
|
|
2414
|
+
typ_source = index_typ.read_text(encoding="utf-8")
|
|
2415
|
+
|
|
2416
|
+
# Compile the emitted .typ to PDF with typst-py, WITHOUT try/except:
|
|
2417
|
+
# this is the crux of GATE-01 -- the double-anchor Critical Pitfall 3
|
|
2418
|
+
# fatal (a captioned table's ids[0] defined twice) would abort the
|
|
2419
|
+
# ENTIRE compile here, failing every dependent test method loudly,
|
|
2420
|
+
# rather than merely failing a string-agreement check.
|
|
2421
|
+
pdf_output = build_dir / "index.pdf"
|
|
2422
|
+
typst.compile(str(index_typ), output=str(pdf_output))
|
|
2423
|
+
|
|
2424
|
+
assert pdf_output.exists(), "PDF file was not created"
|
|
2425
|
+
assert pdf_output.stat().st_size > 0, "PDF file is empty"
|
|
2426
|
+
with open(pdf_output, "rb") as f:
|
|
2427
|
+
magic = f.read(4)
|
|
2428
|
+
assert magic == b"%PDF", "Generated file is not a valid PDF"
|
|
2429
|
+
|
|
2430
|
+
reader = pypdf.PdfReader(str(pdf_output))
|
|
2431
|
+
full_text = "\n".join(page.extract_text() for page in reader.pages)
|
|
2432
|
+
|
|
2433
|
+
return _CaptionedTableRenderGateArtifacts(typ_source, full_text)
|
|
2434
|
+
|
|
2435
|
+
|
|
2436
|
+
@pytest.mark.slow
|
|
2437
|
+
@pytest.mark.skipif(
|
|
2438
|
+
not (TYPST_AVAILABLE and PYPDF_AVAILABLE),
|
|
2439
|
+
reason="typst-py and pypdf are both required for the GATE-01 render gate",
|
|
2440
|
+
)
|
|
2441
|
+
class TestCaptionedTableRenderGate:
|
|
2442
|
+
"""
|
|
2443
|
+
Real-compile acceptance gate for TBL-01/TBL-02: the Plan 25-01
|
|
2444
|
+
depart_table figure-wrap + single <label> fix, covering two
|
|
2445
|
+
consecutive captioned tables (the stale-buffer proof), a
|
|
2446
|
+
caption+:width: composition case, a :numref:/:ref:-resolves case, and
|
|
2447
|
+
a captioned csv-table + list-table case (D-05).
|
|
2448
|
+
|
|
2449
|
+
Requirements: TBL-01, TBL-02, GATE-01 (25-CONTEXT.md, 25-RESEARCH.md
|
|
2450
|
+
Validation Architecture, 25-01-SUMMARY.md).
|
|
2451
|
+
|
|
2452
|
+
All four test methods share the SAME real-compile artifact via the
|
|
2453
|
+
class-scoped ``captioned_table_render_gate_artifacts`` fixture above --
|
|
2454
|
+
sphinx-build/typst.compile() run exactly once per class, not once per
|
|
2455
|
+
test method.
|
|
2456
|
+
"""
|
|
2457
|
+
|
|
2458
|
+
def test_each_caption_sentinel_appears_exactly_once(
|
|
2459
|
+
self, captioned_table_render_gate_artifacts
|
|
2460
|
+
):
|
|
2461
|
+
"""
|
|
2462
|
+
Each of the five caption sentinels appears EXACTLY ONCE in the
|
|
2463
|
+
extracted PDF text -- proving neither the stray-heading bug
|
|
2464
|
+
(Verified Mechanism 1) nor the 2nd-table stale-buffer
|
|
2465
|
+
caption-loss bug (Verified Mechanism 2) regressed. The 2nd
|
|
2466
|
+
table's sentinel (TBLCAPSECONDSENTINEL) is the specific
|
|
2467
|
+
stale-buffer proof: a single-table fixture cannot expose it.
|
|
2468
|
+
"""
|
|
2469
|
+
full_text = captioned_table_render_gate_artifacts.full_text
|
|
2470
|
+
|
|
2471
|
+
for sentinel in (
|
|
2472
|
+
TBLCAP_FIRST_SENTINEL,
|
|
2473
|
+
TBLCAP_SECOND_SENTINEL,
|
|
2474
|
+
TBLCAP_WIDTH_SENTINEL,
|
|
2475
|
+
TBLCAP_CSV_SENTINEL,
|
|
2476
|
+
TBLCAP_LIST_SENTINEL,
|
|
2477
|
+
):
|
|
2478
|
+
assert full_text.count(sentinel) == 1, (
|
|
2479
|
+
f"Expected caption sentinel '{sentinel}' exactly once in "
|
|
2480
|
+
"extracted PDF text -- possible stray-heading or "
|
|
2481
|
+
"stale-buffer caption-loss regression (TBL-01)"
|
|
2482
|
+
)
|
|
2483
|
+
|
|
2484
|
+
def test_typ_source_uses_figure_kind_table_with_no_heading_above(
|
|
2485
|
+
self, captioned_table_render_gate_artifacts
|
|
2486
|
+
):
|
|
2487
|
+
"""
|
|
2488
|
+
The emitted index.typ wraps every captioned table in
|
|
2489
|
+
figure(..., kind: table) -- native Typst "Table N" numbering --
|
|
2490
|
+
and no heading() call appears anywhere in the captioned-table
|
|
2491
|
+
region (from the first figure( call onward): this fixture's only
|
|
2492
|
+
heading() is the document's own top-level title, which precedes
|
|
2493
|
+
every table (SC#1).
|
|
2494
|
+
"""
|
|
2495
|
+
typ_source = captioned_table_render_gate_artifacts.typ_source
|
|
2496
|
+
|
|
2497
|
+
assert "figure(" in typ_source, (
|
|
2498
|
+
"Expected 'figure(' in generated Typst source -- depart_table "
|
|
2499
|
+
"figure-wrap regression"
|
|
2500
|
+
)
|
|
2501
|
+
assert "kind: table" in typ_source, (
|
|
2502
|
+
"Expected 'kind: table' in generated Typst source -- "
|
|
2503
|
+
"depart_table figure-wrap regression"
|
|
2504
|
+
)
|
|
2505
|
+
|
|
2506
|
+
first_figure_index = typ_source.index("figure(")
|
|
2507
|
+
captioned_table_region = typ_source[first_figure_index:]
|
|
2508
|
+
assert "heading(" not in captioned_table_region, (
|
|
2509
|
+
"Found a stray 'heading(' call inside the captioned-table "
|
|
2510
|
+
"region -- a table caption fell through to the generic "
|
|
2511
|
+
"section-heading path instead of being figure-wrapped "
|
|
2512
|
+
"(Verified Mechanism 1 regression)"
|
|
2513
|
+
)
|
|
2514
|
+
|
|
2515
|
+
def test_xref_numref_ref_resolve_no_empty_link(
|
|
2516
|
+
self, captioned_table_render_gate_artifacts
|
|
2517
|
+
):
|
|
2518
|
+
"""
|
|
2519
|
+
The :numref:/:ref: cross-references to the first captioned table
|
|
2520
|
+
both resolve to a real link(<label>, ...) anchor reference in the
|
|
2521
|
+
generated Typst source, with no link("", ...) leak -- mirrors
|
|
2522
|
+
TestXrefRefidRenderGate's D-04 contract, applied to a captioned
|
|
2523
|
+
table's own <label> instead of a section/glossary anchor.
|
|
2524
|
+
"""
|
|
2525
|
+
typ_source = captioned_table_render_gate_artifacts.typ_source
|
|
2526
|
+
|
|
2527
|
+
assert "link(<" in typ_source, (
|
|
2528
|
+
"Expected at least one refid link(<...>, ...) anchor "
|
|
2529
|
+
"reference in the generated Typst source -- the "
|
|
2530
|
+
":numref:/:ref: to a captioned table may have regressed"
|
|
2531
|
+
)
|
|
2532
|
+
assert 'link("",' not in typ_source, (
|
|
2533
|
+
'Found link("", ...) in generated Typst source -- the '
|
|
2534
|
+
'never-link("", ...) contract has regressed'
|
|
2535
|
+
)
|
|
2536
|
+
|
|
2537
|
+
def test_no_literal_source_leak(self, captioned_table_render_gate_artifacts):
|
|
2538
|
+
"""No LEAK_SIGNATURES token reaches the extracted PDF text."""
|
|
2539
|
+
full_text = captioned_table_render_gate_artifacts.full_text
|
|
2540
|
+
|
|
2541
|
+
for leaked_token in LEAK_SIGNATURES:
|
|
2542
|
+
assert leaked_token not in full_text, (
|
|
2543
|
+
f"Literal Typst source '{leaked_token}' leaked into "
|
|
2544
|
+
"rendered PDF text -- captioned-table markup/code-mode "
|
|
2545
|
+
"regression"
|
|
2546
|
+
)
|
|
2547
|
+
|
|
2548
|
+
|
|
2549
|
+
@pytest.mark.slow
|
|
2550
|
+
@pytest.mark.skipif(
|
|
2551
|
+
not TYPST_AVAILABLE,
|
|
2552
|
+
reason="typst-py is required for the GATE-01 pre-fix-basis failure proof",
|
|
2553
|
+
)
|
|
2554
|
+
class TestCaptionedTablePreFixBasisFailureProof:
|
|
2555
|
+
"""
|
|
2556
|
+
Standing pre-fix-basis failure proof (D-06): reconstructs the two
|
|
2557
|
+
pre-fix defect shapes from first principles -- the buggy code no
|
|
2558
|
+
longer exists post Plan 25-01 -- and proves each one is genuinely
|
|
2559
|
+
fail-pre-fix, mirroring the
|
|
2560
|
+
``tests/test_package_only_config_gate.py::TestPreFixBasisFailureProof``
|
|
2561
|
+
convention (reconstruct the pre-fix compile basis; never assert on the
|
|
2562
|
+
TEXT of a compiler error message).
|
|
2563
|
+
|
|
2564
|
+
(a) Double-anchor basis (Critical Pitfall 3, 25-RESEARCH.md): a
|
|
2565
|
+
minimal Typst source in which one label id is defined TWICE -- once
|
|
2566
|
+
as a ``[#metadata(none) <dup>]`` anchor (the unconditional
|
|
2567
|
+
``_emit_id_anchors`` call ``visit_table`` used to make even for a
|
|
2568
|
+
captioned table) and once as a ``figure(...) <dup>`` postfix (the
|
|
2569
|
+
NEW figure-wrap self-anchor this phase adds) -- plus a
|
|
2570
|
+
``link(<dup>, ...)`` reference to that label, reproducing the actual
|
|
2571
|
+
:numref:/:ref: xref shape that resolves it. Live-verified while
|
|
2572
|
+
writing this module: Typst does NOT raise merely from defining a
|
|
2573
|
+
label twice with no reference to it; the "label ... occurs multiple
|
|
2574
|
+
times" fatal fires only when something (here, the xref's own
|
|
2575
|
+
``link(<dup>, ...)``) tries to RESOLVE the ambiguous label -- exactly
|
|
2576
|
+
the real-world shape a :numref:/:ref: to a captioned table produces.
|
|
2577
|
+
|
|
2578
|
+
(b) Stale-buffer basis (Verified Mechanism 2, 25-RESEARCH.md): a
|
|
2579
|
+
reconstructed pre-fix two-table Typst source -- a plain ``table(...)``
|
|
2580
|
+
for each, a stray ``heading(...)`` above the first (the OLD
|
|
2581
|
+
stray-heading caption bug), and the SECOND caption sentinel
|
|
2582
|
+
deliberately ABSENT (the observed pre-fix stale-buffer shape, where a
|
|
2583
|
+
2nd table's caption silently vanished into a leftover buffer from the
|
|
2584
|
+
first). This half is a pure string reconstruction (no compile needed)
|
|
2585
|
+
proving the positive gate's ``full_text.count(sentinel) == 1``
|
|
2586
|
+
assertion is genuinely fail-pre-fix: applied to this reconstruction,
|
|
2587
|
+
the second sentinel's count is 0, not 1.
|
|
2588
|
+
|
|
2589
|
+
Requirements: TBL-01, TBL-02, D-06.
|
|
2590
|
+
"""
|
|
2591
|
+
|
|
2592
|
+
def test_double_anchor_basis_raises(self, tmp_path):
|
|
2593
|
+
"""
|
|
2594
|
+
BUG basis (Critical Pitfall 3): reconstruct a captioned table
|
|
2595
|
+
whose id is anchored TWICE -- once via the old unconditional
|
|
2596
|
+
``_emit_id_anchors`` metadata anchor, once via the new figure
|
|
2597
|
+
``<label>`` postfix -- with a ``link(<dup>, ...)`` xref
|
|
2598
|
+
resolving it, and assert a real ``typst.compile()`` RAISES. No
|
|
2599
|
+
assertion matches the exception's message text (D-06) -- only
|
|
2600
|
+
that it raises.
|
|
2601
|
+
"""
|
|
2602
|
+
reconstructed = (
|
|
2603
|
+
"#metadata(none) <dup>\n"
|
|
2604
|
+
"\n"
|
|
2605
|
+
"#figure(\n"
|
|
2606
|
+
" table(\n"
|
|
2607
|
+
" columns: (1fr, 1fr),\n"
|
|
2608
|
+
" [A], [B],\n"
|
|
2609
|
+
" ),\n"
|
|
2610
|
+
" caption: [Caption],\n"
|
|
2611
|
+
" kind: table,\n"
|
|
2612
|
+
") <dup>\n"
|
|
2613
|
+
"\n"
|
|
2614
|
+
"See #link(<dup>, [Ref]).\n"
|
|
2615
|
+
)
|
|
2616
|
+
target = tmp_path / "double_anchor_basis.typ"
|
|
2617
|
+
target.write_text(reconstructed, encoding="utf-8")
|
|
2618
|
+
|
|
2619
|
+
with pytest.raises(Exception):
|
|
2620
|
+
typst.compile(str(target), root=str(tmp_path))
|
|
2621
|
+
|
|
2622
|
+
def test_stale_buffer_basis_second_sentinel_count_is_zero(self):
|
|
2623
|
+
"""
|
|
2624
|
+
Stale-buffer basis (Verified Mechanism 2): a reconstructed pre-fix
|
|
2625
|
+
two-table Typst source in which the SECOND table's caption
|
|
2626
|
+
sentinel is deliberately absent (the observed pre-fix shape --
|
|
2627
|
+
the second table's caption silently vanished into a stale
|
|
2628
|
+
``table_cell_content`` buffer left over from the first table).
|
|
2629
|
+
Asserts the positive gate's ``count(...) == 1`` check FAILS
|
|
2630
|
+
(count is 0) against this reconstruction -- proof the gate is
|
|
2631
|
+
genuinely fail-pre-fix, not vacuously green.
|
|
2632
|
+
"""
|
|
2633
|
+
reconstructed_pre_fix_shape = (
|
|
2634
|
+
"heading(level: 1, {text(" + repr(TBLCAP_FIRST_SENTINEL) + ")})\n"
|
|
2635
|
+
"\n"
|
|
2636
|
+
"table(\n"
|
|
2637
|
+
" columns: (1fr, 1fr),\n"
|
|
2638
|
+
" [A], [B],\n"
|
|
2639
|
+
")\n"
|
|
2640
|
+
"\n"
|
|
2641
|
+
"table(\n"
|
|
2642
|
+
" columns: (1fr, 1fr),\n"
|
|
2643
|
+
" [C], [D],\n"
|
|
2644
|
+
")\n"
|
|
2645
|
+
)
|
|
2646
|
+
|
|
2647
|
+
# The pre-fix stray-heading bug: the FIRST table's caption leaked
|
|
2648
|
+
# into a heading() call (Verified Mechanism 1) instead of a
|
|
2649
|
+
# figure caption -- present here to keep the reconstruction
|
|
2650
|
+
# faithful to the actually-observed pre-fix output shape.
|
|
2651
|
+
assert TBLCAP_FIRST_SENTINEL in reconstructed_pre_fix_shape
|
|
2652
|
+
|
|
2653
|
+
# The proof this test exists for: the SECOND table's caption
|
|
2654
|
+
# sentinel never reached the reconstructed output at all -- the
|
|
2655
|
+
# stale-buffer bug swallowed it. count() is 0, not 1, so the
|
|
2656
|
+
# positive gate's exact-once assertion would have failed loudly
|
|
2657
|
+
# against this pre-fix basis.
|
|
2658
|
+
assert reconstructed_pre_fix_shape.count(TBLCAP_SECOND_SENTINEL) == 0, (
|
|
2659
|
+
"Expected the reconstructed pre-fix stale-buffer basis to have "
|
|
2660
|
+
"ZERO occurrences of the second table's caption sentinel -- "
|
|
2661
|
+
"if this fails, the reconstruction no longer reproduces the "
|
|
2662
|
+
"pre-fix bug shape and the fail-pre-fix proof is void"
|
|
2663
|
+
)
|