typsphinx 0.6.2__tar.gz → 0.6.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 (100) hide show
  1. {typsphinx-0.6.2 → typsphinx-0.6.3}/PKG-INFO +2 -3
  2. {typsphinx-0.6.2 → typsphinx-0.6.3}/README.md +1 -2
  3. {typsphinx-0.6.2 → typsphinx-0.6.3}/pyproject.toml +1 -1
  4. {typsphinx-0.6.2 → typsphinx-0.6.3}/tests/test_pdf_render_gate.py +328 -0
  5. {typsphinx-0.6.2 → typsphinx-0.6.3}/tests/test_preview_version_sync.py +44 -1
  6. {typsphinx-0.6.2 → typsphinx-0.6.3}/tests/test_template_engine.py +428 -0
  7. {typsphinx-0.6.2 → typsphinx-0.6.3}/tests/test_translator.py +309 -0
  8. typsphinx-0.6.3/tests/test_typst_elements_pass_through_gate.py +422 -0
  9. typsphinx-0.6.3/tests/test_typst_lang_gate.py +860 -0
  10. {typsphinx-0.6.2 → typsphinx-0.6.3}/typsphinx/__init__.py +0 -1
  11. {typsphinx-0.6.2 → typsphinx-0.6.3}/typsphinx/template_engine.py +267 -24
  12. {typsphinx-0.6.2 → typsphinx-0.6.3}/typsphinx/templates/base.typ +2 -1
  13. {typsphinx-0.6.2 → typsphinx-0.6.3}/typsphinx/translator.py +167 -25
  14. {typsphinx-0.6.2 → typsphinx-0.6.3}/typsphinx/writer.py +51 -6
  15. {typsphinx-0.6.2 → typsphinx-0.6.3}/typsphinx.egg-info/PKG-INFO +2 -3
  16. {typsphinx-0.6.2 → typsphinx-0.6.3}/typsphinx.egg-info/SOURCES.txt +2 -2
  17. typsphinx-0.6.2/tests/test_config_toctree_defaults.py +0 -245
  18. typsphinx-0.6.2/tests/test_documentation_configuration.py +0 -156
  19. {typsphinx-0.6.2 → typsphinx-0.6.3}/LICENSE +0 -0
  20. {typsphinx-0.6.2 → typsphinx-0.6.3}/setup.cfg +0 -0
  21. {typsphinx-0.6.2 → typsphinx-0.6.3}/tests/test_abbr_pep_separator_render_gate.py +0 -0
  22. {typsphinx-0.6.2 → typsphinx-0.6.3}/tests/test_admonitions.py +0 -0
  23. {typsphinx-0.6.2 → typsphinx-0.6.3}/tests/test_builder.py +0 -0
  24. {typsphinx-0.6.2 → typsphinx-0.6.3}/tests/test_builder_output_stem.py +0 -0
  25. {typsphinx-0.6.2 → typsphinx-0.6.3}/tests/test_builder_requirement13.py +0 -0
  26. {typsphinx-0.6.2 → typsphinx-0.6.3}/tests/test_codly_caption_listitem_leak_render_gate.py +0 -0
  27. {typsphinx-0.6.2 → typsphinx-0.6.3}/tests/test_config.py +0 -0
  28. {typsphinx-0.6.2 → typsphinx-0.6.3}/tests/test_config_other_options.py +0 -0
  29. {typsphinx-0.6.2 → typsphinx-0.6.3}/tests/test_config_template_mapping.py +0 -0
  30. {typsphinx-0.6.2 → typsphinx-0.6.3}/tests/test_confval_field_body_render_gate.py +0 -0
  31. {typsphinx-0.6.2 → typsphinx-0.6.3}/tests/test_confval_field_spacing_render_gate.py +0 -0
  32. {typsphinx-0.6.2 → typsphinx-0.6.3}/tests/test_corpus_gate.py +0 -0
  33. {typsphinx-0.6.2 → typsphinx-0.6.3}/tests/test_cross_doc_label_namespace_render_gate.py +0 -0
  34. {typsphinx-0.6.2 → typsphinx-0.6.3}/tests/test_deflist_definition_multiblock_render_gate.py +0 -0
  35. {typsphinx-0.6.2 → typsphinx-0.6.3}/tests/test_deflist_nested_definition_render_gate.py +0 -0
  36. {typsphinx-0.6.2 → typsphinx-0.6.3}/tests/test_deflist_term_concat_render_gate.py +0 -0
  37. {typsphinx-0.6.2 → typsphinx-0.6.3}/tests/test_deflist_term_inline_children_gate.py +0 -0
  38. {typsphinx-0.6.2 → typsphinx-0.6.3}/tests/test_desc_bodyless_concat_render_gate.py +0 -0
  39. {typsphinx-0.6.2 → typsphinx-0.6.3}/tests/test_desc_container_propagated_target_render_gate.py +0 -0
  40. {typsphinx-0.6.2 → typsphinx-0.6.3}/tests/test_desc_sig_space_render_gate.py +0 -0
  41. {typsphinx-0.6.2 → typsphinx-0.6.3}/tests/test_desc_signature_anchor_render_gate.py +0 -0
  42. {typsphinx-0.6.2 → typsphinx-0.6.3}/tests/test_desc_signature_concat_render_gate.py +0 -0
  43. {typsphinx-0.6.2 → typsphinx-0.6.3}/tests/test_documentation_installation.py +0 -0
  44. {typsphinx-0.6.2 → typsphinx-0.6.3}/tests/test_documentation_usage.py +0 -0
  45. {typsphinx-0.6.2 → typsphinx-0.6.3}/tests/test_duplicate_include_label_render_gate.py +0 -0
  46. {typsphinx-0.6.2 → typsphinx-0.6.3}/tests/test_entry_points.py +0 -0
  47. {typsphinx-0.6.2 → typsphinx-0.6.3}/tests/test_epigraph_render_gate.py +0 -0
  48. {typsphinx-0.6.2 → typsphinx-0.6.3}/tests/test_examples_basic.py +0 -0
  49. {typsphinx-0.6.2 → typsphinx-0.6.3}/tests/test_examples_charged_ieee_gate.py +0 -0
  50. {typsphinx-0.6.2 → typsphinx-0.6.3}/tests/test_extension.py +0 -0
  51. {typsphinx-0.6.2 → typsphinx-0.6.3}/tests/test_external_link_style_render_gate.py +0 -0
  52. {typsphinx-0.6.2 → typsphinx-0.6.3}/tests/test_field_list_in_list_item_render_gate.py +0 -0
  53. {typsphinx-0.6.2 → typsphinx-0.6.3}/tests/test_footnotes.py +0 -0
  54. {typsphinx-0.6.2 → typsphinx-0.6.3}/tests/test_glob_image_render_gate.py +0 -0
  55. {typsphinx-0.6.2 → typsphinx-0.6.3}/tests/test_inline_literal_overflow_render_gate.py +0 -0
  56. {typsphinx-0.6.2 → typsphinx-0.6.3}/tests/test_inline_references.py +0 -0
  57. {typsphinx-0.6.2 → typsphinx-0.6.3}/tests/test_integration_advanced.py +0 -0
  58. {typsphinx-0.6.2 → typsphinx-0.6.3}/tests/test_integration_basic.py +0 -0
  59. {typsphinx-0.6.2 → typsphinx-0.6.3}/tests/test_integration_multi_doc.py +0 -0
  60. {typsphinx-0.6.2 → typsphinx-0.6.3}/tests/test_integration_nested_toctree.py +0 -0
  61. {typsphinx-0.6.2 → typsphinx-0.6.3}/tests/test_label_at_char_render_gate.py +0 -0
  62. {typsphinx-0.6.2 → typsphinx-0.6.3}/tests/test_line_blocks.py +0 -0
  63. {typsphinx-0.6.2 → typsphinx-0.6.3}/tests/test_list_item_nested_block_render_gate.py +0 -0
  64. {typsphinx-0.6.2 → typsphinx-0.6.3}/tests/test_math_fallback.py +0 -0
  65. {typsphinx-0.6.2 → typsphinx-0.6.3}/tests/test_math_mitex.py +0 -0
  66. {typsphinx-0.6.2 → typsphinx-0.6.3}/tests/test_math_native.py +0 -0
  67. {typsphinx-0.6.2 → typsphinx-0.6.3}/tests/test_missing_and_malformed_master_gate.py +0 -0
  68. {typsphinx-0.6.2 → typsphinx-0.6.3}/tests/test_nested_master_render_gate.py +0 -0
  69. {typsphinx-0.6.2 → typsphinx-0.6.3}/tests/test_nested_toctree_paths.py +0 -0
  70. {typsphinx-0.6.2 → typsphinx-0.6.3}/tests/test_package_only_config_gate.py +0 -0
  71. {typsphinx-0.6.2 → typsphinx-0.6.3}/tests/test_package_template_routing.py +0 -0
  72. {typsphinx-0.6.2 → typsphinx-0.6.3}/tests/test_paragraph_concat_render_gate.py +0 -0
  73. {typsphinx-0.6.2 → typsphinx-0.6.3}/tests/test_paragraph_propagated_target_render_gate.py +0 -0
  74. {typsphinx-0.6.2 → typsphinx-0.6.3}/tests/test_paragraph_soft_newline_render_gate.py +0 -0
  75. {typsphinx-0.6.2 → typsphinx-0.6.3}/tests/test_pdf_generation.py +0 -0
  76. {typsphinx-0.6.2 → typsphinx-0.6.3}/tests/test_preview_smoke_gate.py +0 -0
  77. {typsphinx-0.6.2 → typsphinx-0.6.3}/tests/test_readme_version_sync.py +0 -0
  78. {typsphinx-0.6.2 → typsphinx-0.6.3}/tests/test_ref_target_nested_list_render_gate.py +0 -0
  79. {typsphinx-0.6.2 → typsphinx-0.6.3}/tests/test_rubric_option_concat_render_gate.py +0 -0
  80. {typsphinx-0.6.2 → typsphinx-0.6.3}/tests/test_rubric_propagated_target_render_gate.py +0 -0
  81. {typsphinx-0.6.2 → typsphinx-0.6.3}/tests/test_static_asset_copy_gate.py +0 -0
  82. {typsphinx-0.6.2 → typsphinx-0.6.3}/tests/test_substitution_definition_render_gate.py +0 -0
  83. {typsphinx-0.6.2 → typsphinx-0.6.3}/tests/test_table_in_list_item_render_gate.py +0 -0
  84. {typsphinx-0.6.2 → typsphinx-0.6.3}/tests/test_target_label_render_gate.py +0 -0
  85. {typsphinx-0.6.2 → typsphinx-0.6.3}/tests/test_target_name_render_gate.py +0 -0
  86. {typsphinx-0.6.2 → typsphinx-0.6.3}/tests/test_template_assets.py +0 -0
  87. {typsphinx-0.6.2 → typsphinx-0.6.3}/tests/test_template_codly.py +0 -0
  88. {typsphinx-0.6.2 → typsphinx-0.6.3}/tests/test_template_import_path.py +0 -0
  89. {typsphinx-0.6.2 → typsphinx-0.6.3}/tests/test_template_mitex.py +0 -0
  90. {typsphinx-0.6.2 → typsphinx-0.6.3}/tests/test_toctree_requirement13.py +0 -0
  91. {typsphinx-0.6.2 → typsphinx-0.6.3}/tests/test_topics.py +0 -0
  92. {typsphinx-0.6.2 → typsphinx-0.6.3}/tests/test_typst_string_escape_gate.py +0 -0
  93. {typsphinx-0.6.2 → typsphinx-0.6.3}/tests/test_wide_table_render_gate.py +0 -0
  94. {typsphinx-0.6.2 → typsphinx-0.6.3}/tests/test_xref_orphan_degrade_render_gate.py +0 -0
  95. {typsphinx-0.6.2 → typsphinx-0.6.3}/typsphinx/builder.py +0 -0
  96. {typsphinx-0.6.2 → typsphinx-0.6.3}/typsphinx/pdf.py +0 -0
  97. {typsphinx-0.6.2 → typsphinx-0.6.3}/typsphinx.egg-info/dependency_links.txt +0 -0
  98. {typsphinx-0.6.2 → typsphinx-0.6.3}/typsphinx.egg-info/entry_points.txt +0 -0
  99. {typsphinx-0.6.2 → typsphinx-0.6.3}/typsphinx.egg-info/requires.txt +0 -0
  100. {typsphinx-0.6.2 → typsphinx-0.6.3}/typsphinx.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: typsphinx
3
- Version: 0.6.2
3
+ Version: 0.6.3
4
4
  Summary: Sphinx extension for Typst output
5
5
  Author-email: YuSabo <yusabo90002@gmail.com>
6
6
  License-Expression: MIT
@@ -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
 
@@ -357,5 +356,5 @@ See [CHANGELOG.md](CHANGELOG.md) for detailed version history.
357
356
 
358
357
  ---
359
358
 
360
- **Status**: Stable (v0.6.2) - Production ready
359
+ **Status**: Stable (v0.6.3) - Production ready
361
360
  **Python**: 3.12+ | **Sphinx**: 9.1+ | **Typst**: 0.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
 
@@ -313,5 +312,5 @@ See [CHANGELOG.md](CHANGELOG.md) for detailed version history.
313
312
 
314
313
  ---
315
314
 
316
- **Status**: Stable (v0.6.2) - Production ready
315
+ **Status**: Stable (v0.6.3) - Production ready
317
316
  **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.2"
7
+ version = "0.6.3"
8
8
  description = "Sphinx extension for Typst output"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.12"
@@ -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
+ )
@@ -1,5 +1,5 @@
1
1
  """
2
- Tests guarding the 3-way `@preview` version-sync hazard.
2
+ Tests guarding the `@preview` version-sync hazard.
3
3
 
4
4
  typsphinx declares the same four Typst Universe `@preview` package versions
5
5
  (codly, codly-languages, mitex, gentle-clues) in three separate places:
@@ -9,6 +9,17 @@ Typst documents can end up importing mismatched package versions depending
9
9
  on which code path produced them. This module asserts the three declaration
10
10
  sites agree, so a future single-file edit fails CI loudly instead of
11
11
  silently (D-03).
12
+
13
+ The bundled `examples/` templates are a fourth surface. They are not part of
14
+ the extension's own import generation, so they carry no lockstep *identity*
15
+ requirement across all four packages -- an example may legitimately use only
16
+ some of them, or a different package entirely (charged-ieee). But when an
17
+ example does pin one of the four, a stale pin is not cosmetic: it makes the
18
+ shipped sample fail to compile outright (`codly-languages` older than 0.1.10
19
+ aborts with `unknown variable: kai`). That is exactly what happened to
20
+ `examples/advanced/_templates/custom.typ`, which sat three milestones behind
21
+ the v0.5.0 bump because nothing watched it. `test_example_templates_match_canonical_versions`
22
+ closes that gap.
12
23
  """
13
24
 
14
25
  import re
@@ -20,6 +31,8 @@ WRITER_PATH = REPO_ROOT / "typsphinx" / "writer.py"
20
31
  TEMPLATE_ENGINE_PATH = REPO_ROOT / "typsphinx" / "template_engine.py"
21
32
  BASE_TYP_PATH = REPO_ROOT / "typsphinx" / "templates" / "base.typ"
22
33
 
34
+ EXAMPLES_DIR = REPO_ROOT / "examples"
35
+
23
36
  EXPECTED_PACKAGES = {"codly", "codly-languages", "mitex", "gentle-clues"}
24
37
 
25
38
  # Matches an actual Typst `#import "@preview/<name>:<version>"` statement
@@ -103,3 +116,33 @@ def test_all_four_packages_declared():
103
116
  f"{filename} is missing expected @preview packages: {missing} "
104
117
  f"(declared: {declared})"
105
118
  )
119
+
120
+
121
+ def test_example_templates_match_canonical_versions():
122
+ """Bundled `examples/` .typ templates must not pin a stale version.
123
+
124
+ Scans every `.typ` file under `examples/` and, for each of the four
125
+ packages typsphinx itself pins, asserts the example agrees with the
126
+ canonical version in `base.typ`. Packages outside that set (e.g. an
127
+ example built on `charged-ieee`) are ignored, and an example that
128
+ imports none of the four is trivially fine -- this guards against
129
+ *drift*, not against an example choosing a different toolkit.
130
+ """
131
+ canonical = _extract_preview_versions(BASE_TYP_PATH)
132
+
133
+ stale = []
134
+ for typ_path in sorted(EXAMPLES_DIR.rglob("*.typ")):
135
+ for package, version in _extract_preview_versions(typ_path).items():
136
+ if package not in canonical:
137
+ continue
138
+ if version != canonical[package]:
139
+ stale.append(
140
+ f"{typ_path.relative_to(REPO_ROOT)}: {package} pins "
141
+ f"{version}, base.typ pins {canonical[package]}"
142
+ )
143
+
144
+ assert not stale, (
145
+ "bundled example template(s) pin @preview versions that diverge from "
146
+ "typsphinx/templates/base.typ -- a shipped sample that cannot compile: "
147
+ + "; ".join(stale)
148
+ )