pdfmd-cli 3.29.36__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.
- {pdfmd_cli-3.29.36 → pdfmd_cli-3.31.46}/CHANGELOG.md +1540 -29
- {pdfmd_cli-3.29.36/pdfmd_cli.egg-info → pdfmd_cli-3.31.46}/PKG-INFO +653 -72
- {pdfmd_cli-3.29.36 → pdfmd_cli-3.31.46}/README.md +652 -71
- {pdfmd_cli-3.29.36 → pdfmd_cli-3.31.46}/docs/agents.md +2 -2
- {pdfmd_cli-3.29.36 → pdfmd_cli-3.31.46}/docs/recipes.md +115 -7
- {pdfmd_cli-3.29.36 → pdfmd_cli-3.31.46}/pdfmd.py +4675 -563
- pdfmd_cli-3.31.46/pdfmd_ansi/__init__.py +11 -0
- pdfmd_cli-3.31.46/pdfmd_ansi/highlight.py +249 -0
- pdfmd_cli-3.31.46/pdfmd_ansi/mathbox.py +325 -0
- pdfmd_cli-3.31.46/pdfmd_ansi/native.py +242 -0
- pdfmd_cli-3.31.46/pdfmd_ansi/render.py +578 -0
- pdfmd_cli-3.31.46/pdfmd_ansi/text.py +199 -0
- pdfmd_cli-3.31.46/pdfmd_buildinfo/__init__.py +7 -0
- pdfmd_cli-3.31.46/pdfmd_buildinfo/record.py +109 -0
- pdfmd_cli-3.31.46/pdfmd_buildinfo/timing.py +185 -0
- {pdfmd_cli-3.29.36 → pdfmd_cli-3.31.46}/pdfmd_check/lint.py +101 -2
- {pdfmd_cli-3.29.36 → pdfmd_cli-3.31.46}/pdfmd_chem/batch.py +1 -1
- {pdfmd_cli-3.29.36 → pdfmd_cli-3.31.46}/pdfmd_chem/siunitx.py +8 -1
- {pdfmd_cli-3.29.36 → pdfmd_cli-3.31.46/pdfmd_cli.egg-info}/PKG-INFO +653 -72
- {pdfmd_cli-3.29.36 → pdfmd_cli-3.31.46}/pdfmd_cli.egg-info/SOURCES.txt +68 -0
- {pdfmd_cli-3.29.36 → pdfmd_cli-3.31.46}/pdfmd_cli.egg-info/entry_points.txt +1 -0
- {pdfmd_cli-3.29.36 → pdfmd_cli-3.31.46}/pdfmd_cli.egg-info/top_level.txt +5 -0
- pdfmd_cli-3.31.46/pdfmd_crop/__init__.py +164 -0
- pdfmd_cli-3.31.46/pdfmd_crop/images.py +96 -0
- {pdfmd_cli-3.29.36 → pdfmd_cli-3.31.46}/pdfmd_direct/defaults.py +1 -1
- pdfmd_cli-3.31.46/pdfmd_docs/__init__.py +11 -0
- {pdfmd_cli-3.29.36 → pdfmd_cli-3.31.46}/pdfmd_edit/__init__.py +0 -1
- {pdfmd_cli-3.29.36 → pdfmd_cli-3.31.46}/pdfmd_flat/asciify.py +1 -1
- {pdfmd_cli-3.29.36 → pdfmd_cli-3.31.46}/pdfmd_flat/keep.py +9 -2
- {pdfmd_cli-3.29.36 → pdfmd_cli-3.31.46}/pdfmd_help/__init__.py +14 -9
- {pdfmd_cli-3.29.36 → pdfmd_cli-3.31.46}/pdfmd_history/hst.py +3 -3
- {pdfmd_cli-3.29.36 → pdfmd_cli-3.31.46}/pdfmd_history/versions.py +2 -2
- {pdfmd_cli-3.29.36 → pdfmd_cli-3.31.46}/pdfmd_images/svg.py +9 -7
- pdfmd_cli-3.31.46/pdfmd_kits/__init__.py +13 -0
- pdfmd_cli-3.31.46/pdfmd_kits/discovery.py +207 -0
- pdfmd_cli-3.31.46/pdfmd_kits/kitfile.py +282 -0
- pdfmd_cli-3.31.46/pdfmd_kits/preamble.py +144 -0
- pdfmd_cli-3.31.46/pdfmd_kits/selection.py +188 -0
- {pdfmd_cli-3.29.36 → pdfmd_cli-3.31.46}/pdfmd_lua/ascii.lua +3 -1
- pdfmd_cli-3.31.46/pdfmd_lua/calc.lua +1656 -0
- {pdfmd_cli-3.29.36 → pdfmd_cli-3.31.46}/pdfmd_lua/chem.lua +16 -1
- {pdfmd_cli-3.29.36 → pdfmd_cli-3.31.46}/pdfmd_lua/csv_table.lua +89 -29
- {pdfmd_cli-3.29.36 → pdfmd_cli-3.31.46}/pdfmd_lua/diagram.lua +4 -0
- pdfmd_cli-3.31.46/pdfmd_lua/indent.lua +187 -0
- pdfmd_cli-3.31.46/pdfmd_lua/metadata_append.lua +24 -0
- pdfmd_cli-3.31.46/pdfmd_lua/smiles.lua +212 -0
- pdfmd_cli-3.31.46/pdfmd_lua/table_caption.lua +201 -0
- pdfmd_cli-3.31.46/pdfmd_lua/table_width.lua +415 -0
- {pdfmd_cli-3.29.36 → pdfmd_cli-3.31.46}/pdfmd_lua/typst.lua +23 -1
- {pdfmd_cli-3.29.36 → pdfmd_cli-3.31.46}/pdfmd_lua/weasyprint.lua +11 -1
- {pdfmd_cli-3.29.36 → pdfmd_cli-3.31.46}/pdfmd_office/__init__.py +7 -1
- {pdfmd_cli-3.29.36 → pdfmd_cli-3.31.46}/pdfmd_office/docx.py +7 -1
- {pdfmd_cli-3.29.36 → pdfmd_cli-3.31.46}/pdfmd_office/docx_post.py +27 -0
- {pdfmd_cli-3.29.36 → pdfmd_cli-3.31.46}/pdfmd_office/fragments.py +1 -1
- {pdfmd_cli-3.29.36 → pdfmd_cli-3.31.46}/pdfmd_office/odt.py +37 -0
- {pdfmd_cli-3.29.36 → pdfmd_cli-3.31.46}/pdfmd_office/properties.py +2 -2
- {pdfmd_cli-3.29.36 → pdfmd_cli-3.31.46}/pdfmd_office/units.py +2 -4
- {pdfmd_cli-3.29.36 → pdfmd_cli-3.31.46}/pdfmd_setup/__init__.py +7 -5
- {pdfmd_cli-3.29.36 → pdfmd_cli-3.31.46}/pdfmd_setup/fancy.py +13 -6
- {pdfmd_cli-3.29.36 → pdfmd_cli-3.31.46}/pdfmd_setup/inventory.py +46 -15
- {pdfmd_cli-3.29.36 → pdfmd_cli-3.31.46}/pdfmd_setup/plain.py +18 -9
- {pdfmd_cli-3.29.36 → pdfmd_cli-3.31.46}/pdfmd_setup/registry.py +218 -17
- pdfmd_cli-3.31.46/pdfmd_setup/userpath.py +306 -0
- pdfmd_cli-3.31.46/pdfmd_smiles/__init__.py +105 -0
- pdfmd_cli-3.31.46/pdfmd_smiles/__main__.py +12 -0
- pdfmd_cli-3.31.46/pdfmd_smiles/batch.py +154 -0
- pdfmd_cli-3.31.46/pdfmd_tables/captions.py +26 -0
- {pdfmd_cli-3.29.36 → pdfmd_cli-3.31.46}/pdfmd_tables/convert.py +1 -2
- {pdfmd_cli-3.29.36 → pdfmd_cli-3.31.46}/pdfmd_tables/scan.py +5 -3
- {pdfmd_cli-3.29.36 → pdfmd_cli-3.31.46}/pdfmd_tables/sheets.py +7 -7
- {pdfmd_cli-3.29.36 → pdfmd_cli-3.31.46}/pdfmd_unicode/fonts.py +1 -1
- {pdfmd_cli-3.29.36 → pdfmd_cli-3.31.46}/pdfmd_unicode/tessdata.py +0 -1
- {pdfmd_cli-3.29.36 → pdfmd_cli-3.31.46}/pyproject.toml +4 -2
- pdfmd_cli-3.31.46/tests/test_00_environment.py +7 -0
- pdfmd_cli-3.31.46/tests/test_ansi.py +651 -0
- {pdfmd_cli-3.29.36 → pdfmd_cli-3.31.46}/tests/test_ascii.py +19 -3
- pdfmd_cli-3.31.46/tests/test_atomic.py +176 -0
- {pdfmd_cli-3.29.36 → pdfmd_cli-3.31.46}/tests/test_attach.py +10 -6
- {pdfmd_cli-3.29.36 → pdfmd_cli-3.31.46}/tests/test_attach_format.py +9 -8
- pdfmd_cli-3.31.46/tests/test_attach_layers.py +96 -0
- {pdfmd_cli-3.29.36 → pdfmd_cli-3.31.46}/tests/test_auto_states.py +1 -1
- {pdfmd_cli-3.29.36 → pdfmd_cli-3.31.46}/tests/test_autolog.py +19 -4
- pdfmd_cli-3.31.46/tests/test_backup_summary.py +81 -0
- {pdfmd_cli-3.29.36 → pdfmd_cli-3.31.46}/tests/test_browser_engine.py +1 -1
- pdfmd_cli-3.31.46/tests/test_build_log.py +209 -0
- {pdfmd_cli-3.29.36 → pdfmd_cli-3.31.46}/tests/test_build_record.py +34 -1
- pdfmd_cli-3.31.46/tests/test_build_time.py +165 -0
- pdfmd_cli-3.31.46/tests/test_calc.py +1406 -0
- {pdfmd_cli-3.29.36 → pdfmd_cli-3.31.46}/tests/test_calibre.py +8 -5
- {pdfmd_cli-3.29.36 → pdfmd_cli-3.31.46}/tests/test_calibre_markdown.py +1 -1
- {pdfmd_cli-3.29.36 → pdfmd_cli-3.31.46}/tests/test_calibre_targets.py +3 -3
- {pdfmd_cli-3.29.36 → pdfmd_cli-3.31.46}/tests/test_check.py +140 -1
- {pdfmd_cli-3.29.36 → pdfmd_cli-3.31.46}/tests/test_chem.py +40 -1
- {pdfmd_cli-3.29.36 → pdfmd_cli-3.31.46}/tests/test_code_blocks.py +3 -3
- pdfmd_cli-3.31.46/tests/test_command_words.py +34 -0
- {pdfmd_cli-3.29.36 → pdfmd_cli-3.31.46}/tests/test_compat.py +6 -2
- {pdfmd_cli-3.29.36 → pdfmd_cli-3.31.46}/tests/test_completion.py +8 -2
- pdfmd_cli-3.31.46/tests/test_crop.py +325 -0
- pdfmd_cli-3.31.46/tests/test_csv_switch.py +211 -0
- {pdfmd_cli-3.29.36 → pdfmd_cli-3.31.46}/tests/test_csv_table.py +140 -8
- pdfmd_cli-3.31.46/tests/test_diagram_shim.py +45 -0
- {pdfmd_cli-3.29.36 → pdfmd_cli-3.31.46}/tests/test_diagrams.py +4 -2
- {pdfmd_cli-3.29.36 → pdfmd_cli-3.31.46}/tests/test_direct.py +13 -9
- {pdfmd_cli-3.29.36 → pdfmd_cli-3.31.46}/tests/test_display_path.py +7 -1
- {pdfmd_cli-3.29.36 → pdfmd_cli-3.31.46}/tests/test_doc.py +17 -3
- pdfmd_cli-3.31.46/tests/test_docs_consistency.py +118 -0
- {pdfmd_cli-3.29.36 → pdfmd_cli-3.31.46}/tests/test_doctor.py +56 -2
- {pdfmd_cli-3.29.36 → pdfmd_cli-3.31.46}/tests/test_edit.py +3 -2
- {pdfmd_cli-3.29.36 → pdfmd_cli-3.31.46}/tests/test_edit_pdf.py +1 -1
- {pdfmd_cli-3.29.36 → pdfmd_cli-3.31.46}/tests/test_embedding.py +5 -5
- {pdfmd_cli-3.29.36 → pdfmd_cli-3.31.46}/tests/test_engine_chain.py +9 -0
- {pdfmd_cli-3.29.36 → pdfmd_cli-3.31.46}/tests/test_engine_filters.py +5 -5
- pdfmd_cli-3.31.46/tests/test_formula.py +63 -0
- {pdfmd_cli-3.29.36 → pdfmd_cli-3.31.46}/tests/test_header_includes.py +3 -3
- {pdfmd_cli-3.29.36 → pdfmd_cli-3.31.46}/tests/test_help.py +2 -1
- {pdfmd_cli-3.29.36 → pdfmd_cli-3.31.46}/tests/test_history.py +1 -1
- {pdfmd_cli-3.29.36 → pdfmd_cli-3.31.46}/tests/test_images.py +6 -4
- pdfmd_cli-3.31.46/tests/test_indent.py +261 -0
- pdfmd_cli-3.31.46/tests/test_indent_office.py +145 -0
- {pdfmd_cli-3.29.36 → pdfmd_cli-3.31.46}/tests/test_install_calibre.py +41 -1
- {pdfmd_cli-3.29.36 → pdfmd_cli-3.31.46}/tests/test_install_diagram.py +0 -2
- pdfmd_cli-3.31.46/tests/test_interrupt.py +35 -0
- {pdfmd_cli-3.29.36 → pdfmd_cli-3.31.46}/tests/test_jobs.py +1 -1
- {pdfmd_cli-3.29.36 → pdfmd_cli-3.31.46}/tests/test_keep_source.py +29 -0
- pdfmd_cli-3.31.46/tests/test_kit_carry.py +150 -0
- pdfmd_cli-3.31.46/tests/test_kit_preamble.py +245 -0
- pdfmd_cli-3.31.46/tests/test_kit_selection.py +324 -0
- pdfmd_cli-3.31.46/tests/test_kit_texinputs.py +79 -0
- pdfmd_cli-3.31.46/tests/test_kits.py +435 -0
- {pdfmd_cli-3.29.36 → pdfmd_cli-3.31.46}/tests/test_labels.py +5 -5
- {pdfmd_cli-3.29.36 → pdfmd_cli-3.31.46}/tests/test_lookup.py +1 -0
- pdfmd_cli-3.31.46/tests/test_lua_missing.py +99 -0
- {pdfmd_cli-3.29.36 → pdfmd_cli-3.31.46}/tests/test_markup_center.py +4 -1
- {pdfmd_cli-3.29.36 → pdfmd_cli-3.31.46}/tests/test_markup_highlight.py +4 -1
- {pdfmd_cli-3.29.36 → pdfmd_cli-3.31.46}/tests/test_markup_keep.py +4 -1
- {pdfmd_cli-3.29.36 → pdfmd_cli-3.31.46}/tests/test_markup_landscape.py +5 -2
- {pdfmd_cli-3.29.36 → pdfmd_cli-3.31.46}/tests/test_markup_toc.py +1 -1
- {pdfmd_cli-3.29.36 → pdfmd_cli-3.31.46}/tests/test_markup_word.py +3 -1
- {pdfmd_cli-3.29.36 → pdfmd_cli-3.31.46}/tests/test_mathjax.py +3 -4
- pdfmd_cli-3.31.46/tests/test_metadata_append.py +146 -0
- {pdfmd_cli-3.29.36 → pdfmd_cli-3.31.46}/tests/test_native.py +51 -3
- {pdfmd_cli-3.29.36 → pdfmd_cli-3.31.46}/tests/test_office.py +12 -13
- {pdfmd_cli-3.29.36 → pdfmd_cli-3.31.46}/tests/test_office_latex.py +28 -22
- {pdfmd_cli-3.29.36 → pdfmd_cli-3.31.46}/tests/test_package_helper.py +45 -1
- {pdfmd_cli-3.29.36 → pdfmd_cli-3.31.46}/tests/test_packaging.py +22 -0
- {pdfmd_cli-3.29.36 → pdfmd_cli-3.31.46}/tests/test_pagebreak.py +1 -1
- pdfmd_cli-3.31.46/tests/test_paper.py +127 -0
- pdfmd_cli-3.31.46/tests/test_partial_history.py +136 -0
- pdfmd_cli-3.31.46/tests/test_partial_provenance.py +110 -0
- pdfmd_cli-3.31.46/tests/test_path_display.py +135 -0
- {pdfmd_cli-3.29.36 → pdfmd_cli-3.31.46}/tests/test_pdf_input.py +9 -0
- {pdfmd_cli-3.29.36 → pdfmd_cli-3.31.46}/tests/test_picture_cache_protocol.py +4 -2
- {pdfmd_cli-3.29.36 → pdfmd_cli-3.31.46}/tests/test_plot_cache.py +14 -0
- {pdfmd_cli-3.29.36 → pdfmd_cli-3.31.46}/tests/test_polish.py +94 -0
- pdfmd_cli-3.31.46/tests/test_probes.py +204 -0
- {pdfmd_cli-3.29.36 → pdfmd_cli-3.31.46}/tests/test_raw.py +6 -4
- pdfmd_cli-3.31.46/tests/test_readonly_home.py +211 -0
- pdfmd_cli-3.31.46/tests/test_rewrite.py +100 -0
- {pdfmd_cli-3.29.36 → pdfmd_cli-3.31.46}/tests/test_sections.py +84 -2
- {pdfmd_cli-3.29.36 → pdfmd_cli-3.31.46}/tests/test_setup.py +2 -2
- {pdfmd_cli-3.29.36 → pdfmd_cli-3.31.46}/tests/test_setup_folder.py +2 -2
- {pdfmd_cli-3.29.36 → pdfmd_cli-3.31.46}/tests/test_show_comments.py +4 -1
- pdfmd_cli-3.31.46/tests/test_smiles.py +232 -0
- {pdfmd_cli-3.29.36 → pdfmd_cli-3.31.46}/tests/test_soffice_parts.py +2 -2
- pdfmd_cli-3.31.46/tests/test_soffice_timeout.py +118 -0
- {pdfmd_cli-3.29.36 → pdfmd_cli-3.31.46}/tests/test_spreadsheets.py +0 -1
- {pdfmd_cli-3.29.36 → pdfmd_cli-3.31.46}/tests/test_standalone_source.py +2 -4
- pdfmd_cli-3.31.46/tests/test_stdin.py +86 -0
- pdfmd_cli-3.31.46/tests/test_svg_profile.py +35 -0
- pdfmd_cli-3.31.46/tests/test_symlinked_parts.py +118 -0
- pdfmd_cli-3.31.46/tests/test_table_captions.py +146 -0
- pdfmd_cli-3.31.46/tests/test_table_keep.py +47 -0
- pdfmd_cli-3.31.46/tests/test_table_widths.py +314 -0
- {pdfmd_cli-3.29.36 → pdfmd_cli-3.31.46}/tests/test_tables.py +66 -3
- pdfmd_cli-3.31.46/tests/test_tex_bin.py +75 -0
- pdfmd_cli-3.31.46/tests/test_tex_cause.py +119 -0
- {pdfmd_cli-3.29.36 → pdfmd_cli-3.31.46}/tests/test_texpackages.py +1 -1
- pdfmd_cli-3.31.46/tests/test_tool_locations.py +134 -0
- {pdfmd_cli-3.29.36 → pdfmd_cli-3.31.46}/tests/test_translit.py +1 -0
- {pdfmd_cli-3.29.36 → pdfmd_cli-3.31.46}/tests/test_unicode.py +0 -1
- pdfmd_cli-3.31.46/tests/test_user_path.py +361 -0
- {pdfmd_cli-3.29.36 → pdfmd_cli-3.31.46}/tests/test_versions.py +1 -2
- pdfmd_cli-3.29.36/pdfmd_docs/__init__.py +0 -4
- pdfmd_cli-3.29.36/pdfmd_lua/table_width.lua +0 -231
- pdfmd_cli-3.29.36/tests/test_table_widths.py +0 -114
- {pdfmd_cli-3.29.36 → pdfmd_cli-3.31.46}/LICENSE +0 -0
- {pdfmd_cli-3.29.36 → pdfmd_cli-3.31.46}/MANIFEST.in +0 -0
- {pdfmd_cli-3.29.36 → pdfmd_cli-3.31.46}/pdfmd_check/__init__.py +0 -0
- {pdfmd_cli-3.29.36 → pdfmd_cli-3.31.46}/pdfmd_chem/__init__.py +0 -0
- {pdfmd_cli-3.29.36 → pdfmd_cli-3.31.46}/pdfmd_chem/__main__.py +0 -0
- {pdfmd_cli-3.29.36 → pdfmd_cli-3.31.46}/pdfmd_chem/render.py +0 -0
- {pdfmd_cli-3.29.36 → pdfmd_cli-3.31.46}/pdfmd_cli.egg-info/dependency_links.txt +0 -0
- {pdfmd_cli-3.29.36 → pdfmd_cli-3.31.46}/pdfmd_cli.egg-info/requires.txt +0 -0
- {pdfmd_cli-3.29.36 → pdfmd_cli-3.31.46}/pdfmd_css/__init__.py +0 -0
- {pdfmd_cli-3.29.36 → pdfmd_cli-3.31.46}/pdfmd_direct/__init__.py +0 -0
- {pdfmd_cli-3.29.36 → pdfmd_cli-3.31.46}/pdfmd_direct/html.py +0 -0
- {pdfmd_cli-3.29.36 → pdfmd_cli-3.31.46}/pdfmd_direct/typst.py +0 -0
- {pdfmd_cli-3.29.36 → pdfmd_cli-3.31.46}/pdfmd_flat/__init__.py +0 -0
- {pdfmd_cli-3.29.36 → pdfmd_cli-3.31.46}/pdfmd_history/__init__.py +0 -0
- {pdfmd_cli-3.29.36 → pdfmd_cli-3.31.46}/pdfmd_history/git.py +0 -0
- {pdfmd_cli-3.29.36 → pdfmd_cli-3.31.46}/pdfmd_images/__init__.py +0 -0
- {pdfmd_cli-3.29.36 → pdfmd_cli-3.31.46}/pdfmd_images/remote.py +0 -0
- {pdfmd_cli-3.29.36 → pdfmd_cli-3.31.46}/pdfmd_images/scan.py +0 -0
- {pdfmd_cli-3.29.36 → pdfmd_cli-3.31.46}/pdfmd_inkmd/LICENSE +0 -0
- {pdfmd_cli-3.29.36 → pdfmd_cli-3.31.46}/pdfmd_inkmd/VENDORED.md +0 -0
- {pdfmd_cli-3.29.36 → pdfmd_cli-3.31.46}/pdfmd_inkmd/__init__.py +0 -0
- {pdfmd_cli-3.29.36 → pdfmd_cli-3.31.46}/pdfmd_inkmd/_kerning_data.py +0 -0
- {pdfmd_cli-3.29.36 → pdfmd_cli-3.31.46}/pdfmd_inkmd/assets/fonts/DejaVuSans-LICENSE.txt +0 -0
- {pdfmd_cli-3.29.36 → pdfmd_cli-3.31.46}/pdfmd_inkmd/assets/fonts/DejaVuSans.ttf +0 -0
- {pdfmd_cli-3.29.36 → pdfmd_cli-3.31.46}/pdfmd_inkmd/ast.py +0 -0
- {pdfmd_cli-3.29.36 → pdfmd_cli-3.31.46}/pdfmd_inkmd/cidfont.py +0 -0
- {pdfmd_cli-3.29.36 → pdfmd_cli-3.31.46}/pdfmd_inkmd/embedded.py +0 -0
- {pdfmd_cli-3.29.36 → pdfmd_cli-3.31.46}/pdfmd_inkmd/embedded_metrics.py +0 -0
- {pdfmd_cli-3.29.36 → pdfmd_cli-3.31.46}/pdfmd_inkmd/emoji.py +0 -0
- {pdfmd_cli-3.29.36 → pdfmd_cli-3.31.46}/pdfmd_inkmd/emoji_font.py +0 -0
- {pdfmd_cli-3.29.36 → pdfmd_cli-3.31.46}/pdfmd_inkmd/fonts.py +0 -0
- {pdfmd_cli-3.29.36 → pdfmd_cli-3.31.46}/pdfmd_inkmd/html_filter.py +0 -0
- {pdfmd_cli-3.29.36 → pdfmd_cli-3.31.46}/pdfmd_inkmd/image_loader.py +0 -0
- {pdfmd_cli-3.29.36 → pdfmd_cli-3.31.46}/pdfmd_inkmd/layout.py +0 -0
- {pdfmd_cli-3.29.36 → pdfmd_cli-3.31.46}/pdfmd_inkmd/parser.py +0 -0
- {pdfmd_cli-3.29.36 → pdfmd_cli-3.31.46}/pdfmd_inkmd/pdf.py +0 -0
- {pdfmd_cli-3.29.36 → pdfmd_cli-3.31.46}/pdfmd_inkmd/py.typed +0 -0
- {pdfmd_cli-3.29.36 → pdfmd_cli-3.31.46}/pdfmd_inkmd/render.py +0 -0
- {pdfmd_cli-3.29.36 → pdfmd_cli-3.31.46}/pdfmd_inkmd/truetype.py +0 -0
- {pdfmd_cli-3.29.36 → pdfmd_cli-3.31.46}/pdfmd_inkmd/url_filter.py +0 -0
- {pdfmd_cli-3.29.36 → pdfmd_cli-3.31.46}/pdfmd_labels/__init__.py +0 -0
- {pdfmd_cli-3.29.36 → pdfmd_cli-3.31.46}/pdfmd_labels/replay.py +0 -0
- {pdfmd_cli-3.29.36 → pdfmd_cli-3.31.46}/pdfmd_labels/scan.py +0 -0
- {pdfmd_cli-3.29.36 → pdfmd_cli-3.31.46}/pdfmd_lua/__init__.py +0 -0
- {pdfmd_cli-3.29.36 → pdfmd_cli-3.31.46}/pdfmd_lua/chem_client.lua +0 -0
- {pdfmd_cli-3.29.36 → pdfmd_cli-3.31.46}/pdfmd_lua/code.lua +0 -0
- {pdfmd_cli-3.29.36 → pdfmd_cli-3.31.46}/pdfmd_lua/code_wrap.tex +0 -0
- {pdfmd_cli-3.29.36 → pdfmd_cli-3.31.46}/pdfmd_lua/cover.lua +0 -0
- {pdfmd_cli-3.29.36 → pdfmd_cli-3.31.46}/pdfmd_lua/ebook.lua +0 -0
- {pdfmd_cli-3.29.36 → pdfmd_cli-3.31.46}/pdfmd_lua/ebook_input.lua +0 -0
- {pdfmd_cli-3.29.36 → pdfmd_cli-3.31.46}/pdfmd_lua/flat.lua +0 -0
- {pdfmd_cli-3.29.36 → pdfmd_cli-3.31.46}/pdfmd_lua/graphicx.tex +0 -0
- {pdfmd_cli-3.29.36 → pdfmd_cli-3.31.46}/pdfmd_lua/graphviz.cjs +0 -0
- {pdfmd_cli-3.29.36 → pdfmd_cli-3.31.46}/pdfmd_lua/images.lua +0 -0
- {pdfmd_cli-3.29.36 → pdfmd_cli-3.31.46}/pdfmd_lua/katex.cjs +0 -0
- {pdfmd_cli-3.29.36 → pdfmd_cli-3.31.46}/pdfmd_lua/markup.lua +0 -0
- {pdfmd_cli-3.29.36 → pdfmd_cli-3.31.46}/pdfmd_lua/mathjax.cjs +0 -0
- {pdfmd_cli-3.29.36 → pdfmd_cli-3.31.46}/pdfmd_lua/pagebreak.lua +0 -0
- {pdfmd_cli-3.29.36 → pdfmd_cli-3.31.46}/pdfmd_lua/pdf_images.lua +0 -0
- {pdfmd_cli-3.29.36 → pdfmd_cli-3.31.46}/pdfmd_lua/raw.lua +0 -0
- {pdfmd_cli-3.29.36 → pdfmd_cli-3.31.46}/pdfmd_lua/select.lua +0 -0
- {pdfmd_cli-3.29.36 → pdfmd_cli-3.31.46}/pdfmd_lua/title_meta.lua +0 -0
- {pdfmd_cli-3.29.36 → pdfmd_cli-3.31.46}/pdfmd_mhchem/LICENSE +0 -0
- {pdfmd_cli-3.29.36 → pdfmd_cli-3.31.46}/pdfmd_mhchem/__init__.py +0 -0
- {pdfmd_cli-3.29.36 → pdfmd_cli-3.31.46}/pdfmd_mhchem/parser.py +0 -0
- {pdfmd_cli-3.29.36 → pdfmd_cli-3.31.46}/pdfmd_office/fonts.py +0 -0
- {pdfmd_cli-3.29.36 → pdfmd_cli-3.31.46}/pdfmd_office/labels.py +0 -0
- {pdfmd_cli-3.29.36 → pdfmd_cli-3.31.46}/pdfmd_office/office.lua +0 -0
- {pdfmd_cli-3.29.36 → pdfmd_cli-3.31.46}/pdfmd_office/spec.py +0 -0
- {pdfmd_cli-3.29.36 → pdfmd_cli-3.31.46}/pdfmd_raw/__init__.py +0 -0
- {pdfmd_cli-3.29.36 → pdfmd_cli-3.31.46}/pdfmd_tables/__init__.py +0 -0
- {pdfmd_cli-3.29.36 → pdfmd_cli-3.31.46}/pdfmd_tables/verify.py +0 -0
- {pdfmd_cli-3.29.36 → pdfmd_cli-3.31.46}/pdfmd_templates/__init__.py +0 -0
- {pdfmd_cli-3.29.36 → pdfmd_cli-3.31.46}/pdfmd_templates/data/article/__name__.md +0 -0
- {pdfmd_cli-3.29.36 → pdfmd_cli-3.31.46}/pdfmd_templates/data/article/description.txt +0 -0
- {pdfmd_cli-3.29.36 → pdfmd_cli-3.31.46}/pdfmd_templates/data/article/next.txt +0 -0
- {pdfmd_cli-3.29.36 → pdfmd_cli-3.31.46}/pdfmd_templates/data/article/refs.bib +0 -0
- {pdfmd_cli-3.29.36 → pdfmd_cli-3.31.46}/pdfmd_templates/data/book/__name__.md +0 -0
- {pdfmd_cli-3.29.36 → pdfmd_cli-3.31.46}/pdfmd_templates/data/book/description.txt +0 -0
- {pdfmd_cli-3.29.36 → pdfmd_cli-3.31.46}/pdfmd_templates/data/book/next.txt +0 -0
- {pdfmd_cli-3.29.36 → pdfmd_cli-3.31.46}/pdfmd_templates/data/book/parts/10-introduction.md +0 -0
- {pdfmd_cli-3.29.36 → pdfmd_cli-3.31.46}/pdfmd_templates/data/book/parts/20-methods.md +0 -0
- {pdfmd_cli-3.29.36 → pdfmd_cli-3.31.46}/pdfmd_templates/data/book/parts/30-results.md +0 -0
- {pdfmd_cli-3.29.36 → pdfmd_cli-3.31.46}/pdfmd_templates/data/notes/__name__.md +0 -0
- {pdfmd_cli-3.29.36 → pdfmd_cli-3.31.46}/pdfmd_templates/data/notes/description.txt +0 -0
- {pdfmd_cli-3.29.36 → pdfmd_cli-3.31.46}/pdfmd_templates/data/notes/next.txt +0 -0
- {pdfmd_cli-3.29.36 → pdfmd_cli-3.31.46}/pdfmd_templates/data/report/__name__.md +0 -0
- {pdfmd_cli-3.29.36 → pdfmd_cli-3.31.46}/pdfmd_templates/data/report/data.csv +0 -0
- {pdfmd_cli-3.29.36 → pdfmd_cli-3.31.46}/pdfmd_templates/data/report/description.txt +0 -0
- {pdfmd_cli-3.29.36 → pdfmd_cli-3.31.46}/pdfmd_templates/data/report/next.txt +0 -0
- {pdfmd_cli-3.29.36 → pdfmd_cli-3.31.46}/pdfmd_templates/data/slides/__name__.md +0 -0
- {pdfmd_cli-3.29.36 → pdfmd_cli-3.31.46}/pdfmd_templates/data/slides/description.txt +0 -0
- {pdfmd_cli-3.29.36 → pdfmd_cli-3.31.46}/pdfmd_templates/data/slides/next.txt +0 -0
- {pdfmd_cli-3.29.36 → pdfmd_cli-3.31.46}/pdfmd_unicode/__init__.py +0 -0
- {pdfmd_cli-3.29.36 → pdfmd_cli-3.31.46}/pdfmd_unicode/_catalog.py +0 -0
- {pdfmd_cli-3.29.36 → pdfmd_cli-3.31.46}/pdfmd_unicode/_scripts.py +0 -0
- {pdfmd_cli-3.29.36 → pdfmd_cli-3.31.46}/pdfmd_unicode/_tessdata_catalog.py +0 -0
- {pdfmd_cli-3.29.36 → pdfmd_cli-3.31.46}/pdfmd_unicode/colorfont.py +0 -0
- {pdfmd_cli-3.29.36 → pdfmd_cli-3.31.46}/pdfmd_unicode/emoji.py +0 -0
- {pdfmd_cli-3.29.36 → pdfmd_cli-3.31.46}/pdfmd_unicode/install.py +0 -0
- {pdfmd_cli-3.29.36 → pdfmd_cli-3.31.46}/pdfmd_unicode/plan.py +0 -0
- {pdfmd_cli-3.29.36 → pdfmd_cli-3.31.46}/pdfmd_unicode/scripts.py +0 -0
- {pdfmd_cli-3.29.36 → pdfmd_cli-3.31.46}/pdfmd_unicode/translit.py +0 -0
- {pdfmd_cli-3.29.36 → pdfmd_cli-3.31.46}/setup.cfg +0 -0
- {pdfmd_cli-3.29.36 → pdfmd_cli-3.31.46}/setup.py +0 -0
- {pdfmd_cli-3.29.36 → pdfmd_cli-3.31.46}/tests/test_api.py +0 -0
- {pdfmd_cli-3.29.36 → pdfmd_cli-3.31.46}/tests/test_apple_emoji.py +0 -0
- {pdfmd_cli-3.29.36 → pdfmd_cli-3.31.46}/tests/test_bopen.py +0 -0
- {pdfmd_cli-3.29.36 → pdfmd_cli-3.31.46}/tests/test_budget.py +0 -0
- {pdfmd_cli-3.29.36 → pdfmd_cli-3.31.46}/tests/test_cache_location.py +0 -0
- {pdfmd_cli-3.29.36 → pdfmd_cli-3.31.46}/tests/test_check_source.py +0 -0
- {pdfmd_cli-3.29.36 → pdfmd_cli-3.31.46}/tests/test_config.py +0 -0
- {pdfmd_cli-3.29.36 → pdfmd_cli-3.31.46}/tests/test_cover.py +0 -0
- {pdfmd_cli-3.29.36 → pdfmd_cli-3.31.46}/tests/test_css.py +0 -0
- {pdfmd_cli-3.29.36 → pdfmd_cli-3.31.46}/tests/test_css_v2.py +0 -0
- {pdfmd_cli-3.29.36 → pdfmd_cli-3.31.46}/tests/test_docx_tables.py +0 -0
- {pdfmd_cli-3.29.36 → pdfmd_cli-3.31.46}/tests/test_ebook_input.py +0 -0
- {pdfmd_cli-3.29.36 → pdfmd_cli-3.31.46}/tests/test_flat.py +0 -0
- {pdfmd_cli-3.29.36 → pdfmd_cli-3.31.46}/tests/test_folder_settings.py +0 -0
- {pdfmd_cli-3.29.36 → pdfmd_cli-3.31.46}/tests/test_install_core_tex.py +0 -0
- {pdfmd_cli-3.29.36 → pdfmd_cli-3.31.46}/tests/test_install_ocr.py +0 -0
- {pdfmd_cli-3.29.36 → pdfmd_cli-3.31.46}/tests/test_install_python.py +0 -0
- {pdfmd_cli-3.29.36 → pdfmd_cli-3.31.46}/tests/test_katex_scale.py +0 -0
- {pdfmd_cli-3.29.36 → pdfmd_cli-3.31.46}/tests/test_latex_errors.py +0 -0
- {pdfmd_cli-3.29.36 → pdfmd_cli-3.31.46}/tests/test_lua_trust.py +0 -0
- {pdfmd_cli-3.29.36 → pdfmd_cli-3.31.46}/tests/test_markdown_features.py +0 -0
- {pdfmd_cli-3.29.36 → pdfmd_cli-3.31.46}/tests/test_markup_appendix.py +0 -0
- {pdfmd_cli-3.29.36 → pdfmd_cli-3.31.46}/tests/test_markup_callouts.py +0 -0
- {pdfmd_cli-3.29.36 → pdfmd_cli-3.31.46}/tests/test_markup_space.py +0 -0
- {pdfmd_cli-3.29.36 → pdfmd_cli-3.31.46}/tests/test_mathfont.py +0 -0
- {pdfmd_cli-3.29.36 → pdfmd_cli-3.31.46}/tests/test_mhchem.py +0 -0
- {pdfmd_cli-3.29.36 → pdfmd_cli-3.31.46}/tests/test_old_python_syntax.py +0 -0
- {pdfmd_cli-3.29.36 → pdfmd_cli-3.31.46}/tests/test_pan_tec.py +0 -0
- {pdfmd_cli-3.29.36 → pdfmd_cli-3.31.46}/tests/test_presets.py +0 -0
- {pdfmd_cli-3.29.36 → pdfmd_cli-3.31.46}/tests/test_rawcode.py +0 -0
- {pdfmd_cli-3.29.36 → pdfmd_cli-3.31.46}/tests/test_reader.py +0 -0
- {pdfmd_cli-3.29.36 → pdfmd_cli-3.31.46}/tests/test_remote_csl.py +0 -0
- {pdfmd_cli-3.29.36 → pdfmd_cli-3.31.46}/tests/test_templates.py +0 -0
- {pdfmd_cli-3.29.36 → pdfmd_cli-3.31.46}/tests/test_tessdata.py +0 -0
- {pdfmd_cli-3.29.36 → pdfmd_cli-3.31.46}/tests/test_text_input.py +0 -0
- {pdfmd_cli-3.29.36 → pdfmd_cli-3.31.46}/tests/test_typst_input.py +0 -0
|
@@ -2,42 +2,1532 @@
|
|
|
2
2
|
|
|
3
3
|
Newest first. Versions follow semver loosely: a **major** bump changes
|
|
4
4
|
behavior in a way that could surprise an existing user (a default flips, a
|
|
5
|
-
flag's meaning changes)
|
|
6
|
-
|
|
5
|
+
flag's meaning changes). One **minor** bump (`3.N.0`) opens a feature line,
|
|
6
|
+
however large it is; every change after it in that line is a **patch**
|
|
7
|
+
(`3.N.x`), whatever it adds, so the version count stays small.
|
|
7
8
|
|
|
8
|
-
The version string lives in one place, `pdfmd.py
|
|
9
|
-
docstring
|
|
9
|
+
The version string lives in one place, the `PDFMD_VERSION` line of `pdfmd.py`
|
|
10
|
+
(a little below its module docstring; `pyproject.toml` and the publish workflow
|
|
11
|
+
read it from there):
|
|
10
12
|
|
|
11
13
|
```python
|
|
12
14
|
PDFMD_VERSION = "2.0.0"
|
|
13
15
|
```
|
|
14
16
|
|
|
15
17
|
`pdfmd --version` prints it. Bump it there and add an entry here in the
|
|
16
|
-
same
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
18
|
+
same commit as the change.
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
## v3.31.46c — 2026-11-02
|
|
23
|
+
|
|
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.
|
|
796
|
+
|
|
797
|
+
## v3.30.32 — 2026-10-26
|
|
798
|
+
|
|
799
|
+
Pre-edit state: commit `0f6d680` (v3.30.31). Issue 15.
|
|
800
|
+
|
|
801
|
+
### Fixed
|
|
802
|
+
|
|
803
|
+
- A TeX Live or MiKTeX that is installed but not on `PATH` was reported as "no TeX engine installed": on Windows `--doctor` listed only Typst, the default run fell back to Typst without a word (a document with `\ce` or `\SI` built without LaTeX) and `-e tex` stopped with an error. When no TeX engine is on `PATH`, pdfmd now looks in the usual places (TeX Live: `C:\texlive\<year>\bin\*`, `%ProgramFiles%\texlive`, `~/texlive`, `/usr/local/texlive/<year>/bin/*`, `/Library/TeX/texbin`; MiKTeX: `%LOCALAPPDATA%\Programs\MiKTeX`, `%ProgramFiles%\MiKTeX*`), newest year first, and puts the folder it found behind `PATH` for its own runs. `PDFMD_TEX_BIN` names a folder for an install in an unusual place. `--doctor` prints `NOTE TeX found at ..., which is not on PATH`. Nothing changes when a TeX engine is on `PATH`.
|
|
804
|
+
- The message of `-e tex` with nothing found now says the engines are not installed "on PATH".
|
|
805
|
+
- `-e lualatex.exe` (and `-e typst.exe,lualatex.EXE`): a trailing `.exe` is dropped from an engine word, so the name Windows shows for the engine works. A word with a path in it is left as written.
|
|
806
|
+
|
|
807
|
+
### Tests
|
|
808
|
+
|
|
809
|
+
- `tests/test_tex_bin.py`. `tests.test_calc.PartBuiltAlone` shows Pandoc's own error when its first run fails (it hid it behind `check=True`).
|
|
810
|
+
- Windows with MiKTeX (the non-gating `windows-latex` job, found by a short debug run): the job puts poppler's folder in front of Git Bash's, whose `pdftotext` is xpdf's (no `-bbox`, Latin-1 output); the poppler calls in the tests decode UTF-8 with `errors="replace"`; `test_calc` writes Pandoc's discarded output to a file (Pandoc cannot open `nul`); `test_check` compares paths with `/`; `test_embedding` passes `-e lualatex`, not `lualatex.EXE` (which pdfmd now also accepts). Test code and CI only.
|
|
811
|
+
|
|
812
|
+
## v3.30.31 — 2026-10-26
|
|
813
|
+
|
|
814
|
+
Pre-edit state: commit `16fad47` (v3.30.30 + the CLAUDE.md rule).
|
|
815
|
+
|
|
816
|
+
### Added
|
|
817
|
+
|
|
818
|
+
- A failed TeX run names its cause when the log has a known signature: a second line under the `WARN ... failed` line, `cause: the LaTeX package soul.sty is missing; install it (tlmgr install soul; Debian/Ubuntu: apt install texlive-plain-generic)`. Known: a missing `.sty` (the package that has it for lmodern, soul, selnolig, lua-ul, luacolor; `tlmgr install NAME` for any other), a missing luaotfload, a corrupt luaotfload font database, an unwritable TeX cache. The signatures come from real logs of an outside reviewer's six cases (2026-10-14). Once per document, not once per engine tried.
|
|
819
|
+
- Not done: the unwritable cache and the corrupt database reach pdfmd as Pandoc's bare `Fatal error occurred, no output PDF file produced!`, the cause is only in the engine's own console output, so those two are named only when the log text is at hand (not through Pandoc). Naming them needs a probe run (a one-page `lualatex` smoke document after such a failure, as `--doctor --deep` already has); queued.
|
|
820
|
+
|
|
821
|
+
### Tests
|
|
822
|
+
|
|
823
|
+
- `tests/test_tex_cause.py`.
|
|
824
|
+
- `.gitignore` lists `pdfmd-reviews/` (an outside reviewer's raw logs, kept in the working folder for the classifier; not part of the package: `MANIFEST.in` names the files it ships).
|
|
825
|
+
- Windows `native` CI (first run of the combined tree): `test_doc` reads `--help` output with `errors="replace"`, `test_install_calibre.Detach` and `test_sections.PartRanges` compare paths the way the platform spells them (`Path(...)`, `as_posix()`). Test code only.
|
|
826
|
+
|
|
827
|
+
## v3.30.30 — 2026-10-26
|
|
828
|
+
|
|
829
|
+
Pre-edit state: commit `69dc4a6` (v3.30.29). Issue 13.
|
|
830
|
+
|
|
831
|
+
### Fixed
|
|
832
|
+
|
|
833
|
+
- The PDF made by `-e calibre` lost its bookmarks with Calibre 9.16 (`tests.test_calibre.Builds.test_a_document` failed on every machine with it since 2026-10-14). The bookmarks were there in what `ebook-convert` wrote: pdfmd's own stamp of the finished PDF (`PdfmdVersions`, `Creator`) copied it with pypdf's `append()`, which drops the whole outline of a PDF whose bookmarks are typed `/Type /Outlines`, as Calibre 9.16 writes them. The stamp clones the document instead.
|
|
834
|
+
- Every `subprocess` call of pdfmd that reads text names its encoding (`utf-8`, undecodable bytes replaced): `text=True` alone decodes with the ANSI code page on Windows, and Pandoc, TeX, `fc-list` and `kpsewhich` write UTF-8, so a byte the page could not decode made the reader thread die and `.stdout` come back as `None` (issue 13: every build stopped in `installed_font_families()` with TeX Live's `fc-list` on the PATH; that call itself was repaired by `ec8cbfe`, unreleased until now). 46 calls in `pdfmd.py` and five helper modules.
|
|
835
|
+
|
|
836
|
+
### Tests
|
|
837
|
+
|
|
838
|
+
- `tests.test_partial_provenance.OutlineSurvives`, `tests.test_probes.SubprocessDecoding` (no `run`/`Popen`/`check_output` call with `text=` and no `encoding=`).
|
|
839
|
+
- `tests.test_chem.WhatItCannotConvert` (v3.30.25) runs pdfmd on a Typst route that needs Pandoc, so it is skipped without Pandoc, like its neighbours; the first CI run of the combined tree failed the `native` jobs on it.
|
|
840
|
+
|
|
841
|
+
## v3.30.29 — 2026-10-26
|
|
842
|
+
|
|
843
|
+
Pre-edit state: commit `470de88` (v3.30.28). Merged from PR 9 (`cloud/calc-checks`), squashed.
|
|
844
|
+
|
|
845
|
+
### Added
|
|
846
|
+
|
|
847
|
+
- `--check` reports the calculator's undefined names (`calc-undefined`, an error): a span, a `::: calc` line or an `{ans}` that uses a name nothing defines, with its line and its part, in document order. A document without a calculator never starts Pandoc for it.
|
|
848
|
+
- `--check` notes a name defined more than once (`calc-redefined`, severity `note`, a third severity that is counted and never fails `--check`, not even with `--strict`). A normal build says nothing about it.
|
|
849
|
+
- `calc: {export: FILE}`, `--calc-export FILE`, `PDFMD_CALC_EXPORT` (for `-j` workers): after a build, `name,value,digits` with one row per name, sorted, `%.17g`, written atomically, readable back as a `constants` file. Not written without names, with `calc` off, after a failed build, or for a part built alone without the seed. `--attach-source` and `--embed-metadata files` leave it out unless it is also read. A `--setup` row (`options.calc.export`), the inventory entry, the help topic, README and recipes.
|
|
850
|
+
|
|
851
|
+
## v3.30.28 — 2026-10-26
|
|
852
|
+
|
|
853
|
+
Pre-edit state: commit `b065576` (v3.30.27). Merged from PR 11 (`cloud/partial-provenance`), squashed; it met 3.30.23's partial history in `pdfmd.py`, resolved by keeping both.
|
|
854
|
+
|
|
855
|
+
### Fixed
|
|
856
|
+
|
|
857
|
+
- A partial parts build (`report#intro`, `@60`) keeps the provenance of its PDF (Creator suffix, `PdfmdVersions`, `PdfmdBuildDate`, and a new `PdfmdPartial` naming the parts) and never stamps the `.md`. Word and OpenDocument files never carried these properties. With `stamp: {partial: true}` the "partial" entry of the history (3.30.23) is still written, after the PDF's provenance, by one function (`stamp_build`) that replaces `stamp_unless_partial`; the cache route's intermediate `.tex` stage writes neither.
|
|
858
|
+
- The snapshots of one build are one `BACKUP` line (`report.md + 6 parts -> .backups/, parts/.backups/ (...)`); the per-file lines follow only with `--verbose`.
|
|
859
|
+
|
|
860
|
+
### Tests
|
|
861
|
+
|
|
862
|
+
- `tests.test_partial_provenance`, `tests.test_backup_summary`.
|
|
863
|
+
|
|
864
|
+
## v3.30.27 — 2026-10-26
|
|
865
|
+
|
|
866
|
+
Pre-edit state: commit `b575748` (v3.30.26). Merged from PR 10 (`cloud/csv-notes`), squashed.
|
|
867
|
+
|
|
868
|
+
### Fixed
|
|
869
|
+
|
|
870
|
+
- The note under a cut `.csv` table states the fact only, only for the dimension that was cut (it said "2 of 2 columns" when no column was cut), without code spans. The advice moved to a once-per-table `WARN` on stderr.
|
|
871
|
+
- `--extract-inline-csv`, `--expand-tables` and `--check` know the bare `::: csv`.
|
|
872
|
+
- The stray `unittest.main()` in the middle of `tests/test_csv_table.py` is at the end.
|
|
873
|
+
|
|
874
|
+
### Added
|
|
875
|
+
|
|
876
|
+
- `note=short|off|full` (default `short`): a div's `note=`, `csv: {note}`, `--csv-note`, `PDFMD_CSV_NOTE`, a `--setup` row, the inventory entry, the help topic, README and recipes; in the Lua filter and in the built-in renderer. `full` is the only mode that keeps code spans (other writers get them as Pandoc writes them).
|
|
877
|
+
|
|
878
|
+
## v3.30.26 — 2026-10-26
|
|
879
|
+
|
|
880
|
+
Pre-edit state: commit `9b054bb` (v3.30.25). Merged from PR 8 (`cloud/portability`), squashed.
|
|
881
|
+
|
|
882
|
+
### Fixed (Windows)
|
|
883
|
+
|
|
884
|
+
- `report#section/sub` kept its slash on Windows (`Path()` turned it into a backslash, so the section was never found).
|
|
885
|
+
- A pager or editor typed with a Windows path (`C:\Program Files\...`) keeps its backslashes (`shlex` in POSIX mode ate them): `split_command()`.
|
|
886
|
+
- The calculator's seed run wrote Pandoc's output to the null device; Pandoc's binary writers cannot open `NUL` on Windows. It writes to a file in the scratch folder.
|
|
887
|
+
- A file with a kept source (`--keep-source`) whose line ends had become CRLF (git's `autocrlf`, an editor) "kept no pdfmd source", `--restore` could not give it back, and it was reported as edited. The trailer is read as LF text and the hash ignores the difference.
|
|
888
|
+
- The ASCII filter's helper (`--to ascii`) never started on Windows: its command and paths went through Pandoc's metadata with their backslashes. The paths have forward slashes, and the `WARN` says why the helper did not answer.
|
|
889
|
+
- TeX engines were started with a temporary folder under its Windows short name (`C:\Users\RUNNER~1\...`); the engine reads its command line as TeX, where `~` is active, and MiKTeX's LuaLaTeX opened no file. The label draft pass, the pictures drawn for Word files and the cache build failed there. `tex_argument()` gives the long name.
|
|
890
|
+
- A picture's folder was never taken out of a Word description on Windows.
|
|
891
|
+
- `--verbose` says how the label draft pass ended when it left no `.aux`; `provide_writable_places` creates the temporary `texmf-var`/`texmf-config` folders it points at.
|
|
892
|
+
- A busy disk image (macOS, `Calibre` install) is detached again before it is forced: four tries a second apart, then `-force`.
|
|
893
|
+
|
|
894
|
+
### Added
|
|
895
|
+
|
|
896
|
+
- The built-in parser (no Pandoc) draws an HTML `<table>`'s `colspan` and `rowspan` (`pdfmd_ansi/native.py`). Golden tests.
|
|
897
|
+
|
|
898
|
+
### Changed (CI, tests)
|
|
899
|
+
|
|
900
|
+
- The lint job runs `ruff check --select F` on the code and the tests (two unused locals, seven unused imports, a duplicated dictionary key; no behaviour changes). The Windows job (`windows-latex`) installs poppler, the fonts the tests name, and runs with Python's UTF-8 mode.
|
|
901
|
+
- The tests: the OS separator in what a run prints, Lua paths in forward slashes, `TEXINPUTS` with `os.pathsep`, a file to discard instead of `NUL`, line ends as Pandoc wrote them, a closed `.docx` before the folder is removed. Two `test_readonly_home.Builds` tests are skipped on Windows (MiKTeX's LuaLaTeX stops in luaotfload when LOCALAPPDATA is redirected).
|
|
902
|
+
|
|
903
|
+
## v3.30.25 — 2026-10-26
|
|
904
|
+
|
|
905
|
+
Pre-edit state: commit `abb652a` (v3.30.24). Issue 12.
|
|
906
|
+
|
|
907
|
+
### Fixed
|
|
908
|
+
|
|
909
|
+
- siunitx 2 style `\SI{1.8e-5}{}` and `\qty{1.8e-5}{}` (an empty unit) print the number alone, as `\num{1.8e-5}` does; `\si{}` and `\unit{}`
|
|
910
|
+
print nothing; the same for `\SIlist`, `\SIrange` and their `qty` names. The empty unit made the converter refuse the command, so in
|
|
911
|
+
a Typst, HTML or text output the command was left out of the text without a word, and a formula holding one (even beside a `\ce`) stayed raw
|
|
912
|
+
TeX, with a warning that only blamed the missing `pdftocairo`.
|
|
913
|
+
- A command or a formula the converter refuses is named in one `WARN` with the reason (`[pdfmd] WARN \SI{3}{\bogus} cannot be converted
|
|
914
|
+
(siunitx unit \bogus is not known); it is left out`), once per command, for the outputs the LaTeX filter does not run for.
|
|
915
|
+
|
|
916
|
+
### Tests
|
|
917
|
+
|
|
918
|
+
- `tests.test_chem.Siunitx.test_an_empty_unit_prints_the_number_alone`, `tests.test_chem.WhatItCannotConvert`.
|
|
919
|
+
|
|
920
|
+
## v3.30.24 — 2026-10-25
|
|
921
|
+
|
|
922
|
+
Pre-edit state: commit `de9c29a` (v3.30.23).
|
|
923
|
+
|
|
924
|
+
### Added
|
|
925
|
+
|
|
926
|
+
- `history:` is accepted in `pdfmd-options` (a document, a metadata file, the config) as another name for `stamp:`: the build history is that
|
|
927
|
+
one setting (`enabled`, `store`, `mode`, `scope`, `packages`, `partial`, ...), not a second one. `history: {store: file, partial: true}` is
|
|
928
|
+
`stamp: {store: file, partial: true}`; a field both set is taken from `stamp`. Older pdfmd versions do not know `history:`, so a shared
|
|
929
|
+
metadata file that must work with them keeps `stamp:`.
|
|
930
|
+
|
|
931
|
+
### Tests
|
|
932
|
+
|
|
933
|
+
- `tests.test_partial_history.HistoryAlias`.
|
|
934
|
+
|
|
935
|
+
## v3.30.23 — 2026-10-25
|
|
936
|
+
|
|
937
|
+
Pre-edit state: commit `4e38e85` (v3.30.22).
|
|
938
|
+
|
|
939
|
+
### Added
|
|
940
|
+
|
|
941
|
+
- **Draft builds in the history.** The `.hst` held full builds only: a build of some parts or sections (`pdfmd @60`, `doc#section`) writes no
|
|
942
|
+
BUILD NOTES line, and nothing took its place. With `pdfmd-options: {stamp: {partial: true}}` (or `--stamp-partial`; `--no-stamp-partial`
|
|
943
|
+
overrides a document that set it; a `--setup` row, `options.stamp.partial`) and the history in a file (`store: file` or `both`), such a build
|
|
944
|
+
leaves a new kind of entry, `partial`:
|
|
945
|
+
`... | partial | parts 50-calculations, 60-discussion; changed: 60-discussion; with nulabreport v1.28.20, pdfmd v3.30.23 | parts=... changed=... new=... shas=... out=...`.
|
|
946
|
+
`changed` compares each part's text with the last entry that saw it; a full build in parts mode now records every part's hash (`shas=`) for
|
|
947
|
+
that, and a part no entry has seen is `first seen` (`new=`). `--history` lists the entries as `partial`. Off by default (a history that
|
|
948
|
+
held full builds only gains lines); older pdfmd versions keep a `partial` line as a comment. A `partial` entry has no `sha`, so it is
|
|
949
|
+
never offered by `--history-restore`.
|
|
950
|
+
|
|
951
|
+
### Tests
|
|
952
|
+
|
|
953
|
+
- `tests.test_partial_history` (the entry round trip, the key, parts built and changed against a full build, first seen, off by default,
|
|
954
|
+
the flags, a section of a plain document).
|
|
955
|
+
|
|
956
|
+
## v3.30.22 — 2026-10-25
|
|
957
|
+
|
|
958
|
+
Pre-edit state: commit `85a9283` (v3.30.21).
|
|
959
|
+
|
|
960
|
+
### Added
|
|
961
|
+
|
|
962
|
+
- **A range of parts:** `pdfmd @45-60` (or `--section 45-60`, `doc#45-60`) builds every top-level part numbered from 45 to 60, ends included,
|
|
963
|
+
whether or not a part has that exact number; a reversed range is the same, and it joins other names (`@10,45-60`). A part literally named
|
|
964
|
+
`45-60` still wins, and a range that covers no part is the usual "matches no part" error. Only digits on both sides count, so
|
|
965
|
+
`10-introduction` is a name as before.
|
|
966
|
+
|
|
967
|
+
### Tests
|
|
968
|
+
|
|
969
|
+
- `tests.test_sections.PartRanges`.
|
|
970
|
+
|
|
971
|
+
## v3.30.21 — 2026-10-25
|
|
972
|
+
|
|
973
|
+
Pre-edit state: commit `b20d62b` (v3.30.20).
|
|
974
|
+
|
|
975
|
+
### Fixed
|
|
976
|
+
|
|
977
|
+
- **A build printed `{ans}` lines and bare names instead of numbers (a regression of 3.30.20).** The new `.calc-table` handler was chained
|
|
978
|
+
to the `::: calc` handler with `calc_div(div) or calc_table_div(div)`, and `or` keeps only the first of the two values the first handler
|
|
979
|
+
returns, so the walk went into the div it had just written and the second pass counted its places twice: every span and `{ans}` line after
|
|
980
|
+
the first calc div failed with "the passes disagree about this place" (LR5: 71 lines in the Calculations, the numbers of the Discussion as
|
|
981
|
+
code). The two results are passed on now.
|
|
982
|
+
|
|
983
|
+
### Added
|
|
984
|
+
|
|
985
|
+
- When three or more places of a document could not be evaluated, the calculator ends with one line,
|
|
986
|
+
`WARN calc: 21 places were not evaluated (each is above); the output shows their code or text where the numbers belong`, so a build
|
|
987
|
+
that failed on a whole section is not mistaken for one that printed a single odd span (`--strict` already fails on any WARN).
|
|
988
|
+
- A `.calc-table` caption can be written inside the div: a line `Table: Student's *t*, 95%` (or `: ...` after a blank line); `caption="..."` still works.
|
|
989
|
+
|
|
990
|
+
### Tests
|
|
991
|
+
|
|
992
|
+
- `tests.test_calc.Statistics`: a silent span glued to an `{ans}` line (the four-line example of the report), the summary line, the three
|
|
993
|
+
caption spellings.
|
|
994
|
+
|
|
995
|
+
## v3.30.20 — 2026-10-25
|
|
996
|
+
|
|
997
|
+
Pre-edit state: commit `4f34b63` (v3.30.19).
|
|
998
|
+
|
|
999
|
+
### Added
|
|
1000
|
+
|
|
1001
|
+
- **Statistics in the calculator:** `sum mean median`, `var sd stdev sem` (sample, n - 1), `rsd` (relative standard deviation in percent),
|
|
1002
|
+
`ci(0.95, x1, x2, ...)` (half-width of the confidence interval, t * s / sqrt(n), through the existing `qt`), `gstat(x1, x2, ...)`
|
|
1003
|
+
(Grubbs' G) and `gcrit(n, alpha)` (its two-sided critical value; agrees with SciPy to 10 figures). They take names as well as numbers:
|
|
1004
|
+
`` ~`R_E4 / mean(R_A, R_B2)`{=0dp} times lower `` works as written. These names stay free for a variable (`mean = 5` and `mean(1, 3)`
|
|
1005
|
+
side by side, so no existing document breaks); the older function names are still reserved.
|
|
1006
|
+
- **`::: calc-table`, a line `t# tinf`, `:::`** (or `::: {.calc-table names="t# tinf"}`): a real table (Pandoc `Table`, so the document's table styling, widths and numbering apply)
|
|
1007
|
+
of calculator names with their values, in the digits each was defined with (`digits="3dp"` for all). The names are the text of the div or `names=`: names and patterns
|
|
1008
|
+
(`*` any characters, `?` one, `#` a run of digits, so `t#` leaves out `t_welch` and `tinf`), a pattern in natural order (t2 before t10); `labels="t1=$t_{1}$; V=Volume"` by name or one per row in order (Markdown and math
|
|
1009
|
+
work); `head="Symbol; Value"` or `none`; `caption="..."` (Markdown); the identifier, other classes and attributes go to the table, so
|
|
1010
|
+
`{#tbl:t}` can be cited. A name counts as defined at the place of the div, as for a span there; in a part built alone the names of the
|
|
1011
|
+
parts above it are known (3.30.19). A name that is not defined, a pattern with no match, a value that cannot be computed: one `WARN
|
|
1012
|
+
calc-table` naming it; a div without names prints its own code. `::: {.calc-table t*}` is not a div for Pandoc (braces take only
|
|
1013
|
+
`.class`, `#id`, `key=value`) and gets one WARN with the right spelling instead of printing as text.
|
|
1014
|
+
|
|
1015
|
+
### Tests
|
|
1016
|
+
|
|
1017
|
+
- `tests.test_calc.Statistics` (13): the summary functions against Python's `statistics`, `ci` and `gcrit` against SciPy, the sentence of a
|
|
1018
|
+
Discussion, bad calls (one WARN each, the code kept), the new names still free for a variable and the old ones reserved, the table
|
|
1019
|
+
(natural order, labels by name, header, caption and identifier on a real `Table`, the place of the div, digits, everything that goes
|
|
1020
|
+
wrong, the malformed spelling), and the detection for a document with only a table.
|
|
1021
|
+
|
|
1022
|
+
## v3.30.19 — 2026-10-24
|
|
1023
|
+
|
|
1024
|
+
Pre-edit state: commit `65dc1da` (v3.30.18).
|
|
1025
|
+
|
|
1026
|
+
### Fixed
|
|
1027
|
+
|
|
1028
|
+
- **A part (or a section) built alone knows the calculator's names from what stands above it.** `pdfmd @60` printed nothing where
|
|
1029
|
+
`` `mean_A`{=2dp} `` had been (the name is defined in part 50), and the build was silent about it. Before the real pass pdfmd now has
|
|
1030
|
+
Pandoc read the scaffold and the parts left out that stand above the last one built, with `calc.lua` only (`-t native -o /dev/null`,
|
|
1031
|
+
about a second for 128 names); `calc.lua` writes every name with the value of its last definition (`%.17g`, full precision) and its
|
|
1032
|
+
digits (`-M pdfmd-calc-dump=FILE`), and the build reads the file back (`-M pdfmd-calc-seed=FILE`) as definitions above everything in the
|
|
1033
|
+
document, after the constants file. A section of an ordinary document (`doc#discussion`) does the same with the sections above it. The
|
|
1034
|
+
build says `NOTE calc: 128 names taken from parts 10-50 (evaluated, not typeset)`. Nothing is stored between builds, so nothing is
|
|
1035
|
+
stale. HTML, Word, Typst and the other writers that number the whole document and keep the part already evaluated everything.
|
|
1036
|
+
- **A span the calculator cannot evaluate no longer vanishes.** `` `name`{=2dp} `` is a raw inline of the format `2dp` for Pandoc; the
|
|
1037
|
+
filter put that item back, and the LaTeX and other writers drop a raw inline of a format they do not know, so the sentence had a hole and
|
|
1038
|
+
the WARN's "the code is kept" was untrue. The code is kept now (printed as code), with the one WARN, which names the format as written
|
|
1039
|
+
(`` `ghost`{=2dp} ``).
|
|
1040
|
+
|
|
1041
|
+
### Added
|
|
1042
|
+
|
|
1043
|
+
- `pdfmd-options: {seed-calc: auto|scan|off}` and `--seed-calc MODE` (a `--setup` row under Cache): `off` gives the seeding up. `auto` and
|
|
1044
|
+
`scan` are the same.
|
|
1045
|
+
|
|
1046
|
+
### Tests
|
|
1047
|
+
|
|
1048
|
+
- `tests.test_calc.PartBuiltAlone`: the dump and seed through the filter alone (the last value of a redefined name at full precision, its
|
|
1049
|
+
digits, a name with no digits); a split report's part 60 built alone against the full build (same text, the NOTE); an unknown name printed
|
|
1050
|
+
as code with exactly one WARN in both builds; the setting in the document and on the command line; a part that redefines a name is not
|
|
1051
|
+
overridden; a part with no calculator and a document without one are built as before; sections above a section (`doc#Discussion`,
|
|
1052
|
+
`doc#Notes`).
|
|
1053
|
+
|
|
1054
|
+
## v3.30.18 — 2026-10-24
|
|
1055
|
+
|
|
1056
|
+
Pre-edit state: commit `ad8efcd` (v3.30.17).
|
|
1057
|
+
|
|
1058
|
+
### Fixed
|
|
1059
|
+
|
|
1060
|
+
- **Every file pdfmd rewrites is written atomically**: to a temporary file in the same folder, then renamed over the old one (`os.replace`),
|
|
1061
|
+
so a crash, a full disk or ^C leaves the old file whole. The mode of the replaced file is kept and a symlink is written through. Covers the
|
|
1062
|
+
BUILD NOTES stamp, outputs, `--history-restore`, the backup options, `--extract-tables`, restored attachments, the config and metadata
|
|
1063
|
+
files of `--setup` and `--init-backups --global`, `--init-config`, the places that rewrite a finished PDF, and the built-in renderers. A
|
|
1064
|
+
process killed between write and rename leaves a `.NAME....part` file.
|
|
1065
|
+
- **A read-only home, cache or document folder no longer stops a build.** LuaLaTeX failed with `luaotfload: no writeable cache path`, `--cache`
|
|
1066
|
+
crashed on a `PermissionError`, and the import itself died when the home could not be looked into. What cannot be written goes to a
|
|
1067
|
+
temporary folder of the run (the cache folder, `TEXMFVAR`, `TEXMFCONFIG`, also for child processes), with one `NOTE`; Pandoc runs in a
|
|
1068
|
+
temporary folder when the document's folder is read-only, the document's folder staying on `--resource-path` and `TEXINPUTS`.
|
|
1069
|
+
- **Probes have a timeout.** A version or capability probe (`pandoc --version`/`--help`/`--list-extensions`, `kpsewhich`, `lualatex`/`typst
|
|
1070
|
+
--version`, the checks after `--install typst|node|d2|tectonic`, the Pandoc reference-document probe) is cut after 10 seconds
|
|
1071
|
+
(`PROBE_TIMEOUT`); one that does not answer counts as "not available" (an `AUTO PROBE` line under `--verbose`). Build commands never get a
|
|
1072
|
+
timeout.
|
|
1073
|
+
- **A missing `pdfmd_lua` is loud.** A lone copy of `pdfmd.py` gave silence while the Lua filters did nothing; it is one `WARN lua:` per run
|
|
1074
|
+
now. A document that needs no filter says nothing.
|
|
1075
|
+
- **Windows: a font list the console could not decode** came back as `None` and stopped the whole build. `installed_font_families()` and the
|
|
1076
|
+
fontconfig and `luaotfload-tool` calls decode the child's output themselves, replacing what cannot be read.
|
|
1077
|
+
|
|
1078
|
+
### Changed
|
|
1079
|
+
|
|
1080
|
+
- **CI** (`test.yml`, which `publish.yml` calls; `publish.yml` itself is untouched): a `linux-pandoc` job (Pandoc 3.1.3 and latest, LuaLaTeX
|
|
1081
|
+
and XeLaTeX, the whole suite) and a `golden` job (`scripts/vendor_inkmd.py --check` and the golden test) now gate a release. A `lint` job
|
|
1082
|
+
(`ruff --select F`, 39 findings, none silenced) and a `windows-latex` job (MiKTeX, the whole suite) run but do not gate.
|
|
1083
|
+
|
|
1084
|
+
### Tests
|
|
1085
|
+
|
|
1086
|
+
- New `tests.test_atomic`, `test_readonly_home`, `test_probes`, `test_lua_missing`; `test_auto_states` no longer
|
|
1087
|
+
flakes when a scratch folder name ends in `a`.
|
|
1088
|
+
- From the cloud session (branch `cloud/robustness-ci`, PR 7). The `windows-latex` job fails on 55 tests and 14 errors of 1652 (Windows paths
|
|
1089
|
+
in test Lua files, a `\` in `--section`, MiKTeX refusing to run as an administrator, missing fonts); it was the first whole-suite run on
|
|
1090
|
+
Windows and the job does not gate.
|
|
1091
|
+
|
|
1092
|
+
## v3.30.17 — 2026-10-24
|
|
1093
|
+
|
|
1094
|
+
Pre-edit state: commit `fbe5640` (v3.30.16).
|
|
1095
|
+
|
|
1096
|
+
### Added
|
|
1097
|
+
|
|
1098
|
+
- **`--to ansi` / `--view` draw spanning table cells.** A cell with a colspan or rowspan is drawn across the merged columns and rows, its text
|
|
1099
|
+
wraps to the merged width, the borders inside it are left out and each crossing uses the box character its neighbours ask for. A table
|
|
1100
|
+
without spans is drawn exactly as before.
|
|
1101
|
+
- **Coloured code.** Fenced code in python, bash/sh/shell/zsh, json, yaml, c, cpp, java, javascript/typescript, rust and go (and the usual
|
|
1102
|
+
aliases) gets keywords, strings, comments and numbers in four 256-colour codes, with no dependency. With colour off (`NO_COLOR`,
|
|
1103
|
+
`--color never`, a pipe) or an unknown language the block is exactly as before.
|
|
1104
|
+
- **Stacked display math.** A `$$..$$` that is the whole paragraph is drawn on several rows where it has `\frac`/`\dfrac`/`\tfrac`, `\sqrt`
|
|
1105
|
+
(with an overbar, and `\sqrt[n]{x}`), big operators with limits (`\sum \prod \coprod \int \iint \iiint \oint`) and `\lim`/`\max`/`\min`/
|
|
1106
|
+
`\sup`/`\inf` with the limit below. A formula with anything that cannot be stacked, or wider than the line, prints its linear form whole,
|
|
1107
|
+
never half stacked. Inline math stays linear.
|
|
1108
|
+
- **`options.pager_command`** (`pdfmd-options: {pager_command: "bat --paging=always"}`, also spelled `pager-command`; a word list works too),
|
|
1109
|
+
the pager of `--view`. Order: `$PDFMD_PAGER`, `$PAGER`, the setting, `less -FRX`. `--setup` row `options.pager_command`, also offered per folder.
|
|
1110
|
+
|
|
1111
|
+
### Tests
|
|
1112
|
+
|
|
1113
|
+
- `tests.test_ansi` extended with golden tests (a header spanning two columns, a rowspan of 2, both at once; the code colours; stacked math).
|
|
1114
|
+
A fuzz run of about 9000 random span tables had no misaligned line.
|
|
1115
|
+
- From the cloud session (branch `cloud/ansi-limits`, PR 6). Not verified: a real terminal and pager (checked through a pipe). The no-Pandoc
|
|
1116
|
+
tier (`pdfmd_ansi/native.py`) builds tables without spans.
|
|
1117
|
+
|
|
1118
|
+
## v3.30.16 — 2026-10-24
|
|
1119
|
+
|
|
1120
|
+
Pre-edit state: commit `05c6e73` (v3.30.15).
|
|
1121
|
+
|
|
1122
|
+
### Fixed
|
|
1123
|
+
|
|
1124
|
+
- The built-in renderers (inkmd, md2pdf) set `b5`, `a6`, `b4`, `b6`, `executive`, `folio`, `a0` to `a2`, `c4` and `c5` as A4 in silence;
|
|
1125
|
+
they have real sizes now and honour `landscape` (`papersize: a4,landscape`, `classoption`, `geometry`). A paper they do not know is a
|
|
1126
|
+
`NOTE` and A4.
|
|
1127
|
+
- `pdfmd --doc changelog 1` returned a block hidden in an HTML comment: `--doc` skips HTML comments now, and `--doc search` skips the
|
|
1128
|
+
README's contents list.
|
|
1129
|
+
- `docs/agents.md` counted its commands wrongly and called attaching the source the default (it is off unless asked). The README's
|
|
1130
|
+
`pdfmd --doc readme tectonic` example and its sample search output were invented; they are real output now. The README no longer calls
|
|
1131
|
+
macOS's Python 3.9 too old.
|
|
1132
|
+
- The zipapp carries the documents, so `pdfmd --doc` works in a `.pyz`. `pyproject.toml` requires `setuptools>=77` (PEP 639 license fields).
|
|
1133
|
+
|
|
1134
|
+
### Changed
|
|
1135
|
+
|
|
1136
|
+
- README: a contents list at the top and a shorter "Why".
|
|
1137
|
+
- `pdfmd_docs.version()` derives the version the docs name from `PDFMD_VERSION`, and `tests.test_docs_consistency` fails on drift.
|
|
1138
|
+
- `CHANGELOG.md` header: the patch rule and the `PDFMD_VERSION` location are described as CLAUDE.md states them, and the "why this starts
|
|
1139
|
+
at 2.0.0" paragraph is visible text instead of a comment.
|
|
1140
|
+
|
|
1141
|
+
### Removed
|
|
1142
|
+
|
|
1143
|
+
- Four unused functions: `pandoc_missing_message`, `kpsewhich_path_text`, `normalize_stamp_value`, `normalize_backup_value`.
|
|
1144
|
+
|
|
1145
|
+
### Tests
|
|
1146
|
+
|
|
1147
|
+
- New `tests.test_docs_consistency`; `test_native`, `test_doc` and `test_packaging` extended.
|
|
1148
|
+
- One reviewer finding was skipped as wrong: the stamp is written before the backup on purpose (a backup per compile would flood the folder).
|
|
1149
|
+
- From the cloud session (branch `cloud/review-fixes`, PR 5). Not verified: md2pdf with `executive` and `folio`.
|
|
1150
|
+
|
|
1151
|
+
## v3.30.15 — 2026-10-24
|
|
1152
|
+
|
|
1153
|
+
Pre-edit state: commit `c43923e` (v3.30.14).
|
|
1154
|
+
|
|
1155
|
+
### Added
|
|
1156
|
+
|
|
1157
|
+
- **An indent div takes its value in the class word.** `::: .indent=3in`, `::: indent=3in`, `::: indent-3in`, `::: {.indent-3in}` and
|
|
1158
|
+
`::: {.indent=3in}` (and `::: .indent`, `::: .noindent`) all mean `{indent=3in}`, through the same length parser (false, true, a number,
|
|
1159
|
+
units). Pandoc reads the unbraced forms as a single class word, so only the braced `{indent=...}` carried a value before.
|
|
1160
|
+
- **A near miss warns.** A class that looks like an indent div but is none of the accepted spellings (`.indent3in`, `.noindent=2em`) gets one
|
|
1161
|
+
`WARN indent: '<class>' is not an indent div; write ::: {indent=3in} (or ::: noindent)` per run and is left as written. A project's own
|
|
1162
|
+
`.indented`, `.indentation` and `.indent-block` are not looked at.
|
|
1163
|
+
- **A bare `::: csv` builds a table** like `::: {.csv}`, in the Pandoc route and in the built-in renderer, when it holds inline data.
|
|
1164
|
+
`file=` and `align=` still need the braces; `::: csvfile` and `::: csv-x` do not match.
|
|
1165
|
+
- **`csv: off`** (`pdfmd-options: {csv: off}`, short for `csv: {enabled: false}`), `--csv` / `--no-csv` and `PDFMD_CSV` (for `-j` workers)
|
|
1166
|
+
switch the csv-in-a-div feature off: the filter is not added and the div stays a plain div with its text, with no WARN, also in the
|
|
1167
|
+
no-Pandoc renderer. A mapping such as `csv: {rows: 30}` still works and leaves tables on. The command line wins over the options.
|
|
1168
|
+
`--setup` row `options.csv.enabled`.
|
|
1169
|
+
|
|
1170
|
+
### Tests
|
|
1171
|
+
|
|
1172
|
+
- New `tests.test_csv_switch` (17 tests); `tests.test_indent` extended (every accepted spelling, a measured x position in a real PDF, near
|
|
1173
|
+
misses that warn, `.indented` that does not). One assertion fixed: it now accepts Pandoc 3.1's `\hypertarget` as well as `\label`.
|
|
1174
|
+
- From the cloud session (branch `cloud/small-patches`, PR 4).
|
|
1175
|
+
|
|
1176
|
+
## v3.30.14 — 2026-10-24
|
|
1177
|
+
|
|
1178
|
+
Pre-edit state: commit `2c0b9a3` (v3.30.13).
|
|
1179
|
+
|
|
1180
|
+
### Added
|
|
1181
|
+
|
|
1182
|
+
- **Room around a `::: calc` div, and optional centring.** The block ran straight into the paragraphs above and below it. It has `0.75em`
|
|
1183
|
+
of room on each side now (`calc: {space: 0.75em}`, `--calc-space LENGTH|off`, `PDFMD_CALC_SPACE`; pt, mm, cm, in or em; a `--setup` row):
|
|
1184
|
+
`\par\addvspace{..}` for LaTeX and Beamer (it merges with the paragraph skip next to it instead of adding to it), `#v(.., weak: true)`
|
|
1185
|
+
for Typst, vertical padding on a wrapping `<div>` for HTML and the other HTML writers. `calc: {align: center}` / `--calc-align center`
|
|
1186
|
+
(left is the default; a `--setup` row) centres the lines: a `\begingroup\centering` group, `#align(center)[..]`, `text-align: center`.
|
|
1187
|
+
Markdown, Word and the plain writers get the block as before. A hidden `::: {.calc .hide}` div has no room, as it prints nothing.
|
|
1188
|
+
- **Ctrl-C is one line.** An interrupted run printed a page of traceback. It prints `Interrupted (Ctrl-C).` and exits with status 130 now
|
|
1189
|
+
(`main()` wraps `run_main()`); Pandoc and the engines get the signal from the terminal, and the temporary folders are removed.
|
|
1190
|
+
|
|
1191
|
+
### Tests
|
|
1192
|
+
|
|
1193
|
+
- `tests.test_calc.Switches.test_a_calc_div_has_room_around_it_and_can_be_centred`, `test_a_bad_space_or_align_is_an_error_and_the_other_writers_get_nothing`;
|
|
1194
|
+
the exact argument lists now carry `pdfmd-calc-space=0.75em`; `tests.test_interrupt`.
|
|
1195
|
+
- Measured in a LuaLaTeX PDF: the gap between the paragraphs around a calc line grew by the room set (2 cm: +113 pt), and `align: center`
|
|
1196
|
+
moved the block's left edge from the margin to the middle of the line.
|
|
1197
|
+
|
|
1198
|
+
## v3.30.13 — 2026-10-24
|
|
1199
|
+
|
|
1200
|
+
Pre-edit state: commit `67e53d2` (v3.30.12).
|
|
1201
|
+
|
|
1202
|
+
### Added
|
|
1203
|
+
|
|
1204
|
+
- **A trailing comment in a statement block.** In a ```` ```{=calc} ```` block, a `::: {.calc .hide}` div and the constants file a `#` starts
|
|
1205
|
+
a comment to the end of the line, so `M_Mg <- 24.305 # g/mol` defines `M_Mg`; a whole-line `#` was skipped already. Before, the `#` was read
|
|
1206
|
+
as arithmetic: the WARN "'#' is not arithmetic" and an undefined name for every later use. `--` is not a comment (it can be a double minus).
|
|
1207
|
+
|
|
1208
|
+
### Tests
|
|
1209
|
+
|
|
1210
|
+
- `tests.test_calc.Blocks.test_a_hash_starts_a_comment_at_the_end_of_a_line_too`.
|
|
1211
|
+
|
|
1212
|
+
## v3.30.12 — 2026-10-24
|
|
1213
|
+
|
|
1214
|
+
Pre-edit state: commit `52f3d0f` (v3.30.11).
|
|
1215
|
+
|
|
1216
|
+
The calculator's constants file is part of the source that `--attach-source`, `--keep-source` and `--embed-metadata files` carry.
|
|
1217
|
+
|
|
1218
|
+
### Fixed
|
|
1219
|
+
|
|
1220
|
+
- **A constants file named in `metadata.yaml` was left out.** `calc: {constants: tables/constants.csv}` was found only when the document's own
|
|
1221
|
+
text mentioned the path (the scan for referenced files), so a file named in a metadata file, the usual place for a report, was not attached
|
|
1222
|
+
and `pdfmd --restore` gave back a source that failed with "no file". It travels now like the `.hst` history file (`add_calc_constants`),
|
|
1223
|
+
and comes back under the same relative path. A constants file outside the document's folder gets the usual "outside" warning.
|
|
1224
|
+
|
|
1225
|
+
### Tests
|
|
1226
|
+
|
|
1227
|
+
- `tests.test_calc.Blocks.test_the_constants_file_named_in_a_metadata_file_travels_with_the_source`.
|
|
1228
|
+
|
|
1229
|
+
## v3.30.11 — 2026-10-24
|
|
1230
|
+
|
|
1231
|
+
Pre-edit state: commit `2e35666` (v3.30.10).
|
|
1232
|
+
|
|
1233
|
+
Definitions in bulk for the calculator: a report that needs 14 constants (t values, a stock concentration, molar masses) kept them as a run of
|
|
1234
|
+
hidden `` `name = value`{=} `` spans on one line (issue file 2, B).
|
|
1235
|
+
|
|
1236
|
+
### Added
|
|
1237
|
+
|
|
1238
|
+
- **A block of statements.** ```` ```{=calc} ```` (Pandoc's native raw block, lines intact), one statement per line, each as the text of a
|
|
1239
|
+
`{=}` span: `name = value` or `name <- value` stores and prints nothing, a line with `{ans}` prints (one paragraph per line), blank lines and
|
|
1240
|
+
`#` lines are skipped, a failing line is kept as code with one WARN and the rest goes on.
|
|
1241
|
+
- **```` ```{=} ```` works as ```` ```{=calc} ````.** Pandoc does not read an empty raw format as a block: it is a paragraph with one code
|
|
1242
|
+
span whose line breaks are gone. `calc.lua` finds those paragraphs and reads the lines again from the input files Pandoc was given
|
|
1243
|
+
(`PANDOC_STATE.input_files`), matching them by their text in document order; a fence shown inside a longer fence is not touched. From
|
|
1244
|
+
standard input there is no file: one WARN says to write `{=calc}`.
|
|
1245
|
+
- **`::: {.calc .hide}`** (also `.hidden`): the same statements, nothing printed. Its lines are read as Markdown.
|
|
1246
|
+
- **`` `code`{=calc} ``** is `` `code`{=} `` (Pandoc's own raw attribute). A ```` ```{.calc} ```` code block stays a code block, never calculated.
|
|
1247
|
+
- **`calc: {constants: FILE}`**, `--calc-constants FILE`, `PDFMD_CALC_CONSTANTS`, a `--setup` row: a CSV (comma, semicolon or tab; columns
|
|
1248
|
+
`name,value[,digits[,note]]`; header row optional; `#` lines skipped) read before the document, found beside the document, beside its
|
|
1249
|
+
metadata files, then in the current folder. The `digits` column is the format that name remembers for `{subs}` and a bare `` `t18`{=} ``.
|
|
1250
|
+
pdfmd turns the file into statements and passes them as `-M pdfmd-calc-constants`; the document defining a name again gets the usual WARN.
|
|
1251
|
+
- The filter is also added for a document that has only such a block.
|
|
1252
|
+
|
|
1253
|
+
Tests: `tests.test_calc.Blocks` (12: the block, the empty-format fence from a file and from standard input, a fence inside a longer one,
|
|
1254
|
+
fourteen constants, the hidden div, failing lines, detection, the constants file by option, flag and variable, a semicolon file, the errors).
|
|
1255
|
+
README "Calculations inside the text" 6, `docs/recipes.md` 1.11.
|
|
1256
|
+
|
|
1257
|
+
## v3.30.10 — 2026-10-24
|
|
1258
|
+
|
|
1259
|
+
Pre-edit state: commit `735569e` (v3.30.9).
|
|
1260
|
+
|
|
1261
|
+
The calculator gets the normal and Student's t distributions, for a lab report that needs a 95 % two-tailed t at 13, 16, 18, 93 or
|
|
1262
|
+
13.947 degrees of freedom, none of which a printed table lists.
|
|
1263
|
+
|
|
1264
|
+
### Added
|
|
1265
|
+
|
|
1266
|
+
- **`qt(p, df)`, `pt(t, df)`, `qnorm(p)`, `pnorm(z)`** in `pdfmd_lua/calc.lua`, lower tail as in R, no library. `qnorm` is Wichura's AS 241;
|
|
1267
|
+
`qt` inverts the incomplete beta function (continued fraction, bisection on the tail so a small `p` keeps its figures), exact for 1 and 2
|
|
1268
|
+
degrees of freedom, and a Cornish-Fisher series above 1000. `df` may be a fraction or `Inf` (`qt(0.975, Inf)` is `qnorm(0.975)`), a name
|
|
1269
|
+
works (`qt(0.975, df_w)`). `p` outside (0, 1) or `df <= 0` keeps the text with one WARN. The constant `Inf` is new.
|
|
1270
|
+
- Accuracy against R 4.6.1 and SciPy: `qnorm` and `pnorm` about 1e-15; `qt` about 1e-12 (3e-11 for `p` near 1e-12); `pt` about 1e-12 up to
|
|
1271
|
+
1000 degrees of freedom and 1e-9 above. `qt(0.975, df)` equals R's to the 12 figures it prints for df 1, 2, 3, 6, 8, 9, 10, 13, 16, 18, 93,
|
|
1272
|
+
96, 13.947016149 and Inf.
|
|
1273
|
+
- **Changed:** `qt`, `pt`, `qnorm`, `pnorm` and `Inf` are now built-in names, so a variable called `pt` is refused like `sin` (it was free
|
|
1274
|
+
before; the calculator is unreleased).
|
|
1275
|
+
|
|
1276
|
+
Tests: `tests.test_calc.Distributions` (R's table, 105 SciPy values, `Inf`, the symmetry, a name as df, the domain errors). README, the
|
|
1277
|
+
docstring.
|
|
1278
|
+
|
|
1279
|
+
## v3.30.9 — 2026-10-24
|
|
1280
|
+
|
|
1281
|
+
Pre-edit state: commit `cbcd7a5` (v3.30.8).
|
|
1282
|
+
|
|
1283
|
+
A `::: calc` div has no first-line indent. The author wrote `::: noindent` around every calc block of a report whose document indents
|
|
1284
|
+
its paragraphs; the div now does it itself.
|
|
1285
|
+
|
|
1286
|
+
### Added
|
|
1287
|
+
|
|
1288
|
+
- **`calc: {noindent: true}`, on by default.** `pdfmd-options: {calc: {noindent: false}}` gives the div the document's own indent again;
|
|
1289
|
+
`--calc-noindent` / `--no-calc-noindent` (the command line wins), `PDFMD_CALC_NOINDENT` for `-j` workers, a `pdfmd --setup` row.
|
|
1290
|
+
`calc.lua` puts `indent="false"` on the finished div and `indent.lua` (now also added whenever a calc div is in the text and the
|
|
1291
|
+
calculator and this option are on) does the rest per writer: LaTeX and Beamer `\begingroup\setlength{\parindent}{0pt}` ... `\endgroup`,
|
|
1292
|
+
Typst a `#set par(first-line-indent: 0pt)` block, HTML a `text-indent` style. Markdown, Word and the plain-text writers get no attribute
|
|
1293
|
+
and no NOTE (Word follows with the indent item queued for docx/odt). An `indent=` or `.indent` / `.noindent` on the same div wins, and
|
|
1294
|
+
an outer `::: noindent` works as before. The default is on because the `::: calc` div was never published (3.30.6 onward).
|
|
1295
|
+
- A div that carries both classes is written `::: {.calc .noindent}`; `::: calc noindent` is not a Pandoc div (one bare word only) and is
|
|
1296
|
+
left as text.
|
|
1297
|
+
|
|
1298
|
+
Tests: `tests/test_calc.py` (the default, an `indent=` and `.indent` that win, the option, the flags, the variable, other writers) and a
|
|
1299
|
+
real LuaLaTeX PDF: body text starts at 86.9 pt, the calc lines at 72.0 pt. README "Calculations inside the text", `docs/recipes.md` 1.11.
|
|
1300
|
+
|
|
1301
|
+
## v3.30.8 — 2026-10-24
|
|
1302
|
+
|
|
1303
|
+
Pre-edit state: commit `56fe65a` (v3.30.7).
|
|
1304
|
+
|
|
1305
|
+
Two fixes and three additions to the calculator, found converting a whole Calculations part of a real lab report (about 130 `{ans}` lines)
|
|
1306
|
+
to `{=}`, `{ans}` and `::: calc`.
|
|
1307
|
+
|
|
1308
|
+
### Fixed
|
|
1309
|
+
|
|
1310
|
+
- **A `` `...`{=} `` span inside a `::: calc` div sees the definition above it, not the last one.** The div's lines were registered before
|
|
1311
|
+
the spans in them, so in `a = 1 + 1 = {ans:1dp}, so `a * 10`{=}` followed by the same line with `3 + 3`, both spans took the last `a`
|
|
1312
|
+
(60 twice, no WARN). Lines and spans are now read together, line by line, in the order of the text, and the div no longer hands its
|
|
1313
|
+
paragraphs to the document-wide pass a second time.
|
|
1314
|
+
- **A `::: calc` line may carry anything after its last `{ans}`.** `y = 2 + 2 = {ans:1dp} ppm \ce{Ca^2+}` (raw TeX), a superscript, bold,
|
|
1315
|
+
a link or `$..$` after the value used to leave the whole line as written, `{ans}` and all. Only the text up to the last marker has to be
|
|
1316
|
+
plain; the rest is copied as it is. Formatting before the last `{ans}` is still refused with the WARN.
|
|
1317
|
+
|
|
1318
|
+
### Added
|
|
1319
|
+
|
|
1320
|
+
- **More than one `{ans}` in a span or a line.** After text that does not end in `=` it repeats the value just shown in its own format
|
|
1321
|
+
(`= {ans:1dp}, so {ans:0dp}`); after text that ends in `=` it is a new calculation whose expression is the longest tail of that text
|
|
1322
|
+
that reads as one (`so round(n) = {ans:0dp}`; `so k = round(n) = {ans:0dp}` also stores `k`).
|
|
1323
|
+
- **`{subs}`: the expression with the names replaced by their values.** `ppm1 = C_dil * V / 100.0 * 40.078 * 1000 = {subs} = {ans:2dp} ppm`
|
|
1324
|
+
prints `... = 0.002039 * 34.41 / 100.0 * 40.078 * 1000 = 28.12 ppm`. Each name shows the digits it was defined with (a hidden span's use
|
|
1325
|
+
`calc: digits`); the computation always uses the full values. Literals, operators, functions, `pi` and `deg` stay as typed; a negative value
|
|
1326
|
+
gets parentheses (`(-0.5)^2`) unless nothing before it could be read as a sign. `calc: {subs: true}` (also `--calc-subs` /
|
|
1327
|
+
`--no-calc-subs`, `PDFMD_CALC_SUBS`, a `pdfmd --setup` row) puts the copy before every `{ans}` that follows an expression with names; a
|
|
1328
|
+
lone name, an expression without names and a repeated `{ans}` get none. `{subs}` forces it on a line, `{nosubs}` or `{subs:off}` stops it.
|
|
1329
|
+
- **`<-` assigns like `=`**: `` `t18 <- 2.10092204`{=} ``, `ppm1 <- C_dil * V = {ans:2dp}`.
|
|
1330
|
+
|
|
1331
|
+
The Unicode operators (`×`, `·`, `÷`, `−`) are now read by the tokenizer itself (the parser needs byte positions for `{subs}`). Queued, not
|
|
1332
|
+
done: `qt(p, df)` and `qnorm(p)`.
|
|
1333
|
+
|
|
1334
|
+
## v3.30.7 — 2026-10-24
|
|
1335
|
+
|
|
1336
|
+
Pre-edit state: commit `230ff0b` (v3.30.6).
|
|
1337
|
+
|
|
1338
|
+
### Fixed
|
|
1339
|
+
|
|
1340
|
+
- **A partial build lists its metadata again.** `pdfmd @10,40` (or `report#section`) printed `FOUND nulabreport.lua, 2 parts` and
|
|
1341
|
+
`DEFAULTS packages mhchem`, without the metadata file, the preamble, the global metadata or the balanced tables a full build lists.
|
|
1342
|
+
The files were used all along (the Pandoc command had them); only the summary was wrong. The hidden pass over the whole document, which
|
|
1343
|
+
gives the left-out parts their numbers, printed its own summary to nowhere and emptied the items already recorded. The summary is now saved
|
|
1344
|
+
before that pass and put back after it (`AutoLog.snapshot` / `restore`). Test: `tests/test_autolog.py` (`Builds`).
|
|
1345
|
+
|
|
1346
|
+
## v3.30.6 — 2026-10-24
|
|
1347
|
+
|
|
1348
|
+
Pre-edit state: commit `1725405` (v3.30.5).
|
|
1349
|
+
|
|
1350
|
+
### Added
|
|
1351
|
+
|
|
1352
|
+
- **`{ans}`: the calculator prints the text it was given.** Inside a `` `...`{=} `` span, `` `V = 20.60 - 0.00 = {ans:2dp} mL`{=} `` becomes
|
|
1353
|
+
"V = 20.60 - 0.00 = 20.60 mL" (the text as typed, `{ans}` replaced); the expression is what stands between the last `=` before `{ans}` and that
|
|
1354
|
+
`= {ans}`, and what stands before it is a label. `` `2-4={ans}`{=} `` works unspaced and prints as typed. Several `{ans}` may share a span.
|
|
1355
|
+
- **Names.** A plain identifier on the left (`C_EDTA = ... = {ans}`, or the word after a `:` `;` `,` of a label: `Run 1: V = ...`) is stored with the
|
|
1356
|
+
FULL value, not the displayed one, so later calculations cannot drift from what the page shows. `` `A = 4 - 2`{=} `` only stores (prints nothing);
|
|
1357
|
+
`` `A`{=} `` and `` `A * 2`{=} `` print values. A name is found anywhere in the document, parts included (Results may cite Calculations): the
|
|
1358
|
+
nearest definition above its use, else the first below; a name may be defined again; a cycle, an unknown name or a name that is a function or
|
|
1359
|
+
constant (`pi`, `sqrt`) is a WARN and the text is kept. A stored name remembers the digits it was given.
|
|
1360
|
+
- **Number formats.** `{ans:4sf}` four significant figures (trailing zeros kept: 20.60; the digits before the point are never dropped, 1049 at 3sf is
|
|
1361
|
+
1049; scientific only below 1e-6 or from 1e15), `{ans:2dp}` two decimal places, and after a span `` `1/3`{=3sf} `` (Pandoc reads `{=3sf}` as a
|
|
1362
|
+
raw attribute: the filter takes `NNsf` / `NNdp` raw inlines and leaves every other raw format alone).
|
|
1363
|
+
- **`::: calc`** (also `::: {.calc}`, `::: {#id .calc}`; the class alone, as in Pandoc) calculates every line that holds `{ans}`, in paragraphs, list
|
|
1364
|
+
items and quotes, without backticks; other lines are plain text; a line with bold, a link or math is left as written with a WARN. The detection
|
|
1365
|
+
adds the filter for the div as well. It is the opt-in form of calculating prose: nothing outside it is read.
|
|
1366
|
+
- `--calc-digits 4sf|2dp` (and `PDFMD_CALC_DIGITS` for `-j` workers) and `pdfmd-options: {calc: {digits: 4sf}}` set the default format of a document
|
|
1367
|
+
(unset: as R prints, up to 7 figures); a span's own format wins. `--setup` rows. README "Calculations inside the text" (for anyone writing with it),
|
|
1368
|
+
docs/recipes.md 1.11.
|
|
1369
|
+
|
|
1370
|
+
### Changed
|
|
1371
|
+
|
|
1372
|
+
- **The calculator's options live under `calc:`** (like `csv:` and `backup:`): `pdfmd-options: {calc: {enabled, digits, round}}`, `--setup` rows
|
|
1373
|
+
`options.calc.enabled|digits|round`. `calc: off` / `calc: false` stays as the short form of `enabled: false`. The flat `calc-round` key of 3.30.4
|
|
1374
|
+
is gone (3.30.4 was not released); `--calc`, `--no-calc`, `--calc-round` are unchanged. A wrong `digits` or `round` is an error naming the key.
|
|
1375
|
+
- `tests/test_calc.py`: 33 tests (the new ones: 16 statement forms, 20 formats, the div in its spellings, two placeholders in a line, the warnings,
|
|
1376
|
+
the options, flags and variables, and a PDF in which Results cites a name that Calculations defines in a `::: calc` div).
|
|
1377
|
+
|
|
1378
|
+
### Not done (queued)
|
|
1379
|
+
|
|
1380
|
+
- Spacing the operators for you (`C=n/V={ans}` printed as `C = n / V = 0.0203`), calculating plain prose without `{=}` or a div, `$..$` math, statistics
|
|
1381
|
+
(mean, stdev, rsd, a t quantile, Grubbs), a check mode for a number already typed, significant-figure tracking, `.qmd` spellings.
|
|
1382
|
+
|
|
1383
|
+
---
|
|
1384
|
+
|
|
1385
|
+
## v3.30.5 — 2026-10-24
|
|
1386
|
+
|
|
1387
|
+
Pre-edit state: commit `2d56ccf` (v3.30.4).
|
|
1388
|
+
|
|
1389
|
+
### Fixed
|
|
1390
|
+
|
|
1391
|
+
- **A symlink in `parts/` crashed the build** (`ValueError: '...' is not in the subpath of '.../parts'`). `collect_parts()` resolved every part to its real
|
|
1392
|
+
path, so a `.md` file linked into the parts folder, or one inside a linked folder, no longer lay under it and `part_key()` / `select_parts()`
|
|
1393
|
+
could not name it. A part now keeps the path it has in the parts folder (`os.path.abspath`, not `resolve()`), so a linked file or folder is built
|
|
1394
|
+
as a part, in its place in the order and under its link's name. (A folder in `parts/` is always walked: to keep a reference folder beside the parts
|
|
1395
|
+
without building it, name it with a leading `_` or `.`.)
|
|
1396
|
+
|
|
1397
|
+
### Added
|
|
1398
|
+
|
|
1399
|
+
- **`@` for `#` in the section shorthand**: `pdfmd report@methods`, `pdfmd doc@@yield` (a level-2 heading), `pdfmd @yield` (the folder's only Markdown
|
|
1400
|
+
file), `doc@a+b`. An interactive bash (and some other shells) takes a `#` for the start of a comment and never passes it on, so `pdfmd #10` ran
|
|
1401
|
+
without its argument. A real file whose name has an `@` still wins, as with `#`; `--section` is unchanged. `tests/test_symlinked_parts.py`.
|
|
1402
|
+
|
|
1403
|
+
---
|
|
1404
|
+
|
|
1405
|
+
## v3.30.4 — 2026-10-24
|
|
1406
|
+
|
|
1407
|
+
Pre-edit state: commit `b55a83b` (v3.30.3).
|
|
1408
|
+
|
|
1409
|
+
### Added
|
|
1410
|
+
|
|
1411
|
+
- **An inline calculator: `` `2026 - 2006`{=} `` becomes `20`.** Inline code followed at once by a bare `{=}` is replaced by its value, in every
|
|
1412
|
+
output format a Pandoc build writes. `pdfmd_lua/calc.lua` has its own small parser and runs nothing (no `load`, no R, no Python), so a document from
|
|
1413
|
+
anybody is safe and nothing asks for trust. Operators: `+ - * /`, `^` and `**` (right to left; `-2^2` is -4), `%` and `%%` (remainder, floored:
|
|
1414
|
+
`-7 % 3` is 2), `//` and `%/%` (floor division: `7 %/% 2` is 3), parentheses, unary minus. `|a - b|` is the absolute value. A `%` that no operand follows is a percent sign, a label and not an operator (`0.5 / 4 * 100%` is 12.5, as a lab report
|
|
1415
|
+
writes it); `×`, `·`, `÷` and `−` are `*`, `*`, `/` and `-`. Numbers: `12`, `.5`, `6.022e23`. Constants `pi` and `deg`
|
|
1416
|
+
(= pi/180, so `sin(30*deg)` is 0.5). Functions (angles in radians): `sin cos tan cot sec csc`, `asin acos atan acot` (`atan(y, x)` is the two-argument
|
|
1417
|
+
arctangent), `sinh cosh tanh asinh acosh atanh`, `sind cosd tand` (degrees in) and `asind acosd atand` (degrees out; `asin(x) / deg` is the same), `exp ln log` (`log(x, base)`), `log10 log2 sqrt cbrt abs sign`,
|
|
1418
|
+
`floor ceil trunc`, `round(x)` and `round(x, digits)` (an exact half goes to the even digit, as in R and Python: `round(2.5)` is 2, `round(3.5)` is 4; `calc-round: away` makes it 3), `min max` (any number of
|
|
1419
|
+
arguments), `hypot factorial`. A trig result closer to 0 than 1e-15 is 0 (`sin(pi)`). The value prints as R does: up to 7 significant digits, `20`
|
|
1420
|
+
and not `20.0`, `0.3` for `0.1 + 0.2`, `1.2044e+24` for a very large one.
|
|
1421
|
+
- What the parser cannot evaluate (a name it does not know, a division by zero, `asin(2)`, an unfinished expression, an infinite or undefined
|
|
1422
|
+
result) is kept as the code it was, without the `{=}`, with one `WARN calc: ...` naming the span and the reason. Inline code without `{=}`, code
|
|
1423
|
+
blocks and a `{=}` anywhere else are never touched.
|
|
1424
|
+
- `--calc` / `--no-calc` (and `PDFMD_CALC=on|off`, which is how a `-j` worker gets it) and `pdfmd-options: {calc: off}` (the document, a metadata file or the
|
|
1425
|
+
config file, in that order of precedence after the flag) turn the calculator on or off, with a `--setup` row;
|
|
1426
|
+
`--calc-round even|away`, `pdfmd-options: {calc-round: away}` and a `--setup` row choose how `round()` settles an exact half (default even, like R and Python); the filter is
|
|
1427
|
+
attached only to a document that has a `` `...`{=} `` span. Without Pandoc the built-in renderer shows the span as written. docs/recipes.md 1.11.
|
|
1428
|
+
- `tests/test_calc.py`: 88 expressions with their text, 23 failures with their warning, the neighbouring text, code that must stay, the HTML, LaTeX
|
|
1429
|
+
and Typst writers, the detection, the switch (document and metadata file), and real PDFs (a part included) read back with `pdftotext`.
|
|
1430
|
+
|
|
1431
|
+
### Not done (queued)
|
|
1432
|
+
|
|
1433
|
+
- spellings for `.qmd` (`` `{r} 2 + 2` ``, `{python}`) and an `all` mode that evaluates every
|
|
1434
|
+
arithmetic-only code span.
|
|
1435
|
+
|
|
1436
|
+
---
|
|
1437
|
+
|
|
1438
|
+
## v3.30.3 — 2026-10-24
|
|
1439
|
+
|
|
1440
|
+
Pre-edit state: commit `ab380d1` (v3.30.2).
|
|
1441
|
+
|
|
1442
|
+
### Added
|
|
1443
|
+
|
|
1444
|
+
- **`--to ansi`: a document as styled text for a terminal.** Bold, italic, inline code, strikethrough, links, headings, ordered and bullet lists, block
|
|
1445
|
+
quotes, code blocks, rules, footnotes (listed at the end), a title block from the front matter, and every table as a box-drawn grid: a rule between
|
|
1446
|
+
rows, a bold header, each column as wide as its widest cell, the alignment the table gives, cells wrapped when the window is narrower than the table,
|
|
1447
|
+
wide (East Asian) characters counted as two cells. The text is wrapped to `--width N`, else the terminal's width, else 120. It is a build like the others:
|
|
1448
|
+
Pandoc reads the document through the usual steps (metadata files, filters, `pdfmd-options`, citations, `\ce`, the CSV tables) and writes its JSON AST,
|
|
1449
|
+
which the new package `pdfmd_ansi/` (a pure function, no other pdfmd module imported) draws; the file is `NAME.ansi`, and `-o x.ansi` selects the format.
|
|
1450
|
+
Without Pandoc the built-in parser (`pdfmd_inkmd`) reads the Markdown instead, with a NOTE: no filters, citations, footnotes or metadata files.
|
|
1451
|
+
- **Math as text, level (a).** `MathText` (the converter the built-in renderer uses) gets an ANSI back-end: Greek letters, operators, Unicode sub- and
|
|
1452
|
+
superscripts, `(a+b)/(c+d)`, `√(...)`, variables in italic, a function name after a variable set off by a space (`RT ln K`); a formula alone on its line is
|
|
1453
|
+
centred; one the converter cannot read is shown as its TeX.
|
|
1454
|
+
- **`--view`: read a document in the terminal.** = `--to ansi -o -` + a pager. It takes files (a folder: its Markdown files), `-` or a pipe on standard
|
|
1455
|
+
input (written to a hidden temporary `.md` beside the cwd so relative pictures resolve, and removed), else the Markdown files of the current folder, else
|
|
1456
|
+
what is typed or pasted into the terminal (ended with Ctrl-D; a hint says so on standard error). Several files are one stream in the order given, with a
|
|
1457
|
+
dim rule naming each. Each file is built on its own by a child pdfmd into a scratch folder, so all discovery and notes apply; the notes (`OK`, `DEFAULTS`)
|
|
1458
|
+
stay off the screen unless a build fails or `--verbose` is given, every other message goes to standard error. The styling stays when the output is piped
|
|
1459
|
+
(`pdfmd --view x.md | less -R`). `--view -o FILE` saves the file and shows nothing; `--view` with another `--to` is an error.
|
|
1460
|
+
- **The pager** (on by default, `--pager` / `--no-pager`, `pdfmd-options: {pager: false}`, a `--setup` row, also in the folder screen): only when
|
|
1461
|
+
standard output is a terminal, through `$PDFMD_PAGER`, else `$PAGER`, else `less -FRX` (`-F` quits at once when the text fits one screen, `-R` keeps the
|
|
1462
|
+
colours; a `less` given without `-R` gets it).
|
|
1463
|
+
- **`-o -` is standard output**, for `--view` and for any format built from a file or a pipe: the build goes to a scratch folder and its bytes are copied to
|
|
1464
|
+
stdout; several documents are joined only for text formats (a binary format, `pdf` or `docx`, with several inputs is refused). `--color auto|always|never`:
|
|
1465
|
+
a file and `--view` are styled, `-o -` into a pipe is plain, `NO_COLOR` turns `auto` off, `always` wins over it.
|
|
1466
|
+
- Links are `words (address)`, or OSC 8 hyperlinks in a terminal known to draw them (iTerm2, WezTerm, Ghostty, VS Code) when the text is not paged. Text
|
|
1467
|
+
from a document is cleaned before it is shown: control characters (an ESC would start a terminal sequence) and bidi overrides are dropped.
|
|
1468
|
+
- `--help view` lists `--view --pager --color --width`; `--view` is on the short help page. The flags are in `pdfmd_setup/inventory.py` (`--pager` is the row
|
|
1469
|
+
`options.pager`; `--view` a command; `--color` and `--width` per run); `pdfmd_ansi` is in `pyproject.toml` and `scripts/build_zipapp.py`.
|
|
1470
|
+
|
|
1471
|
+
### Known limits
|
|
1472
|
+
|
|
1473
|
+
- A cell that spans rows or columns is drawn in its first cell only. A formula is linear (stacked fractions, roots and sums with limits, and pictures of
|
|
1474
|
+
display math in terminals that show them, are later patches). Raw LaTeX, HTML and Word pieces are left out (`--verbose` counts them). Code is not highlighted.
|
|
1475
|
+
- The vendored built-in parser reads a `>` quote that follows a list, after a blank line, as a paragraph plus a one-line quote; that is in `pdfmd_inkmd`
|
|
1476
|
+
(to be fixed upstream in inkmd, the vendored copy is not edited by hand), so it shows only without Pandoc.
|
|
1477
|
+
|
|
1478
|
+
Tests: `tests/test_ansi.py` — widths and wrapping, escape-code cleaning, inline styles with and without colour, links, grid widths at several window
|
|
1479
|
+
widths (wide characters, shrinking, wrapping), lists/quotes/code, math through the callback; and through the full pipeline: `--to ansi` to a file, `-o x.ansi`,
|
|
1480
|
+
`--view` into a pipe, `NO_COLOR` / `--color`, `-o -`, several files, a pipe and `-`, `--to docx` refused, `-o` replacing `--view`, no Pandoc.
|
|
1481
|
+
|
|
1482
|
+
---
|
|
1483
|
+
|
|
1484
|
+
## v3.30.2 — 2026-10-22
|
|
1485
|
+
|
|
1486
|
+
Pre-edit state: commit `814acf5` (v3.30.1).
|
|
1487
|
+
|
|
1488
|
+
### Added
|
|
1489
|
+
|
|
1490
|
+
- **`--table-keep N` and `pdfmd-options: {table-keep: N}`.** pdfmd passes `-M tablekeep=N` to Pandoc, for a Lua filter that keeps a short Markdown
|
|
1491
|
+
table on one page (nulabreport.lua, v1.28.16: a table of up to `N` body rows moves to the next page whole instead of splitting; its built-in 10 was
|
|
1492
|
+
hard-coded before). `0` lets every table break. Order: the command line (it reaches `-j` workers as `PDFMD_TABLE_KEEP`), then `pdfmd-options.table-keep`
|
|
1493
|
+
(document, metadata files, config), then nothing: pdfmd passes no `-M`, and the metadata's own `tablekeep:` or the filter's default stands. Pandoc's `-M`
|
|
1494
|
+
wins over a metadata file, so the option also overrides a `tablekeep:` there. A value that is not a whole number stops the run with a message naming the
|
|
1495
|
+
file. It is set at all three places the command is built (PDF and the other outputs, the Office route). A row in `pdfmd --setup` (Tables), in the
|
|
1496
|
+
folder screen too. A filter that does not read `tablekeep` ignores it. Tested through the full pipeline: the flag, the option, the metadata key and
|
|
1497
|
+
their order, and a bad value.
|
|
1498
|
+
|
|
1499
|
+
---
|
|
1500
|
+
|
|
1501
|
+
## v3.30.1 — 2026-10-21
|
|
1502
|
+
|
|
1503
|
+
Pre-edit state: commit `5617970` (v3.30.0).
|
|
1504
|
+
|
|
1505
|
+
### Fixed
|
|
1506
|
+
|
|
1507
|
+
- **The closing line names the PDF, not the `.tex` the cache made on the way.** A build through the cache folder (a part alone, `report#second`, with `cache:` on,
|
|
1508
|
+
as the lab reports have it) printed `OK report.md > ~/.cache/pdfmd/b888f4388b-report/report.tex (lualatex)` although the PDF was written beside the source:
|
|
1509
|
+
the nested Markdown-to-`.tex` stage of `convert_via_cache()` recorded its own file as the build's output, and nothing put the PDF back. It is restored after the
|
|
1510
|
+
stage, so the line is `OK report.md > report.second.pdf (lualatex)`; `--to latex` still names the `.tex`, and `--verbose` still shows the cache folder (the
|
|
1511
|
+
`CMD` and `TEXDIRECT` lines). Tested with the cache route on, which fails without the fix (`tests/test_build_record.py`).
|
|
1512
|
+
|
|
1513
|
+
---
|
|
1514
|
+
|
|
1515
|
+
## v3.30.0 — 2026-10-21
|
|
1516
|
+
|
|
1517
|
+
Pre-edit state: commit `a030424` (v3.29.36).
|
|
1518
|
+
|
|
1519
|
+
### Added
|
|
1520
|
+
|
|
1521
|
+
- **A paragraph indent for a stretch of text: `::: {indent=false}`.** A fenced div with `indent=` (`false`, `no`, `off`, `0`; `true`, which is 1.5em; or a
|
|
1522
|
+
length: `2em`, `12pt`, `5mm`, a bare number is em), or the classes `.noindent` / `.indent`, sets the first-line indent of the paragraphs inside it and puts the
|
|
1523
|
+
document's own back at the closing `:::`, so there is no "turn it back on" line to forget. Divs nest (the inner one wins, the outer comes back after it), and
|
|
1524
|
+
the div's `#id` and other classes stay. The new `pdfmd_lua/indent.lua` does it per writer: LaTeX and Beamer set `\parindent` in a `\begingroup` ...
|
|
1525
|
+
`\endgroup`, Typst a `first-line-indent` rule in a content block (not in a `#block`, where it did not reach a paragraph that followed another), HTML and
|
|
1526
|
+
EPUB a `text-indent` style on the div. Pandoc Markdown, GFM and CommonMark keep the div as written; Word, OpenDocument and the rest have no per-block indent,
|
|
1527
|
+
so the text stays and a NOTE says the setting was left out. A value that is not an indent is a WARN and changes nothing. The filter is added only for a
|
|
1528
|
+
document (or one of its parts) that has such a div (`indent_filter_args()`, `INDENT_DIV_RE`); there is no switch, since the div is the opt-in. Found with the
|
|
1529
|
+
calculations of a lab report, where the lines of arithmetic should not be indented but the prose between them should. New `tests/test_indent.py`: the
|
|
1530
|
+
detection, every writer, and real PDFs measured with `pdftotext -bbox` (a plain document and a report in parts). `docs/recipes.md` 1.9.
|
|
41
1531
|
|
|
42
1532
|
---
|
|
43
1533
|
|
|
@@ -8260,6 +9750,27 @@ package being built.
|
|
|
8260
9750
|
page shape trips the same "wrongly rebalanced" or "wrongly left alone"
|
|
8261
9751
|
symptom in the other direction.
|
|
8262
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
|
+
|
|
8263
9774
|
---
|
|
8264
9775
|
|
|
8265
9776
|
# Before 2.0.0 — reconstructed history (NOT reliable)
|