ctrl-kd 4.8.1__tar.gz → 4.10.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (123) hide show
  1. {ctrl_kd-4.8.1/src/ctrl_kd.egg-info → ctrl_kd-4.10.0}/PKG-INFO +28 -1
  2. {ctrl_kd-4.8.1 → ctrl_kd-4.10.0}/README.md +27 -0
  3. {ctrl_kd-4.8.1 → ctrl_kd-4.10.0/src/ctrl_kd.egg-info}/PKG-INFO +28 -1
  4. {ctrl_kd-4.8.1 → ctrl_kd-4.10.0}/src/ctrl_kd.egg-info/SOURCES.txt +11 -0
  5. {ctrl_kd-4.8.1 → ctrl_kd-4.10.0}/src/ctrlkd/__init__.py +4 -1
  6. {ctrl_kd-4.8.1 → ctrl_kd-4.10.0}/src/ctrlkd/cli.py +83 -9
  7. {ctrl_kd-4.8.1 → ctrl_kd-4.10.0}/src/ctrlkd/core.py +217 -121
  8. {ctrl_kd-4.8.1 → ctrl_kd-4.10.0}/src/ctrlkd/emit.py +721 -77
  9. {ctrl_kd-4.8.1 → ctrl_kd-4.10.0}/src/ctrlkd/layout.py +128 -17
  10. {ctrl_kd-4.8.1 → ctrl_kd-4.10.0}/src/ctrlkd/pdf.py +574 -76
  11. ctrl_kd-4.10.0/src/ctrlkd/quirks.py +501 -0
  12. {ctrl_kd-4.8.1 → ctrl_kd-4.10.0}/tests/test_ctrlkd.py +96 -25
  13. {ctrl_kd-4.8.1 → ctrl_kd-4.10.0}/tests/test_graphic_cells.py +2 -2
  14. ctrl_kd-4.10.0/tests/test_html_column_sections.py +74 -0
  15. {ctrl_kd-4.8.1 → ctrl_kd-4.10.0}/tests/test_html_print_stylesheet.py +8 -2
  16. {ctrl_kd-4.8.1 → ctrl_kd-4.10.0}/tests/test_justification.py +1 -1
  17. ctrl_kd-4.10.0/tests/test_markdown_escaping_and_breaks.py +238 -0
  18. {ctrl_kd-4.8.1 → ctrl_kd-4.10.0}/tests/test_merge_page_number_variable.py +7 -3
  19. ctrl_kd-4.10.0/tests/test_modern_bullet_gap.py +141 -0
  20. ctrl_kd-4.10.0/tests/test_modern_html_measure.py +77 -0
  21. {ctrl_kd-4.8.1 → ctrl_kd-4.10.0}/tests/test_modern_lint.py +17 -1
  22. ctrl_kd-4.10.0/tests/test_no_syntax_warnings.py +105 -0
  23. {ctrl_kd-4.8.1 → ctrl_kd-4.10.0}/tests/test_pcl_v4_column_geometry.py +1 -1
  24. ctrl_kd-4.10.0/tests/test_per_page_orientation.py +167 -0
  25. {ctrl_kd-4.8.1 → ctrl_kd-4.10.0}/tests/test_printed_fidelity.py +1 -1
  26. ctrl_kd-4.10.0/tests/test_printed_html_single_break.py +57 -0
  27. ctrl_kd-4.10.0/tests/test_printed_rtf_ruler_column.py +116 -0
  28. ctrl_kd-4.10.0/tests/test_quirks.py +640 -0
  29. ctrl_kd-4.10.0/tests/test_reverse_video_ground.py +86 -0
  30. {ctrl_kd-4.8.1 → ctrl_kd-4.10.0}/tests/test_rtf_facing_pages.py +12 -4
  31. {ctrl_kd-4.8.1 → ctrl_kd-4.10.0}/tests/test_rtf_head_style_attrs.py +9 -5
  32. {ctrl_kd-4.8.1 → ctrl_kd-4.10.0}/tests/test_rtf_page_numbers.py +22 -13
  33. {ctrl_kd-4.8.1 → ctrl_kd-4.10.0}/tests/test_structural_checks.py +1 -1
  34. {ctrl_kd-4.8.1 → ctrl_kd-4.10.0}/LICENSE +0 -0
  35. {ctrl_kd-4.8.1 → ctrl_kd-4.10.0}/pyproject.toml +0 -0
  36. {ctrl_kd-4.8.1 → ctrl_kd-4.10.0}/setup.cfg +0 -0
  37. {ctrl_kd-4.8.1 → ctrl_kd-4.10.0}/src/ctrl_kd.egg-info/dependency_links.txt +0 -0
  38. {ctrl_kd-4.8.1 → ctrl_kd-4.10.0}/src/ctrl_kd.egg-info/entry_points.txt +0 -0
  39. {ctrl_kd-4.8.1 → ctrl_kd-4.10.0}/src/ctrl_kd.egg-info/top_level.txt +0 -0
  40. {ctrl_kd-4.8.1 → ctrl_kd-4.10.0}/src/ctrlkd/afm.py +0 -0
  41. {ctrl_kd-4.8.1 → ctrl_kd-4.10.0}/src/ctrlkd/convert.py +0 -0
  42. {ctrl_kd-4.8.1 → ctrl_kd-4.10.0}/src/ctrlkd/fontmap.py +0 -0
  43. {ctrl_kd-4.8.1 → ctrl_kd-4.10.0}/src/ctrlkd/info.py +0 -0
  44. {ctrl_kd-4.8.1 → ctrl_kd-4.10.0}/src/ctrlkd/pictures.py +0 -0
  45. {ctrl_kd-4.8.1 → ctrl_kd-4.10.0}/src/ctrlkd/pix.py +0 -0
  46. {ctrl_kd-4.8.1 → ctrl_kd-4.10.0}/src/ctrlkd/piximg.py +0 -0
  47. {ctrl_kd-4.8.1 → ctrl_kd-4.10.0}/src/ctrlkd/samples/LYING.WS +0 -0
  48. {ctrl_kd-4.8.1 → ctrl_kd-4.10.0}/src/ctrlkd/samples/OCAPTAIN.WS +0 -0
  49. {ctrl_kd-4.8.1 → ctrl_kd-4.10.0}/src/ctrlkd/samples/TWAINLET.WS +0 -0
  50. {ctrl_kd-4.8.1 → ctrl_kd-4.10.0}/src/ctrlkd/samples/WARPRAYR.WS +0 -0
  51. {ctrl_kd-4.8.1 → ctrl_kd-4.10.0}/src/ctrlkd/symbolmap.py +0 -0
  52. {ctrl_kd-4.8.1 → ctrl_kd-4.10.0}/src/ctrlkd/typestyles.py +0 -0
  53. {ctrl_kd-4.8.1 → ctrl_kd-4.10.0}/src/ctrlkd/writer.py +0 -0
  54. {ctrl_kd-4.8.1 → ctrl_kd-4.10.0}/src/ctrlkd/wschange.py +0 -0
  55. {ctrl_kd-4.8.1 → ctrl_kd-4.10.0}/tests/test_bare_tab_modulus8.py +0 -0
  56. {ctrl_kd-4.8.1 → ctrl_kd-4.10.0}/tests/test_dot_comment_prints_nothing.py +0 -0
  57. {ctrl_kd-4.8.1 → ctrl_kd-4.10.0}/tests/test_driver_substitutions_exports.py +0 -0
  58. {ctrl_kd-4.8.1 → ctrl_kd-4.10.0}/tests/test_endnote_leading_gap.py +0 -0
  59. {ctrl_kd-4.8.1 → ctrl_kd-4.10.0}/tests/test_fidelity_gate.py +0 -0
  60. {ctrl_kd-4.8.1 → ctrl_kd-4.10.0}/tests/test_flags_toc_inline.py +0 -0
  61. {ctrl_kd-4.8.1 → ctrl_kd-4.10.0}/tests/test_form_feed_line_marks.py +0 -0
  62. {ctrl_kd-4.8.1 → ctrl_kd-4.10.0}/tests/test_glyph_aspect.py +0 -0
  63. {ctrl_kd-4.8.1 → ctrl_kd-4.10.0}/tests/test_graphic_cell_ops.py +0 -0
  64. {ctrl_kd-4.8.1 → ctrl_kd-4.10.0}/tests/test_head_foot_placement_m16.py +0 -0
  65. {ctrl_kd-4.8.1 → ctrl_kd-4.10.0}/tests/test_head_foot_timing.py +0 -0
  66. {ctrl_kd-4.8.1 → ctrl_kd-4.10.0}/tests/test_layout_marks.py +0 -0
  67. {ctrl_kd-4.8.1 → ctrl_kd-4.10.0}/tests/test_lj6dtp_bullet_glyph.py +0 -0
  68. {ctrl_kd-4.8.1 → ctrl_kd-4.10.0}/tests/test_lj6dtp_char_substitution.py +0 -0
  69. {ctrl_kd-4.8.1 → ctrl_kd-4.10.0}/tests/test_lj6dtp_colour_restore.py +0 -0
  70. {ctrl_kd-4.8.1 → ctrl_kd-4.10.0}/tests/test_lj6dtp_heading_face.py +0 -0
  71. {ctrl_kd-4.8.1 → ctrl_kd-4.10.0}/tests/test_lj6dtp_hp_patterns.py +0 -0
  72. {ctrl_kd-4.8.1 → ctrl_kd-4.10.0}/tests/test_lj6dtp_legend_line_spacing.py +0 -0
  73. {ctrl_kd-4.8.1 → ctrl_kd-4.10.0}/tests/test_lj6dtp_pcl_rectangles.py +0 -0
  74. {ctrl_kd-4.8.1 → ctrl_kd-4.10.0}/tests/test_lj6dtp_shading_table_rules.py +0 -0
  75. {ctrl_kd-4.8.1 → ctrl_kd-4.10.0}/tests/test_lj6dtp_table_rule_weight.py +0 -0
  76. {ctrl_kd-4.8.1 → ctrl_kd-4.10.0}/tests/test_load_plugins.py +0 -0
  77. {ctrl_kd-4.8.1 → ctrl_kd-4.10.0}/tests/test_mailmerge_data_files.py +0 -0
  78. {ctrl_kd-4.8.1 → ctrl_kd-4.10.0}/tests/test_modern_blank_after_tightened_line.py +0 -0
  79. {ctrl_kd-4.8.1 → ctrl_kd-4.10.0}/tests/test_modern_box_regions.py +0 -0
  80. {ctrl_kd-4.8.1 → ctrl_kd-4.10.0}/tests/test_modern_centring_clip.py +0 -0
  81. {ctrl_kd-4.8.1 → ctrl_kd-4.10.0}/tests/test_modern_columns.py +0 -0
  82. {ctrl_kd-4.8.1 → ctrl_kd-4.10.0}/tests/test_modern_line_spacing.py +0 -0
  83. {ctrl_kd-4.8.1 → ctrl_kd-4.10.0}/tests/test_modern_page_baseline.py +0 -0
  84. {ctrl_kd-4.8.1 → ctrl_kd-4.10.0}/tests/test_modern_page_number.py +0 -0
  85. {ctrl_kd-4.8.1 → ctrl_kd-4.10.0}/tests/test_modern_rtf_structure_rows.py +0 -0
  86. {ctrl_kd-4.8.1 → ctrl_kd-4.10.0}/tests/test_modern_sheet_height.py +0 -0
  87. {ctrl_kd-4.8.1 → ctrl_kd-4.10.0}/tests/test_modern_verse_defrow.py +0 -0
  88. {ctrl_kd-4.8.1 → ctrl_kd-4.10.0}/tests/test_note_rulings_20260824.py +0 -0
  89. {ctrl_kd-4.8.1 → ctrl_kd-4.10.0}/tests/test_page_parity.py +0 -0
  90. {ctrl_kd-4.8.1 → ctrl_kd-4.10.0}/tests/test_paper_verdicts.py +0 -0
  91. {ctrl_kd-4.8.1 → ctrl_kd-4.10.0}/tests/test_pcl_fidelity.py +0 -0
  92. {ctrl_kd-4.8.1 → ctrl_kd-4.10.0}/tests/test_pcl_page_membership.py +0 -0
  93. {ctrl_kd-4.8.1 → ctrl_kd-4.10.0}/tests/test_pcl_page_size_eject.py +0 -0
  94. {ctrl_kd-4.8.1 → ctrl_kd-4.10.0}/tests/test_pcl_tolerance.py +0 -0
  95. {ctrl_kd-4.8.1 → ctrl_kd-4.10.0}/tests/test_pcl_transparent_print.py +0 -0
  96. {ctrl_kd-4.8.1 → ctrl_kd-4.10.0}/tests/test_pcl_v4_untriaged_fixes.py +0 -0
  97. {ctrl_kd-4.8.1 → ctrl_kd-4.10.0}/tests/test_pcl_vertical_wraparound.py +0 -0
  98. {ctrl_kd-4.8.1 → ctrl_kd-4.10.0}/tests/test_peseta_euro_driver.py +0 -0
  99. {ctrl_kd-4.8.1 → ctrl_kd-4.10.0}/tests/test_pf_print_reformat.py +0 -0
  100. {ctrl_kd-4.8.1 → ctrl_kd-4.10.0}/tests/test_pictures.py +0 -0
  101. {ctrl_kd-4.8.1 → ctrl_kd-4.10.0}/tests/test_pix.py +0 -0
  102. {ctrl_kd-4.8.1 → ctrl_kd-4.10.0}/tests/test_pn_page_timing.py +0 -0
  103. {ctrl_kd-4.8.1 → ctrl_kd-4.10.0}/tests/test_polarity_gate.py +0 -0
  104. {ctrl_kd-4.8.1 → ctrl_kd-4.10.0}/tests/test_print_control_labels_invisible.py +0 -0
  105. {ctrl_kd-4.8.1 → ctrl_kd-4.10.0}/tests/test_rtf_keep_with_next.py +0 -0
  106. {ctrl_kd-4.8.1 → ctrl_kd-4.10.0}/tests/test_rtf_section_spine.py +0 -0
  107. {ctrl_kd-4.8.1 → ctrl_kd-4.10.0}/tests/test_samples.py +0 -0
  108. {ctrl_kd-4.8.1 → ctrl_kd-4.10.0}/tests/test_sawyer_corpus.py +0 -0
  109. {ctrl_kd-4.8.1 → ctrl_kd-4.10.0}/tests/test_screenplay_detection.py +0 -0
  110. {ctrl_kd-4.8.1 → ctrl_kd-4.10.0}/tests/test_screenplay_pdf.py +0 -0
  111. {ctrl_kd-4.8.1 → ctrl_kd-4.10.0}/tests/test_screenplay_rendering.py +0 -0
  112. {ctrl_kd-4.8.1 → ctrl_kd-4.10.0}/tests/test_sentence_spacing_n9.py +0 -0
  113. {ctrl_kd-4.8.1 → ctrl_kd-4.10.0}/tests/test_style_leading.py +0 -0
  114. {ctrl_kd-4.8.1 → ctrl_kd-4.10.0}/tests/test_style_strikeout_runs_until_cleared.py +0 -0
  115. {ctrl_kd-4.8.1 → ctrl_kd-4.10.0}/tests/test_style_symbol_map_resolved_face.py +0 -0
  116. {ctrl_kd-4.8.1 → ctrl_kd-4.10.0}/tests/test_symbol_span_graphic_chars.py +0 -0
  117. {ctrl_kd-4.8.1 → ctrl_kd-4.10.0}/tests/test_trailing_pa_break.py +0 -0
  118. {ctrl_kd-4.8.1 → ctrl_kd-4.10.0}/tests/test_verse_quote_couplet.py +0 -0
  119. {ctrl_kd-4.8.1 → ctrl_kd-4.10.0}/tests/test_verse_spacing.py +0 -0
  120. {ctrl_kd-4.8.1 → ctrl_kd-4.10.0}/tests/test_width_and_cp1252_memo.py +0 -0
  121. {ctrl_kd-4.8.1 → ctrl_kd-4.10.0}/tests/test_writer.py +0 -0
  122. {ctrl_kd-4.8.1 → ctrl_kd-4.10.0}/tests/test_wschange.py +0 -0
  123. {ctrl_kd-4.8.1 → ctrl_kd-4.10.0}/tests/test_zero_width_tab.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ctrl-kd
3
- Version: 4.8.1
3
+ Version: 4.10.0
4
4
  Summary: Convert WordStar 4-7 documents and print-to-disk files to text, Markdown, HTML, RTF, or PDF. ^KD: save and done.
5
5
  Author: Jon Michaels
6
6
  License: MIT
@@ -46,6 +46,8 @@ $ ctrl-kd --diagnose MYSTERY.FIL # what IS this file?
46
46
  $ ctrl-kd --comments MEMO.WS # include the author's hidden comments
47
47
  $ ctrl-kd --no-notes PAPER.WS # body text only, no notes
48
48
  $ ctrl-kd --samples DIR # write 4 bundled public-domain sample .WS files into DIR
49
+ $ ctrl-kd --list-quirks ESSAY.WS # which known quirks this file has, and why
50
+ $ ctrl-kd --quirk sawyer-strikeout NOVEL.WS # turn one on
49
51
  ```
50
52
 
51
53
  ## Modes
@@ -70,6 +72,31 @@ Download Windows x86_64: [Latest Version](https://github.com/jonmichaels/ctrl-kd
70
72
 
71
73
  Python ≥ 3.9, no dependencies. Library API: `ctrlkd.convert(data, to='html')`.
72
74
 
75
+ ## Quirks
76
+
77
+ Some WordStar files ask for something the bytes do not quite mean. A file's own
78
+ header names the printer it was last printed through, and some of those
79
+ printers were modified so that a character prints as something else — a peseta
80
+ printing as a euro sign, an underscore printing as an em dash. Other files
81
+ carry a setting somebody left switched on by accident, like a paragraph style
82
+ that crosses out every line it touches.
83
+
84
+ ctrl-kd is faithful by default and never quietly cleans anything up. Instead
85
+ each such departure is a named **quirk** you can see and switch:
86
+
87
+ ```console
88
+ $ ctrl-kd --list-quirks # every quirk, with a plain-language description
89
+ $ ctrl-kd --list-quirks NOVEL.WS # which of them apply to this file, and why
90
+ $ ctrl-kd --quirk NAME FILE # turn one on (repeatable)
91
+ $ ctrl-kd --no-quirk NAME FILE # turn one off (repeatable)
92
+ $ ctrl-kd --quirks off FILE # none at all: the most literal reading
93
+ ```
94
+
95
+ Quirks whose evidence is in the file itself (its own printer header) are on by
96
+ default; ones that are somebody's judgement rather than the file's own evidence
97
+ are off until you ask. Writing your own — including as a pip-installable
98
+ plugin — is in **[EXTENDING.md](EXTENDING.md)**.
99
+
73
100
  ## Adding an output format
74
101
 
75
102
  An output format is one function over the parsed document — register it with the
@@ -27,6 +27,8 @@ $ ctrl-kd --diagnose MYSTERY.FIL # what IS this file?
27
27
  $ ctrl-kd --comments MEMO.WS # include the author's hidden comments
28
28
  $ ctrl-kd --no-notes PAPER.WS # body text only, no notes
29
29
  $ ctrl-kd --samples DIR # write 4 bundled public-domain sample .WS files into DIR
30
+ $ ctrl-kd --list-quirks ESSAY.WS # which known quirks this file has, and why
31
+ $ ctrl-kd --quirk sawyer-strikeout NOVEL.WS # turn one on
30
32
  ```
31
33
 
32
34
  ## Modes
@@ -51,6 +53,31 @@ Download Windows x86_64: [Latest Version](https://github.com/jonmichaels/ctrl-kd
51
53
 
52
54
  Python ≥ 3.9, no dependencies. Library API: `ctrlkd.convert(data, to='html')`.
53
55
 
56
+ ## Quirks
57
+
58
+ Some WordStar files ask for something the bytes do not quite mean. A file's own
59
+ header names the printer it was last printed through, and some of those
60
+ printers were modified so that a character prints as something else — a peseta
61
+ printing as a euro sign, an underscore printing as an em dash. Other files
62
+ carry a setting somebody left switched on by accident, like a paragraph style
63
+ that crosses out every line it touches.
64
+
65
+ ctrl-kd is faithful by default and never quietly cleans anything up. Instead
66
+ each such departure is a named **quirk** you can see and switch:
67
+
68
+ ```console
69
+ $ ctrl-kd --list-quirks # every quirk, with a plain-language description
70
+ $ ctrl-kd --list-quirks NOVEL.WS # which of them apply to this file, and why
71
+ $ ctrl-kd --quirk NAME FILE # turn one on (repeatable)
72
+ $ ctrl-kd --no-quirk NAME FILE # turn one off (repeatable)
73
+ $ ctrl-kd --quirks off FILE # none at all: the most literal reading
74
+ ```
75
+
76
+ Quirks whose evidence is in the file itself (its own printer header) are on by
77
+ default; ones that are somebody's judgement rather than the file's own evidence
78
+ are off until you ask. Writing your own — including as a pip-installable
79
+ plugin — is in **[EXTENDING.md](EXTENDING.md)**.
80
+
54
81
  ## Adding an output format
55
82
 
56
83
  An output format is one function over the parsed document — register it with the
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ctrl-kd
3
- Version: 4.8.1
3
+ Version: 4.10.0
4
4
  Summary: Convert WordStar 4-7 documents and print-to-disk files to text, Markdown, HTML, RTF, or PDF. ^KD: save and done.
5
5
  Author: Jon Michaels
6
6
  License: MIT
@@ -46,6 +46,8 @@ $ ctrl-kd --diagnose MYSTERY.FIL # what IS this file?
46
46
  $ ctrl-kd --comments MEMO.WS # include the author's hidden comments
47
47
  $ ctrl-kd --no-notes PAPER.WS # body text only, no notes
48
48
  $ ctrl-kd --samples DIR # write 4 bundled public-domain sample .WS files into DIR
49
+ $ ctrl-kd --list-quirks ESSAY.WS # which known quirks this file has, and why
50
+ $ ctrl-kd --quirk sawyer-strikeout NOVEL.WS # turn one on
49
51
  ```
50
52
 
51
53
  ## Modes
@@ -70,6 +72,31 @@ Download Windows x86_64: [Latest Version](https://github.com/jonmichaels/ctrl-kd
70
72
 
71
73
  Python ≥ 3.9, no dependencies. Library API: `ctrlkd.convert(data, to='html')`.
72
74
 
75
+ ## Quirks
76
+
77
+ Some WordStar files ask for something the bytes do not quite mean. A file's own
78
+ header names the printer it was last printed through, and some of those
79
+ printers were modified so that a character prints as something else — a peseta
80
+ printing as a euro sign, an underscore printing as an em dash. Other files
81
+ carry a setting somebody left switched on by accident, like a paragraph style
82
+ that crosses out every line it touches.
83
+
84
+ ctrl-kd is faithful by default and never quietly cleans anything up. Instead
85
+ each such departure is a named **quirk** you can see and switch:
86
+
87
+ ```console
88
+ $ ctrl-kd --list-quirks # every quirk, with a plain-language description
89
+ $ ctrl-kd --list-quirks NOVEL.WS # which of them apply to this file, and why
90
+ $ ctrl-kd --quirk NAME FILE # turn one on (repeatable)
91
+ $ ctrl-kd --no-quirk NAME FILE # turn one off (repeatable)
92
+ $ ctrl-kd --quirks off FILE # none at all: the most literal reading
93
+ ```
94
+
95
+ Quirks whose evidence is in the file itself (its own printer header) are on by
96
+ default; ones that are somebody's judgement rather than the file's own evidence
97
+ are off until you ask. Writing your own — including as a pip-installable
98
+ plugin — is in **[EXTENDING.md](EXTENDING.md)**.
99
+
73
100
  ## Adding an output format
74
101
 
75
102
  An output format is one function over the parsed document — register it with the
@@ -19,6 +19,7 @@ src/ctrlkd/pdf.py
19
19
  src/ctrlkd/pictures.py
20
20
  src/ctrlkd/pix.py
21
21
  src/ctrlkd/piximg.py
22
+ src/ctrlkd/quirks.py
22
23
  src/ctrlkd/symbolmap.py
23
24
  src/ctrlkd/typestyles.py
24
25
  src/ctrlkd/writer.py
@@ -40,6 +41,7 @@ tests/test_graphic_cell_ops.py
40
41
  tests/test_graphic_cells.py
41
42
  tests/test_head_foot_placement_m16.py
42
43
  tests/test_head_foot_timing.py
44
+ tests/test_html_column_sections.py
43
45
  tests/test_html_print_stylesheet.py
44
46
  tests/test_justification.py
45
47
  tests/test_layout_marks.py
@@ -54,11 +56,14 @@ tests/test_lj6dtp_shading_table_rules.py
54
56
  tests/test_lj6dtp_table_rule_weight.py
55
57
  tests/test_load_plugins.py
56
58
  tests/test_mailmerge_data_files.py
59
+ tests/test_markdown_escaping_and_breaks.py
57
60
  tests/test_merge_page_number_variable.py
58
61
  tests/test_modern_blank_after_tightened_line.py
59
62
  tests/test_modern_box_regions.py
63
+ tests/test_modern_bullet_gap.py
60
64
  tests/test_modern_centring_clip.py
61
65
  tests/test_modern_columns.py
66
+ tests/test_modern_html_measure.py
62
67
  tests/test_modern_line_spacing.py
63
68
  tests/test_modern_lint.py
64
69
  tests/test_modern_page_baseline.py
@@ -66,6 +71,7 @@ tests/test_modern_page_number.py
66
71
  tests/test_modern_rtf_structure_rows.py
67
72
  tests/test_modern_sheet_height.py
68
73
  tests/test_modern_verse_defrow.py
74
+ tests/test_no_syntax_warnings.py
69
75
  tests/test_note_rulings_20260824.py
70
76
  tests/test_page_parity.py
71
77
  tests/test_paper_verdicts.py
@@ -77,6 +83,7 @@ tests/test_pcl_transparent_print.py
77
83
  tests/test_pcl_v4_column_geometry.py
78
84
  tests/test_pcl_v4_untriaged_fixes.py
79
85
  tests/test_pcl_vertical_wraparound.py
86
+ tests/test_per_page_orientation.py
80
87
  tests/test_peseta_euro_driver.py
81
88
  tests/test_pf_print_reformat.py
82
89
  tests/test_pictures.py
@@ -85,6 +92,10 @@ tests/test_pn_page_timing.py
85
92
  tests/test_polarity_gate.py
86
93
  tests/test_print_control_labels_invisible.py
87
94
  tests/test_printed_fidelity.py
95
+ tests/test_printed_html_single_break.py
96
+ tests/test_printed_rtf_ruler_column.py
97
+ tests/test_quirks.py
98
+ tests/test_reverse_video_ground.py
88
99
  tests/test_rtf_facing_pages.py
89
100
  tests/test_rtf_head_style_attrs.py
90
101
  tests/test_rtf_keep_with_next.py
@@ -4,8 +4,11 @@ from .core import (detect, parse, parse_ws, parse_printstream, merged_lines,
4
4
  from .emit import (emit_text, emit_markdown, emit_html, emit_rtf,
5
5
  emitter, get_emitter, formats, load_plugins)
6
6
  from .layout import modern_flow, emit_layout # registers the 'layout' format
7
+ from . import quirks # registers the built-in quirks
8
+ from .quirks import (Quirk, quirk, apply_quirks, list_quirks,
9
+ UnknownQuirk)
7
10
  from .pdf import emit_pdf # registers the 'pdf' format
8
11
  from .convert import convert, select_notes, DEFAULT_NOTE_KINDS, ALL_NOTE_KINDS
9
12
  from .info import document_info
10
13
 
11
- __version__ = '4.8.1'
14
+ __version__ = '4.10.0'
@@ -22,7 +22,7 @@ BANNER = r""" __ __ __ __
22
22
  # because the name never changes; no .flf machinery needed.
23
23
 
24
24
 
25
- from . import core, emit, pictures
25
+ from . import core, emit, pictures, quirks
26
26
  from .convert import DEFAULT_NOTE_KINDS # module attr, not the re-exported convert()
27
27
 
28
28
  def diagnose(path, data):
@@ -51,8 +51,21 @@ def write_samples(outdir):
51
51
  written.append(dest)
52
52
  return written
53
53
 
54
+ def _quirked(ap, doc, a):
55
+ """`doc` with this run's quirks resolved and applied. A bad NAME is a
56
+ usage error (a typo, or a plugin that isn't installed), not something to
57
+ swallow -- silently ignoring it would hand back output the caller did not
58
+ ask for."""
59
+ try:
60
+ return quirks.apply_quirks(doc, enable=a.quirk, disable=a.no_quirk,
61
+ mode=a.quirks)
62
+ except quirks.UnknownQuirk as e:
63
+ ap.error(str(e))
64
+
65
+
54
66
  def main(argv=None):
55
67
  emit.load_plugins() # third-party emitters (ctrlkd.emitters entry points)
68
+ quirks.load_plugins() # third-party quirks (ctrlkd.quirks entry points)
56
69
  ap = argparse.ArgumentParser(
57
70
  prog='ctrl-kd',
58
71
  formatter_class=argparse.RawDescriptionHelpFormatter,
@@ -130,17 +143,23 @@ def main(argv=None):
130
143
  help='omit paragraph-style pass-through (HTML classes + '
131
144
  'generated CSS, RTF stylesheet) from the output')
132
145
  ap.add_argument('--headers', choices=('on', 'off'), default='on',
133
- help="the document's own running heads and feet (.he/.fo) "
134
- 'in the paged surfaces -- Printed PDF, Modern PDF and '
135
- "both RTF modes. WordStar's own automatic page number "
136
- 'is --page-numbers\' business alone. Default: on')
146
+ help="the document's own running heads and feet (.he/.fo). "
147
+ 'PDF AND RTF ONLY -- those are the page formats; '
148
+ 'text, Markdown and HTML are unpaged by design and '
149
+ 'carry no running heads, feet or page numbers at '
150
+ 'all, so this flag is accepted and ignored for them '
151
+ "(no error). WordStar's own automatic page number is "
152
+ "--page-numbers' business alone. Default: on")
137
153
  ap.add_argument('--page-numbers', choices=('auto', 'on', 'off'),
138
154
  default='auto',
139
155
  help="WordStar's own AUTOMATIC page number -- the one "
140
156
  '.pc positions, a separate mechanism from a # the '
141
157
  'author placed inside a real .he/.fo (that goes '
142
158
  'with its head, under --headers, never this flag). '
143
- 'Every paged surface. auto (DEFAULT): the '
159
+ 'PDF AND RTF ONLY, for the same reason --headers is: '
160
+ 'text, Markdown and HTML are unpaged by design, so '
161
+ 'the flag is accepted and ignored for them (no '
162
+ 'error). auto (DEFAULT): the '
144
163
  'document\'s own dot '
145
164
  'commands decide -- .pn/.pg turn it on, .op turns '
146
165
  'it off, exactly like real WordStar; a document '
@@ -179,9 +198,9 @@ def main(argv=None):
179
198
  'each entry to a real page number, every other '
180
199
  'format lists entries without one. Default: off')
181
200
  ap.add_argument('--inline-styling', choices=('on', 'off'), default='on',
182
- help='inline colour (^A) and font-size (^B... a symmetric '
201
+ help='inline color (^A) and font-size (^B... a symmetric '
183
202
  'type-2 font block) changes the author placed mid-'
184
- 'text -- RTF gets \\cf from a 16-colour screen '
203
+ 'text -- RTF gets \\cf from a 16-color screen '
185
204
  'palette and \\fsN; HTML gets a span with color/'
186
205
  'font-size. Default: on')
187
206
  ap.add_argument('--pictures', choices=('off', 'embed', 'export'), default='embed',
@@ -202,6 +221,36 @@ def main(argv=None):
202
221
  ap.add_argument('--comments', action='store_true',
203
222
  help="include WordStar comments, which it never printed "
204
223
  "(author's asides, hidden since the file was written)")
224
+ ap.add_argument('--quirk', action='append', metavar='NAME', default=[],
225
+ help='turn a named quirk ON for this conversion '
226
+ '(repeatable). A quirk is a NAMED departure from a '
227
+ 'literal reading of the bytes -- see --list-quirks '
228
+ 'for every name this build knows, what it does, and '
229
+ 'whether it applies to your file. Naming a quirk '
230
+ 'the document does not trip does nothing (so one '
231
+ 'standing list can be passed to every file); naming '
232
+ 'one that does not exist is an error.')
233
+ ap.add_argument('--no-quirk', action='append', metavar='NAME', default=[],
234
+ help='turn a named quirk OFF (repeatable). The quirks '
235
+ 'that are on by default are the ones the document '
236
+ "itself points at -- its own last-used printer "
237
+ 'driver changed what some characters print as -- so '
238
+ 'this is how you see the raw character instead.')
239
+ ap.add_argument('--quirks', choices=('auto', 'off', 'all'), default='auto',
240
+ help='the baseline --quirk/--no-quirk then adjust. auto '
241
+ '(DEFAULT): the quirks the document itself points '
242
+ 'at, and no others. off: none at all, the most '
243
+ 'literal reading of the bytes this converter can '
244
+ 'give. all: every quirk that applies to this '
245
+ 'document, including the ones that are off by '
246
+ 'default because they are somebody\'s judgement '
247
+ 'rather than the file\'s own evidence.')
248
+ ap.add_argument('--list-quirks', action='store_true',
249
+ help='list every quirk this build knows as JSON -- name, '
250
+ 'one-line description, and whether it is on by '
251
+ 'default; WITH file arguments, also whether each '
252
+ 'applies to that file, why, and whether it is in '
253
+ 'force. No conversion.')
205
254
  ap.add_argument('--diagnose', action='store_true',
206
255
  help='report what the file is (variant, margin, dot commands, '
207
256
  'unknown codes) as JSON; no conversion')
@@ -210,6 +259,27 @@ def main(argv=None):
210
259
  for dest in write_samples(a.samples):
211
260
  print(f'-> {dest}', file=sys.stderr)
212
261
  return 0
262
+ if a.list_quirks:
263
+ if not a.files:
264
+ print(json.dumps(quirks.list_quirks(), indent=2, ensure_ascii=False,
265
+ sort_keys=True))
266
+ return 0
267
+ status = 0
268
+ for path in a.files:
269
+ try:
270
+ data = open(path, 'rb').read()
271
+ doc = core.parse(data, encoding=a.encoding, variant=a.variant)
272
+ except (OSError, ValueError) as e:
273
+ print(f'ctrl-kd: {path}: {e}', file=sys.stderr)
274
+ status = 1
275
+ continue
276
+ doc = _quirked(ap, doc, a)
277
+ # sort_keys/ensure_ascii chosen to MATCH sr's own JSON writer exactly,
278
+ # so the two CLIs' --list-quirks output is byte-identical.
279
+ print(json.dumps({'file': os.path.basename(path),
280
+ 'quirks': quirks.list_quirks(doc)},
281
+ indent=2, ensure_ascii=False, sort_keys=True))
282
+ return status
213
283
  if not a.files:
214
284
  ap.error('the following arguments are required: files')
215
285
  mode_explicit = a.mode is not None
@@ -284,6 +354,10 @@ def main(argv=None):
284
354
  f'--variant to force)', file=sys.stderr)
285
355
  status = 1
286
356
  continue
357
+ # Quirks are resolved ONCE per document, here, before any emitter
358
+ # reads it -- so every requested format renders the same decision and
359
+ # the layout JSON's own report can never disagree with what ran.
360
+ doc = _quirked(ap, doc, a)
287
361
  # D5 notice (ruled 2026-08-05): when the user EXPLICITLY asked for
288
362
  # modern and the input can only render printed, say so once instead
289
363
  # of silently disobeying the flag. The override itself stands -- a
@@ -320,7 +394,7 @@ def main(argv=None):
320
394
  if (a.mode == 'modern'
321
395
  and doc.meta.get('printer_driver') == 'LJ6DTP'):
322
396
  print(f'ctrl-kd: {path}: LJ6DTP driver document -- its '
323
- f'print-time page art (boxes, rules, colour) does not '
397
+ f'print-time page art (boxes, rules, color) does not '
324
398
  f'reflow; character substitutions applied. '
325
399
  f'--mode printed reproduces the page', file=sys.stderr)
326
400
  # --page-settings applies ONCE to the resolved page dict, so every