pdfmd-cli 3.30.32__tar.gz → 3.31.46__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 (326) hide show
  1. {pdfmd_cli-3.30.32 → pdfmd_cli-3.31.46}/CHANGELOG.md +795 -20
  2. {pdfmd_cli-3.30.32/pdfmd_cli.egg-info → pdfmd_cli-3.31.46}/PKG-INFO +288 -25
  3. {pdfmd_cli-3.30.32 → pdfmd_cli-3.31.46}/README.md +287 -24
  4. {pdfmd_cli-3.30.32 → pdfmd_cli-3.31.46}/docs/recipes.md +10 -4
  5. {pdfmd_cli-3.30.32 → pdfmd_cli-3.31.46}/pdfmd.py +2810 -342
  6. pdfmd_cli-3.31.46/pdfmd_buildinfo/__init__.py +7 -0
  7. pdfmd_cli-3.31.46/pdfmd_buildinfo/record.py +109 -0
  8. pdfmd_cli-3.31.46/pdfmd_buildinfo/timing.py +185 -0
  9. {pdfmd_cli-3.30.32 → pdfmd_cli-3.31.46/pdfmd_cli.egg-info}/PKG-INFO +288 -25
  10. {pdfmd_cli-3.30.32 → pdfmd_cli-3.31.46}/pdfmd_cli.egg-info/SOURCES.txt +42 -0
  11. {pdfmd_cli-3.30.32 → pdfmd_cli-3.31.46}/pdfmd_cli.egg-info/entry_points.txt +1 -0
  12. {pdfmd_cli-3.30.32 → pdfmd_cli-3.31.46}/pdfmd_cli.egg-info/top_level.txt +4 -0
  13. pdfmd_cli-3.31.46/pdfmd_crop/__init__.py +164 -0
  14. pdfmd_cli-3.31.46/pdfmd_crop/images.py +96 -0
  15. {pdfmd_cli-3.30.32 → pdfmd_cli-3.31.46}/pdfmd_help/__init__.py +6 -4
  16. {pdfmd_cli-3.30.32 → pdfmd_cli-3.31.46}/pdfmd_images/svg.py +8 -6
  17. pdfmd_cli-3.31.46/pdfmd_kits/__init__.py +13 -0
  18. pdfmd_cli-3.31.46/pdfmd_kits/discovery.py +207 -0
  19. pdfmd_cli-3.31.46/pdfmd_kits/kitfile.py +282 -0
  20. pdfmd_cli-3.31.46/pdfmd_kits/preamble.py +144 -0
  21. pdfmd_cli-3.31.46/pdfmd_kits/selection.py +188 -0
  22. {pdfmd_cli-3.30.32 → pdfmd_cli-3.31.46}/pdfmd_lua/calc.lua +60 -4
  23. {pdfmd_cli-3.30.32 → pdfmd_cli-3.31.46}/pdfmd_lua/csv_table.lua +60 -20
  24. {pdfmd_cli-3.30.32 → pdfmd_cli-3.31.46}/pdfmd_lua/diagram.lua +4 -0
  25. {pdfmd_cli-3.30.32 → pdfmd_cli-3.31.46}/pdfmd_lua/indent.lua +8 -1
  26. pdfmd_cli-3.31.46/pdfmd_lua/metadata_append.lua +24 -0
  27. pdfmd_cli-3.31.46/pdfmd_lua/smiles.lua +212 -0
  28. pdfmd_cli-3.31.46/pdfmd_lua/table_caption.lua +201 -0
  29. pdfmd_cli-3.31.46/pdfmd_lua/table_width.lua +415 -0
  30. {pdfmd_cli-3.30.32 → pdfmd_cli-3.31.46}/pdfmd_lua/typst.lua +23 -1
  31. {pdfmd_cli-3.30.32 → pdfmd_cli-3.31.46}/pdfmd_lua/weasyprint.lua +11 -1
  32. {pdfmd_cli-3.30.32 → pdfmd_cli-3.31.46}/pdfmd_office/__init__.py +7 -1
  33. {pdfmd_cli-3.30.32 → pdfmd_cli-3.31.46}/pdfmd_office/docx_post.py +27 -0
  34. {pdfmd_cli-3.30.32 → pdfmd_cli-3.31.46}/pdfmd_office/odt.py +37 -0
  35. {pdfmd_cli-3.30.32 → pdfmd_cli-3.31.46}/pdfmd_office/units.py +2 -4
  36. {pdfmd_cli-3.30.32 → pdfmd_cli-3.31.46}/pdfmd_setup/__init__.py +7 -5
  37. {pdfmd_cli-3.30.32 → pdfmd_cli-3.31.46}/pdfmd_setup/fancy.py +13 -6
  38. {pdfmd_cli-3.30.32 → pdfmd_cli-3.31.46}/pdfmd_setup/inventory.py +32 -8
  39. {pdfmd_cli-3.30.32 → pdfmd_cli-3.31.46}/pdfmd_setup/plain.py +18 -9
  40. {pdfmd_cli-3.30.32 → pdfmd_cli-3.31.46}/pdfmd_setup/registry.py +158 -14
  41. pdfmd_cli-3.31.46/pdfmd_setup/userpath.py +306 -0
  42. pdfmd_cli-3.31.46/pdfmd_smiles/__init__.py +105 -0
  43. pdfmd_cli-3.31.46/pdfmd_smiles/__main__.py +12 -0
  44. pdfmd_cli-3.31.46/pdfmd_smiles/batch.py +154 -0
  45. pdfmd_cli-3.31.46/pdfmd_tables/captions.py +26 -0
  46. {pdfmd_cli-3.30.32 → pdfmd_cli-3.31.46}/pdfmd_tables/sheets.py +7 -7
  47. {pdfmd_cli-3.30.32 → pdfmd_cli-3.31.46}/pyproject.toml +3 -1
  48. pdfmd_cli-3.31.46/tests/test_00_environment.py +7 -0
  49. {pdfmd_cli-3.30.32 → pdfmd_cli-3.31.46}/tests/test_ansi.py +7 -4
  50. {pdfmd_cli-3.30.32 → pdfmd_cli-3.31.46}/tests/test_attach.py +1 -0
  51. {pdfmd_cli-3.30.32 → pdfmd_cli-3.31.46}/tests/test_attach_format.py +9 -8
  52. pdfmd_cli-3.31.46/tests/test_attach_layers.py +96 -0
  53. {pdfmd_cli-3.30.32 → pdfmd_cli-3.31.46}/tests/test_autolog.py +3 -3
  54. {pdfmd_cli-3.30.32 → pdfmd_cli-3.31.46}/tests/test_browser_engine.py +1 -1
  55. pdfmd_cli-3.31.46/tests/test_build_log.py +209 -0
  56. {pdfmd_cli-3.30.32 → pdfmd_cli-3.31.46}/tests/test_build_record.py +6 -3
  57. pdfmd_cli-3.31.46/tests/test_build_time.py +165 -0
  58. {pdfmd_cli-3.30.32 → pdfmd_cli-3.31.46}/tests/test_calc.py +92 -0
  59. {pdfmd_cli-3.30.32 → pdfmd_cli-3.31.46}/tests/test_calibre.py +8 -5
  60. {pdfmd_cli-3.30.32 → pdfmd_cli-3.31.46}/tests/test_calibre_markdown.py +1 -1
  61. {pdfmd_cli-3.30.32 → pdfmd_cli-3.31.46}/tests/test_calibre_targets.py +3 -3
  62. {pdfmd_cli-3.30.32 → pdfmd_cli-3.31.46}/tests/test_check.py +2 -1
  63. {pdfmd_cli-3.30.32 → pdfmd_cli-3.31.46}/tests/test_chem.py +4 -2
  64. {pdfmd_cli-3.30.32 → pdfmd_cli-3.31.46}/tests/test_code_blocks.py +2 -2
  65. {pdfmd_cli-3.30.32 → pdfmd_cli-3.31.46}/tests/test_compat.py +4 -2
  66. {pdfmd_cli-3.30.32 → pdfmd_cli-3.31.46}/tests/test_completion.py +8 -2
  67. pdfmd_cli-3.31.46/tests/test_crop.py +325 -0
  68. {pdfmd_cli-3.30.32 → pdfmd_cli-3.31.46}/tests/test_csv_table.py +35 -0
  69. pdfmd_cli-3.31.46/tests/test_diagram_shim.py +45 -0
  70. {pdfmd_cli-3.30.32 → pdfmd_cli-3.31.46}/tests/test_diagrams.py +4 -2
  71. {pdfmd_cli-3.30.32 → pdfmd_cli-3.31.46}/tests/test_direct.py +9 -5
  72. {pdfmd_cli-3.30.32 → pdfmd_cli-3.31.46}/tests/test_display_path.py +7 -1
  73. {pdfmd_cli-3.30.32 → pdfmd_cli-3.31.46}/tests/test_doc.py +3 -2
  74. {pdfmd_cli-3.30.32 → pdfmd_cli-3.31.46}/tests/test_doctor.py +56 -2
  75. {pdfmd_cli-3.30.32 → pdfmd_cli-3.31.46}/tests/test_edit.py +3 -2
  76. {pdfmd_cli-3.30.32 → pdfmd_cli-3.31.46}/tests/test_edit_pdf.py +1 -1
  77. {pdfmd_cli-3.30.32 → pdfmd_cli-3.31.46}/tests/test_embedding.py +2 -2
  78. pdfmd_cli-3.31.46/tests/test_formula.py +63 -0
  79. {pdfmd_cli-3.30.32 → pdfmd_cli-3.31.46}/tests/test_help.py +2 -1
  80. {pdfmd_cli-3.30.32 → pdfmd_cli-3.31.46}/tests/test_history.py +1 -1
  81. {pdfmd_cli-3.30.32 → pdfmd_cli-3.31.46}/tests/test_images.py +6 -4
  82. pdfmd_cli-3.31.46/tests/test_indent_office.py +145 -0
  83. {pdfmd_cli-3.30.32 → pdfmd_cli-3.31.46}/tests/test_install_calibre.py +4 -0
  84. pdfmd_cli-3.31.46/tests/test_kit_carry.py +150 -0
  85. pdfmd_cli-3.31.46/tests/test_kit_preamble.py +245 -0
  86. pdfmd_cli-3.31.46/tests/test_kit_selection.py +324 -0
  87. pdfmd_cli-3.31.46/tests/test_kit_texinputs.py +79 -0
  88. pdfmd_cli-3.31.46/tests/test_kits.py +435 -0
  89. {pdfmd_cli-3.30.32 → pdfmd_cli-3.31.46}/tests/test_lookup.py +1 -0
  90. {pdfmd_cli-3.30.32 → pdfmd_cli-3.31.46}/tests/test_markup_word.py +3 -1
  91. pdfmd_cli-3.31.46/tests/test_metadata_append.py +146 -0
  92. {pdfmd_cli-3.30.32 → pdfmd_cli-3.31.46}/tests/test_native.py +3 -1
  93. {pdfmd_cli-3.30.32 → pdfmd_cli-3.31.46}/tests/test_office.py +2 -1
  94. {pdfmd_cli-3.30.32 → pdfmd_cli-3.31.46}/tests/test_office_latex.py +24 -18
  95. {pdfmd_cli-3.30.32 → pdfmd_cli-3.31.46}/tests/test_package_helper.py +42 -0
  96. {pdfmd_cli-3.30.32 → pdfmd_cli-3.31.46}/tests/test_packaging.py +1 -0
  97. pdfmd_cli-3.31.46/tests/test_paper.py +127 -0
  98. {pdfmd_cli-3.30.32 → pdfmd_cli-3.31.46}/tests/test_partial_history.py +1 -1
  99. pdfmd_cli-3.31.46/tests/test_path_display.py +135 -0
  100. {pdfmd_cli-3.30.32 → pdfmd_cli-3.31.46}/tests/test_pdf_input.py +9 -0
  101. {pdfmd_cli-3.30.32 → pdfmd_cli-3.31.46}/tests/test_plot_cache.py +14 -0
  102. {pdfmd_cli-3.30.32 → pdfmd_cli-3.31.46}/tests/test_polish.py +94 -0
  103. {pdfmd_cli-3.30.32 → pdfmd_cli-3.31.46}/tests/test_probes.py +31 -0
  104. {pdfmd_cli-3.30.32 → pdfmd_cli-3.31.46}/tests/test_raw.py +6 -4
  105. pdfmd_cli-3.31.46/tests/test_rewrite.py +100 -0
  106. {pdfmd_cli-3.30.32 → pdfmd_cli-3.31.46}/tests/test_sections.py +54 -2
  107. {pdfmd_cli-3.30.32 → pdfmd_cli-3.31.46}/tests/test_setup.py +2 -2
  108. pdfmd_cli-3.31.46/tests/test_smiles.py +232 -0
  109. pdfmd_cli-3.31.46/tests/test_soffice_timeout.py +118 -0
  110. {pdfmd_cli-3.30.32 → pdfmd_cli-3.31.46}/tests/test_standalone_source.py +1 -0
  111. pdfmd_cli-3.31.46/tests/test_stdin.py +86 -0
  112. pdfmd_cli-3.31.46/tests/test_svg_profile.py +35 -0
  113. pdfmd_cli-3.31.46/tests/test_table_captions.py +146 -0
  114. pdfmd_cli-3.31.46/tests/test_table_widths.py +314 -0
  115. {pdfmd_cli-3.30.32 → pdfmd_cli-3.31.46}/tests/test_tables.py +5 -4
  116. pdfmd_cli-3.31.46/tests/test_tex_cause.py +119 -0
  117. pdfmd_cli-3.31.46/tests/test_tool_locations.py +134 -0
  118. {pdfmd_cli-3.30.32 → pdfmd_cli-3.31.46}/tests/test_translit.py +1 -0
  119. pdfmd_cli-3.31.46/tests/test_user_path.py +361 -0
  120. {pdfmd_cli-3.30.32 → pdfmd_cli-3.31.46}/tests/test_versions.py +1 -1
  121. pdfmd_cli-3.30.32/pdfmd_lua/table_width.lua +0 -231
  122. pdfmd_cli-3.30.32/tests/test_table_widths.py +0 -114
  123. pdfmd_cli-3.30.32/tests/test_tex_cause.py +0 -57
  124. {pdfmd_cli-3.30.32 → pdfmd_cli-3.31.46}/LICENSE +0 -0
  125. {pdfmd_cli-3.30.32 → pdfmd_cli-3.31.46}/MANIFEST.in +0 -0
  126. {pdfmd_cli-3.30.32 → pdfmd_cli-3.31.46}/docs/agents.md +0 -0
  127. {pdfmd_cli-3.30.32 → pdfmd_cli-3.31.46}/pdfmd_ansi/__init__.py +0 -0
  128. {pdfmd_cli-3.30.32 → pdfmd_cli-3.31.46}/pdfmd_ansi/highlight.py +0 -0
  129. {pdfmd_cli-3.30.32 → pdfmd_cli-3.31.46}/pdfmd_ansi/mathbox.py +0 -0
  130. {pdfmd_cli-3.30.32 → pdfmd_cli-3.31.46}/pdfmd_ansi/native.py +0 -0
  131. {pdfmd_cli-3.30.32 → pdfmd_cli-3.31.46}/pdfmd_ansi/render.py +0 -0
  132. {pdfmd_cli-3.30.32 → pdfmd_cli-3.31.46}/pdfmd_ansi/text.py +0 -0
  133. {pdfmd_cli-3.30.32 → pdfmd_cli-3.31.46}/pdfmd_check/__init__.py +0 -0
  134. {pdfmd_cli-3.30.32 → pdfmd_cli-3.31.46}/pdfmd_check/lint.py +0 -0
  135. {pdfmd_cli-3.30.32 → pdfmd_cli-3.31.46}/pdfmd_chem/__init__.py +0 -0
  136. {pdfmd_cli-3.30.32 → pdfmd_cli-3.31.46}/pdfmd_chem/__main__.py +0 -0
  137. {pdfmd_cli-3.30.32 → pdfmd_cli-3.31.46}/pdfmd_chem/batch.py +0 -0
  138. {pdfmd_cli-3.30.32 → pdfmd_cli-3.31.46}/pdfmd_chem/render.py +0 -0
  139. {pdfmd_cli-3.30.32 → pdfmd_cli-3.31.46}/pdfmd_chem/siunitx.py +0 -0
  140. {pdfmd_cli-3.30.32 → pdfmd_cli-3.31.46}/pdfmd_cli.egg-info/dependency_links.txt +0 -0
  141. {pdfmd_cli-3.30.32 → pdfmd_cli-3.31.46}/pdfmd_cli.egg-info/requires.txt +0 -0
  142. {pdfmd_cli-3.30.32 → pdfmd_cli-3.31.46}/pdfmd_css/__init__.py +0 -0
  143. {pdfmd_cli-3.30.32 → pdfmd_cli-3.31.46}/pdfmd_direct/__init__.py +0 -0
  144. {pdfmd_cli-3.30.32 → pdfmd_cli-3.31.46}/pdfmd_direct/defaults.py +0 -0
  145. {pdfmd_cli-3.30.32 → pdfmd_cli-3.31.46}/pdfmd_direct/html.py +0 -0
  146. {pdfmd_cli-3.30.32 → pdfmd_cli-3.31.46}/pdfmd_direct/typst.py +0 -0
  147. {pdfmd_cli-3.30.32 → pdfmd_cli-3.31.46}/pdfmd_docs/__init__.py +0 -0
  148. {pdfmd_cli-3.30.32 → pdfmd_cli-3.31.46}/pdfmd_edit/__init__.py +0 -0
  149. {pdfmd_cli-3.30.32 → pdfmd_cli-3.31.46}/pdfmd_flat/__init__.py +0 -0
  150. {pdfmd_cli-3.30.32 → pdfmd_cli-3.31.46}/pdfmd_flat/asciify.py +0 -0
  151. {pdfmd_cli-3.30.32 → pdfmd_cli-3.31.46}/pdfmd_flat/keep.py +0 -0
  152. {pdfmd_cli-3.30.32 → pdfmd_cli-3.31.46}/pdfmd_history/__init__.py +0 -0
  153. {pdfmd_cli-3.30.32 → pdfmd_cli-3.31.46}/pdfmd_history/git.py +0 -0
  154. {pdfmd_cli-3.30.32 → pdfmd_cli-3.31.46}/pdfmd_history/hst.py +0 -0
  155. {pdfmd_cli-3.30.32 → pdfmd_cli-3.31.46}/pdfmd_history/versions.py +0 -0
  156. {pdfmd_cli-3.30.32 → pdfmd_cli-3.31.46}/pdfmd_images/__init__.py +0 -0
  157. {pdfmd_cli-3.30.32 → pdfmd_cli-3.31.46}/pdfmd_images/remote.py +0 -0
  158. {pdfmd_cli-3.30.32 → pdfmd_cli-3.31.46}/pdfmd_images/scan.py +0 -0
  159. {pdfmd_cli-3.30.32 → pdfmd_cli-3.31.46}/pdfmd_inkmd/LICENSE +0 -0
  160. {pdfmd_cli-3.30.32 → pdfmd_cli-3.31.46}/pdfmd_inkmd/VENDORED.md +0 -0
  161. {pdfmd_cli-3.30.32 → pdfmd_cli-3.31.46}/pdfmd_inkmd/__init__.py +0 -0
  162. {pdfmd_cli-3.30.32 → pdfmd_cli-3.31.46}/pdfmd_inkmd/_kerning_data.py +0 -0
  163. {pdfmd_cli-3.30.32 → pdfmd_cli-3.31.46}/pdfmd_inkmd/assets/fonts/DejaVuSans-LICENSE.txt +0 -0
  164. {pdfmd_cli-3.30.32 → pdfmd_cli-3.31.46}/pdfmd_inkmd/assets/fonts/DejaVuSans.ttf +0 -0
  165. {pdfmd_cli-3.30.32 → pdfmd_cli-3.31.46}/pdfmd_inkmd/ast.py +0 -0
  166. {pdfmd_cli-3.30.32 → pdfmd_cli-3.31.46}/pdfmd_inkmd/cidfont.py +0 -0
  167. {pdfmd_cli-3.30.32 → pdfmd_cli-3.31.46}/pdfmd_inkmd/embedded.py +0 -0
  168. {pdfmd_cli-3.30.32 → pdfmd_cli-3.31.46}/pdfmd_inkmd/embedded_metrics.py +0 -0
  169. {pdfmd_cli-3.30.32 → pdfmd_cli-3.31.46}/pdfmd_inkmd/emoji.py +0 -0
  170. {pdfmd_cli-3.30.32 → pdfmd_cli-3.31.46}/pdfmd_inkmd/emoji_font.py +0 -0
  171. {pdfmd_cli-3.30.32 → pdfmd_cli-3.31.46}/pdfmd_inkmd/fonts.py +0 -0
  172. {pdfmd_cli-3.30.32 → pdfmd_cli-3.31.46}/pdfmd_inkmd/html_filter.py +0 -0
  173. {pdfmd_cli-3.30.32 → pdfmd_cli-3.31.46}/pdfmd_inkmd/image_loader.py +0 -0
  174. {pdfmd_cli-3.30.32 → pdfmd_cli-3.31.46}/pdfmd_inkmd/layout.py +0 -0
  175. {pdfmd_cli-3.30.32 → pdfmd_cli-3.31.46}/pdfmd_inkmd/parser.py +0 -0
  176. {pdfmd_cli-3.30.32 → pdfmd_cli-3.31.46}/pdfmd_inkmd/pdf.py +0 -0
  177. {pdfmd_cli-3.30.32 → pdfmd_cli-3.31.46}/pdfmd_inkmd/py.typed +0 -0
  178. {pdfmd_cli-3.30.32 → pdfmd_cli-3.31.46}/pdfmd_inkmd/render.py +0 -0
  179. {pdfmd_cli-3.30.32 → pdfmd_cli-3.31.46}/pdfmd_inkmd/truetype.py +0 -0
  180. {pdfmd_cli-3.30.32 → pdfmd_cli-3.31.46}/pdfmd_inkmd/url_filter.py +0 -0
  181. {pdfmd_cli-3.30.32 → pdfmd_cli-3.31.46}/pdfmd_labels/__init__.py +0 -0
  182. {pdfmd_cli-3.30.32 → pdfmd_cli-3.31.46}/pdfmd_labels/replay.py +0 -0
  183. {pdfmd_cli-3.30.32 → pdfmd_cli-3.31.46}/pdfmd_labels/scan.py +0 -0
  184. {pdfmd_cli-3.30.32 → pdfmd_cli-3.31.46}/pdfmd_lua/__init__.py +0 -0
  185. {pdfmd_cli-3.30.32 → pdfmd_cli-3.31.46}/pdfmd_lua/ascii.lua +0 -0
  186. {pdfmd_cli-3.30.32 → pdfmd_cli-3.31.46}/pdfmd_lua/chem.lua +0 -0
  187. {pdfmd_cli-3.30.32 → pdfmd_cli-3.31.46}/pdfmd_lua/chem_client.lua +0 -0
  188. {pdfmd_cli-3.30.32 → pdfmd_cli-3.31.46}/pdfmd_lua/code.lua +0 -0
  189. {pdfmd_cli-3.30.32 → pdfmd_cli-3.31.46}/pdfmd_lua/code_wrap.tex +0 -0
  190. {pdfmd_cli-3.30.32 → pdfmd_cli-3.31.46}/pdfmd_lua/cover.lua +0 -0
  191. {pdfmd_cli-3.30.32 → pdfmd_cli-3.31.46}/pdfmd_lua/ebook.lua +0 -0
  192. {pdfmd_cli-3.30.32 → pdfmd_cli-3.31.46}/pdfmd_lua/ebook_input.lua +0 -0
  193. {pdfmd_cli-3.30.32 → pdfmd_cli-3.31.46}/pdfmd_lua/flat.lua +0 -0
  194. {pdfmd_cli-3.30.32 → pdfmd_cli-3.31.46}/pdfmd_lua/graphicx.tex +0 -0
  195. {pdfmd_cli-3.30.32 → pdfmd_cli-3.31.46}/pdfmd_lua/graphviz.cjs +0 -0
  196. {pdfmd_cli-3.30.32 → pdfmd_cli-3.31.46}/pdfmd_lua/images.lua +0 -0
  197. {pdfmd_cli-3.30.32 → pdfmd_cli-3.31.46}/pdfmd_lua/katex.cjs +0 -0
  198. {pdfmd_cli-3.30.32 → pdfmd_cli-3.31.46}/pdfmd_lua/markup.lua +0 -0
  199. {pdfmd_cli-3.30.32 → pdfmd_cli-3.31.46}/pdfmd_lua/mathjax.cjs +0 -0
  200. {pdfmd_cli-3.30.32 → pdfmd_cli-3.31.46}/pdfmd_lua/pagebreak.lua +0 -0
  201. {pdfmd_cli-3.30.32 → pdfmd_cli-3.31.46}/pdfmd_lua/pdf_images.lua +0 -0
  202. {pdfmd_cli-3.30.32 → pdfmd_cli-3.31.46}/pdfmd_lua/raw.lua +0 -0
  203. {pdfmd_cli-3.30.32 → pdfmd_cli-3.31.46}/pdfmd_lua/select.lua +0 -0
  204. {pdfmd_cli-3.30.32 → pdfmd_cli-3.31.46}/pdfmd_lua/title_meta.lua +0 -0
  205. {pdfmd_cli-3.30.32 → pdfmd_cli-3.31.46}/pdfmd_mhchem/LICENSE +0 -0
  206. {pdfmd_cli-3.30.32 → pdfmd_cli-3.31.46}/pdfmd_mhchem/__init__.py +0 -0
  207. {pdfmd_cli-3.30.32 → pdfmd_cli-3.31.46}/pdfmd_mhchem/parser.py +0 -0
  208. {pdfmd_cli-3.30.32 → pdfmd_cli-3.31.46}/pdfmd_office/docx.py +0 -0
  209. {pdfmd_cli-3.30.32 → pdfmd_cli-3.31.46}/pdfmd_office/fonts.py +0 -0
  210. {pdfmd_cli-3.30.32 → pdfmd_cli-3.31.46}/pdfmd_office/fragments.py +0 -0
  211. {pdfmd_cli-3.30.32 → pdfmd_cli-3.31.46}/pdfmd_office/labels.py +0 -0
  212. {pdfmd_cli-3.30.32 → pdfmd_cli-3.31.46}/pdfmd_office/office.lua +0 -0
  213. {pdfmd_cli-3.30.32 → pdfmd_cli-3.31.46}/pdfmd_office/properties.py +0 -0
  214. {pdfmd_cli-3.30.32 → pdfmd_cli-3.31.46}/pdfmd_office/spec.py +0 -0
  215. {pdfmd_cli-3.30.32 → pdfmd_cli-3.31.46}/pdfmd_raw/__init__.py +0 -0
  216. {pdfmd_cli-3.30.32 → pdfmd_cli-3.31.46}/pdfmd_tables/__init__.py +0 -0
  217. {pdfmd_cli-3.30.32 → pdfmd_cli-3.31.46}/pdfmd_tables/convert.py +0 -0
  218. {pdfmd_cli-3.30.32 → pdfmd_cli-3.31.46}/pdfmd_tables/scan.py +0 -0
  219. {pdfmd_cli-3.30.32 → pdfmd_cli-3.31.46}/pdfmd_tables/verify.py +0 -0
  220. {pdfmd_cli-3.30.32 → pdfmd_cli-3.31.46}/pdfmd_templates/__init__.py +0 -0
  221. {pdfmd_cli-3.30.32 → pdfmd_cli-3.31.46}/pdfmd_templates/data/article/__name__.md +0 -0
  222. {pdfmd_cli-3.30.32 → pdfmd_cli-3.31.46}/pdfmd_templates/data/article/description.txt +0 -0
  223. {pdfmd_cli-3.30.32 → pdfmd_cli-3.31.46}/pdfmd_templates/data/article/next.txt +0 -0
  224. {pdfmd_cli-3.30.32 → pdfmd_cli-3.31.46}/pdfmd_templates/data/article/refs.bib +0 -0
  225. {pdfmd_cli-3.30.32 → pdfmd_cli-3.31.46}/pdfmd_templates/data/book/__name__.md +0 -0
  226. {pdfmd_cli-3.30.32 → pdfmd_cli-3.31.46}/pdfmd_templates/data/book/description.txt +0 -0
  227. {pdfmd_cli-3.30.32 → pdfmd_cli-3.31.46}/pdfmd_templates/data/book/next.txt +0 -0
  228. {pdfmd_cli-3.30.32 → pdfmd_cli-3.31.46}/pdfmd_templates/data/book/parts/10-introduction.md +0 -0
  229. {pdfmd_cli-3.30.32 → pdfmd_cli-3.31.46}/pdfmd_templates/data/book/parts/20-methods.md +0 -0
  230. {pdfmd_cli-3.30.32 → pdfmd_cli-3.31.46}/pdfmd_templates/data/book/parts/30-results.md +0 -0
  231. {pdfmd_cli-3.30.32 → pdfmd_cli-3.31.46}/pdfmd_templates/data/notes/__name__.md +0 -0
  232. {pdfmd_cli-3.30.32 → pdfmd_cli-3.31.46}/pdfmd_templates/data/notes/description.txt +0 -0
  233. {pdfmd_cli-3.30.32 → pdfmd_cli-3.31.46}/pdfmd_templates/data/notes/next.txt +0 -0
  234. {pdfmd_cli-3.30.32 → pdfmd_cli-3.31.46}/pdfmd_templates/data/report/__name__.md +0 -0
  235. {pdfmd_cli-3.30.32 → pdfmd_cli-3.31.46}/pdfmd_templates/data/report/data.csv +0 -0
  236. {pdfmd_cli-3.30.32 → pdfmd_cli-3.31.46}/pdfmd_templates/data/report/description.txt +0 -0
  237. {pdfmd_cli-3.30.32 → pdfmd_cli-3.31.46}/pdfmd_templates/data/report/next.txt +0 -0
  238. {pdfmd_cli-3.30.32 → pdfmd_cli-3.31.46}/pdfmd_templates/data/slides/__name__.md +0 -0
  239. {pdfmd_cli-3.30.32 → pdfmd_cli-3.31.46}/pdfmd_templates/data/slides/description.txt +0 -0
  240. {pdfmd_cli-3.30.32 → pdfmd_cli-3.31.46}/pdfmd_templates/data/slides/next.txt +0 -0
  241. {pdfmd_cli-3.30.32 → pdfmd_cli-3.31.46}/pdfmd_unicode/__init__.py +0 -0
  242. {pdfmd_cli-3.30.32 → pdfmd_cli-3.31.46}/pdfmd_unicode/_catalog.py +0 -0
  243. {pdfmd_cli-3.30.32 → pdfmd_cli-3.31.46}/pdfmd_unicode/_scripts.py +0 -0
  244. {pdfmd_cli-3.30.32 → pdfmd_cli-3.31.46}/pdfmd_unicode/_tessdata_catalog.py +0 -0
  245. {pdfmd_cli-3.30.32 → pdfmd_cli-3.31.46}/pdfmd_unicode/colorfont.py +0 -0
  246. {pdfmd_cli-3.30.32 → pdfmd_cli-3.31.46}/pdfmd_unicode/emoji.py +0 -0
  247. {pdfmd_cli-3.30.32 → pdfmd_cli-3.31.46}/pdfmd_unicode/fonts.py +0 -0
  248. {pdfmd_cli-3.30.32 → pdfmd_cli-3.31.46}/pdfmd_unicode/install.py +0 -0
  249. {pdfmd_cli-3.30.32 → pdfmd_cli-3.31.46}/pdfmd_unicode/plan.py +0 -0
  250. {pdfmd_cli-3.30.32 → pdfmd_cli-3.31.46}/pdfmd_unicode/scripts.py +0 -0
  251. {pdfmd_cli-3.30.32 → pdfmd_cli-3.31.46}/pdfmd_unicode/tessdata.py +0 -0
  252. {pdfmd_cli-3.30.32 → pdfmd_cli-3.31.46}/pdfmd_unicode/translit.py +0 -0
  253. {pdfmd_cli-3.30.32 → pdfmd_cli-3.31.46}/setup.cfg +0 -0
  254. {pdfmd_cli-3.30.32 → pdfmd_cli-3.31.46}/setup.py +0 -0
  255. {pdfmd_cli-3.30.32 → pdfmd_cli-3.31.46}/tests/test_api.py +0 -0
  256. {pdfmd_cli-3.30.32 → pdfmd_cli-3.31.46}/tests/test_apple_emoji.py +0 -0
  257. {pdfmd_cli-3.30.32 → pdfmd_cli-3.31.46}/tests/test_ascii.py +0 -0
  258. {pdfmd_cli-3.30.32 → pdfmd_cli-3.31.46}/tests/test_atomic.py +0 -0
  259. {pdfmd_cli-3.30.32 → pdfmd_cli-3.31.46}/tests/test_auto_states.py +0 -0
  260. {pdfmd_cli-3.30.32 → pdfmd_cli-3.31.46}/tests/test_backup_summary.py +0 -0
  261. {pdfmd_cli-3.30.32 → pdfmd_cli-3.31.46}/tests/test_bopen.py +0 -0
  262. {pdfmd_cli-3.30.32 → pdfmd_cli-3.31.46}/tests/test_budget.py +0 -0
  263. {pdfmd_cli-3.30.32 → pdfmd_cli-3.31.46}/tests/test_cache_location.py +0 -0
  264. {pdfmd_cli-3.30.32 → pdfmd_cli-3.31.46}/tests/test_check_source.py +0 -0
  265. {pdfmd_cli-3.30.32 → pdfmd_cli-3.31.46}/tests/test_command_words.py +0 -0
  266. {pdfmd_cli-3.30.32 → pdfmd_cli-3.31.46}/tests/test_config.py +0 -0
  267. {pdfmd_cli-3.30.32 → pdfmd_cli-3.31.46}/tests/test_cover.py +0 -0
  268. {pdfmd_cli-3.30.32 → pdfmd_cli-3.31.46}/tests/test_css.py +0 -0
  269. {pdfmd_cli-3.30.32 → pdfmd_cli-3.31.46}/tests/test_css_v2.py +0 -0
  270. {pdfmd_cli-3.30.32 → pdfmd_cli-3.31.46}/tests/test_csv_switch.py +0 -0
  271. {pdfmd_cli-3.30.32 → pdfmd_cli-3.31.46}/tests/test_docs_consistency.py +0 -0
  272. {pdfmd_cli-3.30.32 → pdfmd_cli-3.31.46}/tests/test_docx_tables.py +0 -0
  273. {pdfmd_cli-3.30.32 → pdfmd_cli-3.31.46}/tests/test_ebook_input.py +0 -0
  274. {pdfmd_cli-3.30.32 → pdfmd_cli-3.31.46}/tests/test_engine_chain.py +0 -0
  275. {pdfmd_cli-3.30.32 → pdfmd_cli-3.31.46}/tests/test_engine_filters.py +0 -0
  276. {pdfmd_cli-3.30.32 → pdfmd_cli-3.31.46}/tests/test_flat.py +0 -0
  277. {pdfmd_cli-3.30.32 → pdfmd_cli-3.31.46}/tests/test_folder_settings.py +0 -0
  278. {pdfmd_cli-3.30.32 → pdfmd_cli-3.31.46}/tests/test_header_includes.py +0 -0
  279. {pdfmd_cli-3.30.32 → pdfmd_cli-3.31.46}/tests/test_indent.py +0 -0
  280. {pdfmd_cli-3.30.32 → pdfmd_cli-3.31.46}/tests/test_install_core_tex.py +0 -0
  281. {pdfmd_cli-3.30.32 → pdfmd_cli-3.31.46}/tests/test_install_diagram.py +0 -0
  282. {pdfmd_cli-3.30.32 → pdfmd_cli-3.31.46}/tests/test_install_ocr.py +0 -0
  283. {pdfmd_cli-3.30.32 → pdfmd_cli-3.31.46}/tests/test_install_python.py +0 -0
  284. {pdfmd_cli-3.30.32 → pdfmd_cli-3.31.46}/tests/test_interrupt.py +0 -0
  285. {pdfmd_cli-3.30.32 → pdfmd_cli-3.31.46}/tests/test_jobs.py +0 -0
  286. {pdfmd_cli-3.30.32 → pdfmd_cli-3.31.46}/tests/test_katex_scale.py +0 -0
  287. {pdfmd_cli-3.30.32 → pdfmd_cli-3.31.46}/tests/test_keep_source.py +0 -0
  288. {pdfmd_cli-3.30.32 → pdfmd_cli-3.31.46}/tests/test_labels.py +0 -0
  289. {pdfmd_cli-3.30.32 → pdfmd_cli-3.31.46}/tests/test_latex_errors.py +0 -0
  290. {pdfmd_cli-3.30.32 → pdfmd_cli-3.31.46}/tests/test_lua_missing.py +0 -0
  291. {pdfmd_cli-3.30.32 → pdfmd_cli-3.31.46}/tests/test_lua_trust.py +0 -0
  292. {pdfmd_cli-3.30.32 → pdfmd_cli-3.31.46}/tests/test_markdown_features.py +0 -0
  293. {pdfmd_cli-3.30.32 → pdfmd_cli-3.31.46}/tests/test_markup_appendix.py +0 -0
  294. {pdfmd_cli-3.30.32 → pdfmd_cli-3.31.46}/tests/test_markup_callouts.py +0 -0
  295. {pdfmd_cli-3.30.32 → pdfmd_cli-3.31.46}/tests/test_markup_center.py +0 -0
  296. {pdfmd_cli-3.30.32 → pdfmd_cli-3.31.46}/tests/test_markup_highlight.py +0 -0
  297. {pdfmd_cli-3.30.32 → pdfmd_cli-3.31.46}/tests/test_markup_keep.py +0 -0
  298. {pdfmd_cli-3.30.32 → pdfmd_cli-3.31.46}/tests/test_markup_landscape.py +0 -0
  299. {pdfmd_cli-3.30.32 → pdfmd_cli-3.31.46}/tests/test_markup_space.py +0 -0
  300. {pdfmd_cli-3.30.32 → pdfmd_cli-3.31.46}/tests/test_markup_toc.py +0 -0
  301. {pdfmd_cli-3.30.32 → pdfmd_cli-3.31.46}/tests/test_mathfont.py +0 -0
  302. {pdfmd_cli-3.30.32 → pdfmd_cli-3.31.46}/tests/test_mathjax.py +0 -0
  303. {pdfmd_cli-3.30.32 → pdfmd_cli-3.31.46}/tests/test_mhchem.py +0 -0
  304. {pdfmd_cli-3.30.32 → pdfmd_cli-3.31.46}/tests/test_old_python_syntax.py +0 -0
  305. {pdfmd_cli-3.30.32 → pdfmd_cli-3.31.46}/tests/test_pagebreak.py +0 -0
  306. {pdfmd_cli-3.30.32 → pdfmd_cli-3.31.46}/tests/test_pan_tec.py +0 -0
  307. {pdfmd_cli-3.30.32 → pdfmd_cli-3.31.46}/tests/test_partial_provenance.py +0 -0
  308. {pdfmd_cli-3.30.32 → pdfmd_cli-3.31.46}/tests/test_picture_cache_protocol.py +0 -0
  309. {pdfmd_cli-3.30.32 → pdfmd_cli-3.31.46}/tests/test_presets.py +0 -0
  310. {pdfmd_cli-3.30.32 → pdfmd_cli-3.31.46}/tests/test_rawcode.py +0 -0
  311. {pdfmd_cli-3.30.32 → pdfmd_cli-3.31.46}/tests/test_reader.py +0 -0
  312. {pdfmd_cli-3.30.32 → pdfmd_cli-3.31.46}/tests/test_readonly_home.py +0 -0
  313. {pdfmd_cli-3.30.32 → pdfmd_cli-3.31.46}/tests/test_remote_csl.py +0 -0
  314. {pdfmd_cli-3.30.32 → pdfmd_cli-3.31.46}/tests/test_setup_folder.py +0 -0
  315. {pdfmd_cli-3.30.32 → pdfmd_cli-3.31.46}/tests/test_show_comments.py +0 -0
  316. {pdfmd_cli-3.30.32 → pdfmd_cli-3.31.46}/tests/test_soffice_parts.py +0 -0
  317. {pdfmd_cli-3.30.32 → pdfmd_cli-3.31.46}/tests/test_spreadsheets.py +0 -0
  318. {pdfmd_cli-3.30.32 → pdfmd_cli-3.31.46}/tests/test_symlinked_parts.py +0 -0
  319. {pdfmd_cli-3.30.32 → pdfmd_cli-3.31.46}/tests/test_table_keep.py +0 -0
  320. {pdfmd_cli-3.30.32 → pdfmd_cli-3.31.46}/tests/test_templates.py +0 -0
  321. {pdfmd_cli-3.30.32 → pdfmd_cli-3.31.46}/tests/test_tessdata.py +0 -0
  322. {pdfmd_cli-3.30.32 → pdfmd_cli-3.31.46}/tests/test_tex_bin.py +0 -0
  323. {pdfmd_cli-3.30.32 → pdfmd_cli-3.31.46}/tests/test_texpackages.py +0 -0
  324. {pdfmd_cli-3.30.32 → pdfmd_cli-3.31.46}/tests/test_text_input.py +0 -0
  325. {pdfmd_cli-3.30.32 → pdfmd_cli-3.31.46}/tests/test_typst_input.py +0 -0
  326. {pdfmd_cli-3.30.32 → pdfmd_cli-3.31.46}/tests/test_unicode.py +0 -0
@@ -17,28 +17,782 @@ 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.31.46c — 2026-11-02
40
23
 
41
- ---
24
+ 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.
25
+
26
+ Lint and Windows, which kept `Test and publish` from releasing.
27
+
28
+ - `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.
29
+ - 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`.
30
+
31
+ ## v3.31.46b — 2026-11-02
32
+
33
+ Pre-edit state: commit `63d353d`. No shipped code changes (tests only), so no version bump.
34
+
35
+ Five test modules no longer depend on the machine they run on.
36
+
37
+ - 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.
38
+ - `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.
39
+
40
+ ## v3.31.46 — 2026-11-02
41
+
42
+ 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).
43
+
44
+ A caption typed by hand next to a table can be the table's caption.
45
+
46
+ - `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.
47
+ - 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.
48
+ - 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).
49
+ - 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.
50
+ - Tests: `tests/test_table_captions.py`.
51
+
52
+ ## v3.31.45 — 2026-11-01
53
+
54
+ 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").
55
+
56
+ A part name that fits several top-level files with the same name builds all of them.
57
+
58
+ - `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.
59
+ - Tests: `tests/test_sections.py` (`SameNameParts`). README: the example in the parts section.
60
+
61
+ ## v3.31.44 — 2026-11-01
62
+
63
+ 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).
64
+
65
+ A part built alone sees the calculator's names of the whole document, below it as well as above it.
66
+
67
+ - 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.
68
+ - 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.
69
+ - 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.
70
+ - 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).
71
+
72
+ ## v3.31.43 — 2026-10-31
73
+
74
+ 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).
75
+
76
+ The atom colours of a structure are a setting.
77
+
78
+ - `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.)
79
+ - Tests: `tests/test_smiles.py`.
80
+
81
+ ## v3.31.42b — 2026-10-31
82
+
83
+ 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).
84
+
85
+ The Homebrew formula lives in this repository.
86
+
87
+ - `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.
88
+ - `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.
89
+ - `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.
90
+ - Tests: `tests/test_formula.py`.
91
+
92
+ ## v3.31.42 — 2026-10-31
93
+
94
+ 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.
95
+
96
+ A chemical structure written as text is drawn as a picture.
97
+
98
+ - `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.
99
+ - 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`).
100
+ - 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.
101
+ - 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.)
102
+ - 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.
103
+ - 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.
104
+ - `--no-auto smiles` leaves the blocks as code. `--doctor` says whether RDKit is installed.
105
+ - 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.
106
+ - `CLAUDE.md` names the new `[smiles]` tag next to `[kits]` and `[tables]`.
107
+ - `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).
108
+ - Tests: `tests/test_smiles.py` (settings, reading, drawing and the cache, no RDKit, builds through `pdfmd` itself).
109
+
110
+ ## v3.31.41 — 2026-10-31
111
+
112
+ 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/`).
113
+
114
+ A path in a message is written as short as it can be, by a setting.
115
+
116
+ - 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.
117
+ - `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`.
118
+ - 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.
119
+ - `--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.
120
+ - A symlink is still written where it sits, as before.
121
+ - 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.
122
+ - Setup: the two rows are in a folder's setup too (`FOLDER_OPTIONS`); the inventory has `--path-style`, `--path-up` and the keys.
123
+ - Tests: `tests/test_path_display.py`.
124
+
125
+ ## v3.31.40 — 2026-10-31
126
+
127
+ Pre-edit state: commit `25ec92a`. Still under 3.31. `[kits]`, entry 3du, point 20 (the author's wish: a course whose lab partners rotate).
128
+
129
+ A metadata value can add to the layers below instead of replacing them.
130
+
131
+ - `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.
132
+ - 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.
133
+ - 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.
134
+ - 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.
135
+ - 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.
136
+ - Not covered: the built-in renderer (no Pandoc) does not run Lua filters, so it takes the layers as before and does not add.
137
+ - Setup: the `options.metadata-append` row, and the key in `pdfmd_setup/inventory.py`.
138
+ - 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).
139
+
140
+ ## v3.31.39 — 2026-10-31
141
+
142
+ Pre-edit state: commit `6120d64`. Still under 3.31. `[tables]`, entry 3dz (reported by the author from a pK table).
143
+
144
+ CSV rows typed straight into a `::: {.csv}` block keep their inline markup.
145
+
146
+ - 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.
147
+ - 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.
148
+ - 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.
149
+
150
+ ## v3.31.38 — 2026-10-31
151
+
152
+ 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).
153
+
154
+ An attached or assembled source whose kit is fully embedded says `kit: none`.
155
+
156
+ - 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.
157
+ - 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.
158
+ - 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.
159
+ - Tests: `tests/test_kit_carry.py`.
160
+
161
+ ## v3.31.37b — 2026-10-31
162
+
163
+ Pre-edit state: commit `48864de`. Documentation only (`CLAUDE.md`): `PDFMD_VERSION` stays 3.31.37.
164
+
165
+ ### Docs
166
+ - `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).
167
+
168
+ ## v3.31.37 — 2026-10-31
169
+
170
+ Pre-edit state: commit `6a21c23`. Still under 3.31. `[kits]`, the spike of entry 3du (point 12).
171
+
172
+ A chosen kit works from its own folder: no install into the TeX tree.
173
+
174
+ - 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.
175
+ - 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.
176
+ - `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`.
177
+
178
+ ## v3.31.36 — 2026-10-31
179
+
180
+ Pre-edit state: commit `e9c09d3`. Still under 3.31. `[kits]`, C4 of entry 3du (the folder layers' part).
181
+
182
+ An attached source carries the folder settings the build used, and `--restore` writes them back.
183
+
184
+ - 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.
185
+ - **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.
186
+ - README: the kit's carrying paragraph says `--embed-metadata` holds the preamble only (3.31.35 said it held the parts).
187
+ - 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.
188
+ - Tests: `tests/test_attach_layers.py`, `tests/test_attach_format.py`.
189
+
190
+ ## v3.31.35 — 2026-10-31
191
+
192
+ Pre-edit state: commit `6ab64f0`. Still under 3.31. `[kits]`, C4 of entry 3du (the kit's part).
193
+
194
+ 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.
195
+
196
+ - **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`).
197
+ - **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".
198
+ - 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.
199
+ - Tests: `tests/test_kit_carry.py`.
200
+
201
+ ## v3.31.34 — 2026-10-31
202
+
203
+ Pre-edit state: commit `5420849`. Still under 3.31. `[kits]`.
204
+
205
+ A kit that leaves out `version:` takes it from its own package.
206
+
207
+ - 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.
208
+ - `pdfmd --kits` shows it like a written one.
209
+ - Tests: `tests/test_kits.py`.
210
+
211
+ ## v3.31.33 — 2026-10-31
212
+
213
+ Pre-edit state: commit `db18fa3`. Still under 3.31. `[kits]`, found by building a lean copy of a real pre-lab with the kit.
214
+
215
+ Pandoc, and the TeX engine with it, no longer run in the folder of a metadata file that is not the document's.
216
+
217
+ - 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.
218
+ - `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.
219
+ - 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`.
220
+
221
+ ## v3.31.32 — 2026-10-31
222
+
223
+ Pre-edit state: commit `1ee4fb9`. Still under 3.31. `[kits]`, C3 of entry 3du.
224
+
225
+ 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).
226
+
227
+ - **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.
228
+ - **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.
229
+ - 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.
230
+ - `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).
231
+
232
+ ## v3.31.31 — 2026-10-31
233
+
234
+ Pre-edit state: commit `abe26bb`. Still under 3.31. `[kits]`, C2 of entry 3du.
235
+
236
+ 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.)
237
+
238
+ - **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".
239
+ - **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.
240
+ - **`--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").
241
+ - **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.
242
+ - 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).
243
+
244
+ ## v3.31.30 — 2026-10-31
245
+
246
+ Pre-edit state: commit `37bfee6`. Still under 3.31. `[kits]`, C1 of entry 3du.
247
+
248
+ Kits can be read and found (nothing chooses one for a document yet).
249
+
250
+ - 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".
251
+ - 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.
252
+ - 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.
253
+ - `--no-auto kit` takes the searching off (a registered `kits:` folder and a path stay); `--no-auto kpsewhich` only the TeX tree.
254
+ - `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`.
255
+
256
+ ## v3.31.29 — 2026-10-31
257
+
258
+ Pre-edit state: commit `c268035`. Still under 3.31.
259
+
260
+ A lone copy of a package's `.sty` beside the document no longer loses what the package tells pdfmd about itself (3dw).
261
+
262
+ - 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.
263
+ - `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.
264
+ - 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.
265
+ - `no_auto` now reaches `package_latex_keys`, `package_lua_names`, `find_lua_filters`, `package_draft_skips`, `document_header_file` and `office_defaults_sources`.
266
+ - 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.
267
+ - 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.
268
+
269
+ ## v3.31.28 — 2026-10-31
270
+
271
+ Pre-edit state: commit `fe93e0a`. Still under 3.31.
272
+
273
+ `--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).
274
+
275
+ - 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.
276
+ - `-o` with more than one format must be a folder (made if need be); otherwise each result is beside its source.
277
+ - Not yet: a document or a setting asking for several formats (`default-output` takes one); logged with the post-build step (3dv).
278
+ - Test: `tests/test_crop.py` (a folder of `html` and `png` from one command; `-o` as a file refused).
279
+
280
+ ## v3.31.27 — 2026-10-31
281
+
282
+ Pre-edit state: commit `1eb332d`. Still under 3.31.
283
+
284
+ `--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".
285
+
286
+ - 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).
287
+ - Test: `tests/test_crop.py` (beside the document; the option in front matter, the flag over it).
288
+
289
+ ## v3.31.26 — 2026-10-31
290
+
291
+ Pre-edit state: commit `0655fc9`. Still under 3.31.
292
+
293
+ `--to png,pdf`: several picture targets in one run, from one build and one crop. The author's wish (3dv).
294
+
295
+ - 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).
296
+ - A PDF source with `pdf` among the targets and no `--crop` writes nothing for pdf and says so (NOTE).
297
+ - 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).
298
+
299
+ ## v3.31.25 — 2026-10-31
300
+
301
+ Pre-edit state: commit `cd59372`. Still under 3.31.
302
+
303
+ `--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).
304
+
305
+ - 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).
306
+ - Test: `tests/test_crop.py` (`test_tiff_and_postscript_hold_all_pages_in_one_file_and_eps_one_each`).
307
+
308
+ ## v3.31.24 — 2026-10-31
309
+
310
+ Pre-edit state: commit `36cd49e`. Still under 3.31.
311
+
312
+ `--to jpg` (also `jpeg`; `-o x.jpg` and `-o x.jpeg` choose it by suffix). The author's wish (3dv).
313
+
314
+ - 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.
315
+ - `--image-dpi` and `--image-quality` without a picture target are an error.
316
+ - Test: `tests/test_crop.py` (`test_jpeg_is_white_and_takes_a_quality`).
317
+
318
+ ## v3.31.23 — 2026-10-31
319
+
320
+ Pre-edit state: commit `19d38ab`. Still under 3.31.
321
+
322
+ `--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.
323
+
324
+ - **`-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`.
325
+ - `-b` and `--recursive` are not handed on to the build of each file's PDF (the files are built one at a time).
326
+ - 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).
327
+
328
+ ## v3.31.22 — 2026-10-31
329
+
330
+ Pre-edit state: commit `a2a987e`. Still under 3.31.
331
+
332
+ 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).
333
+
334
+ - Test: `tests/test_crop.py` (`test_png_without_crop_is_the_whole_page_one_file_per_page`).
335
+
336
+ ## v3.31.21 — 2026-10-31
337
+
338
+ Pre-edit state: commit `27c28e8`. Still under 3.31.
339
+
340
+ `--crop` and the new targets `--to png` and `--to svg` work on a Markdown source as well as on a PDF.
341
+
342
+ - **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.
343
+ - **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`.
344
+ - **`--to png|svg` without `--crop`** draws the whole page: a crop is asked for, never implied.
345
+ - **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.
346
+ - **`--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.
347
+ - `png` and `svg` are in `FORMAT_EXTENSION` and `EXTENSION_FORMAT`; new module `pdfmd_crop/images.py`.
348
+ - 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).
349
+ - The decisions are suggestions the author asked for; they are in the maintainer's notes (3dv) and can be changed.
350
+
351
+ ## v3.31.20 — 2026-10-31
352
+
353
+ Pre-edit state: commit `db904bd`. Still under 3.31.
354
+
355
+ `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.
356
+
357
+ - 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.
358
+ - 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.
359
+ - 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`.
360
+ - New package `pdfmd_crop` (`crop_pdf`, `ink_boxes`); `--help crop` is its topic. Without Ghostscript or pypdf the command says what to install.
361
+ - 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).
362
+
363
+ ## v3.31.19 — 2026-10-30
364
+
365
+ Pre-edit state: commit `aeaad80`. Still under 3.31. Merged from a pull request of the Windows test session.
366
+
367
+ 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).
368
+
369
+ - 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.
370
+ - Test: `tests/test_plot_cache.py` (`test_a_lower_case_drive_letter_is_still_the_reports_folder`), Windows only, fails without the fix.
371
+
372
+ ## v3.31.18 — 2026-10-31
373
+
374
+ Pre-edit state: commit `a66a343`. Still under 3.31. Merged from a pull request of the Windows test session.
375
+
376
+ ### Added
377
+ - **`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.
378
+ - **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.
379
+ - `--doctor` ends with one note on Windows when tools it found are not on the user PATH.
380
+ - 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).
381
+
382
+ ## v3.31.17 — 2026-10-31
383
+
384
+ Pre-edit state: commit `61be750`. Still under 3.31. Merged from a pull request of the Windows test session.
385
+
386
+ ### Fixed
387
+ - **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).
388
+
389
+ ## v3.31.16b — 2026-10-31
390
+
391
+ Pre-edit state: commit `80d19d0`. Tests only (from a pull request of the Windows test session): `PDFMD_VERSION` stays 3.31.16.
392
+
393
+ ### Docs
394
+ - **`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).
395
+
396
+ ## v3.31.16 — 2026-10-31
397
+
398
+ Pre-edit state: commit `e9445e7`. Still under 3.31. Merged from a pull request of the Windows test session.
399
+
400
+ ### Added
401
+ - **`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.
402
+
403
+ ### Notes
404
+ - 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.
405
+
406
+ ## v3.31.15c — 2026-10-31
407
+
408
+ Pre-edit state: commit `939b66f`. Tests only (from a pull request of the Windows test session): `PDFMD_VERSION` stays 3.31.15.
409
+
410
+ ### Tests
411
+ - 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).
412
+
413
+ ## v3.31.15b — 2026-10-31
414
+
415
+ Pre-edit state: commit `9b5b5b5`. Tests only (from a pull request of the Windows test session): `PDFMD_VERSION` stays 3.31.15.
416
+
417
+ ### Tests
418
+ - 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).
419
+
420
+ ## v3.31.15 — 2026-10-31
421
+
422
+ Pre-edit state: commit `0695669`. Still under 3.31. Merged from a pull request of the Windows test session.
423
+
424
+ ### Fixed
425
+ - **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).
426
+
427
+ ## v3.31.14 — 2026-10-31
428
+
429
+ Pre-edit state: commit `0f8f11a`. Still under 3.31. Merged from a pull request of the Windows test session.
430
+
431
+ ### Fixed
432
+ - **`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).
433
+
434
+ ## v3.31.13b — 2026-10-30
435
+
436
+ Pre-edit state: commit `c4491c7`. Docs only: `PDFMD_VERSION` stays 3.31.13.
437
+
438
+ ### Docs
439
+ - `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.
440
+
441
+ ## v3.31.13 — 2026-10-30
442
+
443
+ Pre-edit state: commit `5204a2c`. Still under 3.31. Merged from a pull request of the Windows test session.
444
+
445
+ ### Fixed
446
+ - **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).
447
+
448
+ ## v3.31.12 — 2026-10-30
449
+
450
+ Pre-edit state: commit `83f8331`. Still under 3.31. Merged from a pull request of the Windows test session.
451
+
452
+ ### Fixed
453
+ - **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).
454
+
455
+ ## v3.31.11 — 2026-10-30
456
+
457
+ Pre-edit state: commit `a080381`. Still under 3.31. Merged from a pull request of the Windows test session.
458
+
459
+ ### Added
460
+ - **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).
461
+
462
+ ## v3.31.10 — 2026-10-30
463
+
464
+ Pre-edit state: commit `56c5e9b`. Still under 3.31. Merged from a pull request of the Windows test session.
465
+
466
+ ### Added
467
+ - **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.
468
+ - **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.
469
+ - `--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.
470
+
471
+ ### Changed
472
+ - `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.
473
+
474
+ ### Notes
475
+ - 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).
476
+
477
+ ## v3.31.9h — 2026-10-30
478
+
479
+ Pre-edit state: commit `7c57727`. Tests only (from a pull request of the Windows test session): `PDFMD_VERSION` stays 3.31.9.
480
+
481
+ ### Tests
482
+ - `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).
483
+
484
+ ## v3.31.9g — 2026-10-30
485
+
486
+ Pre-edit state: commit `d4bca2e`. Tests only (from a pull request of the Windows test session): `PDFMD_VERSION` stays 3.31.9.
487
+
488
+ ### Tests
489
+ - `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).
490
+
491
+ ## v3.31.9f — 2026-10-30
492
+
493
+ Pre-edit state: commit `ea1a7f7`. Tests only (from a pull request of the Windows test session): `PDFMD_VERSION` stays 3.31.9.
494
+
495
+ ### Tests
496
+ - 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).
497
+
498
+ ## v3.31.9e — 2026-10-30
499
+
500
+ Pre-edit state: commit `57cf5e8`. Tests only (from a pull request of the Windows test session): `PDFMD_VERSION` stays 3.31.9.
501
+
502
+ ### Tests
503
+ - `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).
504
+
505
+ ## v3.31.9d — 2026-10-30
506
+
507
+ Pre-edit state: commit `63ddd01`. Tests only (from a pull request of the Windows test session): `PDFMD_VERSION` stays 3.31.9.
508
+
509
+ ### Tests
510
+ - 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).
511
+
512
+ ## v3.31.9c — 2026-10-30
513
+
514
+ Pre-edit state: commit `d4f4cf1`. Tests only (from a pull request of the Windows test session): `PDFMD_VERSION` stays 3.31.9.
515
+
516
+ ### Tests
517
+ - `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).
518
+
519
+ ## v3.31.9b — 2026-10-30
520
+
521
+ Pre-edit state: commit `28f1168`. Tests only (from a pull request of the Windows test session): `PDFMD_VERSION` stays 3.31.9.
522
+
523
+ ### Tests
524
+ - 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).
525
+
526
+ ## v3.31.9 — 2026-10-30
527
+
528
+ Pre-edit state: commit `0b1f055`. Still under 3.31. Merged from a pull request of the Windows test session.
529
+
530
+ ### Fixed
531
+ - **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).
532
+
533
+ ## v3.31.8 — 2026-10-30
534
+
535
+ Pre-edit state: commit `57919d1`. Still under 3.31. Merged from a pull request of the Windows test session.
536
+
537
+ ### Fixed
538
+ - **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).
539
+
540
+ ## v3.31.7 — 2026-10-30
541
+
542
+ Pre-edit state: commit `9813fde`. Still under 3.31. Merged from a pull request of the Windows test session.
543
+
544
+ ### Fixed
545
+ - **`--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).
546
+
547
+ ## v3.31.6 — 2026-10-29
548
+
549
+ 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).
550
+
551
+ ### Changed
552
+ - **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`,
553
+ `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
554
+ 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
555
+ (the console shows them anyway); the chatter ladder will move them to one `emit(level, id, text)` and `vprint` becomes its verbose level.
556
+
557
+ ## v3.31.5 — 2026-10-29
558
+
559
+ Pre-edit state: commit `3cb13f4`. Still under 3.31, in front of the rest of the tables work.
560
+
561
+ ### Added
562
+ - **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
563
+ command run (the `CMD` lines, whatever `--verbose` said), everything pdfmd printed, what each engine answered (exit code, stderr, stdout), the steps and their
564
+ times, and what the build answered; the home folder is written as `~`. It sits where `cache: location` says (`~/.cache/pdfmd/builds/NAME-HASH/`, or
565
+ `.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`
566
+ (`--verbose` also names the record of a good one); a build that stopped with an error is recorded too.
567
+ - **`pdfmd --last-build FILE [--failed]`** prints the newest record, or the newest failed one; none: a message and exit status 1.
568
+ - **`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,
569
+ 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`,
570
+ the config and `--setup`.
571
+ - The package `pdfmd_buildinfo.record` (pure): scrubbing, clipping, naming, atomic writing and pruning.
572
+
573
+ ### Notes
574
+ - 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
575
+ 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
576
+ (read-only or full disk) never fails a build.
577
+ - `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
578
+ tests of the record ask for it.
579
+
580
+ ## v3.31.4 — 2026-10-29
581
+
582
+ 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).
583
+
584
+ ### Added
585
+ - **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
586
+ (`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;
587
+ `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
588
+ `{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.
589
+ - **`build-time: on|off`** (default on) and **`time-format`**, in `pdfmd-options`, the config, `--setup`, and as `--build-time` / `--no-build-time` and
590
+ `--time-format STYLE`. A wrong format stops the command line and warns for a document (it reads as `auto`).
591
+ - **`--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,
592
+ polish, attach-source, backup, history, the size budget; and "the rest" (finding files and settings, the pre-passes no step covers).
593
+ - **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.
594
+
595
+ ### Changed
596
+ - The tests that matched the whole closing line take the time into account.
597
+
598
+ ## v3.31.3b — 2026-10-27
599
+
600
+ Pre-edit state: commit `b08a7b4`. Documentation only (`CLAUDE.md`): `PDFMD_VERSION` stays 3.31.3.
601
+
602
+ ### Docs
603
+ - `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.
604
+
605
+ ## v3.31.3 — 2026-10-27
606
+
607
+ 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.
608
+
609
+ ### Added
610
+ - **`column-outlier-rows`** (default 4), **`column-outlier-length`** (30) and **`column-word-floor-limit`** (16, the longest word the floor protects) are
611
+ settings; they were constants in the filter. Every number of the filter (`column-outlier` too) can be given per table, `{column-outlier=3
612
+ 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
613
+ config and in `--setup`.
614
+ - **`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
615
+ tract \SIrange{10}{20}{\minute}" was sized as 17 characters and printed as 34, so its column wrapped while another took the room). `\SI`,
616
+ `\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.
617
+
618
+ ### Changed
619
+ - **`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
620
+ these set the columns'. The older names are still read everywhere (the config, `--setup`, `pdfmd-options`, `-M`).
621
+
622
+ ### Docs
623
+ - `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.
624
+
625
+ ## v3.31.2 — 2026-10-27
626
+
627
+ Pre-edit state: commit `041a8f0`. Still the tables line.
628
+
629
+ ### Added
630
+ - **`column-outlier`** (default 2; 0 switches it off; `pdfmd-options`, the config, `--setup`, or `{column-outlier=N}` after a table's caption): one cell
631
+ 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
632
+ 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
633
+ 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.)
634
+ - **Plain `pdfmd` never reads standard input** (decided: a script's inherited pipe looks the same as `echo x | pdfmd`). In a folder without Markdown
635
+ files the error now names `pdfmd -` when standard input is not a terminal.
636
+
637
+ ## v3.31.1 — 2026-10-27
638
+
639
+ Pre-edit state: commit `12cc24a`. Still the tables line.
640
+
641
+ ### Changed
642
+ - `--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.
643
+
644
+ ## v3.31.0b — 2026-10-27
645
+
646
+ Pre-edit state: commit `15f70ee`. Documentation only (`CLAUDE.md`): `PDFMD_VERSION` stays 3.31.0.
647
+
648
+ ### Docs
649
+ - `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.
650
+
651
+ ## v3.31.0 — 2026-10-27
652
+
653
+ 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.
654
+
655
+ ### Fixed
656
+ - **A column the author chose could break words in the middle.** A pipe table whose separator dashes are unequal (`|:-------|:-------------|`) is a
657
+ 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
658
+ 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
659
+ 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
660
+ words is untouched, byte for byte. The note `AUTO TABLEWIDTH` says when it happened.
661
+
662
+ ### Added
663
+ - **`table-word-floor: on|off`** (`pdfmd-options`, the config, `--setup` "Tables"): off leaves a chosen table exactly as written, for a table where
664
+ breaking a word is meant (long chemical names in a narrow column).
665
+ - **`table-widths: force`** next to `auto` and `keep`: the dashes are ignored and every table gets the widths that give the fewest lines.
666
+ - **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,
667
+ which keeps a caption's attributes on the table; older ones print them in the caption).
668
+ - A document's `pdfmd-options` now wins over the global `table-widths` of the config (the global value used to shadow it).
669
+
670
+ ### Changed
671
+ - The `AUTO TABLEWIDTH` note no longer says the tables "had no column widths" only: it also covers `table-widths: force`.
672
+
673
+ ## v3.30.42b — 2026-10-26
674
+
675
+ Pre-edit state: commit `4efe2c5`. Documentation only (the README and the notes in `pdfmd.py` and `--setup`): `PDFMD_VERSION` stays 3.30.42.
676
+
677
+ ### Fixed
678
+ - 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.
679
+
680
+ ## v3.30.42 — 2026-10-26
681
+
682
+ Pre-edit state: commit `0ae751a` (v3.30.41).
683
+
684
+ ### Fixed
685
+ - **`--paper a4,landscape` (and `papersize: a4,landscape`) turned no page but the built-in renderers'.** Pandoc's `papersize` is a
686
+ LaTeX class option: on the LaTeX engines it printed `[a4,landscapepaper]`, LaTeX said "Unused global option" and the page was
687
+ Letter, silently; Calibre and the browsers lost the orientation too (the Word-file reader did not split the list item); WeasyPrint
688
+ ignored the paper altogether (A4 whatever `papersize:` said); Typst stopped on `a4,landscape`, and on a plain `--paper letter`
689
+ (pdfmd's translation to `us-letter` came as a second `-V papersize=`, and Pandoc joined the two into `letterus-letter`). One
690
+ function, `paper_variables`, now settles the paper for each engine: LaTeX gets the size as a class option and the turn as
691
+ `geometry:landscape`; Typst gets its own name (`us-letter`, `iso-b5`) once and `#set page(flipped: true)` through `typst.lua`;
692
+ WeasyPrint gets `@page { size: ... }` through `weasyprint.lua`; Calibre and the browsers read the list item. The same page comes
693
+ from `geometry: landscape` and `classoption: landscape`, and from the `pagesize:` typo, which now also beats a `papersize:` of the
694
+ settings in the Calibre, browser and Word readers (as it did for LaTeX).
695
+ - Spellings that printed a Letter page on LaTeX: `a4paper` (became `a4paperpaper`), `A4`, `us-letter`, and `a3` (no class option;
696
+ `geometry:a3paper` now). `--paper landscape` alone keeps the size the settings name.
697
+ - `--no-auto papersize` leaves all of this to Pandoc. Measured on Pandoc 3.12 with LuaLaTeX, Typst, WeasyPrint, Calibre and
698
+ the built-in renderer, for `a4,landscape`, `letter,landscape`, `a4`, `letter`, `a4paper`, `a3`, `us-letter,portrait`, `landscape`
699
+ and front matter `papersize`, `pagesize`, `geometry`, `classoption`.
700
+
701
+ ## v3.30.41 — 2026-10-26
702
+
703
+ Pre-edit state: commit `c2732a2` (v3.30.40).
704
+
705
+ ### Added
706
+ - **`mdpdf` answers the npm `mdpdf` too.** `--style FILE` and `MDPDF_STYLES` (a style sheet; `-e html` is added unless an engine is
707
+ named), `--format` (paper), `--orientation landscape` (`-V geometry:landscape`: the LaTeX route; `papersize: a4,landscape` is read
708
+ by the built-in renderers only), `--border` and `--border-top|-left|-bottom|-right` (margins as `geometry:` variables; px count as
709
+ 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
710
+ one's Puppeteer markup and is dropped; a template `"left,middle,right"` stays the PyPI one's. Dropped with a note on standard
711
+ error, never silently: those headers and footers, `--h-height`, `--f-height`, `--no-emoji`, `--gh-style`, `--debug`.
712
+ - **`--doctor` names a command of pdfmd's that another program answers.** `pdfmd`, `mdpdf`, `md2pdf`, `inkmd` and the others are
713
+ shared by several tools, and the one installed last owns each name; a "Command names" section says which names on the PATH are not
714
+ pdfmd's and how to take them back (`pip install --force-reinstall --no-deps pdfmd-cli`). It reads the installed package's own list
715
+ of scripts, so it says nothing from a checkout, and nothing when every name is pdfmd's. Checked in a scratch virtualenv with a
716
+ different `md2pdf` ahead on the PATH.
717
+
718
+ ## v3.30.40 — 2026-10-26
719
+
720
+ Pre-edit state: commit `e606b1b` (v3.30.39).
721
+
722
+ ### Added
723
+ - **An `md2pdf` command.** Several tools install that name; pdfmd now answers the keys of all three: jmaupetit's `md2pdf`
724
+ (`-i -o -c -W`), `md-to-pdf` (`--stylesheet`, `--as-html`, `-w`, `--document-title`) and kxxoling's (`--theme file.css`). A style
725
+ sheet adds `-e html` unless an engine is named (only HTML engines read CSS). `-e` is an engine when its value is one and
726
+ jmaupetit's Markdown extension otherwise. What pdfmd has no counterpart for (extensions, `-C`, `--pdf-options`, `--config-file`,
727
+ `--launch-options`, a string given to `--css`, a built-in `--theme`) is named on standard error and left out.
728
+
729
+ ## v3.30.39 — 2026-10-26
730
+
731
+ Pre-edit state: commit `e069268` (v3.30.38).
732
+
733
+ ### Added
734
+ - **`mdpdf in.md out.pdf`: the output as a second positional.** The Go `mdpdf` and `md2static` name the PDF that way; the
735
+ `mdpdf` command now takes a last positional ending in `.pdf` that follows another positional as `-o`, whether or not options
736
+ 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
737
+ (`--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,
738
+ so nothing it accepted changes. `pdfmd` itself does not do this: for pdfmd a PDF argument is an input.
739
+ - **`pdfmd x.pdf -o -`: the Markdown on standard output.** M1ck4's `pdfmd` (archived 2026-05-08) pipes that way; for PDF inputs it
740
+ 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).
741
+
742
+ ## v3.30.38 — 2026-10-26
743
+
744
+ Pre-edit state: commit `d4ae514` (v3.30.37).
745
+
746
+ ### Fixed
747
+ - **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.
748
+
749
+ ### Added
750
+ - **`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`.
751
+
752
+ ## v3.30.37 — 2026-10-26
753
+
754
+ Pre-edit state: commit `276f947` (v3.30.36).
755
+
756
+ ### Added
757
+ - **`--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`.
758
+
759
+ ## v3.30.36 — 2026-10-26
760
+
761
+ Pre-edit state: commit `bdc81b1` (v3.30.35).
762
+
763
+ ### Added
764
+ - **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`.
765
+
766
+ ## v3.30.35 — 2026-10-26
767
+
768
+ Pre-edit state: commit `bbb6a47` (v3.30.34).
769
+
770
+ ### Added
771
+ - **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`.
772
+
773
+ ## v3.30.34 — 2026-10-26
774
+
775
+ Pre-edit state: commit `210497a` (v3.30.33).
776
+
777
+ ### Added
778
+ - **`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`.
779
+
780
+ ## v3.30.33 — 2026-10-26
781
+
782
+ Pre-edit state: commit `5fe0411` (v3.30.32b).
783
+
784
+ ### Fixed
785
+ - `--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~~`.
786
+
787
+ ## v3.30.32b — 2026-10-26
788
+
789
+ Pre-edit state: commit `3b97509` (v3.30.32). A docs-only commit: `PDFMD_VERSION` stays 3.30.32.
790
+
791
+ ### Added
792
+ - `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.
793
+
794
+ ### Fixed
795
+ - `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
796
 
43
797
  ## v3.30.32 — 2026-10-26
44
798
 
@@ -8996,6 +9750,27 @@ package being built.
8996
9750
  page shape trips the same "wrongly rebalanced" or "wrongly left alone"
8997
9751
  symptom in the other direction.
8998
9752
 
9753
+ **Why this starts at 2.0.0, not 1.0.0.** `pdfmd.py` began as a one-line
9754
+ `pandoc file.md -o file.pdf` wrapper and was revised many times since — across
9755
+ ChatGPT, ChatGPT Codex, Claude Chat, Cowork, and Claude Code sessions — with no
9756
+ version marker or changelog kept at any point. `backups/pdfmd.py.bak-*` has
9757
+ snapshots from a handful of points in that history, but not from the
9758
+ ChatGPT/Codex/Cowork era at all, so reconstructing an honest "v1.x" changelog
9759
+ from what's actually on disk isn't possible — only a partial,
9760
+ misleadingly-precise-looking one. Decided 2026-09-16 (the author): start real
9761
+ changelog tracking now, at 2.0.0, rather than fabricate the gap. If the full
9762
+ history ever matters, it would need pulling the actual chat transcripts from
9763
+ each of those tools, not guessing from diffs between inconsistent backup
9764
+ snapshots.
9765
+
9766
+ **Update, 2026-10-05:** an approximate outline of the earlier history has since
9767
+ been reconstructed, at the very end of this file (see "Before 2.0.0 —
9768
+ reconstructed history"), from the saved Codex transcripts and the surviving
9769
+ backup snapshots. It is explicitly NOT reliable: its 1.x numbers are invented,
9770
+ snapshot-based entries cover windows of days rather than single changes, and
9771
+ anything done in unsaved sessions (other ChatGPT chats, claude.ai, Cowork) is
9772
+ missing. Real tracking still begins at 2.0.0.
9773
+
8999
9774
  ---
9000
9775
 
9001
9776
  # Before 2.0.0 — reconstructed history (NOT reliable)