pdfmd-cli 3.30.32__tar.gz → 3.32.12__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 (351) hide show
  1. {pdfmd_cli-3.30.32 → pdfmd_cli-3.32.12}/CHANGELOG.md +1068 -20
  2. {pdfmd_cli-3.30.32/pdfmd_cli.egg-info → pdfmd_cli-3.32.12}/PKG-INFO +650 -29
  3. {pdfmd_cli-3.30.32 → pdfmd_cli-3.32.12}/README.md +649 -28
  4. {pdfmd_cli-3.30.32 → pdfmd_cli-3.32.12}/docs/recipes.md +13 -4
  5. {pdfmd_cli-3.30.32 → pdfmd_cli-3.32.12}/pdfmd.py +3822 -420
  6. pdfmd_cli-3.32.12/pdfmd_buildinfo/__init__.py +7 -0
  7. pdfmd_cli-3.32.12/pdfmd_buildinfo/record.py +109 -0
  8. pdfmd_cli-3.32.12/pdfmd_buildinfo/timing.py +185 -0
  9. {pdfmd_cli-3.30.32 → pdfmd_cli-3.32.12/pdfmd_cli.egg-info}/PKG-INFO +650 -29
  10. {pdfmd_cli-3.30.32 → pdfmd_cli-3.32.12}/pdfmd_cli.egg-info/SOURCES.txt +66 -0
  11. {pdfmd_cli-3.30.32 → pdfmd_cli-3.32.12}/pdfmd_cli.egg-info/entry_points.txt +1 -0
  12. {pdfmd_cli-3.30.32 → pdfmd_cli-3.32.12}/pdfmd_cli.egg-info/top_level.txt +5 -0
  13. pdfmd_cli-3.32.12/pdfmd_crop/__init__.py +164 -0
  14. pdfmd_cli-3.32.12/pdfmd_crop/images.py +96 -0
  15. {pdfmd_cli-3.30.32 → pdfmd_cli-3.32.12}/pdfmd_help/__init__.py +8 -6
  16. {pdfmd_cli-3.30.32 → pdfmd_cli-3.32.12}/pdfmd_images/svg.py +8 -6
  17. pdfmd_cli-3.32.12/pdfmd_kits/__init__.py +13 -0
  18. pdfmd_cli-3.32.12/pdfmd_kits/discovery.py +207 -0
  19. pdfmd_cli-3.32.12/pdfmd_kits/install.py +367 -0
  20. pdfmd_cli-3.32.12/pdfmd_kits/kitfile.py +300 -0
  21. pdfmd_cli-3.32.12/pdfmd_kits/preamble.py +144 -0
  22. pdfmd_cli-3.32.12/pdfmd_kits/selection.py +188 -0
  23. {pdfmd_cli-3.30.32 → pdfmd_cli-3.32.12}/pdfmd_lua/calc.lua +60 -4
  24. pdfmd_cli-3.32.12/pdfmd_lua/cellalign.lua +110 -0
  25. {pdfmd_cli-3.30.32 → pdfmd_cli-3.32.12}/pdfmd_lua/csv_table.lua +60 -20
  26. {pdfmd_cli-3.30.32 → pdfmd_cli-3.32.12}/pdfmd_lua/diagram.lua +72 -7
  27. {pdfmd_cli-3.30.32 → pdfmd_cli-3.32.12}/pdfmd_lua/indent.lua +8 -1
  28. pdfmd_cli-3.32.12/pdfmd_lua/limited_run.py +65 -0
  29. pdfmd_cli-3.32.12/pdfmd_lua/linebreak.lua +110 -0
  30. pdfmd_cli-3.32.12/pdfmd_lua/md_macro.lua +187 -0
  31. pdfmd_cli-3.32.12/pdfmd_lua/metadata_append.lua +24 -0
  32. pdfmd_cli-3.32.12/pdfmd_lua/smiles.lua +212 -0
  33. pdfmd_cli-3.32.12/pdfmd_lua/table_caption.lua +201 -0
  34. pdfmd_cli-3.32.12/pdfmd_lua/table_width.lua +415 -0
  35. {pdfmd_cli-3.30.32 → pdfmd_cli-3.32.12}/pdfmd_lua/typst.lua +23 -1
  36. {pdfmd_cli-3.30.32 → pdfmd_cli-3.32.12}/pdfmd_lua/weasyprint.lua +11 -1
  37. {pdfmd_cli-3.30.32 → pdfmd_cli-3.32.12}/pdfmd_office/__init__.py +7 -1
  38. {pdfmd_cli-3.30.32 → pdfmd_cli-3.32.12}/pdfmd_office/docx_post.py +27 -0
  39. {pdfmd_cli-3.30.32 → pdfmd_cli-3.32.12}/pdfmd_office/odt.py +37 -0
  40. {pdfmd_cli-3.30.32 → pdfmd_cli-3.32.12}/pdfmd_office/units.py +2 -4
  41. pdfmd_cli-3.32.12/pdfmd_ranking/__init__.py +12 -0
  42. pdfmd_cli-3.32.12/pdfmd_ranking/choose.py +254 -0
  43. pdfmd_cli-3.32.12/pdfmd_ranking/rules.py +459 -0
  44. {pdfmd_cli-3.30.32 → pdfmd_cli-3.32.12}/pdfmd_setup/__init__.py +7 -5
  45. {pdfmd_cli-3.30.32 → pdfmd_cli-3.32.12}/pdfmd_setup/fancy.py +13 -6
  46. {pdfmd_cli-3.30.32 → pdfmd_cli-3.32.12}/pdfmd_setup/inventory.py +38 -8
  47. {pdfmd_cli-3.30.32 → pdfmd_cli-3.32.12}/pdfmd_setup/plain.py +18 -9
  48. {pdfmd_cli-3.30.32 → pdfmd_cli-3.32.12}/pdfmd_setup/registry.py +210 -14
  49. pdfmd_cli-3.32.12/pdfmd_setup/userpath.py +306 -0
  50. pdfmd_cli-3.32.12/pdfmd_smiles/__init__.py +105 -0
  51. pdfmd_cli-3.32.12/pdfmd_smiles/__main__.py +12 -0
  52. pdfmd_cli-3.32.12/pdfmd_smiles/batch.py +154 -0
  53. pdfmd_cli-3.32.12/pdfmd_tables/ascii.py +497 -0
  54. pdfmd_cli-3.32.12/pdfmd_tables/captions.py +26 -0
  55. {pdfmd_cli-3.30.32 → pdfmd_cli-3.32.12}/pdfmd_tables/sheets.py +7 -7
  56. {pdfmd_cli-3.30.32 → pdfmd_cli-3.32.12}/pyproject.toml +3 -1
  57. pdfmd_cli-3.32.12/tests/test_00_environment.py +7 -0
  58. {pdfmd_cli-3.30.32 → pdfmd_cli-3.32.12}/tests/test_ansi.py +7 -4
  59. {pdfmd_cli-3.30.32 → pdfmd_cli-3.32.12}/tests/test_attach.py +1 -0
  60. {pdfmd_cli-3.30.32 → pdfmd_cli-3.32.12}/tests/test_attach_format.py +9 -8
  61. pdfmd_cli-3.32.12/tests/test_attach_layers.py +96 -0
  62. {pdfmd_cli-3.30.32 → pdfmd_cli-3.32.12}/tests/test_autolog.py +3 -3
  63. {pdfmd_cli-3.30.32 → pdfmd_cli-3.32.12}/tests/test_browser_engine.py +1 -1
  64. pdfmd_cli-3.32.12/tests/test_build_log.py +209 -0
  65. {pdfmd_cli-3.30.32 → pdfmd_cli-3.32.12}/tests/test_build_record.py +6 -3
  66. pdfmd_cli-3.32.12/tests/test_build_time.py +165 -0
  67. {pdfmd_cli-3.30.32 → pdfmd_cli-3.32.12}/tests/test_calc.py +92 -0
  68. {pdfmd_cli-3.30.32 → pdfmd_cli-3.32.12}/tests/test_calibre.py +8 -5
  69. {pdfmd_cli-3.30.32 → pdfmd_cli-3.32.12}/tests/test_calibre_markdown.py +1 -1
  70. {pdfmd_cli-3.30.32 → pdfmd_cli-3.32.12}/tests/test_calibre_targets.py +3 -3
  71. pdfmd_cli-3.32.12/tests/test_cellalign.py +110 -0
  72. {pdfmd_cli-3.30.32 → pdfmd_cli-3.32.12}/tests/test_check.py +2 -1
  73. {pdfmd_cli-3.30.32 → pdfmd_cli-3.32.12}/tests/test_chem.py +4 -2
  74. {pdfmd_cli-3.30.32 → pdfmd_cli-3.32.12}/tests/test_code_blocks.py +2 -2
  75. {pdfmd_cli-3.30.32 → pdfmd_cli-3.32.12}/tests/test_compat.py +4 -2
  76. {pdfmd_cli-3.30.32 → pdfmd_cli-3.32.12}/tests/test_completion.py +8 -2
  77. pdfmd_cli-3.32.12/tests/test_crop.py +325 -0
  78. {pdfmd_cli-3.30.32 → pdfmd_cli-3.32.12}/tests/test_csv_table.py +35 -0
  79. pdfmd_cli-3.32.12/tests/test_diagram_shim.py +45 -0
  80. pdfmd_cli-3.32.12/tests/test_diagram_timeout.py +124 -0
  81. {pdfmd_cli-3.30.32 → pdfmd_cli-3.32.12}/tests/test_diagrams.py +53 -2
  82. {pdfmd_cli-3.30.32 → pdfmd_cli-3.32.12}/tests/test_direct.py +9 -5
  83. {pdfmd_cli-3.30.32 → pdfmd_cli-3.32.12}/tests/test_display_path.py +7 -1
  84. {pdfmd_cli-3.30.32 → pdfmd_cli-3.32.12}/tests/test_doc.py +3 -2
  85. {pdfmd_cli-3.30.32 → pdfmd_cli-3.32.12}/tests/test_doctor.py +56 -2
  86. {pdfmd_cli-3.30.32 → pdfmd_cli-3.32.12}/tests/test_edit.py +3 -2
  87. {pdfmd_cli-3.30.32 → pdfmd_cli-3.32.12}/tests/test_edit_pdf.py +1 -1
  88. {pdfmd_cli-3.30.32 → pdfmd_cli-3.32.12}/tests/test_embedding.py +2 -2
  89. pdfmd_cli-3.32.12/tests/test_formula.py +63 -0
  90. {pdfmd_cli-3.30.32 → pdfmd_cli-3.32.12}/tests/test_help.py +2 -1
  91. {pdfmd_cli-3.30.32 → pdfmd_cli-3.32.12}/tests/test_history.py +1 -1
  92. {pdfmd_cli-3.30.32 → pdfmd_cli-3.32.12}/tests/test_images.py +6 -4
  93. pdfmd_cli-3.32.12/tests/test_indent_office.py +145 -0
  94. {pdfmd_cli-3.30.32 → pdfmd_cli-3.32.12}/tests/test_install_calibre.py +4 -0
  95. pdfmd_cli-3.32.12/tests/test_kit_carry.py +150 -0
  96. pdfmd_cli-3.32.12/tests/test_kit_install.py +315 -0
  97. pdfmd_cli-3.32.12/tests/test_kit_no_auto.py +143 -0
  98. pdfmd_cli-3.32.12/tests/test_kit_preamble.py +245 -0
  99. pdfmd_cli-3.32.12/tests/test_kit_selection.py +324 -0
  100. pdfmd_cli-3.32.12/tests/test_kit_texinputs.py +110 -0
  101. pdfmd_cli-3.32.12/tests/test_kits.py +435 -0
  102. pdfmd_cli-3.32.12/tests/test_linebreak.py +127 -0
  103. {pdfmd_cli-3.30.32 → pdfmd_cli-3.32.12}/tests/test_lookup.py +1 -0
  104. {pdfmd_cli-3.30.32 → pdfmd_cli-3.32.12}/tests/test_markup_word.py +3 -1
  105. pdfmd_cli-3.32.12/tests/test_md_macro.py +133 -0
  106. pdfmd_cli-3.32.12/tests/test_metadata_append.py +146 -0
  107. {pdfmd_cli-3.30.32 → pdfmd_cli-3.32.12}/tests/test_native.py +3 -1
  108. {pdfmd_cli-3.30.32 → pdfmd_cli-3.32.12}/tests/test_office.py +2 -1
  109. {pdfmd_cli-3.30.32 → pdfmd_cli-3.32.12}/tests/test_office_latex.py +24 -18
  110. {pdfmd_cli-3.30.32 → pdfmd_cli-3.32.12}/tests/test_package_helper.py +42 -0
  111. {pdfmd_cli-3.30.32 → pdfmd_cli-3.32.12}/tests/test_packaging.py +1 -0
  112. pdfmd_cli-3.32.12/tests/test_paper.py +127 -0
  113. {pdfmd_cli-3.30.32 → pdfmd_cli-3.32.12}/tests/test_partial_history.py +1 -1
  114. pdfmd_cli-3.32.12/tests/test_path_display.py +135 -0
  115. {pdfmd_cli-3.30.32 → pdfmd_cli-3.32.12}/tests/test_pdf_input.py +9 -0
  116. {pdfmd_cli-3.30.32 → pdfmd_cli-3.32.12}/tests/test_plot_cache.py +14 -0
  117. {pdfmd_cli-3.30.32 → pdfmd_cli-3.32.12}/tests/test_polish.py +94 -0
  118. {pdfmd_cli-3.30.32 → pdfmd_cli-3.32.12}/tests/test_probes.py +31 -0
  119. pdfmd_cli-3.32.12/tests/test_ranking_bare.py +129 -0
  120. pdfmd_cli-3.32.12/tests/test_ranking_generated.py +188 -0
  121. pdfmd_cli-3.32.12/tests/test_ranking_layers.py +220 -0
  122. pdfmd_cli-3.32.12/tests/test_ranking_lists.py +163 -0
  123. pdfmd_cli-3.32.12/tests/test_ranking_rules.py +315 -0
  124. pdfmd_cli-3.32.12/tests/test_ranking_which.py +117 -0
  125. {pdfmd_cli-3.30.32 → pdfmd_cli-3.32.12}/tests/test_raw.py +6 -4
  126. pdfmd_cli-3.32.12/tests/test_reader.py +218 -0
  127. pdfmd_cli-3.32.12/tests/test_rewrite.py +100 -0
  128. pdfmd_cli-3.32.12/tests/test_scoop_bucket.py +43 -0
  129. {pdfmd_cli-3.30.32 → pdfmd_cli-3.32.12}/tests/test_sections.py +54 -2
  130. {pdfmd_cli-3.30.32 → pdfmd_cli-3.32.12}/tests/test_setup.py +2 -2
  131. pdfmd_cli-3.32.12/tests/test_slide_notes.py +162 -0
  132. pdfmd_cli-3.32.12/tests/test_smiles.py +232 -0
  133. pdfmd_cli-3.32.12/tests/test_soffice_timeout.py +118 -0
  134. {pdfmd_cli-3.30.32 → pdfmd_cli-3.32.12}/tests/test_standalone_source.py +1 -0
  135. pdfmd_cli-3.32.12/tests/test_stdin.py +86 -0
  136. pdfmd_cli-3.32.12/tests/test_svg_profile.py +35 -0
  137. pdfmd_cli-3.32.12/tests/test_table_ascii.py +314 -0
  138. pdfmd_cli-3.32.12/tests/test_table_captions.py +146 -0
  139. pdfmd_cli-3.32.12/tests/test_table_widths.py +314 -0
  140. {pdfmd_cli-3.30.32 → pdfmd_cli-3.32.12}/tests/test_tables.py +17 -6
  141. pdfmd_cli-3.32.12/tests/test_tex_cause.py +119 -0
  142. pdfmd_cli-3.32.12/tests/test_tool_locations.py +134 -0
  143. {pdfmd_cli-3.30.32 → pdfmd_cli-3.32.12}/tests/test_translit.py +1 -0
  144. pdfmd_cli-3.32.12/tests/test_user_path.py +361 -0
  145. {pdfmd_cli-3.30.32 → pdfmd_cli-3.32.12}/tests/test_versions.py +1 -1
  146. pdfmd_cli-3.30.32/pdfmd_lua/table_width.lua +0 -231
  147. pdfmd_cli-3.30.32/tests/test_reader.py +0 -73
  148. pdfmd_cli-3.30.32/tests/test_table_widths.py +0 -114
  149. pdfmd_cli-3.30.32/tests/test_tex_cause.py +0 -57
  150. {pdfmd_cli-3.30.32 → pdfmd_cli-3.32.12}/LICENSE +0 -0
  151. {pdfmd_cli-3.30.32 → pdfmd_cli-3.32.12}/MANIFEST.in +0 -0
  152. {pdfmd_cli-3.30.32 → pdfmd_cli-3.32.12}/docs/agents.md +0 -0
  153. {pdfmd_cli-3.30.32 → pdfmd_cli-3.32.12}/pdfmd_ansi/__init__.py +0 -0
  154. {pdfmd_cli-3.30.32 → pdfmd_cli-3.32.12}/pdfmd_ansi/highlight.py +0 -0
  155. {pdfmd_cli-3.30.32 → pdfmd_cli-3.32.12}/pdfmd_ansi/mathbox.py +0 -0
  156. {pdfmd_cli-3.30.32 → pdfmd_cli-3.32.12}/pdfmd_ansi/native.py +0 -0
  157. {pdfmd_cli-3.30.32 → pdfmd_cli-3.32.12}/pdfmd_ansi/render.py +0 -0
  158. {pdfmd_cli-3.30.32 → pdfmd_cli-3.32.12}/pdfmd_ansi/text.py +0 -0
  159. {pdfmd_cli-3.30.32 → pdfmd_cli-3.32.12}/pdfmd_check/__init__.py +0 -0
  160. {pdfmd_cli-3.30.32 → pdfmd_cli-3.32.12}/pdfmd_check/lint.py +0 -0
  161. {pdfmd_cli-3.30.32 → pdfmd_cli-3.32.12}/pdfmd_chem/__init__.py +0 -0
  162. {pdfmd_cli-3.30.32 → pdfmd_cli-3.32.12}/pdfmd_chem/__main__.py +0 -0
  163. {pdfmd_cli-3.30.32 → pdfmd_cli-3.32.12}/pdfmd_chem/batch.py +0 -0
  164. {pdfmd_cli-3.30.32 → pdfmd_cli-3.32.12}/pdfmd_chem/render.py +0 -0
  165. {pdfmd_cli-3.30.32 → pdfmd_cli-3.32.12}/pdfmd_chem/siunitx.py +0 -0
  166. {pdfmd_cli-3.30.32 → pdfmd_cli-3.32.12}/pdfmd_cli.egg-info/dependency_links.txt +0 -0
  167. {pdfmd_cli-3.30.32 → pdfmd_cli-3.32.12}/pdfmd_cli.egg-info/requires.txt +0 -0
  168. {pdfmd_cli-3.30.32 → pdfmd_cli-3.32.12}/pdfmd_css/__init__.py +0 -0
  169. {pdfmd_cli-3.30.32 → pdfmd_cli-3.32.12}/pdfmd_direct/__init__.py +0 -0
  170. {pdfmd_cli-3.30.32 → pdfmd_cli-3.32.12}/pdfmd_direct/defaults.py +0 -0
  171. {pdfmd_cli-3.30.32 → pdfmd_cli-3.32.12}/pdfmd_direct/html.py +0 -0
  172. {pdfmd_cli-3.30.32 → pdfmd_cli-3.32.12}/pdfmd_direct/typst.py +0 -0
  173. {pdfmd_cli-3.30.32 → pdfmd_cli-3.32.12}/pdfmd_docs/__init__.py +0 -0
  174. {pdfmd_cli-3.30.32 → pdfmd_cli-3.32.12}/pdfmd_edit/__init__.py +0 -0
  175. {pdfmd_cli-3.30.32 → pdfmd_cli-3.32.12}/pdfmd_flat/__init__.py +0 -0
  176. {pdfmd_cli-3.30.32 → pdfmd_cli-3.32.12}/pdfmd_flat/asciify.py +0 -0
  177. {pdfmd_cli-3.30.32 → pdfmd_cli-3.32.12}/pdfmd_flat/keep.py +0 -0
  178. {pdfmd_cli-3.30.32 → pdfmd_cli-3.32.12}/pdfmd_history/__init__.py +0 -0
  179. {pdfmd_cli-3.30.32 → pdfmd_cli-3.32.12}/pdfmd_history/git.py +0 -0
  180. {pdfmd_cli-3.30.32 → pdfmd_cli-3.32.12}/pdfmd_history/hst.py +0 -0
  181. {pdfmd_cli-3.30.32 → pdfmd_cli-3.32.12}/pdfmd_history/versions.py +0 -0
  182. {pdfmd_cli-3.30.32 → pdfmd_cli-3.32.12}/pdfmd_images/__init__.py +0 -0
  183. {pdfmd_cli-3.30.32 → pdfmd_cli-3.32.12}/pdfmd_images/remote.py +0 -0
  184. {pdfmd_cli-3.30.32 → pdfmd_cli-3.32.12}/pdfmd_images/scan.py +0 -0
  185. {pdfmd_cli-3.30.32 → pdfmd_cli-3.32.12}/pdfmd_inkmd/LICENSE +0 -0
  186. {pdfmd_cli-3.30.32 → pdfmd_cli-3.32.12}/pdfmd_inkmd/VENDORED.md +0 -0
  187. {pdfmd_cli-3.30.32 → pdfmd_cli-3.32.12}/pdfmd_inkmd/__init__.py +0 -0
  188. {pdfmd_cli-3.30.32 → pdfmd_cli-3.32.12}/pdfmd_inkmd/_kerning_data.py +0 -0
  189. {pdfmd_cli-3.30.32 → pdfmd_cli-3.32.12}/pdfmd_inkmd/assets/fonts/DejaVuSans-LICENSE.txt +0 -0
  190. {pdfmd_cli-3.30.32 → pdfmd_cli-3.32.12}/pdfmd_inkmd/assets/fonts/DejaVuSans.ttf +0 -0
  191. {pdfmd_cli-3.30.32 → pdfmd_cli-3.32.12}/pdfmd_inkmd/ast.py +0 -0
  192. {pdfmd_cli-3.30.32 → pdfmd_cli-3.32.12}/pdfmd_inkmd/cidfont.py +0 -0
  193. {pdfmd_cli-3.30.32 → pdfmd_cli-3.32.12}/pdfmd_inkmd/embedded.py +0 -0
  194. {pdfmd_cli-3.30.32 → pdfmd_cli-3.32.12}/pdfmd_inkmd/embedded_metrics.py +0 -0
  195. {pdfmd_cli-3.30.32 → pdfmd_cli-3.32.12}/pdfmd_inkmd/emoji.py +0 -0
  196. {pdfmd_cli-3.30.32 → pdfmd_cli-3.32.12}/pdfmd_inkmd/emoji_font.py +0 -0
  197. {pdfmd_cli-3.30.32 → pdfmd_cli-3.32.12}/pdfmd_inkmd/fonts.py +0 -0
  198. {pdfmd_cli-3.30.32 → pdfmd_cli-3.32.12}/pdfmd_inkmd/html_filter.py +0 -0
  199. {pdfmd_cli-3.30.32 → pdfmd_cli-3.32.12}/pdfmd_inkmd/image_loader.py +0 -0
  200. {pdfmd_cli-3.30.32 → pdfmd_cli-3.32.12}/pdfmd_inkmd/layout.py +0 -0
  201. {pdfmd_cli-3.30.32 → pdfmd_cli-3.32.12}/pdfmd_inkmd/parser.py +0 -0
  202. {pdfmd_cli-3.30.32 → pdfmd_cli-3.32.12}/pdfmd_inkmd/pdf.py +0 -0
  203. {pdfmd_cli-3.30.32 → pdfmd_cli-3.32.12}/pdfmd_inkmd/py.typed +0 -0
  204. {pdfmd_cli-3.30.32 → pdfmd_cli-3.32.12}/pdfmd_inkmd/render.py +0 -0
  205. {pdfmd_cli-3.30.32 → pdfmd_cli-3.32.12}/pdfmd_inkmd/truetype.py +0 -0
  206. {pdfmd_cli-3.30.32 → pdfmd_cli-3.32.12}/pdfmd_inkmd/url_filter.py +0 -0
  207. {pdfmd_cli-3.30.32 → pdfmd_cli-3.32.12}/pdfmd_labels/__init__.py +0 -0
  208. {pdfmd_cli-3.30.32 → pdfmd_cli-3.32.12}/pdfmd_labels/replay.py +0 -0
  209. {pdfmd_cli-3.30.32 → pdfmd_cli-3.32.12}/pdfmd_labels/scan.py +0 -0
  210. {pdfmd_cli-3.30.32 → pdfmd_cli-3.32.12}/pdfmd_lua/__init__.py +0 -0
  211. {pdfmd_cli-3.30.32 → pdfmd_cli-3.32.12}/pdfmd_lua/ascii.lua +0 -0
  212. {pdfmd_cli-3.30.32 → pdfmd_cli-3.32.12}/pdfmd_lua/chem.lua +0 -0
  213. {pdfmd_cli-3.30.32 → pdfmd_cli-3.32.12}/pdfmd_lua/chem_client.lua +0 -0
  214. {pdfmd_cli-3.30.32 → pdfmd_cli-3.32.12}/pdfmd_lua/code.lua +0 -0
  215. {pdfmd_cli-3.30.32 → pdfmd_cli-3.32.12}/pdfmd_lua/code_wrap.tex +0 -0
  216. {pdfmd_cli-3.30.32 → pdfmd_cli-3.32.12}/pdfmd_lua/cover.lua +0 -0
  217. {pdfmd_cli-3.30.32 → pdfmd_cli-3.32.12}/pdfmd_lua/ebook.lua +0 -0
  218. {pdfmd_cli-3.30.32 → pdfmd_cli-3.32.12}/pdfmd_lua/ebook_input.lua +0 -0
  219. {pdfmd_cli-3.30.32 → pdfmd_cli-3.32.12}/pdfmd_lua/flat.lua +0 -0
  220. {pdfmd_cli-3.30.32 → pdfmd_cli-3.32.12}/pdfmd_lua/graphicx.tex +0 -0
  221. {pdfmd_cli-3.30.32 → pdfmd_cli-3.32.12}/pdfmd_lua/graphviz.cjs +0 -0
  222. {pdfmd_cli-3.30.32 → pdfmd_cli-3.32.12}/pdfmd_lua/images.lua +0 -0
  223. {pdfmd_cli-3.30.32 → pdfmd_cli-3.32.12}/pdfmd_lua/katex.cjs +0 -0
  224. {pdfmd_cli-3.30.32 → pdfmd_cli-3.32.12}/pdfmd_lua/markup.lua +0 -0
  225. {pdfmd_cli-3.30.32 → pdfmd_cli-3.32.12}/pdfmd_lua/mathjax.cjs +0 -0
  226. {pdfmd_cli-3.30.32 → pdfmd_cli-3.32.12}/pdfmd_lua/pagebreak.lua +0 -0
  227. {pdfmd_cli-3.30.32 → pdfmd_cli-3.32.12}/pdfmd_lua/pdf_images.lua +0 -0
  228. {pdfmd_cli-3.30.32 → pdfmd_cli-3.32.12}/pdfmd_lua/raw.lua +0 -0
  229. {pdfmd_cli-3.30.32 → pdfmd_cli-3.32.12}/pdfmd_lua/select.lua +0 -0
  230. {pdfmd_cli-3.30.32 → pdfmd_cli-3.32.12}/pdfmd_lua/title_meta.lua +0 -0
  231. {pdfmd_cli-3.30.32 → pdfmd_cli-3.32.12}/pdfmd_mhchem/LICENSE +0 -0
  232. {pdfmd_cli-3.30.32 → pdfmd_cli-3.32.12}/pdfmd_mhchem/__init__.py +0 -0
  233. {pdfmd_cli-3.30.32 → pdfmd_cli-3.32.12}/pdfmd_mhchem/parser.py +0 -0
  234. {pdfmd_cli-3.30.32 → pdfmd_cli-3.32.12}/pdfmd_office/docx.py +0 -0
  235. {pdfmd_cli-3.30.32 → pdfmd_cli-3.32.12}/pdfmd_office/fonts.py +0 -0
  236. {pdfmd_cli-3.30.32 → pdfmd_cli-3.32.12}/pdfmd_office/fragments.py +0 -0
  237. {pdfmd_cli-3.30.32 → pdfmd_cli-3.32.12}/pdfmd_office/labels.py +0 -0
  238. {pdfmd_cli-3.30.32 → pdfmd_cli-3.32.12}/pdfmd_office/office.lua +0 -0
  239. {pdfmd_cli-3.30.32 → pdfmd_cli-3.32.12}/pdfmd_office/properties.py +0 -0
  240. {pdfmd_cli-3.30.32 → pdfmd_cli-3.32.12}/pdfmd_office/spec.py +0 -0
  241. {pdfmd_cli-3.30.32 → pdfmd_cli-3.32.12}/pdfmd_raw/__init__.py +0 -0
  242. {pdfmd_cli-3.30.32 → pdfmd_cli-3.32.12}/pdfmd_tables/__init__.py +0 -0
  243. {pdfmd_cli-3.30.32 → pdfmd_cli-3.32.12}/pdfmd_tables/convert.py +0 -0
  244. {pdfmd_cli-3.30.32 → pdfmd_cli-3.32.12}/pdfmd_tables/scan.py +0 -0
  245. {pdfmd_cli-3.30.32 → pdfmd_cli-3.32.12}/pdfmd_tables/verify.py +0 -0
  246. {pdfmd_cli-3.30.32 → pdfmd_cli-3.32.12}/pdfmd_templates/__init__.py +0 -0
  247. {pdfmd_cli-3.30.32 → pdfmd_cli-3.32.12}/pdfmd_templates/data/article/__name__.md +0 -0
  248. {pdfmd_cli-3.30.32 → pdfmd_cli-3.32.12}/pdfmd_templates/data/article/description.txt +0 -0
  249. {pdfmd_cli-3.30.32 → pdfmd_cli-3.32.12}/pdfmd_templates/data/article/next.txt +0 -0
  250. {pdfmd_cli-3.30.32 → pdfmd_cli-3.32.12}/pdfmd_templates/data/article/refs.bib +0 -0
  251. {pdfmd_cli-3.30.32 → pdfmd_cli-3.32.12}/pdfmd_templates/data/book/__name__.md +0 -0
  252. {pdfmd_cli-3.30.32 → pdfmd_cli-3.32.12}/pdfmd_templates/data/book/description.txt +0 -0
  253. {pdfmd_cli-3.30.32 → pdfmd_cli-3.32.12}/pdfmd_templates/data/book/next.txt +0 -0
  254. {pdfmd_cli-3.30.32 → pdfmd_cli-3.32.12}/pdfmd_templates/data/book/parts/10-introduction.md +0 -0
  255. {pdfmd_cli-3.30.32 → pdfmd_cli-3.32.12}/pdfmd_templates/data/book/parts/20-methods.md +0 -0
  256. {pdfmd_cli-3.30.32 → pdfmd_cli-3.32.12}/pdfmd_templates/data/book/parts/30-results.md +0 -0
  257. {pdfmd_cli-3.30.32 → pdfmd_cli-3.32.12}/pdfmd_templates/data/notes/__name__.md +0 -0
  258. {pdfmd_cli-3.30.32 → pdfmd_cli-3.32.12}/pdfmd_templates/data/notes/description.txt +0 -0
  259. {pdfmd_cli-3.30.32 → pdfmd_cli-3.32.12}/pdfmd_templates/data/notes/next.txt +0 -0
  260. {pdfmd_cli-3.30.32 → pdfmd_cli-3.32.12}/pdfmd_templates/data/report/__name__.md +0 -0
  261. {pdfmd_cli-3.30.32 → pdfmd_cli-3.32.12}/pdfmd_templates/data/report/data.csv +0 -0
  262. {pdfmd_cli-3.30.32 → pdfmd_cli-3.32.12}/pdfmd_templates/data/report/description.txt +0 -0
  263. {pdfmd_cli-3.30.32 → pdfmd_cli-3.32.12}/pdfmd_templates/data/report/next.txt +0 -0
  264. {pdfmd_cli-3.30.32 → pdfmd_cli-3.32.12}/pdfmd_templates/data/slides/__name__.md +0 -0
  265. {pdfmd_cli-3.30.32 → pdfmd_cli-3.32.12}/pdfmd_templates/data/slides/description.txt +0 -0
  266. {pdfmd_cli-3.30.32 → pdfmd_cli-3.32.12}/pdfmd_templates/data/slides/next.txt +0 -0
  267. {pdfmd_cli-3.30.32 → pdfmd_cli-3.32.12}/pdfmd_unicode/__init__.py +0 -0
  268. {pdfmd_cli-3.30.32 → pdfmd_cli-3.32.12}/pdfmd_unicode/_catalog.py +0 -0
  269. {pdfmd_cli-3.30.32 → pdfmd_cli-3.32.12}/pdfmd_unicode/_scripts.py +0 -0
  270. {pdfmd_cli-3.30.32 → pdfmd_cli-3.32.12}/pdfmd_unicode/_tessdata_catalog.py +0 -0
  271. {pdfmd_cli-3.30.32 → pdfmd_cli-3.32.12}/pdfmd_unicode/colorfont.py +0 -0
  272. {pdfmd_cli-3.30.32 → pdfmd_cli-3.32.12}/pdfmd_unicode/emoji.py +0 -0
  273. {pdfmd_cli-3.30.32 → pdfmd_cli-3.32.12}/pdfmd_unicode/fonts.py +0 -0
  274. {pdfmd_cli-3.30.32 → pdfmd_cli-3.32.12}/pdfmd_unicode/install.py +0 -0
  275. {pdfmd_cli-3.30.32 → pdfmd_cli-3.32.12}/pdfmd_unicode/plan.py +0 -0
  276. {pdfmd_cli-3.30.32 → pdfmd_cli-3.32.12}/pdfmd_unicode/scripts.py +0 -0
  277. {pdfmd_cli-3.30.32 → pdfmd_cli-3.32.12}/pdfmd_unicode/tessdata.py +0 -0
  278. {pdfmd_cli-3.30.32 → pdfmd_cli-3.32.12}/pdfmd_unicode/translit.py +0 -0
  279. {pdfmd_cli-3.30.32 → pdfmd_cli-3.32.12}/setup.cfg +0 -0
  280. {pdfmd_cli-3.30.32 → pdfmd_cli-3.32.12}/setup.py +0 -0
  281. {pdfmd_cli-3.30.32 → pdfmd_cli-3.32.12}/tests/test_api.py +0 -0
  282. {pdfmd_cli-3.30.32 → pdfmd_cli-3.32.12}/tests/test_apple_emoji.py +0 -0
  283. {pdfmd_cli-3.30.32 → pdfmd_cli-3.32.12}/tests/test_ascii.py +0 -0
  284. {pdfmd_cli-3.30.32 → pdfmd_cli-3.32.12}/tests/test_atomic.py +0 -0
  285. {pdfmd_cli-3.30.32 → pdfmd_cli-3.32.12}/tests/test_auto_states.py +0 -0
  286. {pdfmd_cli-3.30.32 → pdfmd_cli-3.32.12}/tests/test_backup_summary.py +0 -0
  287. {pdfmd_cli-3.30.32 → pdfmd_cli-3.32.12}/tests/test_bopen.py +0 -0
  288. {pdfmd_cli-3.30.32 → pdfmd_cli-3.32.12}/tests/test_budget.py +0 -0
  289. {pdfmd_cli-3.30.32 → pdfmd_cli-3.32.12}/tests/test_cache_location.py +0 -0
  290. {pdfmd_cli-3.30.32 → pdfmd_cli-3.32.12}/tests/test_check_source.py +0 -0
  291. {pdfmd_cli-3.30.32 → pdfmd_cli-3.32.12}/tests/test_command_words.py +0 -0
  292. {pdfmd_cli-3.30.32 → pdfmd_cli-3.32.12}/tests/test_config.py +0 -0
  293. {pdfmd_cli-3.30.32 → pdfmd_cli-3.32.12}/tests/test_cover.py +0 -0
  294. {pdfmd_cli-3.30.32 → pdfmd_cli-3.32.12}/tests/test_css.py +0 -0
  295. {pdfmd_cli-3.30.32 → pdfmd_cli-3.32.12}/tests/test_css_v2.py +0 -0
  296. {pdfmd_cli-3.30.32 → pdfmd_cli-3.32.12}/tests/test_csv_switch.py +0 -0
  297. {pdfmd_cli-3.30.32 → pdfmd_cli-3.32.12}/tests/test_docs_consistency.py +0 -0
  298. {pdfmd_cli-3.30.32 → pdfmd_cli-3.32.12}/tests/test_docx_tables.py +0 -0
  299. {pdfmd_cli-3.30.32 → pdfmd_cli-3.32.12}/tests/test_ebook_input.py +0 -0
  300. {pdfmd_cli-3.30.32 → pdfmd_cli-3.32.12}/tests/test_engine_chain.py +0 -0
  301. {pdfmd_cli-3.30.32 → pdfmd_cli-3.32.12}/tests/test_engine_filters.py +0 -0
  302. {pdfmd_cli-3.30.32 → pdfmd_cli-3.32.12}/tests/test_flat.py +0 -0
  303. {pdfmd_cli-3.30.32 → pdfmd_cli-3.32.12}/tests/test_folder_settings.py +0 -0
  304. {pdfmd_cli-3.30.32 → pdfmd_cli-3.32.12}/tests/test_header_includes.py +0 -0
  305. {pdfmd_cli-3.30.32 → pdfmd_cli-3.32.12}/tests/test_indent.py +0 -0
  306. {pdfmd_cli-3.30.32 → pdfmd_cli-3.32.12}/tests/test_install_core_tex.py +0 -0
  307. {pdfmd_cli-3.30.32 → pdfmd_cli-3.32.12}/tests/test_install_diagram.py +0 -0
  308. {pdfmd_cli-3.30.32 → pdfmd_cli-3.32.12}/tests/test_install_ocr.py +0 -0
  309. {pdfmd_cli-3.30.32 → pdfmd_cli-3.32.12}/tests/test_install_python.py +0 -0
  310. {pdfmd_cli-3.30.32 → pdfmd_cli-3.32.12}/tests/test_interrupt.py +0 -0
  311. {pdfmd_cli-3.30.32 → pdfmd_cli-3.32.12}/tests/test_jobs.py +0 -0
  312. {pdfmd_cli-3.30.32 → pdfmd_cli-3.32.12}/tests/test_katex_scale.py +0 -0
  313. {pdfmd_cli-3.30.32 → pdfmd_cli-3.32.12}/tests/test_keep_source.py +0 -0
  314. {pdfmd_cli-3.30.32 → pdfmd_cli-3.32.12}/tests/test_labels.py +0 -0
  315. {pdfmd_cli-3.30.32 → pdfmd_cli-3.32.12}/tests/test_latex_errors.py +0 -0
  316. {pdfmd_cli-3.30.32 → pdfmd_cli-3.32.12}/tests/test_lua_missing.py +0 -0
  317. {pdfmd_cli-3.30.32 → pdfmd_cli-3.32.12}/tests/test_lua_trust.py +0 -0
  318. {pdfmd_cli-3.30.32 → pdfmd_cli-3.32.12}/tests/test_markdown_features.py +0 -0
  319. {pdfmd_cli-3.30.32 → pdfmd_cli-3.32.12}/tests/test_markup_appendix.py +0 -0
  320. {pdfmd_cli-3.30.32 → pdfmd_cli-3.32.12}/tests/test_markup_callouts.py +0 -0
  321. {pdfmd_cli-3.30.32 → pdfmd_cli-3.32.12}/tests/test_markup_center.py +0 -0
  322. {pdfmd_cli-3.30.32 → pdfmd_cli-3.32.12}/tests/test_markup_highlight.py +0 -0
  323. {pdfmd_cli-3.30.32 → pdfmd_cli-3.32.12}/tests/test_markup_keep.py +0 -0
  324. {pdfmd_cli-3.30.32 → pdfmd_cli-3.32.12}/tests/test_markup_landscape.py +0 -0
  325. {pdfmd_cli-3.30.32 → pdfmd_cli-3.32.12}/tests/test_markup_space.py +0 -0
  326. {pdfmd_cli-3.30.32 → pdfmd_cli-3.32.12}/tests/test_markup_toc.py +0 -0
  327. {pdfmd_cli-3.30.32 → pdfmd_cli-3.32.12}/tests/test_mathfont.py +0 -0
  328. {pdfmd_cli-3.30.32 → pdfmd_cli-3.32.12}/tests/test_mathjax.py +0 -0
  329. {pdfmd_cli-3.30.32 → pdfmd_cli-3.32.12}/tests/test_mhchem.py +0 -0
  330. {pdfmd_cli-3.30.32 → pdfmd_cli-3.32.12}/tests/test_old_python_syntax.py +0 -0
  331. {pdfmd_cli-3.30.32 → pdfmd_cli-3.32.12}/tests/test_pagebreak.py +0 -0
  332. {pdfmd_cli-3.30.32 → pdfmd_cli-3.32.12}/tests/test_pan_tec.py +0 -0
  333. {pdfmd_cli-3.30.32 → pdfmd_cli-3.32.12}/tests/test_partial_provenance.py +0 -0
  334. {pdfmd_cli-3.30.32 → pdfmd_cli-3.32.12}/tests/test_picture_cache_protocol.py +0 -0
  335. {pdfmd_cli-3.30.32 → pdfmd_cli-3.32.12}/tests/test_presets.py +0 -0
  336. {pdfmd_cli-3.30.32 → pdfmd_cli-3.32.12}/tests/test_rawcode.py +0 -0
  337. {pdfmd_cli-3.30.32 → pdfmd_cli-3.32.12}/tests/test_readonly_home.py +0 -0
  338. {pdfmd_cli-3.30.32 → pdfmd_cli-3.32.12}/tests/test_remote_csl.py +0 -0
  339. {pdfmd_cli-3.30.32 → pdfmd_cli-3.32.12}/tests/test_setup_folder.py +0 -0
  340. {pdfmd_cli-3.30.32 → pdfmd_cli-3.32.12}/tests/test_show_comments.py +0 -0
  341. {pdfmd_cli-3.30.32 → pdfmd_cli-3.32.12}/tests/test_soffice_parts.py +0 -0
  342. {pdfmd_cli-3.30.32 → pdfmd_cli-3.32.12}/tests/test_spreadsheets.py +0 -0
  343. {pdfmd_cli-3.30.32 → pdfmd_cli-3.32.12}/tests/test_symlinked_parts.py +0 -0
  344. {pdfmd_cli-3.30.32 → pdfmd_cli-3.32.12}/tests/test_table_keep.py +0 -0
  345. {pdfmd_cli-3.30.32 → pdfmd_cli-3.32.12}/tests/test_templates.py +0 -0
  346. {pdfmd_cli-3.30.32 → pdfmd_cli-3.32.12}/tests/test_tessdata.py +0 -0
  347. {pdfmd_cli-3.30.32 → pdfmd_cli-3.32.12}/tests/test_tex_bin.py +0 -0
  348. {pdfmd_cli-3.30.32 → pdfmd_cli-3.32.12}/tests/test_texpackages.py +0 -0
  349. {pdfmd_cli-3.30.32 → pdfmd_cli-3.32.12}/tests/test_text_input.py +0 -0
  350. {pdfmd_cli-3.30.32 → pdfmd_cli-3.32.12}/tests/test_typst_input.py +0 -0
  351. {pdfmd_cli-3.30.32 → pdfmd_cli-3.32.12}/tests/test_unicode.py +0 -0
@@ -17,28 +17,1055 @@ PDFMD_VERSION = "2.0.0"
17
17
  `pdfmd --version` prints it. Bump it there and add an entry here in the
18
18
  same commit as the change.
19
19
 
20
- **Why this starts at 2.0.0, not 1.0.0.** `pdfmd.py` began as a one-line
21
- `pandoc file.md -o file.pdf` wrapper and was revised many times since — across
22
- ChatGPT, ChatGPT Codex, Claude Chat, Cowork, and Claude Code sessions — with no
23
- version marker or changelog kept at any point. `backups/pdfmd.py.bak-*` has
24
- snapshots from a handful of points in that history, but not from the
25
- ChatGPT/Codex/Cowork era at all, so reconstructing an honest "v1.x" changelog
26
- from what's actually on disk isn't possible — only a partial,
27
- misleadingly-precise-looking one. Decided 2026-09-16 (the author): start real
28
- changelog tracking now, at 2.0.0, rather than fabricate the gap. If the full
29
- history ever matters, it would need pulling the actual chat transcripts from
30
- each of those tools, not guessing from diffs between inconsistent backup
31
- snapshots.
20
+ ---
32
21
 
33
- **Update, 2026-10-05:** an approximate outline of the earlier history has since
34
- been reconstructed, at the very end of this file (see "Before 2.0.0 —
35
- reconstructed history"), from the saved Codex transcripts and the surviving
36
- backup snapshots. It is explicitly NOT reliable: its 1.x numbers are invented,
37
- snapshot-based entries cover windows of days rather than single changes, and
38
- anything done in unsaved sessions (other ChatGPT chats, claude.ai, Cowork) is
39
- missing. Real tracking still begins at 2.0.0.
22
+ ## v3.32.12c — 2026-11-03
40
23
 
41
- ---
24
+ Tests only; `PDFMD_VERSION` stays 3.32.12. The second CI run (41546953982) failed in three tests of the 3.31.x-3.32.8 commits, which had never run on CI before; none is a bug in pdfmd:
25
+
26
+ - `test_tables`: a document with no front matter used to be read as gfm, so its grid and simple tables were "left as they are" with a NOTE. Since 3.32.5 such a table switches the reader to Pandoc's Markdown, and the tables are read (and skipped, if they cannot be CSV) like any other. The gfm test now asks for `--from gfm+fenced_divs` explicitly, and a new test pins the switch (no NOTE, the two `SKIP` lines).
27
+ - `test_cellalign`: Pandoc's Typst writer gives a cell its own alignment (`table.cell(align: ...)`) only from 3.1.13, so the Typst half of the test is its own test and is skipped on older Pandoc (`needs_pandoc(3, 1, 13)`); the HTML half runs everywhere.
28
+ - `test_md_macro`: Pandoc before 3.1.7 wraps a heading as `\texorpdfstring{Head with\n\emph{em}}{...}`; the test looks for `Head with` followed by white space and `\emph{em}`, which both forms satisfy.
29
+
30
+ ## v3.32.12b — 2026-11-03
31
+
32
+ Tests and lint only; `PDFMD_VERSION` stays 3.32.12. The first CI run of the pushed 3.31.47-3.32.12 commits (run 41540900210) failed in the `native` jobs (no Pandoc), the Windows `which` test and `lint`:
33
+
34
+ - The end-to-end ranking tests (`test_ranking_bare`, `_layers`, `_lists`, `_generated`) run `pdfmd --stop-at markdown`, which still needs Pandoc: they are skipped where there is none (the `native` jobs). `--which`, the watch pin and the engine tests need none and still run there.
35
+ - `test_ranking_which` compared `.pdfmd/ranking.yaml` where Windows prints `.pdfmd\ranking.yaml`: it builds the path with `Path`.
36
+ - `ruff --select F` (the `lint` job): three unused imports from the kits commits (`io` and `dataclasses.field` in `pdfmd_kits/install.py`, `Source` in `tests/test_kit_install.py`) removed.
37
+
38
+ ## v3.32.12 — 2026-11-03
39
+
40
+ Pre-edit state: commit `1bb68be`. Entry 3eu (the author: "isn't which already taken? if not, let's use it. whatever. it can just become multi-purpose if needed at some point in future"). Kept in 3.32. Not pushed. Last of the four commits of the line.
41
+
42
+ `pdfmd --which` explains a choice and builds nothing.
43
+
44
+ - **`pdfmd --which`** in a folder: `WHICH bare pdfmd in . builds report.final.md`, one row per Markdown file (what it says: its `priority`, its name's suffix; what happened to it: `BUILT`, `left: name: draft`, `left: priority: never`, `left: older`, `tied`; when it last changed), then `RULES`: the files the settings came from (nearest first, ending in the built-in default), `names`, `by`, and the classes low to high (`pdfmd_ranking.Ranking.describe()`). The tie WARN is printed as in a build; a choice that would stop says so (`would stop: ...`, exit 1). `--newest`, `--all` and `--no-auto priority` act in it as they do in a build.
45
+ - **`pdfmd --which NAME ...`**: which file each name finds, by the same lookup a build uses (`report -> report.md`), and the lookup's own error for one it cannot find. Meant to grow: the author wants `--which` to be the one place that says what pdfmd would decide.
46
+ - `--which` is a command in `pdfmd_setup/inventory.py` and in the `modes` topic of `--help`. Tests: `tests/test_ranking_which.py` (rows, rules and their origin, a tie, a stop, `--newest` / `--all`, one file, names, a mistake in the ranking). README, module docstring.
47
+
48
+ ## v3.32.11 — 2026-11-03
49
+
50
+ Pre-edit state: commit `fada070`. Entry 3eu (the author: "yes, but there should be a new key like `-b all` or smth that will override never keys" and "pdfmd --newest will look at the modification time and render the newest thing"). Kept in 3.32. Not pushed. Third of four commits of the line.
51
+
52
+ `never` leaves the folder lists, and two flags.
53
+
54
+ - **`priority: never` (or another word of a class with `exclude`, or a negative number) in `-b`, `-r` and the viewer**: the document is left out of what a folder gives, with one `NOTE` naming what was left out. `draft` and the numbers do not matter there (they only choose one document). A document named on the command line (`pdfmd NAME`, `-r a.md b.md`) is always built. `--no-auto priority` switches the leaving-out off with the rest.
55
+ - **`--all`** (alias `--include-never`; `-b all` could not be the spelling: `-b` takes paths and would read `all` as a file): the `never` documents count again. `-b`, `-r` and `--view` include them; a bare `pdfmd` ranks them as if they had written nothing.
56
+ - **`--newest`**: a bare `pdfmd` (or `pdfmd '#section'`) builds the most recently changed file of the folder, by modification time alone (priority and names are not consulted; `never` documents stay out unless `--all`), with no tie warning: it was asked for. It is honoured even with `--no-auto priority`. With a named file, `-b` or `-r` it does nothing, and says so (a WARN). The default `ranking: {by: [priority, newest]}` uses the time only for documents of equal priority.
57
+ - A `never` in a file name (`report_ignore.md` beside `report.md`) hides it from a bare `pdfmd` only; the lists look at the `priority:` key (reading every name of a big `-b -r` tree for it was not worth it).
58
+ - Both flags are `RUN` entries of `pdfmd_setup/inventory.py`. Tests: `tests/test_ranking_lists.py`. README (*Several documents in a folder*, *A whole folder*), module docstring, `--help`.
59
+
60
+ ## v3.32.10 — 2026-11-03
61
+
62
+ Pre-edit state: commit `5702fb0`. Entry 3eu UPDATE 2 (the author: "let's make it possible to define ranking.yaml somewhere globally ... living in .pdfmd per directory or in parent directory's .pdfmd" and "all depend on proximity to the file itself"). Kept in 3.32. Not pushed. Second of four commits of the line.
63
+
64
+ The words of `priority`, and their order, are settings.
65
+
66
+ - **`ranking`**: classes of words on a ladder (`classes: {fruit: {words: [cherry, banana], above: numbers}}`: banana beats cherry, every fruit beats every number, `final` still beats them all), `ladder:` (a nested list is one level), `names: family | folder | off` (whether an inferred `final` beats an unrelated document: the author's "let's make it adjustable"), `by:` (what settles equal priority: `newest`, `oldest`, `name`; `[priority]` makes a tie the error of 3.32.9 again, `[newest]` ignores priority). A class takes `ordered: false`, `degrees`, `exclude`, `names: false`, `above:` / `below:`; a word may hold `{n}`. YAML's `names: off` (read as `false`) works, and `by` may be a comma list (`priority, oldest`), which is what the setup row stores.
67
+ - **Where it is read, nearest first**: the documents' own `pdfmd-options: {ranking: ...}` and a metadata file named after one (classes and ladder only; `by`, `names` and `inherit` are the folder's: a WARN), then folder by folder from theirs upward `.pdfmd/ranking.yaml` > the folder's `metadata.yaml` > the folder settings' `options.ranking`, then the global `ranking.yaml` (beside the config file) and the config's `options.ranking`. `inherit: false` stops the climb. A nearer `ladder`/`names`/`by` replaces a farther one, classes merge by name. Nothing is read, or parsed, until bare `pdfmd` has several documents to choose from, so a broken file never spoils another build.
68
+ - **Two documents that define a class (or the ladder) differently**: a WARN naming both; the newer one's is used. **Mistakes** (a word in two classes, an unknown class on the ladder, a bad `above:`) stop the choice with the file that defined it; a class on no ladder is a WARN and its words are ignored.
69
+ - **`--setup`**: two rows, *Equal priority, when bare pdfmd must choose a document* and *Names that rank versions of a document*, also in a folder's setup; `pdfmd_setup/inventory.py` lists `ranking.by` and `ranking.names` as rows and `ranking.ladder` / `ranking.classes` as left out on purpose (a file's lists). The tie WARN and error now name the `by` setting.
70
+ - Tests: `tests/test_ranking_layers.py` (through pdfmd itself: every place, the order of the places, `inherit`, documents, strict ties, `names`, a custom ladder, mistakes, nothing read for one file), more in `tests/test_ranking_rules.py`. README (*Several documents in a folder*: the settings), module docstring.
71
+
72
+ ## v3.32.9 — 2026-11-03
73
+
74
+ Pre-edit state: commit `8c8293c`. Entry 3eu (the author's idea of 2026-11-03: "a small pdfmd-option called ignore or whatever that will exclude a document from autodiscovery ... actually, let's make it `priority` with different tiers"). Kept in 3.32. Not pushed. First of four commits of the line: the choice and the `priority` key (next: custom classes and the `ranking` settings, `--newest` and `--all`, `--which`).
75
+
76
+ Bare `pdfmd` in a folder with several Markdown files chooses one instead of stopping with the list.
77
+
78
+ - **`priority:`** at the root of a document's front matter or in its `pdfmd-options` (the second wins, with a WARN when both are set and differ). A number (higher wins, floats too), `final` / `last` / `max` (above every number; degrees of lastness: `final final`, `FINAL FINAL!!!`, `final2` and `final67` each beat the one before), `draft` or `0` (loses to every other document, builds when alone), `never` / `ignore` / `skip` / `exclude` or a negative number (not in the list), and nothing at all (`none`, `null`, `nil`, `nill`, `~`, blank: normal, between `draft` and `1`). A value that is neither a number nor a word is a WARN and counts as unset.
79
+ - **Names**: `report.final.md`, `report.rewritten.md`, `report_v3.md` beat `report.md`; `report_draft.md`, `report.old.md` lose to it. They act only inside a family (a base name plus trailing known words, compared ignoring case and separators), so `report_draft.md` beside an unrelated `notes.md` is just another document and `lab2.md` is never a version of `lab.md`; `final_draft` counts as final. A `priority` written in any member of a family switches the names off for it.
80
+ - **Equal tops**: the newest file (modification time) is built, with a WARN that names them all; identical times stop the run with the list and what raised each. The `AUTO MD` line now says what the chosen document beat and why (`over report.md [name: bare name], notes.md [priority: draft]`).
81
+ - **Unchanged**: one Markdown file in the folder; an exact name (`pdfmd report`, still the override); `-b`, `-r` and the viewer. **New kind** `--no-auto priority` brings the old "Multiple Markdown files found" list back.
82
+ - **The rules are pure and in their own package**, `pdfmd_ranking` (words and classes in `rules.py`, the choice in `choose.py`), written as a ladder of word classes from the start: the built-in behaviour is its default setting (`DEFAULT_RANKING`), so the customisation of the next commits adds layers, not new logic.
83
+ - `priority` is a document-only `pdfmd-options` key (`pdfmd_setup/inventory.py`: Omitted, "document"); the `--no-auto` kind has its `--setup` text.
84
+ - **Fixed before the release** (this line is not on PyPI yet; the fixes are one commit after `c19582d`, so they go into the entry of the version that had the bugs, as the versioning rule says): (1) pdfmd's own Markdown output (`X.assembled.md`, `X.gfm.md`, `X.ascii.md`, `X.ascii.gfm.md` beside `X.md`, any assembled file, hidden files such as the temporary `.name.pdfmd-*.md`) was a candidate, so "the newest wins" would have built the last build's output on the next run: `source_markdown()` leaves them out (`--which` names them in a NOTE); (2) `pdfmd -w` re-ran the original command line, so a bare `pdfmd -w` chose again at every rebuild: `pinned_watch_argv()` puts the chosen document in the re-run (and drops `--newest`); (3) `no-auto: [priority]` / `auto: {priority: off}` from a folder or the config file did nothing, only `--no-auto priority` worked: `priority_enabled(folder)` reads the same layers as `effective_no_auto()` (command line, folders, config) for the folder; the module-global `PRIORITY_PICK` is gone, `NO_AUTO_CLI` takes its place and reaches `-j` workers (`tests/test_jobs.py` caught the first). Tests: `tests/test_ranking_generated.py`.
85
+ - Tests: `tests/test_ranking_rules.py` (the engine alone: values, names, classes, ladders, errors, the choice) and `tests/test_ranking_bare.py` (through pdfmd itself, `--stop-at markdown`). README section *Several documents in a folder*, the module docstring and the `--no-auto` help.
86
+
87
+ ## v3.32.8 — 2026-11-03
88
+
89
+ Pre-edit state: commit `1d992bd`. `[kits]`, entry 3et UPDATE 2 (the author: "let's do it. no-auto geometry"). Kept in 3.32. Not pushed.
90
+
91
+ A kit can take pdfmd's own margin, font and table-width defaults over with `no-auto:` in its `.kit` file.
92
+
93
+ - **The key**: `no-auto: [margin]` (a word or a list of `margin`, `mainfont`, `monofont`, `mathfont`, `font`, `tablewidth`). Before, a class kit had to write `geometry: " "` into its metadata to stop the automatic `geometry:margin=...` from clashing with the class's own page; now the kit says what it means, and the metadata can stay about the document.
94
+ - **Where it ranks**: under the command line, the document and the folders' files, over the config file (the same place a kit's metadata has). So a document can turn the default back on with `pdfmd-options: {auto: {margin: on}}`, and `--no-auto` and `-y` still mean what they meant. Applied to a single document and to a report or book (`-r`).
95
+ - **What a kit cannot name**: a kit is chosen from the layers and from the files pdfmd found, so it cannot switch off the finding of metadata, Lua filters, preambles or kits themselves; those kinds, and `true` for everything, are a WARN naming the kind and are ignored.
96
+ - **Not before**: an older pdfmd reads the key as an unknown one (a WARN) and goes on, so a kit that has it still builds there.
97
+ - The kits' own `active_kits` cache is keyed on the two `--no-auto` kinds that decide the kit search (`kit`, `kpsewhich`) instead of the whole list, so one run makes one choice of kits, and prints a kit's warnings once, whatever else is switched off.
98
+ - `pdfmd --kits NAME` shows the kit's `no-auto`. The module docstring now has a short Kits paragraph (it had none), the README's Kits section the key and its ranking.
99
+ - Tests: `tests/test_kit_no_auto.py`, through pdfmd itself (a real build with a kit that has no `geometry:`).
100
+
101
+ ## v3.32.7 — 2026-11-03
102
+
103
+ Pre-edit state: commit `d14c00a`. `[kits]`, entry 3et UPDATE 1 (the author: "make it possible for `pdfmd --install kit` to fetch an arbitrary git repo ... it'll look at all folders of it so nulabreport, which has `.kit` at root, and this one, pdfmd-kits, which has kits in child dirs, would work"). Kept in 3.32. Not pushed.
104
+
105
+ `pdfmd --install kit:SOURCE` installs the kits of any repository.
106
+
107
+ - **Sources**: `owner/repo` (GitHub, its default branch; the tarball over https, no git needed), `owner/repo@REF` (a tag, branch or commit), `#NAME` after either picks one kit, any git URL (https, ssh, scp-like; `git clone --depth 1` with only the https, ssh and file transports, `ext::` refused), `file:///path` (a local git repository, committed state), `./x` `~/x` `/x` (a folder, copied as it is), and a bare `NAME` (a kit of that name in the repositories of `kit-sources`). Plain `http://` and `git://` are refused. `pdfmd --install kit` alone lists the installed kits; `pdfmd --uninstall kit:NAME` removes one.
108
+ - **What it finds**: a folder with `.kit` or `NAME.kit` is a kit; the repository root first, then every folder below it down to `kit-search-depth` (3) levels, hidden folders skipped, and a kit's own sub-folders are the kit's, never searched. So a repository that is one kit (the root shape of a package like nulabreport) and one with a kit per folder (a collection) both work. Each kit is checked with the kit loader; a faulty one is skipped with a WARN and the rest installed.
109
+ - **Where it goes**: `kits/NAME/` beside the config file (kit discovery already reads it), the whole kit folder, with `.pdfmd-install.json` (source, ref, commit, SHA-256 of the archive, path in the repository, date). `pdfmd --kits` shows where an installed kit came from. Installing again updates it (a staged copy, then a swap); a folder there that pdfmd did not install is never replaced; a kit of another source needs `--yes`.
110
+ - **Safe by construction**: nothing of the repository is run (no script, no hook, no kit Lua filter). The archive is read as data: a name that leaves the folder stops the install, links, devices and fifos are dropped, and the files and bytes are capped by `kit-install-max-files` (5000) and `kit-install-max-mb` (200). The install lists the kits that carry a Lua filter and says to read it: pdfmd runs it when a document selects the kit.
111
+ - **Settings** (`options:` of the config file, read at install time, no `--setup` row; the inventory lists them as omitted): `kit-search-depth`, `kit-install-max-mb`, `kit-install-max-files`, `kit-sources` (the repositories a bare name is looked for in).
112
+ - New module `pdfmd_kits/install.py` (pure; the downloader is handed in), `install_kits()` and `uninstall_kit()` in `pdfmd.py`, `kit` in `INSTALL_KINDS`.
113
+ - Tests: `tests/test_kit_install.py` (the forms of a source, both repository shapes, depth and `#NAME`, a faulty kit, the commit from the tarball's folder, update and refusals, uninstall, hostile tarballs, caps, a folder with links, a real local git repository with a tag). Checked through pdfmd itself with a throw-away config: the author's kits repository (ten kits in child folders) and the real nulabreport repository (one kit at the root) were installed, listed, used for a build and uninstalled.
114
+ - README: "Getting a kit" in the Kits section; `--help` and `--install`'s error text name `kit:SOURCE`.
115
+
116
+ ## v3.32.6 — 2026-11-03
117
+
118
+ Pre-edit state: commit `f528e53`. `[kits]`, entry 3et (the author: "yes, follow pdfmd repo rules"). Kept in 3.32 as the author asked on 2026-11-02, though `CLAUDE.md` still names the kits line 3.31.x. Not pushed.
119
+
120
+ A kit's folders reach Tectonic.
121
+
122
+ - **The bug**: a kit puts its folder and its `carry:` folders on `TEXINPUTS`, which Tectonic does not read. With `-e tectonic` a kit that ships a LaTeX class (the apa7 kit of the kits repository, a class with its `config/` files) was never loaded: Tectonic built, without a word, with the copy of that class in its own bundle, or failed when its bundle had none. Found with a copy of the class that stops when it is loaded: xelatex stopped, Tectonic built.
123
+ - **The fix**: for the engine `tectonic`, each chosen kit's folder and every folder under the `carry:` entries that are folders go to Tectonic as `-Zsearch-path=DIR` (`tectonic_search_folders()`; the option names one folder, not a tree, so a carried tree is listed folder by folder, hidden folders left out). The Pandoc route passes them as `--pdf-engine-opt=-Zsearch-path=DIR`; the direct route (`.tex` files, drawn fragments) gets them through the environment `tex_search_env()` already builds (`PDFMD_TECTONIC_SEARCH`) and `latex_engine_command()`. Other engines and a document with no kit are unchanged. Needs a Tectonic that has `-Z search-path` (0.17 has it).
124
+ - Checked through pdfmd itself: the marker copy now stops under `-e tectonic` and the real apa7 kit builds; xelatex and lualatex were never affected.
125
+ - Tests: `tests/test_kit_texinputs.py` (the folders one by one, no hidden folder, no walk of the kit's whole folder, nothing without a kit, the command line of the direct route for Tectonic only).
126
+ - README: the paragraph "A kit needs no install" says it. No new setting.
127
+
128
+ ## v3.32.5 — 2026-11-02
129
+
130
+ Pre-edit state: commit `b5cd684`. Entry 3eo, step 2a of the Obsidian flavour line (the author: "any Pandoc or whatever detected syntax switches the reader, unless those are like examples in fenced code blocks"). Not pushed.
131
+
132
+ - **No more "gfm leaves X as text" NOTE: pdfmd acts on what it finds.** A file with no front matter was read as gfm and, when it used `H~2~O`, `x^2^`, `{#id}` attributes, raw LaTeX or `:::` divs, a NOTE said gfm printed them as text. Now a table of detectors (`READER_NEEDS`, `reader_needs()`) decides, and each syntax needs one of two things: an extension Pandoc accepts on gfm, which is added to the reader (`gfm+subscript`, `+superscript`, `+attributes+bracketed_spans`, `+fenced_divs`, `+definition_lists`, `+fancy_lists`, `+raw_attribute`; the definition-list and `.csv` div cases were added on need before), or Pandoc's own Markdown reader, which the file is switched to (`^[inline footnotes]`, raw LaTeX, grid tables, simple tables, `(@)` example lists, as `@citations`, Org captions and `\md{}` already did). `--verbose` says which syntax decided: `reading as gfm+subscript (it uses subscripts (H~2~O))`, or `it uses raw LaTeX (...), which gfm cannot read; keeping the default markdown reader`.
133
+ - **Only prose is looked at**: fenced code, inline code, math (`$..$`, `$$..$$`; `$5 or $10` is not math) and HTML comments are taken out first, so an example of the syntax in a code block changes nothing. The line that opens a fence is the one exception (a fence with attributes, ```` ```{.smiles} ````, needs `+attributes`). Lettered and Roman-numeral lists count only as two items in a row (`a.` `b.`, `(i)` `(ii)`): `I. Introduction` alone stays text, as in GFM. Not looked for: indented code blocks, line blocks, multiline tables, inline `^x^` inside words (listed in the notes).
134
+ - **Cost, accepted by the author**: Pandoc's Markdown reader sizes pipe-table columns differently from gfm (the reason gfm was chosen); a file with raw LaTeX or an inline footnote and no front matter now gets that reader, so its tables size as they do in a file with front matter.
135
+ - Removed with it: `reader_notice()`, `gfm_blind_spots()`, `GFM_BLIND_SPOTS`, `FENCE_ATTRIBUTES`, `has_definition_list()` (the table replaces them); the NOTE is gone from the output.
136
+ - Tests: `tests/test_reader.py`: the old blind-spot tests became `ReaderNeeds` (the extension each syntax needs, what only Pandoc's reader reads, code and math and comments not counting, a fence's attributes, lettered lists) and the command-line tests (nothing said but under `-v`, the extension added and `<sub>` in the output, a switch to Pandoc's reader for an inline footnote and for raw LaTeX, the same syntax in a code block changes nothing). Checked through the whole `pdfmd` on the three notes of the Obsidian test vault (all read as plain gfm, nothing to add) and on small files.
137
+ - README: the "It decides per document" paragraph and the `READER` row say what is added and what switches.
138
+
139
+ ## v3.32.4 — 2026-11-02
140
+
141
+ Pre-edit state: commit `834e135`. Entry 3eo, step 1 of the Obsidian flavour line: the gfm-or-Pandoc choice made reliable. Not pushed on its own (see the last bullet).
142
+
143
+ - **The global metadata no longer decides the reader.** The file `pdfmd --setup` writes (`metadata.yaml` beside the config, and the presets) was counted as "a linked metadata file", so on a machine that has one every file with no front matter was read as Pandoc's Markdown and the gfm rule never fired: the same file read differently on another machine (found with the author's own notes: `AUTO READER ... metadata.yaml is linked; keeping the default markdown reader instead of gfm` for a file that had none). Now only a metadata file of the document (beside it, `-y`, a folder's) counts. Everything the global file sets still applies: Pandoc reads a metadata file's strings with its own Markdown whatever the reader (measured with Pandoc 3.13: `title`, a `header-includes` macro and `H~2~O` in an `abstract` come out the same under `-f gfm` and `-f markdown`), a global `pdfmd-options: {reader: markdown}` still wins, and `promote_bare_title()` is unchanged (pinned by a test).
144
+ - **`has_yaml_frontmatter()` says what Pandoc says.** It was "the first line is `---`", so a file that opens with a thematic break (`---`, a blank line, text) counted as having front matter and was read as Pandoc's Markdown. Now: blank lines may come first, then `---` and a line that is not blank, and a later `---` or `...` closes it; `---` straight after `---` is an empty block and still counts (the documented way to ask for Pandoc's reader); a BOM and CRLF are fine. Measured against `pandoc -f markdown -t json` for 14 shapes. About twenty other places in `pdfmd.py` read a front-matter block with their own regex (`papersize`, margins, `pdfmd-options` ...); they all require a closing line, so they differ from this only for a file that opens with a rule and has another rule later. Left as they are, listed in the notes.
145
+ - Tests: `tests/test_reader.py` (+8): the truth table both ways, and through the whole `pdfmd` with a config and a global `metadata.yaml` (gfm is chosen and the NOTE is printed; the global title, a raw-LaTeX `header-includes` and Markdown in a metadata string still arrive; a global `reader:` still wins; a `metadata.yaml` beside the document still means Pandoc's Markdown; a leading rule is not front matter; a bare `# Title` is left alone). Run with the modules that use a global file (`test_presets`, `test_setup_folder`, `test_standalone_source`, `test_path_display`, `test_mathfont`, `test_config`, `test_typst_input`, `test_setup`).
146
+ - README: "It decides per document" and the `READER` row say what counts.
147
+ - **Behaviour change to know about:** on a machine with a global metadata file, a file with no front matter that uses `H~2~O` or `x^2^` now prints the existing NOTE and shows them as text, instead of silently rendering them as Pandoc does (a file with `@citation` syntax or a `metadata.yaml` beside it still keeps Pandoc's reader). A document with front matter (every `nulabreport` report has some) is untouched. Step 2 of the line (the `pdfmd` flavour) adds subscripts, superscripts and the other Pandoc blocks to the gfm reader so the NOTE goes away; that is why this patch is not pushed alone.
148
+
149
+ ## v3.32.3 — 2026-11-02
150
+
151
+ Pre-edit state: commit `4613a5a`. Entry 3en, the second look at 3.32.1 and 3.32.2.
152
+
153
+ - `diagram-timeout` without a runner: a copy of pdfmd with no `pdfmd_lua/limited_run.py`, or a Python that does not know its own path (`sys.executable` empty), made every diagram fail with "does the source parse?". Now the filter is told nothing about a limit, the tools run as before, and pdfmd prints one NOTE saying the setting is not applied. (A wheel and the zipapp both carry the file: a wheel ships a package's `.py` files, the zipapp unpacks to real files.)
154
+ - `limited_run.py` waits at most 5 s after it has stopped a tool for the pipes to close (a grandchild in a session of its own can hold one open). It is a constant, not a setting: a safety stop in a helper that decides nothing about the output, and nothing a user would tune.
155
+ - `tests/test_slide_notes.py`: the header reaches the `.tex` of a deck (`--stop-at tex`, no LaTeX needed) with the deck's own `aspectratio=169` and `header-includes` macro still there (3 tests: the header and the deck's own settings, `off`, the setting in a `-y` metadata file). Also checked by hand: a `metadata.yaml` found beside the deck. `tests/test_diagram_timeout.py` (+3): the limit and runner reach the filter, `0` turns diagrams off, a negative number passes no limit, no interpreter means no limit and a NOTE.
156
+
157
+ ## v3.32.2 — 2026-11-02
158
+
159
+ Pre-edit state: commit `c9d0f0a`. Entry 3en (the author: "the setting --- yes, and document it" for a named slide-notes setting).
160
+
161
+ Speaker notes in a Beamer deck. A `::: notes` block was dropped by `pdfmd -p` (0 note lines in the PDF); Pandoc has the feature, Beamer's `show notes`, but it was reachable only through a variable and undocumented.
162
+
163
+ - `slide-notes: off | show | only` (`pdfmd-options` key, config key, `--setup` row "Speaker notes ..." under Output; default `off`, today's behaviour). `show`: a notes page after each slide. `only`: the notes pages alone, for printing. `true` and `on` mean `show`; a bad word stops the build and names the three, like `md-macro`.
164
+ - Written as a header file (`\setbeameroption{show notes}` / `{show only notes}`, cached by mode under the cache's `tex/`), added next to the other pdfmd headers for a `-p` build with a LaTeX engine, NOT as `-V classoption=notes=show`: measured on 3.32.0, that variable replaces the deck's own `classoption`, and a 16:9 deck (`aspectratio=169`, 454 x 255 pt) came out 4:3 (363 x 272 pt). With the header the size stays.
165
+ - Other builds (not `-p`, other writers, a non-LaTeX engine) ignore the setting without a message: a config-level `slide-notes: show` is meant for decks and would otherwise talk on every other document. Pandoc's own revealjs already shows notes.
166
+ - `slide_notes_mode()` and `slide_notes_args()`; `tests/test_slide_notes.py` (7 tests: the default, `show` / `only` / `true`, a bad word, a header only for a deck; and, when `lualatex` and poppler are installed, a 16:9 deck built three ways: 3 pages and no notes by default, 5 pages with `show`, 2 with `only`, the page size 454 x 255 pt each time). README: "Speaker notes" under Slides, with an example; `docs/recipes.md` 7.4 has a line. The `--help` text has no new flag.
167
+ - Not done: Beamer's `show notes on second screen=right` (notes beside the slide, for a presenter view), which would be a fourth value; the native tier and a Typst deck (pdfmd has none).
168
+
169
+ ## v3.32.1 — 2026-11-02
170
+
171
+ Pre-edit state: commit `c52dd09`. Entry 3en (the review of two research reports on Markdown-to-PDF pain points; the author: "diagram limit on at 120 s. the setting --- yes, and document it").
172
+
173
+ A diagram tool that never answered hung the build with no message: a stub `mmdc` that sleeps was still being waited for after 60 s. `pandoc.pipe`, which `pdfmd_lua/diagram.lua` ran the tools with, has no time limit.
174
+
175
+ - `diagram-timeout` (`pdfmd-options` key, config key, `--setup` row "Time limit of one diagram tool run" under Checks and limits): seconds one run of `mmdc`, `dot`, `d2`, or of the `rsvg-convert` / `inkscape` that converts their SVG, may take. Default 120, ON. `0`: no diagram is drawn (the fences stay as code, one WARN naming the setting; it overlaps `--no-auto diagrams`, kept so the value means what `soffice-timeout: 0` means). A negative number: no limit, and no runner.
176
+ - New `pdfmd_lua/limited_run.py`, started by the filter under the limit with the interpreter pdfmd runs on (so no `timeout` command is needed, which macOS and Windows lack): it passes standard input and output through as bytes (a picture written to standard output stays intact), leaves the tool's error output where it was, and when the time is up stops the tool with every process it started (a process group; `taskkill /T` on Windows), prints a marker and exits 124. 127 for a missing program.
177
+ - The filter: a stopped tool leaves its diagram as code with a WARN that says which program did not finish in how many seconds and names the setting (`--strict` makes it an error, like any undrawable diagram), and the tool is not tried again in the build, with one WARN for the first skipped fence: a browser that cannot start would otherwise cost the limit once per diagram. A stopped `dot` is not asked a second time for an SVG to convert. Nothing from a stopped run reaches the cache.
178
+ - Known cost, not fixed: the Word and OpenDocument route runs Pandoc twice, so there a hung tool costs the limit twice (each run remembers on its own).
179
+ - `diagram_timeout_from()` and `diagram_timeout_for()` (the document's key, its metadata files', the config's); the filter gets `pdfmd-diagram-timeout`, `-python` and `-runner`, or `pdfmd-diagram-off`, in the metadata map it already had.
180
+ - `tests/test_diagram_timeout.py` (6 tests: the value as seconds, a document's own limit, bytes through the runner, exit code and error output through the runner, a run stopped with its child, a missing program) and four in `tests/test_diagrams.py` (a hanging `dot` is stopped and the second fence does not start it again, nothing in the cache, `--strict` fails; a hung Graphviz is not asked for an SVG next; `0` runs nothing; `-1` lets a slow tool finish). Not tested on Windows: the runner uses `taskkill /T` and `shutil.which` for `mmdc.cmd`; the end-to-end tests there use shell-script stand-ins and are skipped, as the others in that file.
181
+ - README: "Diagram time limit" beside the LibreOffice one, and a sentence in the diagrams section. The `--help` text has no new flag.
182
+
183
+ ## v3.32.0 — 2026-11-02
184
+
185
+ Pre-edit state: commit `394e482`. Opens the `\md{}` feature line, entry 3dc (queued 2026-10-26; the author, 2026-11-02: "i guess it is a good time to create that \md{} feature we were thinking of").
186
+
187
+ Markdown inside raw LaTeX. Raw LaTeX in a Markdown file reaches LaTeX untouched, so `*emphasis*`, `$x^2$` or `[@key]` inside a `\textbf{...}`, a `\caption{...}` or a hand-written `tabular` stayed as typed.
188
+
189
+ - `\md{...}` (one line or several) and `\begin{md}...\end{md}` hold Markdown: new `pdfmd_lua/md_macro.lua` splits the raw TeX Pandoc hands over into [raw TeX, the argument read as Markdown, raw TeX] with `pandoc.read`. It is the first of pdfmd's Lua filters, before pandoc-crossref and `--citeproc`, so a citation, an `@fig:x` reference, math or a footnote in the argument belongs to the one document.
190
+ - Where: a raw inline (`\textbf{\md{x}}`, a paragraph, heading, list item, table cell, caption) for every writer; a raw block such as a whole `tabular` or `figure` as one Plain block for LaTeX, Beamer and ConTeXt (the other writers drop that raw block as before); `\begin{md}` as real blocks (paragraphs, lists, tables) for every writer, also inside another raw environment. Several paragraphs in an inline `\md{}` are joined with a line break.
191
+ - The argument's braces must balance; a backslash escapes the next character (`\{`, `\}`), `%` and `#` are Markdown, not a TeX comment or parameter. A `\md` followed by a letter (`\mdseries`) is another command. An unbalanced `\md{` is read by Pandoc as plain text; the filter says so once with a WARN. `\begin{md}` has no brace counting, which is the way to write a code span with a lone brace.
192
+ - `md-macro: on` (the default) | `off` in `pdfmd-options`, with a `--setup` row (Input). The filter is loaded only for a document that has a macro outside code; a document that defines `\md` itself (`\newcommand`, `\def`, `\newenvironment{md}`) keeps its own and gets one NOTE.
193
+ - `contains_citations()` asks Pandoc with the filter on, so `--citeproc` is added for a document whose only citations are inside `\md{}`. A file with no front matter that uses the macro is read with Pandoc's `markdown` reader, not `gfm` (no raw LaTeX there), with the usual notice.
194
+ - Not done: the native (no-Pandoc) tier, a `.tex` source compiled directly, and the identity `.sty` for `~/dev/tex` (so the same `.tex` compiles without pdfmd), which is a step in the other repository.
195
+ - `tests/test_md_macro.py` (14 tests: detection outside code and own definitions, a macro inside `\textbf`, in a heading / bare line / list / table cell, in a raw `tabular` for LaTeX and HTML, the environment for tex / html / typst / md and inside `center`, citations resolved and listed, no front matter, `off`, a bad word, an own definition, an unbalanced brace, `\mdseries`). README: "Markdown inside raw LaTeX".
196
+
197
+ ## v3.31.52 — 2026-11-02
198
+
199
+ Pre-edit state: commit `bd497d7`. Still under 3.31. `[tables]`, entry 3el (the author: "it shows that some features we've added are thin ... record it and add todo line to revise the full functionality ... and start with all those three").
200
+
201
+ Three things the author's own example showed missing: a caption above a table that needs no repair, the `#+` lines of Org, and a cell's own alignment.
202
+
203
+ - `#+CAPTION:` (any case) and `#+NAME: tbl:id` above ANY pipe table become its caption, one that needed no repair included (with or without outer bars: `1 | 2 | 3` and `:-|:-:|-:`). That table is otherwise not touched, and nothing is printed for it unless a `#+` line was dropped.
204
+ - `#+ATTR_*:` lines directly above an Org or rule-less table are consumed: `:align lcr` (letters l, c, r; `|` and other characters ignored) sets the alignment when no cookie row does; every other option is dropped with a WARN. Above a valid pipe table its separator row decides and `:align` is reported as not used. `#+TBLFM:` lines directly below are dropped with a WARN naming them (the cells hold the result; nothing is calculated).
205
+ - A cell that starts with `<l>`, `<c>` or `<r>` keeps that alignment: `pdfmd_lua/cellalign.lua` cuts the tag (and the space after it) off. HTML, Typst, Word and the rest use the cell's alignment; Pandoc's LaTeX writer ignores it, so LaTeX and Beamer get raw TeX, `\multicolumn{1}{c}{...}` in a column of natural width and `{\centering ...\par}` in a column with a width, which is why the filter goes in after the table-width filter (a new `cellalign_args()` after every `width_filter_args` call). The Markdown writers keep the text; a cell with more than one block is left alone. `pdfmd-options: cell-align: on|off` (default on, a `--setup` row), loaded only for a document that has such a cell.
206
+ - `tests/test_cellalign.py` (9 tests: detection, natural and wrapped LaTeX columns, Typst, HTML, `off`, a bad word, the Markdown writers, no filter without a tag, the author's example whole); `tests/test_table_ascii.py` (40 tests: captions above valid tables, ATTR and TBLFM, `#+` lines in code, others kept). README: the cell-alignment section and the Org bullet.
207
+ - Not done: the native tier; `#+TBLFM:` is never evaluated. TODO: a line to revise the thin features of this line (the pre-pass kinds, line breaks, table captions, `<c>` cells, Org extras) against unusual input during the documentation programme.
208
+
209
+ ## v3.31.51 — 2026-11-02
210
+
211
+ Pre-edit state: commit `70963c6`. Still under 3.31. `[tables]`, entry 3el (the author, asked whether Org's alignment cookies and `#+CAPTION:` would be useful: "let's do it").
212
+
213
+ An Org table keeps its alignment and its caption.
214
+
215
+ - A row of alignment cookies (`| <l> | <r> | <c> |`, with a width `<r10>`, or `<10>` alone) is dropped from the table and sets the separator row: `:----`, `----:`, `:---:`. Every separator cell has the same width when any column is aligned, because Pandoc's `markdown` reader takes unequal dashes for a choice of column widths. A width alone sets no alignment.
216
+ - `#+CAPTION: text` (also `#+CAPTION[short]:`; several lines are joined) directly above a repaired Org or rule-less table becomes a `: text` line below it; `#+NAME: tbl:id` adds `{#tbl:id}`. A plain `#+NAME:` is dropped (before Pandoc 3.8.2 a plain `{#id}` would show as words in the caption). Other `#+` lines (`#+ATTR_LATEX:`, `#+TBLFM:`) stay as written, and a `#+CAPTION:` that is not directly above such a table stays.
217
+ - A caption needs Pandoc's `markdown` reader (gfm has no table captions), so a file with no front matter that has a `#+CAPTION:` line is read with `markdown` instead of `gfm`, with the usual notice, unless `table-ascii` is `note` or `off`.
218
+ - `pdfmd_tables/ascii.py` (`org_caption`, `COOKIE`, `assemble(aligns=)`), `pdfmd.py` (`ORG_CAPTION_RE`, the reader choice), the `--setup` row text, README. Tests: `tests/test_table_ascii.py` (cookies, caption and name lines, a build with no front matter).
219
+
220
+ ## v3.31.50 — 2026-11-02
221
+
222
+ Pre-edit state: commit `fe19049`. Still under 3.31. `[tables]`, entry 3el (the author, after the explanation of Org-mode: "do the orgmode then").
223
+
224
+ Org-mode tables, and `|` lines with no separator row, are read as pipe tables, with the setting of 3.31.47 (`table-ascii`).
225
+
226
+ - Org: `| a | b |` lines with a rule `|---+---|` or `|-` (a lone `|---|` is a one-column pipe table's separator and stays). Pandoc's `markdown` reader reads the `+` rule, `gfm` and `commonmark_x` make a paragraph of it, so pdfmd rewrites it for both. The rows above the first rule are the header (several are joined like a wrapped header), later rules only group rows and are dropped, leading and trailing rules too; one line is one row (Org has no wrapped cells). A row with another number of cells than the header is fitted, with a WARN of its own.
227
+ - `|` lines with no separator row (an Org table without a rule, what some chat models write): Pandoc reads a paragraph (`gfm`) or a line block (`markdown`). Every line must start and end with an unescaped `|`, have two cells or more and the same number as the others, at least two lines, and none may be a separator row; the first row is the header. A real pipe table, a line block (`| roses`), a lone line and a table with a separator row are never touched.
228
+ - The table keeps its indent (a table inside a list item). `pdfmd_tables/ascii.py` (`org_table`, kinds `org` and `pipe`), the `--setup` row text, README (two more bullets). Tests: `tests/test_table_ascii.py` (header above the first rule, group rules, a ragged row, no separator row, an indent, real pipe tables and line blocks untouched, a build under both readers).
229
+ - Not done: Org's alignment-cookie rows and `#+CAPTION:` lines stay as written; the no-Pandoc native renderer does not do it.
230
+
231
+ ## v3.31.49 — 2026-11-02
232
+
233
+ Pre-edit state: commit `765ce9e`. Still under 3.31. `[tables]`, entry 3el (the author: "`<br>` ... `\par` destroys table, and `<br>` is ignored in latex route. let's do it"; "let's not limit it to `<br>` html only and make it interconvertable, like pagebreak syntax").
234
+
235
+ One line break for every writer.
236
+
237
+ - Pandoc reads `<br>` as raw HTML, which every writer but HTML drops: `one<br>two` printed `onetwo` in LaTeX, and a table cell, where a blank line or `\par` breaks the table, could not hold two lines. `pdfmd_lua/linebreak.lua` (new, in the manner of `pagebreak.lua`) reads `<br>`, `<br/>`, `<br />` (any case), a raw TeX `\newline`, `\linebreak`, `\linebreak[n]`, a raw Typst `#linebreak()`, a raw Word break run and a raw ConTeXt `\crlf`, and gives the target Pandoc's LineBreak (LaTeX `\\`, in a cell a box of lines; Typst `\`; Word a break run; HTML `<br />`).
238
+ - A spelling already in the target's own language stays as written; the Markdown writers keep everything, except pdfmd's flat Markdown (`--to gfm`, raw HTML off), where `<br>` would be dropped and becomes a hard break. A paragraph or cell of nothing but breaks is left alone (the `space` markup kind reads a lone `<br>`); a break at the very start or end of a run of text is dropped.
239
+ - Mirrors pagebreak, with another default (the author: "2. equivalent of auto ... everything else mirrors pagebreaks, with all and families"). Families: `html` (`<br>`, `<br/>`, `<br />`), `tex` (`\\newline`, `\\linebreak[n]`), `typst` (`#linebreak()`), `word` (a raw break run), `context` (`\\crlf`). `pdfmd-options: linebreak:` not set or `auto` carries every family inside table cells only; `true` / `all` every family everywhere; a list those families everywhere; `false` none. `--linebreak [KIND ...]` (bare: all, everywhere) and `--no-linebreak` for a run; a `--setup` row (a multi choice of the families, in Universal markup; unset is `auto`) and the inventory entries. pdfmd passes the families and where as `-M pdfmd-linebreak=html,tex` and `-M pdfmd-linebreak-where=cells|all`; the filter is added only for a document that has a spelling (`LINEBREAK_RE`). A word that is no family is said and ignored. Not done: the native (no-Pandoc) tier.
240
+ - README: "A line break inside a table cell". Tests: `tests/test_linebreak.py` (the default in cells, `true`, the target's own spelling, Typst and Word, families, `false` and the command line, a bad word, the Markdown writers, lone and trailing breaks).
241
+
242
+ ## v3.31.48 — 2026-11-02
243
+
244
+ Pre-edit state: commit `6253c34`. Still under 3.31. `[tables]`, entry 3el (the author: "yes, build all of it as 3.31.48").
245
+
246
+ More text tables are read as pipe tables, with the setting of 3.31.47 (`table-ascii`).
247
+
248
+ - Lazy ASCII forms: a border as short as `+-+` (no inner `+`), rows without outer bars (`1 | 2`), middle borders left out. The table must be bordered at both ends and have a line with two cells. With no border between the body lines, a line whose first cell is empty continues the row above and any other line starts a row; with a border between rows, the lines between two borders are one row, as before.
249
+ - Box-drawing tables (`┌─┬─┐ │ ├─┼─┤ └─┴─┘`, double, rounded and heavy): cells split on `│ ┃ ║`, a `|` inside a cell escaped; the first rule that contains `═` or `━` and is not the first or last is the header rule.
250
+ - psql output: the header line (no outer bars) above `----+----`, rows with as many bars, the `(N rows)` line dropped with the table.
251
+ - MySQL console output: a real grid table, no `=` rule, exactly three rules, a one-line header and two or more body lines, which Pandoc read as one joined row; each line is a row now. A body line with an empty first cell leaves the table alone, so a real grid table with a wrapped row is not touched; any other real grid table is never touched.
252
+ - The WARN names the form (`a box-drawing table was read as a pipe table (2 columns, 2 rows)`); the `--setup` row and the README section ("Text tables Pandoc cannot read") list the four spellings. Not done: the native tier, `<br>` in cells, org `|---+---|` and a pipe table without a separator row (queue items).
253
+ - Tests: `tests/test_table_ascii.py` (`Forms`, and a build for each of box, psql and lazy).
254
+
255
+ ## v3.31.47 — 2026-11-02
256
+
257
+ Pre-edit state: commit `4761d60`. Still under 3.31. `[tables]`, entry 3el (the author pasted a table that NotebookLM wrote; "just implement it now under 3.31").
258
+
259
+ An ASCII table with no `+` at its column boundaries is read as a pipe table.
260
+
261
+ - Chat models write a "plain-text table" with `+-----+` borders that have no inner `+`, no `+===+` under the header, and `|` bars that drift by a character. Pandoc read it as one column without a header (every line a cell, the text off the page), and `table_width.lua` had nothing to balance. `pdfmd_tables/ascii.py` (new) repairs it before Pandoc sees it: a row is split on `|` (the bars' positions are not trusted), the lines between two borders are one row (wrapped cell lines joined with a space), the first row is the header (every row above a `+===+` line, if there is one), and the result is a pipe table with equal columns, which pdfmd's own width filter balances as it does any other. Rows with another number of cells than the header are padded or cut, each with a WARN.
262
+ - A real grid table (an inner `+`), a grid table of one column (no line with two cells), and anything inside a fenced or indented code block, an HTML comment or the YAML front matter are left alone. A literal `|` in a cell must be written `\|`.
263
+ - The text is changed in memory: `prepared_title_source()` (the document and a section built alone) and `scaffold_inputs()` (every part; the new `transform` argument) hand Pandoc a temporary copy, the file is never touched. One `WARN table-ascii: FILE, line N: ...` for each table. Not done: the native (no-Pandoc) tier.
264
+ - `pdfmd-options: table-ascii: repair` (the default) | `note` (leave it, say so once for each table) | `off`; a `--setup` row (`pdfmd_setup/registry.py`) and its inventory entry. A word that is none of them stops the build. No number is a threshold.
265
+ - README: "A plain-text table from a chat model". Tests: `tests/test_table_ascii.py` (the function, then real builds: the default, `note`, `off`, a bad word, a real grid table, a part).
266
+
267
+ ## v3.31.46f — 2026-11-02
268
+
269
+ Pre-edit state: commit `f47c019`. No shipped code changes (a manifest, a script, a workflow and docs; none of it is in the PyPI package), so no version bump. Entry 3ej.
270
+
271
+ A Scoop bucket for Windows, in this repository.
272
+
273
+ - `bucket/pdfmd.json`: depends on Python, Pandoc and Typst (suggests Tectonic); its install script makes a virtual environment in the app folder and has pip install `pdfmd-cli==<version>` into it; `pdfmd.exe` is the shim. `scoop bucket add pdfmd https://github.com/aliperdehan/pdfmd`, then `scoop install pdfmd/pdfmd`.
274
+ - `scripts/bump_scoop.py` (shares the PyPI lookup with `bump_formula.py`): changes the manifest's `version` and nothing else; `tests/test_scoop_bucket.py` checks the manifest and the script.
275
+ - `.github/actions/scoop-test` installs Scoop on a Windows runner, installs pdfmd from the manifest, builds a document with Typst and uninstalls it. `scoop.yml` runs it by hand; the new `scoop` job of `publish.yml` points the manifest at the release, runs it, and commits the manifest to `main` as the bot, next to the Homebrew job (both pushes now pull and try again, since they can meet).
276
+ - README: the Scoop install in the install section.
277
+
278
+ ## v3.31.46e — 2026-11-02
279
+
280
+ Pre-edit state: commit `418ac50`. No shipped code changes (a Dockerfile, a workflow and docs; none of it is in the PyPI package), so no version bump. Entry 3ej, the author's wish: "let's try docker and scoop", with Tectonic and Typst instead of a full TeX Live and Calibre and LibreOffice optional.
281
+
282
+ A container image, `ghcr.io/aliperdehan/pdfmd`.
283
+
284
+ - `docker/Dockerfile`: one file, four variants by build argument. `latest` is Python, pdfmd from PyPI, Pandoc and Typst (`--install full`), Tectonic (`--install core:tex`), the default fonts, and one warm-up build so Tectonic's own files are in the image and a run needs no network (0.8 GB). `TEX=1` adds TeX Live's LuaTeX and XeTeX for documents that set `engine: lualatex`; `OFFICE=1` adds Calibre (`--install calibre`, with the Qt libraries it needs) and LibreOffice (3.4 GB); both are `full` (4.4 GB). The tools and caches are in `/opt/pdfmd`, open to every user, so `docker run --user` works. The default fonts come from a CTAN mirror that sometimes stalls, so the build gives that step a time limit and tries again.
285
+ - `docker/smoke.sh`: what CI runs on every image before it is pushed, as another user than root and with no network: the default engine, Typst, Tectonic (and lualatex for TeX Live); Word, EPUB and ODT output, Calibre and LibreOffice for the office variants. It caught a missing Qt library of Calibre in the first office image.
286
+ - `.github/workflows/docker.yml`: the four variants for amd64 and arm64 (on the native runners, no emulation), the smoke test, a push by digest and one multi-platform tag per variant (`latest`, `tex`, `office`, `full`, and `X.Y.Z[-variant]`). `publish.yml` calls it after the PyPI upload; run by hand from the Actions tab it builds and tests without pushing, or publishes the images of a release that is already on PyPI.
287
+ - README: the container in the install section. `CLAUDE.md`: the release also builds the images.
288
+
289
+ ## v3.31.46d — 2026-11-02
290
+
291
+ Pre-edit state: commit `50e7ed1`. No shipped code changes (a docs file only), so no version bump.
292
+
293
+ - `CLAUDE.md` (Releasing): after a release the `homebrew` job has committed the formula bump to `main`, so the local `main` must be pulled (`git pull --ff-only`) before the next push.
294
+
295
+ ## v3.31.46c — 2026-11-02
296
+
297
+ Pre-edit state: commit `119003b`. No shipped code changes (tests only), so no version bump. The first push of the 3.31.4x commits showed what the CI saw and the machine of the author did not.
298
+
299
+ Lint and Windows, which kept `Test and publish` from releasing.
300
+
301
+ - `ruff check --select F` (the `lint` job) found five unused or redefined imports in `test_build_log`, `test_diagram_shim` and `test_kit_selection`; removed.
302
+ - Windows: `test_display_path` expected the temporary folder to be written absolute, but there it lies below the home folder, where the `auto` style writes `~`: the test asks for the `relative` style. `test_kits`: `/one/place` has no drive on Windows (the test uses an absolute path of its own folder), and the cache of the kit lookup is in `LOCALAPPDATA` there, not `XDG_CACHE_HOME`.
303
+
304
+ ## v3.31.46b — 2026-11-02
305
+
306
+ Pre-edit state: commit `63d353d`. No shipped code changes (tests only), so no version bump.
307
+
308
+ Five test modules no longer depend on the machine they run on.
309
+
310
+ - Without Pandoc on the PATH, `test_build_log`, `test_build_time` and `test_path_display` ran real builds and failed (20 tests); they are skipped now, as the other modules that need Pandoc are. `test_table_widths` crashed when it was imported, reading Pandoc's version for a decorator; with no Pandoc it reads `(0,)` and its tests are skipped.
311
+ - `test_diagrams` gives the pdfmd it starts an empty data folder (`XDG_DATA_HOME`, `LOCALAPPDATA`): the diagram tools that `pdfmd --install diagram` keeps there are on pdfmd's own PATH, and on a machine that had installed them two tests (a missing tool, the doctor's "nothing installed") found a `dot` that the test had taken away.
312
+
313
+ ## v3.31.46 — 2026-11-02
314
+
315
+ Pre-edit state: commit `2a63449`. Still under 3.31 [tables]. Entry 3dy (the author's wish, 2026-10-31, with the answers of 2026-11-01/02).
316
+
317
+ A caption typed by hand next to a table can be the table's caption.
318
+
319
+ - `pdfmd_lua/table_caption.lua` (new, a Pandoc filter, so gfm and Pandoc Markdown alike): a paragraph that is entirely one bold span starting with a label word (`**Table 1: Results**`), right above a table without a caption (or else right below one; above wins), becomes its caption. The label and number are cut off (the engine numbers the table), the rest keeps its markup, a trailing `{#tbl:id}` becomes the table's id when it has none. A paragraph or heading between them, a bold line that is not alone in its paragraph, or a table that already has a caption: left alone. The number of blank lines does not matter.
320
+ - The label words come from `pdfmd_tables/captions.py` (English always, then the document's `lang:`: de, fr, es, it, pt, nl, pl, cs, sv, da, no, fi, ro, hu, el, bg, ru, uk, kk, tr, ar, zh, ja, ko), then `table-caption-words`: data, not code.
321
+ - Four `pdfmd-options` keys, each a `--setup` row (config and folder levels) and in the inventory: `table-captions` (`note`, the default: change nothing and say once, `NOTE table-captions: N lines next to a table read like captions`; `on`; `off`), `table-caption-label` (`strip`, `keep`), `table-caption-need-label` (`on`; `off` takes any line that is one bold span, the author's "skip the label word" toggle) and `table-caption-colon-words` (0 = off, the author's "<= 2 words" sketch as a number: a gfm `: Results` of at most that many words is a caption too; Pandoc Markdown reads it by itself).
322
+ - The filter is added to the Pandoc command only for a document with a line that could be a caption, and after the CSV, markup and page-break filters.
323
+ - Tests: `tests/test_table_captions.py`.
324
+
325
+ ## v3.31.45 — 2026-11-01
326
+
327
+ Pre-edit state: commit `f4cd293`. Still under 3.31. Entry 3eh (the author: "i suggest making it build @05,10, since both of them are introductions"; "yes, i want it done as 3.31").
328
+
329
+ A part name that fits several top-level files with the same name builds all of them.
330
+
331
+ - `report#introduction` (or `#intro`) with both `parts/05-introduction.md` and `parts/10-introduction.md` stopped with "is ambiguous: it matches parts in 05-introduction.md, 10-introduction.md". Top-level files that differ only in their numbers are one section in two files, so the name builds both, in document order. The error stays for what is really ambiguous: the same name in two different folders (`yield` in Calculations and Discussion), and a prefix that fits different names (`intro` with an `introduction` and an `introspection`). A number (`05`) or the full file name (`10-introduction`) still names one file.
332
+ - Tests: `tests/test_sections.py` (`SameNameParts`). README: the example in the parts section.
333
+
334
+ ## v3.31.44 — 2026-11-01
335
+
336
+ Pre-edit state: commit `c16091d`. Still under 3.31. Entry 3ef (the author's decision, with an example: the numbers of a part built alone are the numbers of the full build, names printing in the order they appear).
337
+
338
+ A part built alone sees the calculator's names of the whole document, below it as well as above it.
339
+
340
+ - Until now only the parts above the one built were evaluated (3.30.19), so a name defined in a later part was undefined in a part built alone, and the author had to build the Calculations part along with every part that prints its numbers. Now pdfmd reads the whole document with `calc.lua` alone (nothing typeset, about a second): the scaffold, then every part in order, a one-word paragraph `pdfmd-calc-mark-in` / `-out` around each part built. `calc.lua` writes (`-M pdfmd-calc-halves=FILE`) each name's last definition above the part built and its first definition below it, each evaluated in its own place, and the build reads them back, the first as before (`pdfmd-calc-seed`, standing above everything), the second as `pdfmd-calc-below` (standing after everything, so a name is taken from there only when nothing above or inside the part defines it). That is the full build's rule: the nearest definition above, else the first one below. With `x = 2` in the introduction, `y = x * 2` and then `x = 3` in the calculations, the procedure alone prints 2 and 4, the discussion alone 3 and 4, like the full build; the introduction alone, with every name below, works too.
341
+ - Sections of a plain document (`doc#procedure`) too: the text above and below the sections chosen is read, each section marked. The NOTE says where the names came from: `NOTE calc: 128 names taken from parts 10-50, 3 from parts 70-90 (defined below) (evaluated, not typeset)`. The same `seed-calc` setting (`auto`, `scan`, `off`) governs both halves; no new setting. The export file (`calc: {export:}`) of a part built alone still lists the names above it and inside it only.
342
+ - KNOWN BUG, not fixed here (queued, entry 3eg): two parts built together that are not next to each other (`report#20+40`) take what the parts between them define as defined above both, so a number in part 20 can be the value a name gets only in part 30, silently. It was so before this version (the parts between were already read as "above"). Adjacent parts and sections are exact.
343
+ - README (the paragraph on a part built alone), recipes 3.2, the `--seed-calc` help and its `--setup` row say the new rule. Tests: `tests/test_calc.py` (`NamesBelow`: the example above, the first part alone, the part's own definition winning, two parts side by side, `--seed-calc off`, a name that is nowhere, sections).
344
+
345
+ ## v3.31.43 — 2026-10-31
346
+
347
+ Pre-edit state: commit `c32ad4b`. Still under 3.31. Entry 3ec (the author's answer: "rdkit's colouring is a fun feature, so keep it on"; first said black by default, then corrected to colour by default).
348
+
349
+ The atom colours of a structure are a setting.
350
+
351
+ - `palette` (`color`, `bw`; `pdfmd-options.smiles.palette`, a `palette=` attribute on a block, a `--setup` row under Chemical structures, a folder setting): `color` is RDKit's own colouring (O red, N blue, S yellow...) and the default; `bw` draws every atom black, as a chemfig drawing is. The palette is part of the picture's cache key, so changing it draws a new picture and never reuses the other. (Chemfig can colour with `\color` too, but looks worse: not built.)
352
+ - Tests: `tests/test_smiles.py`.
353
+
354
+ ## v3.31.42b — 2026-10-31
355
+
356
+ Pre-edit state: commit `bbce97f`. No shipped code changes (a formula, a workflow, a script, the README, a test), so no version bump. Entry 3ed (the author: no separate repositories; the tap moves here).
357
+
358
+ The Homebrew formula lives in this repository.
359
+
360
+ - `Formula/pdfmd.rb` is the formula of the `aliperdehan/homebrew-tap` repository (pandoc, typst, libyaml, python@3.13; pyyaml and pypdf pinned by hand). Install: `brew tap aliperdehan/pdfmd https://github.com/aliperdehan/pdfmd`, `brew trust aliperdehan/pdfmd`, `brew install aliperdehan/pdfmd/pdfmd`. The URL is needed because the repository is not named `homebrew-...`; checked with a throwaway tap, which brew clones as `homebrew-<name>`. Homebrew 7 refuses a formula of a tap that is not trusted (`Refusing to load formula ... from untrusted tap`), so the README gives `brew trust` as the step in between; there are no users of the old tap yet, so nothing needs migrating. The old repository is deleted by the author once the new install has worked from GitHub.
361
+ - `scripts/bump_formula.py` points the formula's `url` and `sha256` at a release on PyPI (a version that PyPI does not list yet is asked for again, `--tries` times `--wait` seconds apart); only those two lines change, the pinned resources are tested to stay as they are.
362
+ - `publish.yml` has a `homebrew` job, `environment: homebrew` (a Deployment of its own on the repository page, as `pypi` is), after the PyPI upload: bump, `brew audit --strict`, build from source, `brew test`, then a commit to `main`. A push made with `GITHUB_TOKEN` starts no workflow, so it does not run the tests again. It also runs when the version is already on PyPI (`workflow_dispatch` repairs a formula that fell behind); the tap's daily schedule is gone, the release replaces it. Not run on GitHub yet: the first real run is the first push after this commit.
363
+ - Tests: `tests/test_formula.py`.
364
+
365
+ ## v3.31.42 — 2026-10-31
366
+
367
+ Pre-edit state: commit `a609345`. Still under 3.31 (no minor before the tables are done). Entry 3ec (the author's wish: Obsidian draws SMILES through a plugin, pdfmd should draw the same notes). First slice of the `[smiles]` line.
368
+
369
+ A chemical structure written as text is drawn as a picture.
370
+
371
+ - `smiles` blocks, in the three spellings: a fenced ```` ```smiles ````, a fence with attributes ```` ```{.smiles caption="..." #fig:id} ````, and a `::: smiles` div (what Obsidian's chemistry plugins read, and what the calc blocks accept). A block is one structure per line, side by side (`per-row` to a row) under one caption; a name after the SMILES (`CCO ethanol`, the `.smi` convention) is written under that structure. `kind=` (`smiles`, `mol`, `mol2`, `inchi`, else read from the text or the file) and `file="x.mol"` (a file beside the document: `.mol`, `.sdf`, `.mol2`, `.smi`) are attributes of the block.
372
+ - The div form reads the body back as it was typed: Pandoc takes the nitro group `[N+](=O)[O-]` for a link, and the filter puts the link back together (`pdfmd_lua/smiles.lua`).
373
+ - Drawn by RDKit through `pdfmd_smiles`, one call of `python3 pdfmd_smiles` per document with pdfmd's own interpreter (RDKit is imported once; nothing is shelled to another Python). Every structure has the same bond length (`bond-length`), so a small molecule is not drawn large. Format by output: PDF for LaTeX (through `rsvg-convert`; PNG when there is none), SVG for HTML, EPUB and Typst, PNG for Word and OpenDocument (`office-format: svg` for SVG; Word 365 and LibreOffice read it, older Word, Google Docs and Pages do not), an SVG beside the output for flat Markdown (`--to gfm`, as diagrams are). A captioned block is a figure; `{#fig:id}` reaches pandoc-crossref.
374
+ - The picture cache is keyed by everything that changes the picture: the engine and its version, the kind, the structures, the format, the cell size, the columns, the bond length, the dpi and the module's own version. The same block twice is drawn once; the same block with another engine, format or setting is a new picture, never a stale one. (The chemfig engine's compiled cache, `cache: {smiles}`, comes with that engine.)
375
+ - A block that cannot be drawn (RDKit missing, a structure that does not read, a missing file, an unknown setting in the block) stays as code with one warning that says why; a wrong `pdfmd-options.smiles` stops the build and names the key.
376
+ - Settings, all under one key, `pdfmd-options.smiles`, and every one also an attribute of a block (`columns=` for `per-row`): `engine` (`auto`, `rdkit`), `format` (`auto`, `svg`, `png`, `pdf`), `office-format` (`png`, `svg`), `cell-width` (250), `cell-height` (200), `per-row` (3), `bond-length` (40), `dpi` (300). The numbers are the author's to change, not constants: each is a `--setup` row (Chemical structures) and a folder setting.
377
+ - `--no-auto smiles` leaves the blocks as code. `--doctor` says whether RDKit is installed.
378
+ - A Markdown file with no front matter is read as gfm, which has neither the div nor code attributes: the note about what gfm leaves as plain text now also names a fence that opens with attributes (```` ```{.smiles caption=... } ````, ```` ```{.d2 ...} ````), as it names `:::` divs.
379
+ - `CLAUDE.md` names the new `[smiles]` tag next to `[kits]` and `[tables]`.
380
+ - `scripts/build_zipapp.py` lists `pdfmd_smiles` and, which it was missing since the kits line, `pdfmd_kits` (`tests/test_packaging.py` compares it with the wheel's list and was failing).
381
+ - Tests: `tests/test_smiles.py` (settings, reading, drawing and the cache, no RDKit, builds through `pdfmd` itself).
382
+
383
+ ## v3.31.41 — 2026-10-31
384
+
385
+ Pre-edit state: commit `437c3ef`. Still under 3.31 (no minor before the tables are done). Entry 3ea (the author's wish, from a `FOUND` line that printed the long path of a course folder's `.pdfmd/`).
386
+
387
+ A path in a message is written as short as it can be, by a setting.
388
+
389
+ - Cause: `display_path` wrote a path relative to the working folder only when it was under it, so a folder above it (a course folder's `.pdfmd/`, a shared metadata file) fell back to the absolute path.
390
+ - `path-style` (`auto`, `relative`, `home`, `full`; `--path-style`, `options.path-style`, a `--setup` row under Messages). `auto` (the default): relative to the working folder, up to `path-up` `..` steps above it (`--path-up N`, `options.path-up`, a `--setup` row, default 2: the author's number, a setting and not a constant), else `~/...` under the home folder (the same on every system, `~\` separators on Windows), else absolute. `relative` is that without `~`, `home` is `~/...` whenever the file is under the home folder, `full` is `--full-paths`.
391
+ - Both settings work at every level: the command line, the config file (`--setup`) and a folder's own settings (`pdfmd --setup DIR`, `.pdfmd/config.yaml`; the nearest folder wins, read at the first document of the run, as the style is one for the whole run). They are not `pdfmd-options` keys of a document.
392
+ - `--verbose` and `--debug` still write full paths, as before (the author asked to keep that); an explicit `--path-style` on the same command line wins. Only what is printed changes; the manifests, the caches and the commands' own arguments keep real paths.
393
+ - A symlink is still written where it sits, as before.
394
+ - The `CMD` line (`--verbose`, and the build record) is a command to run again, so it never uses `..`: a path under the working folder is relative, under the home folder `~/...` (outside the quotes, so a shell expands it), else absolute; `full` (so also `--verbose`) keeps every path absolute. The copy of the line in the build record, written whatever `--verbose` said, follows the style, so the account name stays out of it.
395
+ - Setup: the two rows are in a folder's setup too (`FOLDER_OPTIONS`); the inventory has `--path-style`, `--path-up` and the keys.
396
+ - Tests: `tests/test_path_display.py`.
397
+
398
+ ## v3.31.40 — 2026-10-31
399
+
400
+ Pre-edit state: commit `25ec92a`. Still under 3.31. `[kits]`, entry 3du, point 20 (the author's wish: a course whose lab partners rotate).
401
+
402
+ A metadata value can add to the layers below instead of replacing them.
403
+
404
+ - `author+: [B]` in any layer is the value the layers below hold, then `B` (a single value is a list of one; nothing below, just `B`); `author:` and `author+:` in one place are taken in that order. `pdfmd-options.metadata-append: [author]` (a name or a list; `--setup` row *Keys whose document value adds to the layers below*, folders may set it) makes the document's own value add too.
405
+ - The layers, lowest first, are the global metadata, a kit's defaults, the folders' `.pdfmd/metadata.yaml` (the farthest first), the document folder's metadata files and the document's front matter; `tests/test_metadata_append.py` pins that order, which was already how a build behaved.
406
+ - Pandoc cannot do it (the document's front matter beats every metadata file), so pdfmd merges the layers and hands the finished values over in a metadata file under `pdfmd-merged`; `pdfmd_lua/metadata_append.lua` runs before every other filter, sets them and removes the `key+` keys. A build with no plus key and no setting adds no argument at all.
407
+ - A mapping cannot be added to and `header-includes` is not supported (pdfmd lifts it into header files itself): both stop with a message naming the file.
408
+ - An assembled or attached source holds the finished value and no `key+`, and its `pdfmd-options` says `metadata-append: []` (the same reason as `kit: none`, 3.31.38): a restored report rebuilds elsewhere with each name once.
409
+ - Not covered: the built-in renderer (no Pandoc) does not run Lua filters, so it takes the layers as before and does not add.
410
+ - Setup: the `options.metadata-append` row, and the key in `pdfmd_setup/inventory.py`.
411
+ - Tests: `tests/test_metadata_append.py` (11: the order, `+` in each layer, both keys in one place, the setting in the config and in the document, markup in a name, a mapping and `header-includes`, the source and a rebuild in another folder).
412
+
413
+ ## v3.31.39 — 2026-10-31
414
+
415
+ Pre-edit state: commit `6120d64`. Still under 3.31. `[tables]`, entry 3dz (reported by the author from a pK table).
416
+
417
+ CSV rows typed straight into a `::: {.csv}` block keep their inline markup.
418
+
419
+ - Cause: Pandoc has parsed the lines of such a block as a paragraph before the filter sees them; `pdfmd_lua/csv_table.lua` rebuilt the text from the paragraph's inlines and flattened every one but words, quotes and code with `stringify`, so `p*K*~1~` reached the cell reader as `pK1`. A `file=` block and a fenced body never went through that, and neither did a pipe table; `\ce{..}` and `$..$` survived only because Pandoc keeps them as plain text.
420
+ - Emphasis, strong, sub- and superscript, strikeout, links, citations, math and spans are written back as the Markdown that makes them (`pandoc.write` of that one inline, no wrapping), then read as the cell's Markdown as before (`reader`, `pdfmd-csv-reader`). Where that write fails the old flattening is the fallback.
421
+ - Tests: `tests/test_csv_table.py` (`InlineData`): the markup reaches the cells, and the straight form and the fenced form of one table give the same document. Both fail without the change.
422
+
423
+ ## v3.31.38 — 2026-10-31
424
+
425
+ Pre-edit state: commit `e29cb08`. Still under 3.31. `[kits]`, C4 of entry 3du (a hole found by the review of 3.31.35 to 3.31.37).
426
+
427
+ An attached or assembled source whose kit is fully embedded says `kit: none`.
428
+
429
+ - Cause: 3.31.35 embedded what a kit brings (its defaults as metadata files, its preamble, its Lua filter) and stored its parts, but a `kit:` chosen in the document's own front matter stayed in the source. A rebuild on a machine without the kit stopped with "no kit called ...", and on a machine with it (or under a folder whose `.pdfmd/` chooses it) the kit applied again on top of the stored copies: the source of the rebuild was 800 lines longer, with the defaults and the preamble twice.
430
+ - The assembled source's `pdfmd-options` gets `kit: none`, which replaces every `kit:` below the document (the document layer drops what is under it), when metadata and preamble are embedded and so is the Lua filter if the kit has one (`lua` kind embedded or applied; a `ref` or `off` filter, or an embedding with fewer kinds, keeps the kit chosen). The command line's `--kit` still wins. `--restore` says so on its `KIT` line.
431
+ - Checked on copies of a real report with real lualatex: a kit chosen in the report's own front matter, attached and restored, rebuilds pixel-identically (a) in a folder with no `kits/` above it, no config and an empty `TEXMFHOME`, and (b) under a course folder whose `.pdfmd/config.yaml` chooses the kit, where the kit's Lua filter runs once. The transition state (a course `.pdfmd/` with the report's old `metadata/` folder still there) builds pixel-identically to the original as well.
432
+ - Tests: `tests/test_kit_carry.py`.
433
+
434
+ ## v3.31.37b — 2026-10-31
435
+
436
+ Pre-edit state: commit `48864de`. Documentation only (`CLAUDE.md`): `PDFMD_VERSION` stays 3.31.37.
437
+
438
+ ### Docs
439
+ - `CLAUDE.md`, "Version numbers": the kits and tables lines have no minor of their own; each of their commits is a 3.31.x patch with an entry tagged `[kits]` or `[tables]` (the author's decision of entry 3du, point 14).
440
+
441
+ ## v3.31.37 — 2026-10-31
442
+
443
+ Pre-edit state: commit `6a21c23`. Still under 3.31. `[kits]`, the spike of entry 3du (point 12).
444
+
445
+ A chosen kit works from its own folder: no install into the TeX tree.
446
+
447
+ - The kit's folder, and the folders its `carry:` names with `//` (a logo found by its bare name), go on `TEXINPUTS` and `BIBINPUTS` of the engine's environment (`tex_search_env`, new third argument; the six call sites pass the document). They follow `.` and any value the environment had and come before TeX's own path. The kit's whole folder is not searched recursively: it may hold a repository's worth of data, and kpathsea walks every folder under a `//`. No kit, no change: `tex_search_env` still returns None where it did.
448
+ - Checked with real lualatex on a copy of a real report: with `TEXMFHOME` pointing at an empty folder (so `kpsewhich nulabreport.sty` finds nothing) the report builds from the kit's folder alone, pixel-identical to the build with the package installed.
449
+ - `tests/test_kit_preamble.py`'s real compile no longer sets `TEXINPUTS` itself: its package exists only in the kit's folder. Tests: `tests/test_kit_texinputs.py`.
450
+
451
+ ## v3.31.36 — 2026-10-31
452
+
453
+ Pre-edit state: commit `e9c09d3`. Still under 3.31. `[kits]`, C4 of entry 3du (the folder layers' part).
454
+
455
+ An attached source carries the folder settings the build used, and `--restore` writes them back.
456
+
457
+ - The manifest gets `layers: {options: {...}}`: the `options:` of the `.pdfmd/config.yaml` files above the report, nearest layer winning, flattened into one mapping, without `kit` and `kit-stack` (what a kit wrote is in the source already, and a restored `kit:` would name a kit the machine may not have). `--restore` writes it to `.pdfmd/config.yaml` of the restored folder with a comment line, prints a `FOLDER` line, and leaves a `.pdfmd/config.yaml` that is already there as it is, with a NOTE of what the build used besides. The folders' `.pdfmd/metadata.yaml` files already travel as metadata files; the folder's own value wins, as for every restore.
458
+ - **Attachment format 3, but only for a source with layers.** `ATTACH_FORMAT` (what this pdfmd reads) is 3 and `ATTACH_FORMAT_PLAIN` (what it writes otherwise) stays 2, so a PDF is not refused by an older pdfmd unless it needs the layers, as a refusal is better than restoring a report that builds differently.
459
+ - README: the kit's carrying paragraph says `--embed-metadata` holds the preamble only (3.31.35 said it held the parts).
460
+ - Checked on a copy of a real report: a folder layer with `kit: nulabreport:qca` and `bundle-max-mb: 50` is attached, restored, and the restored folder holds `.pdfmd/config.yaml` with the second only.
461
+ - Tests: `tests/test_attach_layers.py`, `tests/test_attach_format.py`.
462
+
463
+ ## v3.31.35 — 2026-10-31
464
+
465
+ Pre-edit state: commit `6ab64f0`. Still under 3.31. `[kits]`, C4 of entry 3du (the kit's part).
466
+
467
+ An attached or assembled source holds what a chosen kit wrote and the kit's parts, so a restored report rebuilds on a machine with no kit.
468
+
469
+ - **The preamble.** `--attach-source`, `--keep-source`, `--bundle` and `--embed-metadata` put the kit's header (`\usepackage[<options>]{package}`, its `latex:` lines) first among the embedded preambles, and the found preambles as the build read them: what the kit writes twice is settled in them (`KIT-SAME`, `KIT-MERGE`, `KIT-YIELD`), so a rebuild gets the same LaTeX in the same order and `Option clash` cannot come back. The settling is one function for the build and the source (`kit_header_settle`).
470
+ - **The parts.** The files a kit's `carry:` names are stored beside the document whether or not the folder is bundled: a named file keeps its path, a named folder is carried by its files (flat), a name the document's folder holds is left to the folder, a name that is not in the kit is a NOTE, a kit over `bundle-max-mb` is not carried (WARN). They go in before the bundle's own TeX-tree files, so the kit's copy wins. The manifest gets `kits: [{name, version, mode, package, carried}]` (additive; `ATTACH_FORMAT` stays 2); `--restore` prints a `KIT` line and labels the files "from a kit".
471
+ - Checked on a copy of a real report with real lualatex: the report built with `kit: nulabreport:qca` and a course `.pdfmd/`, attached, restored, and rebuilt in the restored folder with no config, no kit and an empty `TEXMFHOME`, is pixel-identical.
472
+ - Tests: `tests/test_kit_carry.py`.
473
+
474
+ ## v3.31.34 — 2026-10-31
475
+
476
+ Pre-edit state: commit `5420849`. Still under 3.31. `[kits]`.
477
+
478
+ A kit that leaves out `version:` takes it from its own package.
479
+
480
+ - A `.kit` with a `package:` and no `version:` reads the version from the `\ProvidesPackage{package}[date vX.Y.Z ...]` line of `package.sty` in the kit's folder. A kit that ships a package then cannot name a version the package no longer has (a number typed twice is stale at the next release). A `version:` that is written still wins; no `.sty`, or a line with no number, gives no version, as before.
481
+ - `pdfmd --kits` shows it like a written one.
482
+ - Tests: `tests/test_kits.py`.
483
+
484
+ ## v3.31.33 — 2026-10-31
485
+
486
+ Pre-edit state: commit `db18fa3`. Still under 3.31. `[kits]`, found by building a lean copy of a real pre-lab with the kit.
487
+
488
+ Pandoc, and the TeX engine with it, no longer run in the folder of a metadata file that is not the document's.
489
+
490
+ - Cause: the folder Pandoc runs in was that of the first metadata file in the list, so that a `bibliography:` or an `\input` written beside a `metadata/` file resolves there. A kit's defaults, a folder's `.pdfmd/metadata.yaml` and the global metadata of `pdfmd --setup` come first when the document has no file of its own, and a relative path in raw LaTeX (a pgfplots table at `../ir/x.csv`) then failed: `Could not read table file`. The same held for a document with only a folder layer since 3.29.10.
491
+ - `metadata_cwd` takes the folder of the first metadata file placed for the document (beside it, in its `metadata/` folder, named with `-y`), else the document's own. The folders of the files it skipped go on `--resource-path`, so a relative `bibliography:` or `csl:` beside them is still found (a kit's folder already was). A document that has only the global metadata now runs Pandoc in its own folder, with the config folder on the resource path.
492
+ - Checked on copies of real reports with real lualatex: a pre-lab built from the original `prelab-metadata.yaml` link and the same pre-lab built with `kit: nulabreport:prelab` and no link are pixel-identical on all 13 pages when the kit's Lua filter is left out (`--no-auto lua`); an LR3 (6 pages, parts, figures) and an LR6, each built with its `metadata/` folder and lean with `kit: nulabreport:qca` and a course `.pdfmd/`, are pixel-identical. Tests: `tests/test_kit_selection.py`.
493
+
494
+ ## v3.31.32 — 2026-10-31
495
+
496
+ Pre-edit state: commit `1ee4fb9`. Still under 3.31. `[kits]`, C3 of entry 3du.
497
+
498
+ A chosen kit writes its own preamble, and what it and the user's LaTeX say twice is settled in the build copy (3du, point 9).
499
+
500
+ - **The kit's lines come first.** One header file, `\usepackage[<mode options>]{package}`, then the kit's `latex:` lines and its mode's, goes ahead of every other `--include-in-header` (after pdfmd's code-wrapping header), so `\labsetup{...}` and the like in `preamble.tex`, `header-includes:` (the document's or a folder's metadata) or an `--include-in-header` file follow it. A kit's `latex-keys` now apply with no `preamble.tex` of the user's.
501
+ - **Four outcomes** for the user's text, in copies in the cache folder (a user's file is never edited): the same line (the package with options the kit already gives, or an `\input` the kit writes) is dropped, `KIT-SAME`; extra options (`[qca,draft]` against `[qca]`) are merged into the kit's one line, `KIT-MERGE` (LaTeX itself stops on two different `\usepackage` lines of one package: "Option clash"); an option that belongs to another mode of the same kit is a contradiction: a preamble found in the folder yields with a WARN, `KIT-YIELD`, its line commented out, while `header-includes:`, an `--include-in-header` file or a named `preamble:` stop the build naming both sides; everything else stays after the kit's lines with no word. `KIT-SAME` and `KIT-MERGE` show with `--verbose`; the ids are fixed for the chatter ladder to come.
502
+ - Only a whole-line `\usepackage[...]{name}` (options may span lines; `\RequirePackage` too) and a whole-line `\input{...}` are rewritten. The kit's package behind a macro or inside a list (`\usepackage{a,labstyle}`) stops the build and names the line; an `\input` in another shape is left alone.
503
+ - `keep_header_includes` (the one place the finished command's header files are settled) does the work for every route; `document_header_file` is active when a kit writes a preamble, so the document's own `header-includes` are not dropped. Pure part: `pdfmd_kits/preamble.py`; tests: `tests/test_kit_preamble.py` (the resolver as a table, builds through pdfmd, and a real `pdflatex` run that survives the user's added option).
504
+
505
+ ## v3.31.31 — 2026-10-31
506
+
507
+ Pre-edit state: commit `abe26bb`. Still under 3.31. `[kits]`, C2 of entry 3du.
508
+
509
+ A document can choose a kit, and the kit's defaults, Lua filters and folder reach the build. (The preamble a kit writes is the next patch; until then a kit's package is loaded by the document's own preamble.)
510
+
511
+ - **Choosing.** `pdfmd-options: {kit: ...}` in a document, a metadata file, a folder's `.pdfmd/` files, a preset or the config's `options:`; `--kit` on the command line. The value reads `nulabreport`, `nulabreport:qca` (no quotes needed), `nulabreport[qca]`, `"[qca]nulabreport"` (quoted), `{nulabreport: qca}`, `{nulabreport: {mode: qca}}`, a list for the order, a path (a value with `/`, `\` or a leading `~` or `.`), and `none|off|false|no` for no kit. A mode is matched by name or alias, ignoring case and `- _ .` and spaces; an unknown mode stops and lists the modes. README, section "Kits".
512
+ - **Layers stack**, lowest first: the config's `kit:` (global), the global metadata or a preset, the folders' files (farthest first), the document's metadata files and front matter. The same kit named higher up replaces only the keys it names (a mode, say); different kits are added in order; `none` drops what is below. A folder's `kit:` names a kit and never searches the project's `kits/` folder or takes a path (a folder's options cannot name code); a message says so.
513
+ - **`--kit`** replaces every `kit:` setting; repeated `--kit` stacks in order; `--kit none`, or a bare `--kit` before another option, means no kit. **`--kit-stack`** (also `pdfmd-options: {kit-stack: ...}` and a `--setup` row) says what `--kit` stacks on instead of replacing: `off` (default), `all`, or a list of `global`, `preset`, `folder`, `document`. Switches stay absolute whatever it says. `kit` and `kit-stack` are `--setup` rows ("Kits").
514
+ - **What a chosen kit brings.** Its `defaults:` files, its inline `metadata:`, its mode's `defaults:` and `metadata:` as metadata files between the global metadata and the folders' own (the folder tunes the kit; the document wins; inline metadata is written once into `kit-metadata/` of the cache folder, named by its content); its `lua:` filters; its folder on Pandoc's resource path, so its `csl:` and `bibliography:` are found by name; its package, so `package_helper` and the Lua and Word lookups see it, and `kit_package_folders` (the 3.31.29 slot) answers; the helper blocks of its `.kit` (`latex-keys`, `draft-skip`, `office`, `lua`...) when no `<package>-pdfmd.yaml` stands beside the `.sty`. `--verbose` prints an `AUTO KIT` line per chosen kit. A document that no layer gives a `kit:` is read as before; the kit code is not imported.
515
+ - Tests: `tests/test_kit_selection.py` (the grammar as a table, YAML spellings, stacking, `--kit` and `kit-stack`, and builds through pdfmd itself with a fixture kit).
516
+
517
+ ## v3.31.30 — 2026-10-31
518
+
519
+ Pre-edit state: commit `37bfee6`. Still under 3.31. `[kits]`, C1 of entry 3du.
520
+
521
+ Kits can be read and found (nothing chooses one for a document yet).
522
+
523
+ - A **kit** is a folder with a `.kit` file, a YAML mapping that generalises `<package>-pdfmd.yaml`: `kit-format: 1`, name, version, `package`, `modes` (package options, a defaults file, default metadata, extra `latex` lines, `aliases` for course codes and the like), `default-mode`, `latex`, `lua`, `defaults`, `metadata`, the helper blocks (`latex-keys`, `picture-cache`, `draft-skip`, `office`) and `carry`. `settings:`, `requires:` and `options:` are reserved and accepted silently; an unknown key is a WARN; every fault of a file is listed at once; a path leaving the kit's folder, a missing file and a `kit-format` above 1 stop. README, section "Kits".
524
+ - New package `pdfmd_kits` (`kitfile`: the format and the mode lookup, names compared ignoring case and `- _ .` and spaces; `discovery`: the search and the cache), pure, imported only when a kit is asked for: a build that selects none costs nothing.
525
+ - Finding by name: a `kits/` folder beside the document and above it, `kits:` in the config file (a path, or `{path:, aliases: {MODE: [names]}}` to add mode names without editing the kit), a `kits/` folder beside the config file, then the folder of `NAME.sty` that kpsewhich finds (asked from an empty folder, the 3.31.29 lesson). The TeX-tree answer is cached in `kits.json` of the cache folder, written atomically, trusted only while the `.kit` has the same size and time; a corrupt cache or a miss is never an error and a miss is never stored. Two different kits of one name in one place stop the search naming both; between places the order decides. `--verbose` prints an `AUTO KIT` line.
526
+ - `--no-auto kit` takes the searching off (a registered `kits:` folder and a path stay); `--no-auto kpsewhich` only the TeX tree.
527
+ - `pdfmd --kits` lists the kits and their modes; `pdfmd --kits NAME-OR-PATH` shows one in full and checks it, with "did you mean" for a near name. `kits:` is a config key (a `--setup` row, *Folders of kits*, this computer only); `--kits` is an `Omitted` command in the inventory; tests: `tests/test_kits.py`.
528
+
529
+ ## v3.31.29 — 2026-10-31
530
+
531
+ Pre-edit state: commit `c268035`. Still under 3.31.
532
+
533
+ A lone copy of a package's `.sty` beside the document no longer loses what the package tells pdfmd about itself (3dw).
534
+
535
+ - Cause: TeX takes `<package>.sty` beside the document first, and pdfmd read `<package>-pdfmd.yaml` only in the folder of the `.sty` it had taken. A report folder holding just `nulabreport.sty` therefore had no `latex-keys:`: the experiment number printed `N/A`, and the performance date and the unknowns line were gone, with no message. (The same gap hid `lua:`, `office:` and `draft-skip:`.) The hard-coded table of those keys was removed in 3.28.37 on purpose ("pdfmd names no package") and stays removed.
536
+ - `package_helper` now looks for the file in this order: beside the `.sty` the document uses (as before), beside the `.sty` of a selected kit that ships the package (kits are not built yet: the place is asked and answers nothing), then beside the installed copy `kpsewhich` finds (asked from an empty folder: TeX searches the current folder first, so from the document's own it answers with the lone copy). The file found keeps its own path, so the names it lists resolve beside it. A helper beside the lone copy still wins. The last two places are searched only when the first has none, so a build whose package carries its file pays nothing.
537
+ - Two `--no-auto` kinds, `kit` and `kpsewhich` (`pdfmd-options: {no-auto: [kpsewhich]}` too, and a `--setup` row each), take one place off; a bare `--no-auto` takes both. On by default: a copy older than the installed helper fails loudly (a `\renewcommand` of a macro it lacks), where without the fallback it printed `N/A` silently.
538
+ - `no_auto` now reaches `package_latex_keys`, `package_lua_names`, `find_lua_filters`, `package_draft_skips`, `document_header_file` and `office_defaults_sources`.
539
+ - Checked through pdfmd itself: a copy of a real 28-page report with a lone `nulabreport.sty` beside it printed `#N/A` for the experiment before, and builds with the same text on every page as the copy without one now.
540
+ - Tests (`tests/test_package_helper.py`): the lone copy reads the installed helper for keys, Lua names and draft skips; the helper beside it wins; `kpsewhich` and a bare `--no-auto` stop the borrowing and `kit` does not; a kit's folder is asked before the installed copy.
541
+
542
+ ## v3.31.28 — 2026-10-31
543
+
544
+ Pre-edit state: commit `fe93e0a`. Still under 3.31.
545
+
546
+ `--to png,docx,tex`: the list may now hold any format, not only pictures (3.31.26 refused the others). The author's wish (3dv).
547
+
548
+ - pdfmd runs itself once per format that is not a picture (the same command line, minus the picture options, with that `--to`), then once for the pictures (`picture_command`). A plain `pdf` is one of those ordinary runs, with its build stamp, backup and attached source, unless `--crop` is given or a source is a PDF already, when it belongs to the picture run (cropped, or a NOTE). The list has no spaces (the shell would split it); a quoted `"png, pdf"` works too.
549
+ - `-o` with more than one format must be a folder (made if need be); otherwise each result is beside its source.
550
+ - Not yet: a document or a setting asking for several formats (`default-output` takes one); logged with the post-build step (3dv).
551
+ - Test: `tests/test_crop.py` (a folder of `html` and `png` from one command; `-o` as a file refused).
552
+
553
+ ## v3.31.27 — 2026-10-31
554
+
555
+ Pre-edit state: commit `1eb332d`. Still under 3.31.
556
+
557
+ `--no-page-folder`: the pages of a picture target go beside the document as `NAME-001.png`, `NAME-002.png` ... instead of into the folder `NAME/` (3.31.22); `--page-folder` is the default. The author's wish (3dv): "for people who want exactly this behaviour".
558
+
559
+ - A setting: `pdfmd-options: {page-folder: false}` in a Markdown file's front matter, in a folder layer's `config.yaml` or the config, a `--setup` row ("Several pages of a picture go into a folder"), the command line over all of them. Not yet read from separate metadata files (queued with the post-build step, 3dv).
560
+ - Test: `tests/test_crop.py` (beside the document; the option in front matter, the flag over it).
561
+
562
+ ## v3.31.26 — 2026-10-31
563
+
564
+ Pre-edit state: commit `0655fc9`. Still under 3.31.
565
+
566
+ `--to png,pdf`: several picture targets in one run, from one build and one crop. The author's wish (3dv).
567
+
568
+ - Any of `pdf, png, jpg, tiff, eps, ps, svg`, comma-separated, each written beside the others (`NAME.png`, `NAME.pdf` ...; a PDF source gives `NAME-crop.pdf` for pdf); `-o` is then a folder. A format that is not a picture (`--to pdf,docx`) is refused with the reason: it has to be a run of its own (queued: a loop of runs, 3dv).
569
+ - A PDF source with `pdf` among the targets and no `--crop` writes nothing for pdf and says so (NOTE).
570
+ - Test: `tests/test_crop.py` (several targets from a PDF and from Markdown; `-o` as a file, a non-picture format, a PDF to pdf).
571
+
572
+ ## v3.31.25 — 2026-10-31
573
+
574
+ Pre-edit state: commit `cd59372`. Still under 3.31.
575
+
576
+ `--to tiff` (also `tif`), `--to eps` and `--to ps`; `-o x.tif|x.tiff|x.eps|x.ps` choose them by suffix. The author's wish (3dv).
577
+
578
+ - All three by Ghostscript: `tiff24nc` (LZW, white, at `--image-dpi`; **all pages in one multi-page file**), `eps2write` (vector; **one file per page**, so several pages go into the folder `NAME/` as `001.eps`...; its bounding box is the ink of the page, whether or not `--crop` is given) and `ps2write` (vector; all pages in one file).
579
+ - Test: `tests/test_crop.py` (`test_tiff_and_postscript_hold_all_pages_in_one_file_and_eps_one_each`).
580
+
581
+ ## v3.31.24 — 2026-10-31
582
+
583
+ Pre-edit state: commit `36cd49e`. Still under 3.31.
584
+
585
+ `--to jpg` (also `jpeg`; `-o x.jpg` and `-o x.jpeg` choose it by suffix). The author's wish (3dv).
586
+
587
+ - Drawn by Ghostscript's `jpeg` device on white (a JPEG has no transparency) at `--image-dpi`, with `--image-quality 1-100` (default 90; a flag with its default in `--help`, `--setup`-omitted like the other picture flags). The file is `NAME.jpg` whichever spelling was typed.
588
+ - `--image-dpi` and `--image-quality` without a picture target are an error.
589
+ - Test: `tests/test_crop.py` (`test_jpeg_is_white_and_takes_a_quality`).
590
+
591
+ ## v3.31.23 — 2026-10-31
592
+
593
+ Pre-edit state: commit `19d38ab`. Still under 3.31.
594
+
595
+ `--crop` and `--to png|svg` work with `-b`, `-r` and `-p`; 3.31.21 refused them. The author's wish (3dv): `pdfmd notes -b --crop -o out/` to turn a folder of sources into cropped pictures.
596
+
597
+ - **`-b`** takes the one folder it takes everywhere (`*.md`, `--recursive` for subfolders) and works through its files, `-j` at a time; **`-r`** builds the report and gives one result named after the folder (inside it, like the report itself); **`-p`** builds the slides and draws a picture per slide. Files named on the command line need no `-b`: `pdfmd *.tex --crop -o out/` crops the PDF of every `.tex`.
598
+ - `-b` and `--recursive` are not handed on to the build of each file's PDF (the files are built one at a time).
599
+ - Test: `tests/test_crop.py` (a folder with `-b -j 2 -o`, `-b` without a folder, a report as a picture; the Markdown builds run on the built-in renderer with no Pandoc on the path).
600
+
601
+ ## v3.31.22 — 2026-10-31
602
+
603
+ Pre-edit state: commit `a2a987e`. Still under 3.31.
604
+
605
+ A picture target with more than one page no longer scatters `NAME-1.png`, `NAME-2.png` beside the document: one page is `NAME.png` beside it, several go into a folder `NAME/` as `001.png`, `002.png` ... (zero-padded to at least three digits, wider for a thousand pages, so they sort). `-o a.png` with several pages gives the folder `a/`. The author's wish (3dv).
606
+
607
+ - Test: `tests/test_crop.py` (`test_png_without_crop_is_the_whole_page_one_file_per_page`).
608
+
609
+ ## v3.31.21 — 2026-10-31
610
+
611
+ Pre-edit state: commit `27c28e8`. Still under 3.31.
612
+
613
+ `--crop` and the new targets `--to png` and `--to svg` work on a Markdown source as well as on a PDF.
614
+
615
+ - **Targets.** `--to pdf` (the default), `png`, `svg`; `-o x.png` / `-o x.svg` choose by suffix. PNG is drawn by Ghostscript's `pngalpha` device (transparent) at `--image-dpi` (default 300, a flag with its default in `--help`, `--setup`-omitted); SVG by `pdftocairo`, else `mutool`, else an error naming the packages.
616
+ - **A Markdown source** is built to a PDF first (pdfmd runs itself with the other options of the command line, and `--no-stamp --no-backup --rewrite`, into a scratch folder, as `--check-docx` does), then cropped and drawn. Its result is `NAME.pdf|png|svg` beside the source (`-d`: the current folder). A PDF source with `--to pdf` gives `NAME-crop.pdf`.
617
+ - **`--to png|svg` without `--crop`** draws the whole page: a crop is asked for, never implied.
618
+ - **More than one page.** A PDF keeps all of them, each cut to its own ink (`--crop-page N` keeps one). A picture format is one picture per page: `NAME.png` for one page, `NAME-1.png`, `NAME-2.png` ... for several, with a NOTE.
619
+ - **`--crop` with a target that has no pages** (`--to docx`, `odt`, `html`, `epub` ...) is an error before anything is built, naming pdf, png and svg. `-b`, `-r`, `-p` with these are refused; `--image-dpi` without a picture target is an error.
620
+ - `png` and `svg` are in `FORMAT_EXTENSION` and `EXTENSION_FORMAT`; new module `pdfmd_crop/images.py`.
621
+ - Test: `tests/test_crop.py` (`Pictures`: whole page, cropped, `-o x.png`, svg, a docx target, batch/report, a dpi without a picture, a Markdown source through the built-in renderer with no Pandoc on the path).
622
+ - The decisions are suggestions the author asked for; they are in the maintainer's notes (3dv) and can be changed.
623
+
624
+ ## v3.31.20 — 2026-10-31
625
+
626
+ Pre-edit state: commit `db904bd`. Still under 3.31.
627
+
628
+ `pdfmd --crop FILE.pdf...` cuts a finished PDF to what it draws. It is the crop of eqpop (the macOS popup that renders a formula to a cropped PDF), made portable so eqpop can later hand the job to pdfmd.
629
+
630
+ - The ink of each page is what Ghostscript's `bbox` device finds painted (the same measurement `pdfmd_office/fragments.py` uses for a fragment); pypdf then sets the media box and the crop box (and the trim, bleed and art boxes if the page has them) to that box plus a pad, never beyond the page's own box. The page stays vector. A rotated page is measured as displayed and cut in its own coordinates; a page that draws nothing is left as it is, with a NOTE.
631
+ - The result is `NAME-crop.pdf` beside the source (the source is never changed), `-o FILE` for one PDF or `-o FOLDER` for several; `-o` the source itself crops in place, through a `.part` file.
632
+ - Settings, each a flag with its default written in `--help`: `--crop-pad PT` (default 2), `--crop-ignore-bottom PT` (default 0; leaves the foot of each page out of the measurement, e.g. a page number), `--crop-page N` (keep that page alone; default every page, each to its own ink). They are `--setup`-omitted (about one run), like `--check-docx`.
633
+ - New package `pdfmd_crop` (`crop_pdf`, `ink_boxes`); `--help crop` is its topic. Without Ghostscript or pypdf the command says what to install.
634
+ - Test: `tests/test_crop.py` (pages drawn by Ghostscript from PostScript, so the ink is known: pad, foot, one page, rotation by 90/180/270, a blank page, in place, the command line).
635
+
636
+ ## v3.31.19 — 2026-10-30
637
+
638
+ Pre-edit state: commit `aeaad80`. Still under 3.31. Merged from a pull request of the Windows test session.
639
+
640
+ The plot cache records the files a plot job read, and drops any that is not under the report's folders. TeX writes a Windows drive letter in lower case (`d:/work/lab/ir/product.csv`) where the report's folder is `D:\work\lab`, and the comparison was case-sensitive, so every CSV was dropped as foreign and an edit to it showed as a stale plot. `plot_recorded_inputs` now compares through `os.path.normcase` on both sides (the identity on macOS and Linux).
641
+
642
+ - Found by the nulabreport smoke test on Windows: after a CSV edit the cached build differed from the uncached one in 96 of 1114 words; with the fix the checks pass and the `.deps` files list the CSVs.
643
+ - Test: `tests/test_plot_cache.py` (`test_a_lower_case_drive_letter_is_still_the_reports_folder`), Windows only, fails without the fix.
644
+
645
+ ## v3.31.18 — 2026-10-31
646
+
647
+ Pre-edit state: commit `a66a343`. Still under 3.31. Merged from a pull request of the Windows test session.
648
+
649
+ ### Added
650
+ - **`pdfmd --user-path add|remove|show [FOLDER...]` (Windows): put folders on the user PATH, and take them off again.** Spelled-out forms: `--add-to-path`, `--remove-from-path`, `--show-user-path`; `--yes` skips the question, `--dry-run` writes nothing. Opt-in only: nothing in pdfmd calls it by itself. It writes `Path` under `HKEY_CURRENT_USER\Environment` (no administrator rights), keeps the registry type (`setx` would cut the value at 1024 characters and turn it into `REG_SZ`), copies every other entry byte for byte and appends at the end so a new folder never shadows a program of the machine PATH. `add` with no folder takes the folders of the tools pdfmd found off PATH (3.31.10); a folder you name may be any existing folder. It lists what it will do and asks first (no terminal or `PDFMD_NO_PROMPT` without `--yes` refuses); folders already on the user or machine PATH are skipped; the running programs are told (`WM_SETTINGCHANGE`) and the PowerShell line that refreshes the current window is printed. What was added is recorded in `path-added.json` in pdfmd's settings folder (state, not a setting); `remove` takes off exactly those entries and leaves a folder the user already removed alone. On macOS and Linux the command exits 1 and prints the `export PATH=...` line for the shell profile.
651
+ - **A new kind of `--setup` row: an action** (`pdfmd_setup/__init__.py` takes `actions=`; the plain and the full-screen setup both draw it). The first is "Windows user PATH" in "Tool locations" (add the found folders / add a folder you name / show / undo); per machine, shown on Windows only, never in a folder's setup.
652
+ - `--doctor` ends with one note on Windows when tools it found are not on the user PATH.
653
+ - The code is `pdfmd_setup/userpath.py` (four-method backend: `Registry` is the real one, tests use a fake and, on Windows, a throwaway key named by `PDFMD_USER_ENV_KEY`, a test-only variable); `tests/test_user_path.py` (26 tests). Decision 3dr of the maintainer's notes; by the Windows test session (pull request #35).
654
+
655
+ ## v3.31.17 — 2026-10-31
656
+
657
+ Pre-edit state: commit `61be750`. Still under 3.31. Merged from a pull request of the Windows test session.
658
+
659
+ ### Fixed
660
+ - **The build record of a `-e soffice` build on Windows.** The record runs `soffice --version`; on Windows `soffice.exe` (the window launcher) never answers it, so the probe waited its 10 s, the record said "soffice: no version" and the `soffice.bin` it had started stayed in the process list. `engine_version_line()` now asks the program `resolve_soffice()` found and, on Windows, the `soffice.com` beside it (answers in 0.3 s, leaves nothing). Three tests in `tests/test_probes.py`. By the Windows test session (pull request #35).
661
+
662
+ ## v3.31.16b — 2026-10-31
663
+
664
+ Pre-edit state: commit `80d19d0`. Tests only (from a pull request of the Windows test session): `PDFMD_VERSION` stays 3.31.16.
665
+
666
+ ### Docs
667
+ - **`docs/windows.md`**: pdfmd on Windows, environment problems and how to fix them (16 entries and a checklist: tools off PATH, a terminal that predates an install, non-UTF-8 locales, symlink privilege, the `python3` Store stub, LibreOffice hangs, files in use, Git Bash paths, PowerShell 5.1, memory, backslashes in re-read paths, where pdfmd keeps its files, CRLF scripts, symlinks and `sh` stand-ins in tests), each tied to the pdfmd version that fixed it. Written by the Windows test session (pull request #34). Not yet wired into `pdfmd --doc` (the documentation programme, 3dg).
668
+
669
+ ## v3.31.16 — 2026-10-31
670
+
671
+ Pre-edit state: commit `e9445e7`. Still under 3.31. Merged from a pull request of the Windows test session.
672
+
673
+ ### Added
674
+ - **`soffice-timeout`: a LibreOffice run has a time limit.** Every start of LibreOffice (the Office converter and the `-e soffice` route, the ODF reader of hybrid builds, spreadsheets read through it, the SVG converter) goes through one runner, `run_soffice()`: a run that takes longer than the limit is stopped together with the processes it started (the process group on macOS and Linux, `taskkill /T` on Windows), and its file fails with a message that names the setting; the other files of a batch go on. Default 300 seconds. **`0`: LibreOffice is never run** (it counts as not installed; `--doctor` says it is turned off). **A negative number (`-1`): no limit.** The key is in `pdfmd-options` (per document, and in a folder's setup), the config and `pdfmd --setup` ("Checks and limits"); no command-line flag. The two hard-coded limits it replaces (120 s for an SVG, 180 s for a sheet) are gone; the packages take the runner as an argument (`run=`), and without one keep their old limit. `tests/test_soffice_timeout.py`. Decision 3ds of the maintainer's notes.
675
+
676
+ ### Notes
677
+ - The default is a constant the author has not confirmed as a number (300); a first start with a new profile takes 20 to 30 seconds and a large document more, so a low default would break working builds.
678
+
679
+ ## v3.31.15c — 2026-10-31
680
+
681
+ Pre-edit state: commit `939b66f`. Tests only (from a pull request of the Windows test session): `PDFMD_VERSION` stays 3.31.15.
682
+
683
+ ### Tests
684
+ - A settings folder is named for Windows (`APPDATA`) wherever one is named for `XDG_CONFIG_HOME`: 25 test modules (on Windows pdfmd keeps its settings in `%APPDATA%\pdfmd`, so those tests read and wrote the real folder); `test_indent_office` sets `USERPROFILE` with `HOME`. By the Windows test session (pull request #33).
685
+
686
+ ## v3.31.15b — 2026-10-31
687
+
688
+ Pre-edit state: commit `9b5b5b5`. Tests only (from a pull request of the Windows test session): `PDFMD_VERSION` stays 3.31.15.
689
+
690
+ ### Tests
691
+ - The full run on Windows (cp1251, every tool installed) found tests that assumed a Unix or macOS machine. New `tests/env_support.py`: `hidden_tools(folder)` (a run with no tools on PATH also hides the Windows install folders and the registry lookup, so the nine "without Pandoc" tests of ansi, build_record, check and native really have no Pandoc), `config_env(folder)` (a settings folder named for `XDG_CONFIG_HOME` and `APPDATA`; `test_tables`), `can_symlink()`. `test_direct` puts `typst` on PATH under the program's own name and copies it when a link is refused; the Calibre archive test skips where symbolic links are not allowed. By the Windows test session (pull request #31).
692
+
693
+ ## v3.31.15 — 2026-10-31
694
+
695
+ Pre-edit state: commit `0695669`. Still under 3.31. Merged from a pull request of the Windows test session.
696
+
697
+ ### Fixed
698
+ - **A `.csv` block of an `.xlsx` on Windows.** The map from a sheet to its converted CSV (a `--metadata-file`) held `C:\Users\...\x.csv`; Pandoc reads metadata as Markdown, so the backslashes went and the block came out empty ("could not open the converted sheet"). The path is written with forward slashes (`as_posix()`; no change on macOS and Linux), like 3.31.8 and 3.31.13. By the Windows test session (pull request #32).
699
+
700
+ ## v3.31.14 — 2026-10-31
701
+
702
+ Pre-edit state: commit `0f8f11a`. Still under 3.31. Merged from a pull request of the Windows test session.
703
+
704
+ ### Fixed
705
+ - **`pdfmd --completion` prints its script with Unix line ends on Windows.** Python's text-mode standard output wrote CRLF, and `source <(pdfmd --completion bash)` failed in Git Bash and WSL ("syntax error near unexpected token `$'{\r''"). The command keeps plain newlines (`sys.stdout.reconfigure(newline="\n")`); macOS and Linux are unchanged. A test checks the bytes. By the Windows test session (pull request #30).
706
+
707
+ ## v3.31.13b — 2026-10-30
708
+
709
+ Pre-edit state: commit `c4491c7`. Docs only: `PDFMD_VERSION` stays 3.31.13.
710
+
711
+ ### Docs
712
+ - `CHANGELOG.md`: the note "Why this starts at 2.0.0, not 1.0.0" and its 2026-10-05 update moved from the top of the file to the end of the v2.0.0 entry, so the file opens with the version rule and the newest entry.
713
+
714
+ ## v3.31.13 — 2026-10-30
715
+
716
+ Pre-edit state: commit `5204a2c`. Still under 3.31. Merged from a pull request of the Windows test session.
717
+
718
+ ### Fixed
719
+ - **Converted SVG pictures on Windows.** `image_filter_args` wrote the converted PDF's path into the image map with backslashes; `images.lua` puts it into raw LaTeX, where a backslash is a command (`lualatex`: ``File `C:-1234.pdf' not found``). The path is written with forward slashes (`as_posix()`; no change on macOS and Linux). By the Windows test session (pull request #28).
720
+
721
+ ## v3.31.12 — 2026-10-30
722
+
723
+ Pre-edit state: commit `83f8331`. Still under 3.31. Merged from a pull request of the Windows test session.
724
+
725
+ ### Fixed
726
+ - **Mermaid diagrams on Windows.** `diagram.lua` started `mmdc` by its bare name; npm writes `mmdc.cmd` on Windows, a command script that a program started by name cannot be, so a ```mermaid block stayed code ("could not be drawn") with `mmdc` on PATH. On Windows the filter now starts it as `cmd /c mmdc ...`; macOS and Linux are unchanged. With 3.31.8's slash fix a real `mmdc` 12 builds an inline SVG. `tests/test_diagram_shim.py` (Windows only, a stand-in `mmdc.cmd`) fails without the change. By the Windows test session (pull request #25).
727
+
728
+ ## v3.31.11 — 2026-10-30
729
+
730
+ Pre-edit state: commit `a080381`. Still under 3.31. Merged from a pull request of the Windows test session.
731
+
732
+ ### Added
733
+ - **Pandoc, Typst and Chrome are found and used when a Windows installer leaves them off PATH,** on the lookup of 3.31.10: the keys `options.pandoc-path`, `typst-path` and `chrome-path` (`--setup`, "Tool locations"; a file or a folder), then PATH, then the usual folders (Pandoc under `%LOCALAPPDATA%\Pandoc`, Typst in WinGet's `Links` and `Packages`, Chrome under `Google\Chrome\Application`). The folders of Pandoc and Typst go on the PATH of pdfmd and its programs; a Chrome named by the key is handed to the browser engines through the existing `PDFMD_BROWSER`. `--doctor` adds the same `found at X, not on PATH` / `set by options.KEY` notes. By the Windows test session (pull request #24, entry 3dq).
734
+
735
+ ## v3.31.10 — 2026-10-30
736
+
737
+ Pre-edit state: commit `56c5e9b`. Still under 3.31. Merged from a pull request of the Windows test session.
738
+
739
+ ### Added
740
+ - **Tools that a Windows installer leaves off PATH are found and used: Tesseract, LibreOffice, Graphviz (`dot`), Mermaid (`mmdc`), Poppler.** `winget` and the vendors' installers often skip PATH, so `--doctor` said "not found" for programs that were installed. One lookup (`locate_tool()`, table `FOUND_TOOLS`) now tries, in this order: the config key, PATH, then on Windows the usual install folders (`%ProgramFiles%`, `%ProgramW6432%`, `%ProgramFiles(x86)%`, `%LOCALAPPDATA%\Programs`, `%APPDATA%\npm` for `mmdc`) and the registry's `App Paths`. A tool found off PATH has its folder added to the PATH of pdfmd and of the programs and filters it starts (in front when the config names it, behind otherwise); LibreOffice is resolved by path only, because its folder holds a `python.exe` and DLLs of its own. The folders are a file-system fact about those installers, not a behaviour threshold.
741
+ - **Five config keys, `options.tesseract-path`, `soffice-path`, `dot-path`, `mmdc-path`, `poppler-path`,** in `pdfmd --setup` ("Tool locations", per machine; a folder's `--setup DIR` never offers them): a file or a folder (`poppler-path` takes the folder holding `pdftotext` and `pdftoppm`). A key that names nothing warns once and is passed over. No command-line flag, no `pdfmd-options` key.
742
+ - `--doctor` says where a tool was found: `found at X, not on PATH (pdfmd uses it anyway)` or `set by options.KEY`; the README has a Windows note.
743
+
744
+ ### Changed
745
+ - `resolve_soffice()` and `resolve_calibre()` use the same lookup. `PDFMD_NO_APP_PATHS=1` turns the registry probe off; it is for the tests (so that a test that hides a tool does not find it again), not a setting.
746
+
747
+ ### Notes
748
+ - The next patch adds `pandoc-path`, `typst-path` and `chrome-path` on the same lookup. By the Windows test session (pull request #23, entry 3dq of the maintainer's notes).
749
+
750
+ ## v3.31.9h — 2026-10-30
751
+
752
+ Pre-edit state: commit `7c57727`. Tests only (from a pull request of the Windows test session): `PDFMD_VERSION` stays 3.31.9.
753
+
754
+ ### Tests
755
+ - `tests/test_build_log.py`: the test that needs an `AUTO` line made no file of its own and found the author's global `metadata.yaml`; on a clean machine there was none. It writes its own `metadata.yaml` beside the document and asserts that line. By the Windows test session (pull request #26).
756
+
757
+ ## v3.31.9g — 2026-10-30
758
+
759
+ Pre-edit state: commit `d4bca2e`. Tests only (from a pull request of the Windows test session): `PDFMD_VERSION` stays 3.31.9.
760
+
761
+ ### Tests
762
+ - `tests/test_ansi.py` reads pdfmd's output as UTF-8 (`encoding="utf-8"` on three `subprocess.run` calls); under a non-UTF-8 Windows locale such as cp1251 ten tests failed on the decoding, not on pdfmd, which writes valid UTF-8. By the Windows test session (pull request #29).
763
+
764
+ ## v3.31.9f — 2026-10-30
765
+
766
+ Pre-edit state: commit `ea1a7f7`. Tests only (from a pull request of the Windows test session): `PDFMD_VERSION` stays 3.31.9.
767
+
768
+ ### Tests
769
+ - One helper, `tests/cache_support.py` (`cache_env(folder)`), names the cache folder for every system (`XDG_CACHE_HOME` and `LOCALAPPDATA`); seven test modules use it (build_record, calibre, chem, compat, images, markup_word, raw). By the Windows test session (pull request #27).
770
+
771
+ ## v3.31.9e — 2026-10-30
772
+
773
+ Pre-edit state: commit `57cf5e8`. Tests only (from a pull request of the Windows test session): `PDFMD_VERSION` stays 3.31.9.
774
+
775
+ ### Tests
776
+ - `tests/test_completion.py` hands bash a path Git Bash can read (forward slashes); `tests/test_edit_pdf.py` no longer uses the Unix command `true` as an editor. By the Windows test session (pull request #22).
777
+
778
+ ## v3.31.9d — 2026-10-30
779
+
780
+ Pre-edit state: commit `63ddd01`. Tests only (from a pull request of the Windows test session): `PDFMD_VERSION` stays 3.31.9.
781
+
782
+ ### Tests
783
+ - The build-log and office-LaTeX tests point the cache at their own folder through `LOCALAPPDATA` as well as `XDG_CACHE_HOME` (Windows reads the first; the records went to the real user cache and the tests found none). By the Windows test session (pull request #21).
784
+
785
+ ## v3.31.9c — 2026-10-30
786
+
787
+ Pre-edit state: commit `d4f4cf1`. Tests only (from a pull request of the Windows test session): `PDFMD_VERSION` stays 3.31.9.
788
+
789
+ ### Tests
790
+ - `tests/test_indent_office.py` closes the `ZipFile` it opens before the temporary folder is removed (`WinError 32` on Windows). By the Windows test session (pull request #20).
791
+
792
+ ## v3.31.9b — 2026-10-30
793
+
794
+ Pre-edit state: commit `28f1168`. Tests only (from a pull request of the Windows test session): `PDFMD_VERSION` stays 3.31.9.
795
+
796
+ ### Tests
797
+ - The helpers of `tests/test_office_latex.py` that run Pandoc with the office filter alone pass `pdfmd-office-python` (the running interpreter), as the real pipeline does; on Windows `python3` is the Microsoft Store stub. Nine tests. By the Windows test session (pull request #19).
798
+
799
+ ## v3.31.9 — 2026-10-30
800
+
801
+ Pre-edit state: commit `0b1f055`. Still under 3.31. Merged from a pull request of the Windows test session.
802
+
803
+ ### Fixed
804
+ - **LibreOffice's throwaway profile on Windows.** The three places that start `soffice` with `-env:UserInstallation=file://{profile}` (the Office converter, the ODF reader, the SVG converter of `pdfmd_images`) built `file://C:\...`, which is not a file URL: headless LibreOffice then waited for ever with no output (25+ minutes measured), or exited 1 without a PDF. They pass `Path.as_uri()` now (`file:///C:/...`, percent-encoded; the same string on macOS and Linux). A test (`tests/test_svg_profile.py`) fails without the change. A time limit for the call is not added: it needs a setting and waits for the author. By the Windows test session (pull request #18).
805
+
806
+ ## v3.31.8 — 2026-10-30
807
+
808
+ Pre-edit state: commit `57919d1`. Still under 3.31. Merged from a pull request of the Windows test session.
809
+
810
+ ### Fixed
811
+ - **The raw filters on Windows.** `pdfmd-raw-cache`, `-work` and `-preamble` were written into the generated YAML metadata file with `str(Path)`; Pandoc reads a string there as Markdown, where a backslash escapes, so a Windows path lost its backslashes and the PDF images and the Typst/LaTeX raw pictures were not converted. They are written with forward slashes now (`as_posix()`; no change on macOS and Linux). By the Windows test session (pull request #17).
812
+
813
+ ## v3.31.7 — 2026-10-30
814
+
815
+ Pre-edit state: commit `9813fde`. Still under 3.31. Merged from a pull request of the Windows test session.
816
+
817
+ ### Fixed
818
+ - **`--split` no longer crashes on Windows without the symlink privilege.** `split_into_parts` linked the files and folders next to the source into the scaffold with `symlink_to`; an ordinary Windows user (no Developer Mode, not elevated) gets `WinError 1314`. It now copies the file or folder when the link cannot be made. macOS and Linux still link. A test covers the copy; the command-line split test globs the parts folder only. By the Windows test session (pull request #16).
819
+
820
+ ## v3.31.6 — 2026-10-29
821
+
822
+ Pre-edit state: commit `9f09a1f`. Still under 3.31. The first step of `emit` (the chatter ladder of the tables line, which is NOT started).
823
+
824
+ ### Changed
825
+ - **The record of a build holds the lines that only `--verbose` shows.** The 40 or so places that printed under `--verbose` (the `AUTO` decisions, `CALC`,
826
+ `LABELS`, `OFFICE`, `HYBRID`, the direct routes, backups and the history) call `vprint(shown, ...)`: on the console when `--verbose` is on, as before, and in the record
827
+ always. The metadata files the build used are in the record too. The console without `-v` is unchanged. `WARN`, `NOTE` and `ERROR` lines still print directly
828
+ (the console shows them anyway); the chatter ladder will move them to one `emit(level, id, text)` and `vprint` becomes its verbose level.
829
+
830
+ ## v3.31.5 — 2026-10-29
831
+
832
+ Pre-edit state: commit `3cb13f4`. Still under 3.31, in front of the rest of the tables work.
833
+
834
+ ### Added
835
+ - **The record of a build.** Every build writes a plain-text file to the cache: the command line, the versions (pdfmd, Python, Pandoc, the engines), every
836
+ command run (the `CMD` lines, whatever `--verbose` said), everything pdfmd printed, what each engine answered (exit code, stderr, stdout), the steps and their
837
+ times, and what the build answered; the home folder is written as `~`. It sits where `cache: location` says (`~/.cache/pdfmd/builds/NAME-HASH/`, or
838
+ `.cache/pdfmd/builds/NAME/` beside the document), one file per build (`20261029-140533-ok.log`, `-failed.log`). A failed build ends with `full record: PATH`
839
+ (`--verbose` also names the record of a good one); a build that stopped with an error is recorded too.
840
+ - **`pdfmd --last-build FILE [--failed]`** prints the newest record, or the newest failed one; none: a message and exit status 1.
841
+ - **`build-log: on|failed|off`** (default on; `--build-log MODE`), **`build-log-keep`** (default 5; the newest ones stay, and the newest failed one whatever its age,
842
+ so a good build never erases a failure) and **`build-log-max-size`** (KB, default 512; a longer record keeps its start and more of its end), in `pdfmd-options`,
843
+ the config and `--setup`.
844
+ - The package `pdfmd_buildinfo.record` (pure): scrubbing, clipping, naming, atomic writing and pruning.
845
+
846
+ ### Notes
847
+ - A first version, built from the central points (no `emit` yet): the lines that only `--verbose` prints at their own print sites are not in the record until every
848
+ message goes through one function. A `.qmd`, a direct `.typ`/`.html` build and a report or book build are not recorded yet. A record that cannot be written
849
+ (read-only or full disk) never fails a build.
850
+ - `tests/test_00_environment.py` turns the record off for the suite as a whole (it would make a record for each of the thousand documents the tests build); the
851
+ tests of the record ask for it.
852
+
853
+ ## v3.31.4 — 2026-10-29
854
+
855
+ Pre-edit state: commit `f05d80e`. Still under 3.31, in front of the rest of the tables work (the author asked for it at once).
856
+
857
+ ### Added
858
+ - **How long a build took.** The closing line ends in the time, `OK report.md > report.pdf in 3:07 (lualatex)`, also for a file that failed
859
+ (`FAIL report.md in 0.15 s`) and for a report or book build. Written by `time-format` (the new package `pdfmd_buildinfo`): `auto` (the default;
860
+ `0.42 s`, `12.4 s`, `3:07`, `1:02:03`, `1 d 02:03:04`), `clock` (`03:07`; hours never wrap into days), `seconds`, `words`, or a template with
861
+ `{d} {h} {hh} {m} {mm} {s} {ss} {s1} {s2} {ms}`; a unit wraps only under a larger one of the template, and the time is rounded to the smallest unit shown.
862
+ - **`build-time: on|off`** (default on) and **`time-format`**, in `pdfmd-options`, the config, `--setup`, and as `--build-time` / `--no-build-time` and
863
+ `--time-format STYLE`. A wrong format stops the command line and warns for a document (it reads as `auto`).
864
+ - **`--verbose` prints a `TIME` line for every step**: the engine run (`pandoc + lualatex`; a font retry is a second line), the label and calc passes, plots,
865
+ polish, attach-source, backup, history, the size budget; and "the rest" (finding files and settings, the pre-passes no step covers).
866
+ - **The history file records the seconds** of a build (`took=187.4`) when the history goes to `NAME.hst`; nothing is written into the document's BUILD NOTES.
867
+
868
+ ### Changed
869
+ - The tests that matched the whole closing line take the time into account.
870
+
871
+ ## v3.31.3b — 2026-10-27
872
+
873
+ Pre-edit state: commit `b08a7b4`. Documentation only (`CLAUDE.md`): `PDFMD_VERSION` stays 3.31.3.
874
+
875
+ ### Docs
876
+ - `CLAUDE.md`, "Version numbers": one commit is one version and one entry; a test, workflow or docs file alone takes a lettered entry; a commit that changes only `TODO.md` has no entry; the unreleased-fix clause is for a fix to a bug of that very version only.
877
+
878
+ ## v3.31.3 — 2026-10-27
879
+
880
+ Pre-edit state: commit `4d6291b`. Still the tables line: the settings of the width filter are named for what they set, and none of their numbers is a constant.
881
+
882
+ ### Added
883
+ - **`column-outlier-rows`** (default 4), **`column-outlier-length`** (30) and **`column-word-floor-limit`** (16, the longest word the floor protects) are
884
+ settings; they were constants in the filter. Every number of the filter (`column-outlier` too) can be given per table, `{column-outlier=3
885
+ column-outlier-rows=6 column-outlier-length=40 column-word-floor-limit=24}` after the caption (Pandoc 3.8.2 or newer), in `pdfmd-options`, in the
886
+ config and in `--setup`.
887
+ - **`column-count-raw-tex: on|off`**, a KLUDGE until a real width estimator exists: raw TeX in a cell counted as nothing (a cell of "respiratory
888
+ tract \SIrange{10}{20}{\minute}" was sized as 17 characters and printed as 34, so its column wrapped while another took the room). `\SI`,
889
+ `\SIrange`, `\num`, `\ce` and other raw TeX now count as about what they print. Off (or `{count-raw-tex=off}` on a table) gives the old behaviour.
890
+
891
+ ### Changed
892
+ - **`table-widths` is now `column-widths`, and `table-word-floor` is `column-word-floor`**: "table width" read as the width of the whole table, and
893
+ these set the columns'. The older names are still read everywhere (the config, `--setup`, `pdfmd-options`, `-M`).
894
+
895
+ ### Docs
896
+ - `CLAUDE.md`: "No hardcoded thresholds": a number that changes behaviour is a setting; a constant that must stay in the code is reported to the author.
897
+
898
+ ## v3.31.2 — 2026-10-27
899
+
900
+ Pre-edit state: commit `041a8f0`. Still the tables line.
901
+
902
+ ### Added
903
+ - **`column-outlier`** (default 2; 0 switches it off; `pdfmd-options`, the config, `--setup`, or `{column-outlier=N}` after a table's caption): one cell
904
+ more than twice as long as the next-longest of its column (a column of four rows or more, a cell of at least 30 characters) no longer sizes the
905
+ column, so the others keep the room that holds them on one line; the outlier wraps, and what is left over goes to its column. The one-line "natural
906
+ width" test still counts the real longest cell, so an outlier never runs past the margin. (The two minimums were constants here; 3.31.3 makes them settings.)
907
+ - **Plain `pdfmd` never reads standard input** (decided: a script's inherited pipe looks the same as `echo x | pdfmd`). In a folder without Markdown
908
+ files the error now names `pdfmd -` when standard input is not a terminal.
909
+
910
+ ## v3.31.1 — 2026-10-27
911
+
912
+ Pre-edit state: commit `12cc24a`. Still the tables line.
913
+
914
+ ### Changed
915
+ - `--doctor` lists the Pandoc 3.8.2 floor for a table's own `{widths=... word-floor=...}` (it reads the table's attributes from the caption), and the `--setup` notes of both keys say so.
916
+
917
+ ## v3.31.0b — 2026-10-27
918
+
919
+ Pre-edit state: commit `15f70ee`. Documentation only (`CLAUDE.md`): `PDFMD_VERSION` stays 3.31.0.
920
+
921
+ ### Docs
922
+ - `CLAUDE.md`: "Decisions are recorded first": every decision goes into the maintainer's notes, at full context, and a short `TODO.md` line, before any code.
923
+
924
+ ## v3.31.0 — 2026-10-27
925
+
926
+ Pre-edit state: commit `d296523`. Opens the tables line (the plan is in the maintainer's notes): the first change is to the pipe-table width filter.
927
+
928
+ ### Fixed
929
+ - **A column the author chose could break words in the middle.** A pipe table whose separator dashes are unequal (`|:-------|:-------------|`) is a
930
+ choice and was kept as written, even when a column came out narrower than its own longest word and LaTeX cut "Sustained" into "Sus-tained". A
931
+ chosen column too narrow for its longest word (a word of up to 16 characters; a URL is not worth the room) is now widened to it, and the
932
+ other columns give up the space in proportion to what they had, none below its own longest word. A table where every column already holds its
933
+ words is untouched, byte for byte. The note `AUTO TABLEWIDTH` says when it happened.
934
+
935
+ ### Added
936
+ - **`table-word-floor: on|off`** (`pdfmd-options`, the config, `--setup` "Tables"): off leaves a chosen table exactly as written, for a table where
937
+ breaking a word is meant (long chemical names in a narrow column).
938
+ - **`table-widths: force`** next to `auto` and `keep`: the dashes are ignored and every table gets the widths that give the fewest lines.
939
+ - **A table's own `{widths=auto|keep|force word-floor=on|off}`** after its caption wins over the document and the config (Pandoc 3.8.2 or newer,
940
+ which keeps a caption's attributes on the table; older ones print them in the caption).
941
+ - A document's `pdfmd-options` now wins over the global `table-widths` of the config (the global value used to shadow it).
942
+
943
+ ### Changed
944
+ - The `AUTO TABLEWIDTH` note no longer says the tables "had no column widths" only: it also covers `table-widths: force`.
945
+
946
+ ## v3.30.42b — 2026-10-26
947
+
948
+ Pre-edit state: commit `4efe2c5`. Documentation only (the README and the notes in `pdfmd.py` and `--setup`): `PDFMD_VERSION` stays 3.30.42.
949
+
950
+ ### Fixed
951
+ - The notes on pipe-table column widths claimed more than the code does. Unequal separator dashes are kept only under Pandoc's own Markdown reader (a document with front matter or a linked metadata file); under `gfm` they carry no width and every table is balanced. Colons count as dashes. The balancing runs for the LaTeX engines only; the other writers take Pandoc's widths, equal columns for `---|---` with a long line under the Markdown reader. The behaviour itself is unchanged and queued.
952
+
953
+ ## v3.30.42 — 2026-10-26
954
+
955
+ Pre-edit state: commit `0ae751a` (v3.30.41).
956
+
957
+ ### Fixed
958
+ - **`--paper a4,landscape` (and `papersize: a4,landscape`) turned no page but the built-in renderers'.** Pandoc's `papersize` is a
959
+ LaTeX class option: on the LaTeX engines it printed `[a4,landscapepaper]`, LaTeX said "Unused global option" and the page was
960
+ Letter, silently; Calibre and the browsers lost the orientation too (the Word-file reader did not split the list item); WeasyPrint
961
+ ignored the paper altogether (A4 whatever `papersize:` said); Typst stopped on `a4,landscape`, and on a plain `--paper letter`
962
+ (pdfmd's translation to `us-letter` came as a second `-V papersize=`, and Pandoc joined the two into `letterus-letter`). One
963
+ function, `paper_variables`, now settles the paper for each engine: LaTeX gets the size as a class option and the turn as
964
+ `geometry:landscape`; Typst gets its own name (`us-letter`, `iso-b5`) once and `#set page(flipped: true)` through `typst.lua`;
965
+ WeasyPrint gets `@page { size: ... }` through `weasyprint.lua`; Calibre and the browsers read the list item. The same page comes
966
+ from `geometry: landscape` and `classoption: landscape`, and from the `pagesize:` typo, which now also beats a `papersize:` of the
967
+ settings in the Calibre, browser and Word readers (as it did for LaTeX).
968
+ - Spellings that printed a Letter page on LaTeX: `a4paper` (became `a4paperpaper`), `A4`, `us-letter`, and `a3` (no class option;
969
+ `geometry:a3paper` now). `--paper landscape` alone keeps the size the settings name.
970
+ - `--no-auto papersize` leaves all of this to Pandoc. Measured on Pandoc 3.12 with LuaLaTeX, Typst, WeasyPrint, Calibre and
971
+ the built-in renderer, for `a4,landscape`, `letter,landscape`, `a4`, `letter`, `a4paper`, `a3`, `us-letter,portrait`, `landscape`
972
+ and front matter `papersize`, `pagesize`, `geometry`, `classoption`.
973
+
974
+ ## v3.30.41 — 2026-10-26
975
+
976
+ Pre-edit state: commit `c2732a2` (v3.30.40).
977
+
978
+ ### Added
979
+ - **`mdpdf` answers the npm `mdpdf` too.** `--style FILE` and `MDPDF_STYLES` (a style sheet; `-e html` is added unless an engine is
980
+ named), `--format` (paper), `--orientation landscape` (`-V geometry:landscape`: the LaTeX route; `papersize: a4,landscape` is read
981
+ by the built-in renderers only), `--border` and `--border-top|-left|-bottom|-right` (margins as `geometry:` variables; px count as
982
+ 0.75 pt). `--header`/`--footer` are told apart by the value: an `.html`, `.htm` or `.hbs` file, or any file that exists, is the npm
983
+ one's Puppeteer markup and is dropped; a template `"left,middle,right"` stays the PyPI one's. Dropped with a note on standard
984
+ error, never silently: those headers and footers, `--h-height`, `--f-height`, `--no-emoji`, `--gh-style`, `--debug`.
985
+ - **`--doctor` names a command of pdfmd's that another program answers.** `pdfmd`, `mdpdf`, `md2pdf`, `inkmd` and the others are
986
+ shared by several tools, and the one installed last owns each name; a "Command names" section says which names on the PATH are not
987
+ pdfmd's and how to take them back (`pip install --force-reinstall --no-deps pdfmd-cli`). It reads the installed package's own list
988
+ of scripts, so it says nothing from a checkout, and nothing when every name is pdfmd's. Checked in a scratch virtualenv with a
989
+ different `md2pdf` ahead on the PATH.
990
+
991
+ ## v3.30.40 — 2026-10-26
992
+
993
+ Pre-edit state: commit `e606b1b` (v3.30.39).
994
+
995
+ ### Added
996
+ - **An `md2pdf` command.** Several tools install that name; pdfmd now answers the keys of all three: jmaupetit's `md2pdf`
997
+ (`-i -o -c -W`), `md-to-pdf` (`--stylesheet`, `--as-html`, `-w`, `--document-title`) and kxxoling's (`--theme file.css`). A style
998
+ sheet adds `-e html` unless an engine is named (only HTML engines read CSS). `-e` is an engine when its value is one and
999
+ jmaupetit's Markdown extension otherwise. What pdfmd has no counterpart for (extensions, `-C`, `--pdf-options`, `--config-file`,
1000
+ `--launch-options`, a string given to `--css`, a built-in `--theme`) is named on standard error and left out.
1001
+
1002
+ ## v3.30.39 — 2026-10-26
1003
+
1004
+ Pre-edit state: commit `e069268` (v3.30.38).
1005
+
1006
+ ### Added
1007
+ - **`mdpdf in.md out.pdf`: the output as a second positional.** The Go `mdpdf` and `md2static` name the PDF that way; the
1008
+ `mdpdf` command now takes a last positional ending in `.pdf` that follows another positional as `-o`, whether or not options
1009
+ come after it (`mdpdf notes.md notes.pdf -p A4`). It is left alone when `-o` is given, when it is the value of an option
1010
+ (`--cover c.pdf`), when it is the only argument, and when the one before it is a PDF too. The original `mdpdf` never reads a PDF,
1011
+ so nothing it accepted changes. `pdfmd` itself does not do this: for pdfmd a PDF argument is an input.
1012
+ - **`pdfmd x.pdf -o -`: the Markdown on standard output.** M1ck4's `pdfmd` (archived 2026-05-08) pipes that way; for PDF inputs it
1013
+ now means batchocr's `--stdout` (it was the terminal view, which refuses a PDF). `pdfmd x.pdf --stdout` worked already. The setting reaches `-j` workers (`tests/test_jobs.py` caught the first version, which did not).
1014
+
1015
+ ## v3.30.38 — 2026-10-26
1016
+
1017
+ Pre-edit state: commit `d4ae514` (v3.30.37).
1018
+
1019
+ ### Fixed
1020
+ - **A `.csv` block's caption with several attribute words was printed as text.** `Table: Raw data {#tbl:raw keep=9}` (or the `: Raw data {...}` form) after a `::: {.csv}` block gave the table no id and no attributes and left `{#tbl:raw keep=9}` in the caption: only a block of one word (`{#tbl:raw}`, `{.keep}`) was read. The block may hold several words now, every one an `#id`, a `.class` or a `key=value`, and a `key=value` block (`{keep=9}`) counts. A brace in the caption's prose (`Set {x} and {y}`) is still prose.
1021
+
1022
+ ### Added
1023
+ - **`keep` on a `.csv` div reaches the table.** `::: {.csv file="x.csv" keep=4}`, `::: {.csv .keep}` and `.nokeep` put `keep=4`, `keep` or `nokeep` on the table's own attributes, where a filter that keeps a short table on one page (nulabreport.lua) looks for them; the caption's own attributes win. `tests/test_csv_table.py`.
1024
+
1025
+ ## v3.30.37 — 2026-10-26
1026
+
1027
+ Pre-edit state: commit `276f947` (v3.30.36).
1028
+
1029
+ ### Added
1030
+ - **`--no-rewrite`: keep the old output.** A build replaces an older `report.pdf`; with `--no-rewrite` it writes `report (1).pdf` (the first free number) and leaves the old file alone. `--rewrite` turns it back on for one run. Also `pdfmd-options: {rewrite: false}` (a document, a folder's `.pdfmd`, the config) and a `--setup` row ("Replace an older output of the same name", in the folder screen too). The name is a template, `rewrite-name` (`--setup`: "The name of the numbered copy"), default `{stem} ({n}){ext}`, with `{n}`, `{stem}`, `{ext}` and `{date}`; one without `{n}` or with a folder in it is refused with a warning. It applies to the final output of every build: the `-o` name, each file of a `-b` batch and of a parts build on its own, `pdfmd -` (`stdin (1).pdf`), `--to` formats; the cache stage's `.tex`, the `--view` scratch files and the builds pdfmd runs on itself (`--edit`'s rebuild, the labels pass, the Python API) are never numbered. A watch (`-w`) always rewrites. The first build of a name has no number. `tests/test_rewrite.py`.
1031
+
1032
+ ## v3.30.36 — 2026-10-26
1033
+
1034
+ Pre-edit state: commit `bdc81b1` (v3.30.35).
1035
+
1036
+ ### Added
1037
+ - **The indent div works in Word and OpenDocument.** `::: {indent=false}` (and `noindent`, `indent=2em`, ...) printed a NOTE and changed nothing in a `.docx` or `.odt`, so a Word file did not match the PDF. `pdfmd_lua/indent.lua` now gives the plain paragraphs of the div a paragraph style named for the length, `pdfmd-indent-0pt`, `pdfmd-indent-2em` (Pandoc applies a custom-style div to the paragraphs directly inside it and leaves lists, quotes, code and tables in their own styles); `pdfmd_office` writes the first-line indent into the style afterwards (`w:ind w:firstLine` in a Word file, `fo:text-indent` in an OpenDocument file, where Pandoc only names the style and does not define it). An em is 12 pt there. The paragraphs after the div are the document's own again. A length in `%` or `rem` is still left out with the NOTE. Checked in LibreOffice: the div's first line starts at the margin (72 pt), `2em` at 96 pt, the body text after it at its own indent. `tests/test_indent_office.py`.
1038
+
1039
+ ## v3.30.35 — 2026-10-26
1040
+
1041
+ Pre-edit state: commit `bbb6a47` (v3.30.34).
1042
+
1043
+ ### Added
1044
+ - **A LuaTeX that cannot start names why.** An unwritable TeX cache and a corrupt luaotfload database both reached pdfmd only as Pandoc's `lualatex failed (! ==> Fatal error occurred, no output PDF file produced!)`: the cause is in the engine's own console, and the two looked alike although the fixes differ. When lualatex fails with that line and the text names no known cause, pdfmd runs a one-line page with the same engine in the same environment (once per run, 120 s at most) and classifies what it prints: `cause: the TeX cache folder is not writable; set TEXMFVAR and TEXMFCACHE to a folder you can write to`, or `the LuaTeX font database is corrupt; run luaotfload-tool --update --force, or delete the luatex-cache folders`. `--doctor --deep` shows the same cause after the reason an engine failed. The signature `no writeable cache path` is matched across a wrapped console line (macOS prints `quiting`, Linux `quitting`). Checked with a read-only `TEXMFVAR`/`TEXMFCACHE` on macOS. `tests/test_tex_cause.py`.
1045
+
1046
+ ## v3.30.34 — 2026-10-26
1047
+
1048
+ Pre-edit state: commit `210497a` (v3.30.33).
1049
+
1050
+ ### Added
1051
+ - **`pdfmd -` builds the Markdown on standard input.** `echo '# Hi' | pdfmd -` writes `stdin.pdf` (`stdin.html`, `stdin.docx` ... with `--to`) in the current folder, or the file `-o` names. Before, `-` was "Could not find Markdown file for '-'", and only `--view` and `-o -` read a pipe. The text is kept in a hidden file in the current folder for the length of the run (a relative picture or include resolves there; it is removed on success, failure and ^C), and the OK line names it `stdin.md`. Standard input that is empty is refused with a reason. A plain `pdfmd` with a pipe is unchanged (it builds the folder's Markdown): the `-` is what says that the pipe is the document, so no script that runs `pdfmd` with an inherited stdin changes meaning. `tests/test_stdin.py`.
1052
+
1053
+ ## v3.30.33 — 2026-10-26
1054
+
1055
+ Pre-edit state: commit `5fe0411` (v3.30.32b).
1056
+
1057
+ ### Fixed
1058
+ - `--doctor --deep` builds a page with a struck-out word. A LaTeX installation without `soul.sty` (xelatex, pdflatex) or `lua-ul` (lualatex) passed the check and then failed on the first document with `~~text~~`.
1059
+
1060
+ ## v3.30.32b — 2026-10-26
1061
+
1062
+ Pre-edit state: commit `3b97509` (v3.30.32). A docs-only commit: `PDFMD_VERSION` stays 3.30.32.
1063
+
1064
+ ### Added
1065
+ - `TODO.md`: the open work, one line per item, grouped as next / decide first / agreed features / before 4.0 / investigate first / other repositories. `CLAUDE.md` says to delete an item in the commit that finishes it.
1066
+
1067
+ ### Fixed
1068
+ - `tests/test_doc.py` accepts a lettered newest changelog heading (`v3.30.32b`); it compared the heading with `PDFMD_VERSION` exactly, so the first docs-only commit under the 3.30.30 rule would have failed it.
42
1069
 
43
1070
  ## v3.30.32 — 2026-10-26
44
1071
 
@@ -8996,6 +10023,27 @@ package being built.
8996
10023
  page shape trips the same "wrongly rebalanced" or "wrongly left alone"
8997
10024
  symptom in the other direction.
8998
10025
 
10026
+ **Why this starts at 2.0.0, not 1.0.0.** `pdfmd.py` began as a one-line
10027
+ `pandoc file.md -o file.pdf` wrapper and was revised many times since — across
10028
+ ChatGPT, ChatGPT Codex, Claude Chat, Cowork, and Claude Code sessions — with no
10029
+ version marker or changelog kept at any point. `backups/pdfmd.py.bak-*` has
10030
+ snapshots from a handful of points in that history, but not from the
10031
+ ChatGPT/Codex/Cowork era at all, so reconstructing an honest "v1.x" changelog
10032
+ from what's actually on disk isn't possible — only a partial,
10033
+ misleadingly-precise-looking one. Decided 2026-09-16 (the author): start real
10034
+ changelog tracking now, at 2.0.0, rather than fabricate the gap. If the full
10035
+ history ever matters, it would need pulling the actual chat transcripts from
10036
+ each of those tools, not guessing from diffs between inconsistent backup
10037
+ snapshots.
10038
+
10039
+ **Update, 2026-10-05:** an approximate outline of the earlier history has since
10040
+ been reconstructed, at the very end of this file (see "Before 2.0.0 —
10041
+ reconstructed history"), from the saved Codex transcripts and the surviving
10042
+ backup snapshots. It is explicitly NOT reliable: its 1.x numbers are invented,
10043
+ snapshot-based entries cover windows of days rather than single changes, and
10044
+ anything done in unsaved sessions (other ChatGPT chats, claude.ai, Cowork) is
10045
+ missing. Real tracking still begins at 2.0.0.
10046
+
8999
10047
  ---
9000
10048
 
9001
10049
  # Before 2.0.0 — reconstructed history (NOT reliable)