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.
- {ctrl_kd-4.8.1/src/ctrl_kd.egg-info → ctrl_kd-4.10.0}/PKG-INFO +28 -1
- {ctrl_kd-4.8.1 → ctrl_kd-4.10.0}/README.md +27 -0
- {ctrl_kd-4.8.1 → ctrl_kd-4.10.0/src/ctrl_kd.egg-info}/PKG-INFO +28 -1
- {ctrl_kd-4.8.1 → ctrl_kd-4.10.0}/src/ctrl_kd.egg-info/SOURCES.txt +11 -0
- {ctrl_kd-4.8.1 → ctrl_kd-4.10.0}/src/ctrlkd/__init__.py +4 -1
- {ctrl_kd-4.8.1 → ctrl_kd-4.10.0}/src/ctrlkd/cli.py +83 -9
- {ctrl_kd-4.8.1 → ctrl_kd-4.10.0}/src/ctrlkd/core.py +217 -121
- {ctrl_kd-4.8.1 → ctrl_kd-4.10.0}/src/ctrlkd/emit.py +721 -77
- {ctrl_kd-4.8.1 → ctrl_kd-4.10.0}/src/ctrlkd/layout.py +128 -17
- {ctrl_kd-4.8.1 → ctrl_kd-4.10.0}/src/ctrlkd/pdf.py +574 -76
- ctrl_kd-4.10.0/src/ctrlkd/quirks.py +501 -0
- {ctrl_kd-4.8.1 → ctrl_kd-4.10.0}/tests/test_ctrlkd.py +96 -25
- {ctrl_kd-4.8.1 → ctrl_kd-4.10.0}/tests/test_graphic_cells.py +2 -2
- ctrl_kd-4.10.0/tests/test_html_column_sections.py +74 -0
- {ctrl_kd-4.8.1 → ctrl_kd-4.10.0}/tests/test_html_print_stylesheet.py +8 -2
- {ctrl_kd-4.8.1 → ctrl_kd-4.10.0}/tests/test_justification.py +1 -1
- ctrl_kd-4.10.0/tests/test_markdown_escaping_and_breaks.py +238 -0
- {ctrl_kd-4.8.1 → ctrl_kd-4.10.0}/tests/test_merge_page_number_variable.py +7 -3
- ctrl_kd-4.10.0/tests/test_modern_bullet_gap.py +141 -0
- ctrl_kd-4.10.0/tests/test_modern_html_measure.py +77 -0
- {ctrl_kd-4.8.1 → ctrl_kd-4.10.0}/tests/test_modern_lint.py +17 -1
- ctrl_kd-4.10.0/tests/test_no_syntax_warnings.py +105 -0
- {ctrl_kd-4.8.1 → ctrl_kd-4.10.0}/tests/test_pcl_v4_column_geometry.py +1 -1
- ctrl_kd-4.10.0/tests/test_per_page_orientation.py +167 -0
- {ctrl_kd-4.8.1 → ctrl_kd-4.10.0}/tests/test_printed_fidelity.py +1 -1
- ctrl_kd-4.10.0/tests/test_printed_html_single_break.py +57 -0
- ctrl_kd-4.10.0/tests/test_printed_rtf_ruler_column.py +116 -0
- ctrl_kd-4.10.0/tests/test_quirks.py +640 -0
- ctrl_kd-4.10.0/tests/test_reverse_video_ground.py +86 -0
- {ctrl_kd-4.8.1 → ctrl_kd-4.10.0}/tests/test_rtf_facing_pages.py +12 -4
- {ctrl_kd-4.8.1 → ctrl_kd-4.10.0}/tests/test_rtf_head_style_attrs.py +9 -5
- {ctrl_kd-4.8.1 → ctrl_kd-4.10.0}/tests/test_rtf_page_numbers.py +22 -13
- {ctrl_kd-4.8.1 → ctrl_kd-4.10.0}/tests/test_structural_checks.py +1 -1
- {ctrl_kd-4.8.1 → ctrl_kd-4.10.0}/LICENSE +0 -0
- {ctrl_kd-4.8.1 → ctrl_kd-4.10.0}/pyproject.toml +0 -0
- {ctrl_kd-4.8.1 → ctrl_kd-4.10.0}/setup.cfg +0 -0
- {ctrl_kd-4.8.1 → ctrl_kd-4.10.0}/src/ctrl_kd.egg-info/dependency_links.txt +0 -0
- {ctrl_kd-4.8.1 → ctrl_kd-4.10.0}/src/ctrl_kd.egg-info/entry_points.txt +0 -0
- {ctrl_kd-4.8.1 → ctrl_kd-4.10.0}/src/ctrl_kd.egg-info/top_level.txt +0 -0
- {ctrl_kd-4.8.1 → ctrl_kd-4.10.0}/src/ctrlkd/afm.py +0 -0
- {ctrl_kd-4.8.1 → ctrl_kd-4.10.0}/src/ctrlkd/convert.py +0 -0
- {ctrl_kd-4.8.1 → ctrl_kd-4.10.0}/src/ctrlkd/fontmap.py +0 -0
- {ctrl_kd-4.8.1 → ctrl_kd-4.10.0}/src/ctrlkd/info.py +0 -0
- {ctrl_kd-4.8.1 → ctrl_kd-4.10.0}/src/ctrlkd/pictures.py +0 -0
- {ctrl_kd-4.8.1 → ctrl_kd-4.10.0}/src/ctrlkd/pix.py +0 -0
- {ctrl_kd-4.8.1 → ctrl_kd-4.10.0}/src/ctrlkd/piximg.py +0 -0
- {ctrl_kd-4.8.1 → ctrl_kd-4.10.0}/src/ctrlkd/samples/LYING.WS +0 -0
- {ctrl_kd-4.8.1 → ctrl_kd-4.10.0}/src/ctrlkd/samples/OCAPTAIN.WS +0 -0
- {ctrl_kd-4.8.1 → ctrl_kd-4.10.0}/src/ctrlkd/samples/TWAINLET.WS +0 -0
- {ctrl_kd-4.8.1 → ctrl_kd-4.10.0}/src/ctrlkd/samples/WARPRAYR.WS +0 -0
- {ctrl_kd-4.8.1 → ctrl_kd-4.10.0}/src/ctrlkd/symbolmap.py +0 -0
- {ctrl_kd-4.8.1 → ctrl_kd-4.10.0}/src/ctrlkd/typestyles.py +0 -0
- {ctrl_kd-4.8.1 → ctrl_kd-4.10.0}/src/ctrlkd/writer.py +0 -0
- {ctrl_kd-4.8.1 → ctrl_kd-4.10.0}/src/ctrlkd/wschange.py +0 -0
- {ctrl_kd-4.8.1 → ctrl_kd-4.10.0}/tests/test_bare_tab_modulus8.py +0 -0
- {ctrl_kd-4.8.1 → ctrl_kd-4.10.0}/tests/test_dot_comment_prints_nothing.py +0 -0
- {ctrl_kd-4.8.1 → ctrl_kd-4.10.0}/tests/test_driver_substitutions_exports.py +0 -0
- {ctrl_kd-4.8.1 → ctrl_kd-4.10.0}/tests/test_endnote_leading_gap.py +0 -0
- {ctrl_kd-4.8.1 → ctrl_kd-4.10.0}/tests/test_fidelity_gate.py +0 -0
- {ctrl_kd-4.8.1 → ctrl_kd-4.10.0}/tests/test_flags_toc_inline.py +0 -0
- {ctrl_kd-4.8.1 → ctrl_kd-4.10.0}/tests/test_form_feed_line_marks.py +0 -0
- {ctrl_kd-4.8.1 → ctrl_kd-4.10.0}/tests/test_glyph_aspect.py +0 -0
- {ctrl_kd-4.8.1 → ctrl_kd-4.10.0}/tests/test_graphic_cell_ops.py +0 -0
- {ctrl_kd-4.8.1 → ctrl_kd-4.10.0}/tests/test_head_foot_placement_m16.py +0 -0
- {ctrl_kd-4.8.1 → ctrl_kd-4.10.0}/tests/test_head_foot_timing.py +0 -0
- {ctrl_kd-4.8.1 → ctrl_kd-4.10.0}/tests/test_layout_marks.py +0 -0
- {ctrl_kd-4.8.1 → ctrl_kd-4.10.0}/tests/test_lj6dtp_bullet_glyph.py +0 -0
- {ctrl_kd-4.8.1 → ctrl_kd-4.10.0}/tests/test_lj6dtp_char_substitution.py +0 -0
- {ctrl_kd-4.8.1 → ctrl_kd-4.10.0}/tests/test_lj6dtp_colour_restore.py +0 -0
- {ctrl_kd-4.8.1 → ctrl_kd-4.10.0}/tests/test_lj6dtp_heading_face.py +0 -0
- {ctrl_kd-4.8.1 → ctrl_kd-4.10.0}/tests/test_lj6dtp_hp_patterns.py +0 -0
- {ctrl_kd-4.8.1 → ctrl_kd-4.10.0}/tests/test_lj6dtp_legend_line_spacing.py +0 -0
- {ctrl_kd-4.8.1 → ctrl_kd-4.10.0}/tests/test_lj6dtp_pcl_rectangles.py +0 -0
- {ctrl_kd-4.8.1 → ctrl_kd-4.10.0}/tests/test_lj6dtp_shading_table_rules.py +0 -0
- {ctrl_kd-4.8.1 → ctrl_kd-4.10.0}/tests/test_lj6dtp_table_rule_weight.py +0 -0
- {ctrl_kd-4.8.1 → ctrl_kd-4.10.0}/tests/test_load_plugins.py +0 -0
- {ctrl_kd-4.8.1 → ctrl_kd-4.10.0}/tests/test_mailmerge_data_files.py +0 -0
- {ctrl_kd-4.8.1 → ctrl_kd-4.10.0}/tests/test_modern_blank_after_tightened_line.py +0 -0
- {ctrl_kd-4.8.1 → ctrl_kd-4.10.0}/tests/test_modern_box_regions.py +0 -0
- {ctrl_kd-4.8.1 → ctrl_kd-4.10.0}/tests/test_modern_centring_clip.py +0 -0
- {ctrl_kd-4.8.1 → ctrl_kd-4.10.0}/tests/test_modern_columns.py +0 -0
- {ctrl_kd-4.8.1 → ctrl_kd-4.10.0}/tests/test_modern_line_spacing.py +0 -0
- {ctrl_kd-4.8.1 → ctrl_kd-4.10.0}/tests/test_modern_page_baseline.py +0 -0
- {ctrl_kd-4.8.1 → ctrl_kd-4.10.0}/tests/test_modern_page_number.py +0 -0
- {ctrl_kd-4.8.1 → ctrl_kd-4.10.0}/tests/test_modern_rtf_structure_rows.py +0 -0
- {ctrl_kd-4.8.1 → ctrl_kd-4.10.0}/tests/test_modern_sheet_height.py +0 -0
- {ctrl_kd-4.8.1 → ctrl_kd-4.10.0}/tests/test_modern_verse_defrow.py +0 -0
- {ctrl_kd-4.8.1 → ctrl_kd-4.10.0}/tests/test_note_rulings_20260824.py +0 -0
- {ctrl_kd-4.8.1 → ctrl_kd-4.10.0}/tests/test_page_parity.py +0 -0
- {ctrl_kd-4.8.1 → ctrl_kd-4.10.0}/tests/test_paper_verdicts.py +0 -0
- {ctrl_kd-4.8.1 → ctrl_kd-4.10.0}/tests/test_pcl_fidelity.py +0 -0
- {ctrl_kd-4.8.1 → ctrl_kd-4.10.0}/tests/test_pcl_page_membership.py +0 -0
- {ctrl_kd-4.8.1 → ctrl_kd-4.10.0}/tests/test_pcl_page_size_eject.py +0 -0
- {ctrl_kd-4.8.1 → ctrl_kd-4.10.0}/tests/test_pcl_tolerance.py +0 -0
- {ctrl_kd-4.8.1 → ctrl_kd-4.10.0}/tests/test_pcl_transparent_print.py +0 -0
- {ctrl_kd-4.8.1 → ctrl_kd-4.10.0}/tests/test_pcl_v4_untriaged_fixes.py +0 -0
- {ctrl_kd-4.8.1 → ctrl_kd-4.10.0}/tests/test_pcl_vertical_wraparound.py +0 -0
- {ctrl_kd-4.8.1 → ctrl_kd-4.10.0}/tests/test_peseta_euro_driver.py +0 -0
- {ctrl_kd-4.8.1 → ctrl_kd-4.10.0}/tests/test_pf_print_reformat.py +0 -0
- {ctrl_kd-4.8.1 → ctrl_kd-4.10.0}/tests/test_pictures.py +0 -0
- {ctrl_kd-4.8.1 → ctrl_kd-4.10.0}/tests/test_pix.py +0 -0
- {ctrl_kd-4.8.1 → ctrl_kd-4.10.0}/tests/test_pn_page_timing.py +0 -0
- {ctrl_kd-4.8.1 → ctrl_kd-4.10.0}/tests/test_polarity_gate.py +0 -0
- {ctrl_kd-4.8.1 → ctrl_kd-4.10.0}/tests/test_print_control_labels_invisible.py +0 -0
- {ctrl_kd-4.8.1 → ctrl_kd-4.10.0}/tests/test_rtf_keep_with_next.py +0 -0
- {ctrl_kd-4.8.1 → ctrl_kd-4.10.0}/tests/test_rtf_section_spine.py +0 -0
- {ctrl_kd-4.8.1 → ctrl_kd-4.10.0}/tests/test_samples.py +0 -0
- {ctrl_kd-4.8.1 → ctrl_kd-4.10.0}/tests/test_sawyer_corpus.py +0 -0
- {ctrl_kd-4.8.1 → ctrl_kd-4.10.0}/tests/test_screenplay_detection.py +0 -0
- {ctrl_kd-4.8.1 → ctrl_kd-4.10.0}/tests/test_screenplay_pdf.py +0 -0
- {ctrl_kd-4.8.1 → ctrl_kd-4.10.0}/tests/test_screenplay_rendering.py +0 -0
- {ctrl_kd-4.8.1 → ctrl_kd-4.10.0}/tests/test_sentence_spacing_n9.py +0 -0
- {ctrl_kd-4.8.1 → ctrl_kd-4.10.0}/tests/test_style_leading.py +0 -0
- {ctrl_kd-4.8.1 → ctrl_kd-4.10.0}/tests/test_style_strikeout_runs_until_cleared.py +0 -0
- {ctrl_kd-4.8.1 → ctrl_kd-4.10.0}/tests/test_style_symbol_map_resolved_face.py +0 -0
- {ctrl_kd-4.8.1 → ctrl_kd-4.10.0}/tests/test_symbol_span_graphic_chars.py +0 -0
- {ctrl_kd-4.8.1 → ctrl_kd-4.10.0}/tests/test_trailing_pa_break.py +0 -0
- {ctrl_kd-4.8.1 → ctrl_kd-4.10.0}/tests/test_verse_quote_couplet.py +0 -0
- {ctrl_kd-4.8.1 → ctrl_kd-4.10.0}/tests/test_verse_spacing.py +0 -0
- {ctrl_kd-4.8.1 → ctrl_kd-4.10.0}/tests/test_width_and_cp1252_memo.py +0 -0
- {ctrl_kd-4.8.1 → ctrl_kd-4.10.0}/tests/test_writer.py +0 -0
- {ctrl_kd-4.8.1 → ctrl_kd-4.10.0}/tests/test_wschange.py +0 -0
- {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.
|
|
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.
|
|
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.
|
|
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
|
-
'
|
|
135
|
-
|
|
136
|
-
'
|
|
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
|
-
'
|
|
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
|
|
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-
|
|
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,
|
|
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
|