ctrl-kd 4.8.0__tar.gz → 4.9.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.0/src/ctrl_kd.egg-info → ctrl_kd-4.9.0}/PKG-INFO +28 -1
- {ctrl_kd-4.8.0 → ctrl_kd-4.9.0}/README.md +27 -0
- {ctrl_kd-4.8.0 → ctrl_kd-4.9.0/src/ctrl_kd.egg-info}/PKG-INFO +28 -1
- {ctrl_kd-4.8.0 → ctrl_kd-4.9.0}/src/ctrl_kd.egg-info/SOURCES.txt +4 -0
- {ctrl_kd-4.8.0 → ctrl_kd-4.9.0}/src/ctrlkd/__init__.py +4 -1
- {ctrl_kd-4.8.0 → ctrl_kd-4.9.0}/src/ctrlkd/cli.py +69 -1
- {ctrl_kd-4.8.0 → ctrl_kd-4.9.0}/src/ctrlkd/core.py +237 -92
- {ctrl_kd-4.8.0 → ctrl_kd-4.9.0}/src/ctrlkd/emit.py +46 -0
- {ctrl_kd-4.8.0 → ctrl_kd-4.9.0}/src/ctrlkd/layout.py +55 -8
- {ctrl_kd-4.8.0 → ctrl_kd-4.9.0}/src/ctrlkd/pdf.py +129 -33
- ctrl_kd-4.9.0/src/ctrlkd/quirks.py +461 -0
- {ctrl_kd-4.8.0 → ctrl_kd-4.9.0}/src/ctrlkd/symbolmap.py +40 -3
- {ctrl_kd-4.8.0 → ctrl_kd-4.9.0}/tests/test_ctrlkd.py +3 -3
- {ctrl_kd-4.8.0 → ctrl_kd-4.9.0}/tests/test_graphic_cells.py +2 -2
- {ctrl_kd-4.8.0 → ctrl_kd-4.9.0}/tests/test_justification.py +1 -1
- {ctrl_kd-4.8.0 → ctrl_kd-4.9.0}/tests/test_pcl_v4_column_geometry.py +1 -1
- {ctrl_kd-4.8.0 → ctrl_kd-4.9.0}/tests/test_printed_fidelity.py +1 -1
- ctrl_kd-4.9.0/tests/test_quirks.py +575 -0
- ctrl_kd-4.9.0/tests/test_style_strikeout_runs_until_cleared.py +297 -0
- ctrl_kd-4.9.0/tests/test_style_symbol_map_resolved_face.py +173 -0
- {ctrl_kd-4.8.0 → ctrl_kd-4.9.0}/tests/test_symbol_span_graphic_chars.py +30 -12
- {ctrl_kd-4.8.0 → ctrl_kd-4.9.0}/LICENSE +0 -0
- {ctrl_kd-4.8.0 → ctrl_kd-4.9.0}/pyproject.toml +0 -0
- {ctrl_kd-4.8.0 → ctrl_kd-4.9.0}/setup.cfg +0 -0
- {ctrl_kd-4.8.0 → ctrl_kd-4.9.0}/src/ctrl_kd.egg-info/dependency_links.txt +0 -0
- {ctrl_kd-4.8.0 → ctrl_kd-4.9.0}/src/ctrl_kd.egg-info/entry_points.txt +0 -0
- {ctrl_kd-4.8.0 → ctrl_kd-4.9.0}/src/ctrl_kd.egg-info/top_level.txt +0 -0
- {ctrl_kd-4.8.0 → ctrl_kd-4.9.0}/src/ctrlkd/afm.py +0 -0
- {ctrl_kd-4.8.0 → ctrl_kd-4.9.0}/src/ctrlkd/convert.py +0 -0
- {ctrl_kd-4.8.0 → ctrl_kd-4.9.0}/src/ctrlkd/fontmap.py +0 -0
- {ctrl_kd-4.8.0 → ctrl_kd-4.9.0}/src/ctrlkd/info.py +0 -0
- {ctrl_kd-4.8.0 → ctrl_kd-4.9.0}/src/ctrlkd/pictures.py +0 -0
- {ctrl_kd-4.8.0 → ctrl_kd-4.9.0}/src/ctrlkd/pix.py +0 -0
- {ctrl_kd-4.8.0 → ctrl_kd-4.9.0}/src/ctrlkd/piximg.py +0 -0
- {ctrl_kd-4.8.0 → ctrl_kd-4.9.0}/src/ctrlkd/samples/LYING.WS +0 -0
- {ctrl_kd-4.8.0 → ctrl_kd-4.9.0}/src/ctrlkd/samples/OCAPTAIN.WS +0 -0
- {ctrl_kd-4.8.0 → ctrl_kd-4.9.0}/src/ctrlkd/samples/TWAINLET.WS +0 -0
- {ctrl_kd-4.8.0 → ctrl_kd-4.9.0}/src/ctrlkd/samples/WARPRAYR.WS +0 -0
- {ctrl_kd-4.8.0 → ctrl_kd-4.9.0}/src/ctrlkd/typestyles.py +0 -0
- {ctrl_kd-4.8.0 → ctrl_kd-4.9.0}/src/ctrlkd/writer.py +0 -0
- {ctrl_kd-4.8.0 → ctrl_kd-4.9.0}/src/ctrlkd/wschange.py +0 -0
- {ctrl_kd-4.8.0 → ctrl_kd-4.9.0}/tests/test_bare_tab_modulus8.py +0 -0
- {ctrl_kd-4.8.0 → ctrl_kd-4.9.0}/tests/test_dot_comment_prints_nothing.py +0 -0
- {ctrl_kd-4.8.0 → ctrl_kd-4.9.0}/tests/test_driver_substitutions_exports.py +0 -0
- {ctrl_kd-4.8.0 → ctrl_kd-4.9.0}/tests/test_endnote_leading_gap.py +0 -0
- {ctrl_kd-4.8.0 → ctrl_kd-4.9.0}/tests/test_fidelity_gate.py +0 -0
- {ctrl_kd-4.8.0 → ctrl_kd-4.9.0}/tests/test_flags_toc_inline.py +0 -0
- {ctrl_kd-4.8.0 → ctrl_kd-4.9.0}/tests/test_form_feed_line_marks.py +0 -0
- {ctrl_kd-4.8.0 → ctrl_kd-4.9.0}/tests/test_glyph_aspect.py +0 -0
- {ctrl_kd-4.8.0 → ctrl_kd-4.9.0}/tests/test_graphic_cell_ops.py +0 -0
- {ctrl_kd-4.8.0 → ctrl_kd-4.9.0}/tests/test_head_foot_placement_m16.py +0 -0
- {ctrl_kd-4.8.0 → ctrl_kd-4.9.0}/tests/test_head_foot_timing.py +0 -0
- {ctrl_kd-4.8.0 → ctrl_kd-4.9.0}/tests/test_html_print_stylesheet.py +0 -0
- {ctrl_kd-4.8.0 → ctrl_kd-4.9.0}/tests/test_layout_marks.py +0 -0
- {ctrl_kd-4.8.0 → ctrl_kd-4.9.0}/tests/test_lj6dtp_bullet_glyph.py +0 -0
- {ctrl_kd-4.8.0 → ctrl_kd-4.9.0}/tests/test_lj6dtp_char_substitution.py +0 -0
- {ctrl_kd-4.8.0 → ctrl_kd-4.9.0}/tests/test_lj6dtp_colour_restore.py +0 -0
- {ctrl_kd-4.8.0 → ctrl_kd-4.9.0}/tests/test_lj6dtp_heading_face.py +0 -0
- {ctrl_kd-4.8.0 → ctrl_kd-4.9.0}/tests/test_lj6dtp_hp_patterns.py +0 -0
- {ctrl_kd-4.8.0 → ctrl_kd-4.9.0}/tests/test_lj6dtp_legend_line_spacing.py +0 -0
- {ctrl_kd-4.8.0 → ctrl_kd-4.9.0}/tests/test_lj6dtp_pcl_rectangles.py +0 -0
- {ctrl_kd-4.8.0 → ctrl_kd-4.9.0}/tests/test_lj6dtp_shading_table_rules.py +0 -0
- {ctrl_kd-4.8.0 → ctrl_kd-4.9.0}/tests/test_lj6dtp_table_rule_weight.py +0 -0
- {ctrl_kd-4.8.0 → ctrl_kd-4.9.0}/tests/test_load_plugins.py +0 -0
- {ctrl_kd-4.8.0 → ctrl_kd-4.9.0}/tests/test_mailmerge_data_files.py +0 -0
- {ctrl_kd-4.8.0 → ctrl_kd-4.9.0}/tests/test_merge_page_number_variable.py +0 -0
- {ctrl_kd-4.8.0 → ctrl_kd-4.9.0}/tests/test_modern_blank_after_tightened_line.py +0 -0
- {ctrl_kd-4.8.0 → ctrl_kd-4.9.0}/tests/test_modern_box_regions.py +0 -0
- {ctrl_kd-4.8.0 → ctrl_kd-4.9.0}/tests/test_modern_centring_clip.py +0 -0
- {ctrl_kd-4.8.0 → ctrl_kd-4.9.0}/tests/test_modern_columns.py +0 -0
- {ctrl_kd-4.8.0 → ctrl_kd-4.9.0}/tests/test_modern_line_spacing.py +0 -0
- {ctrl_kd-4.8.0 → ctrl_kd-4.9.0}/tests/test_modern_lint.py +0 -0
- {ctrl_kd-4.8.0 → ctrl_kd-4.9.0}/tests/test_modern_page_baseline.py +0 -0
- {ctrl_kd-4.8.0 → ctrl_kd-4.9.0}/tests/test_modern_page_number.py +0 -0
- {ctrl_kd-4.8.0 → ctrl_kd-4.9.0}/tests/test_modern_rtf_structure_rows.py +0 -0
- {ctrl_kd-4.8.0 → ctrl_kd-4.9.0}/tests/test_modern_sheet_height.py +0 -0
- {ctrl_kd-4.8.0 → ctrl_kd-4.9.0}/tests/test_modern_verse_defrow.py +0 -0
- {ctrl_kd-4.8.0 → ctrl_kd-4.9.0}/tests/test_note_rulings_20260824.py +0 -0
- {ctrl_kd-4.8.0 → ctrl_kd-4.9.0}/tests/test_page_parity.py +0 -0
- {ctrl_kd-4.8.0 → ctrl_kd-4.9.0}/tests/test_paper_verdicts.py +0 -0
- {ctrl_kd-4.8.0 → ctrl_kd-4.9.0}/tests/test_pcl_fidelity.py +0 -0
- {ctrl_kd-4.8.0 → ctrl_kd-4.9.0}/tests/test_pcl_page_membership.py +0 -0
- {ctrl_kd-4.8.0 → ctrl_kd-4.9.0}/tests/test_pcl_page_size_eject.py +0 -0
- {ctrl_kd-4.8.0 → ctrl_kd-4.9.0}/tests/test_pcl_tolerance.py +0 -0
- {ctrl_kd-4.8.0 → ctrl_kd-4.9.0}/tests/test_pcl_transparent_print.py +0 -0
- {ctrl_kd-4.8.0 → ctrl_kd-4.9.0}/tests/test_pcl_v4_untriaged_fixes.py +0 -0
- {ctrl_kd-4.8.0 → ctrl_kd-4.9.0}/tests/test_pcl_vertical_wraparound.py +0 -0
- {ctrl_kd-4.8.0 → ctrl_kd-4.9.0}/tests/test_peseta_euro_driver.py +0 -0
- {ctrl_kd-4.8.0 → ctrl_kd-4.9.0}/tests/test_pf_print_reformat.py +0 -0
- {ctrl_kd-4.8.0 → ctrl_kd-4.9.0}/tests/test_pictures.py +0 -0
- {ctrl_kd-4.8.0 → ctrl_kd-4.9.0}/tests/test_pix.py +0 -0
- {ctrl_kd-4.8.0 → ctrl_kd-4.9.0}/tests/test_pn_page_timing.py +0 -0
- {ctrl_kd-4.8.0 → ctrl_kd-4.9.0}/tests/test_polarity_gate.py +0 -0
- {ctrl_kd-4.8.0 → ctrl_kd-4.9.0}/tests/test_print_control_labels_invisible.py +0 -0
- {ctrl_kd-4.8.0 → ctrl_kd-4.9.0}/tests/test_rtf_facing_pages.py +0 -0
- {ctrl_kd-4.8.0 → ctrl_kd-4.9.0}/tests/test_rtf_head_style_attrs.py +0 -0
- {ctrl_kd-4.8.0 → ctrl_kd-4.9.0}/tests/test_rtf_keep_with_next.py +0 -0
- {ctrl_kd-4.8.0 → ctrl_kd-4.9.0}/tests/test_rtf_page_numbers.py +0 -0
- {ctrl_kd-4.8.0 → ctrl_kd-4.9.0}/tests/test_rtf_section_spine.py +0 -0
- {ctrl_kd-4.8.0 → ctrl_kd-4.9.0}/tests/test_samples.py +0 -0
- {ctrl_kd-4.8.0 → ctrl_kd-4.9.0}/tests/test_sawyer_corpus.py +0 -0
- {ctrl_kd-4.8.0 → ctrl_kd-4.9.0}/tests/test_screenplay_detection.py +0 -0
- {ctrl_kd-4.8.0 → ctrl_kd-4.9.0}/tests/test_screenplay_pdf.py +0 -0
- {ctrl_kd-4.8.0 → ctrl_kd-4.9.0}/tests/test_screenplay_rendering.py +0 -0
- {ctrl_kd-4.8.0 → ctrl_kd-4.9.0}/tests/test_sentence_spacing_n9.py +0 -0
- {ctrl_kd-4.8.0 → ctrl_kd-4.9.0}/tests/test_structural_checks.py +0 -0
- {ctrl_kd-4.8.0 → ctrl_kd-4.9.0}/tests/test_style_leading.py +0 -0
- {ctrl_kd-4.8.0 → ctrl_kd-4.9.0}/tests/test_trailing_pa_break.py +0 -0
- {ctrl_kd-4.8.0 → ctrl_kd-4.9.0}/tests/test_verse_quote_couplet.py +0 -0
- {ctrl_kd-4.8.0 → ctrl_kd-4.9.0}/tests/test_verse_spacing.py +0 -0
- {ctrl_kd-4.8.0 → ctrl_kd-4.9.0}/tests/test_width_and_cp1252_memo.py +0 -0
- {ctrl_kd-4.8.0 → ctrl_kd-4.9.0}/tests/test_writer.py +0 -0
- {ctrl_kd-4.8.0 → ctrl_kd-4.9.0}/tests/test_wschange.py +0 -0
- {ctrl_kd-4.8.0 → ctrl_kd-4.9.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.9.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 stray-style-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 stray-style-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.9.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 stray-style-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
|
|
@@ -85,6 +86,7 @@ tests/test_pn_page_timing.py
|
|
|
85
86
|
tests/test_polarity_gate.py
|
|
86
87
|
tests/test_print_control_labels_invisible.py
|
|
87
88
|
tests/test_printed_fidelity.py
|
|
89
|
+
tests/test_quirks.py
|
|
88
90
|
tests/test_rtf_facing_pages.py
|
|
89
91
|
tests/test_rtf_head_style_attrs.py
|
|
90
92
|
tests/test_rtf_keep_with_next.py
|
|
@@ -98,6 +100,8 @@ tests/test_screenplay_rendering.py
|
|
|
98
100
|
tests/test_sentence_spacing_n9.py
|
|
99
101
|
tests/test_structural_checks.py
|
|
100
102
|
tests/test_style_leading.py
|
|
103
|
+
tests/test_style_strikeout_runs_until_cleared.py
|
|
104
|
+
tests/test_style_symbol_map_resolved_face.py
|
|
101
105
|
tests/test_symbol_span_graphic_chars.py
|
|
102
106
|
tests/test_trailing_pa_break.py
|
|
103
107
|
tests/test_verse_quote_couplet.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.9.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,
|
|
@@ -202,6 +215,36 @@ def main(argv=None):
|
|
|
202
215
|
ap.add_argument('--comments', action='store_true',
|
|
203
216
|
help="include WordStar comments, which it never printed "
|
|
204
217
|
"(author's asides, hidden since the file was written)")
|
|
218
|
+
ap.add_argument('--quirk', action='append', metavar='NAME', default=[],
|
|
219
|
+
help='turn a named quirk ON for this conversion '
|
|
220
|
+
'(repeatable). A quirk is a NAMED departure from a '
|
|
221
|
+
'literal reading of the bytes -- see --list-quirks '
|
|
222
|
+
'for every name this build knows, what it does, and '
|
|
223
|
+
'whether it applies to your file. Naming a quirk '
|
|
224
|
+
'the document does not trip does nothing (so one '
|
|
225
|
+
'standing list can be passed to every file); naming '
|
|
226
|
+
'one that does not exist is an error.')
|
|
227
|
+
ap.add_argument('--no-quirk', action='append', metavar='NAME', default=[],
|
|
228
|
+
help='turn a named quirk OFF (repeatable). The quirks '
|
|
229
|
+
'that are on by default are the ones the document '
|
|
230
|
+
"itself points at -- its own last-used printer "
|
|
231
|
+
'driver changed what some characters print as -- so '
|
|
232
|
+
'this is how you see the raw character instead.')
|
|
233
|
+
ap.add_argument('--quirks', choices=('auto', 'off', 'all'), default='auto',
|
|
234
|
+
help='the baseline --quirk/--no-quirk then adjust. auto '
|
|
235
|
+
'(DEFAULT): the quirks the document itself points '
|
|
236
|
+
'at, and no others. off: none at all, the most '
|
|
237
|
+
'literal reading of the bytes this converter can '
|
|
238
|
+
'give. all: every quirk that applies to this '
|
|
239
|
+
'document, including the ones that are off by '
|
|
240
|
+
'default because they are somebody\'s judgement '
|
|
241
|
+
'rather than the file\'s own evidence.')
|
|
242
|
+
ap.add_argument('--list-quirks', action='store_true',
|
|
243
|
+
help='list every quirk this build knows as JSON -- name, '
|
|
244
|
+
'one-line description, and whether it is on by '
|
|
245
|
+
'default; WITH file arguments, also whether each '
|
|
246
|
+
'applies to that file, why, and whether it is in '
|
|
247
|
+
'force. No conversion.')
|
|
205
248
|
ap.add_argument('--diagnose', action='store_true',
|
|
206
249
|
help='report what the file is (variant, margin, dot commands, '
|
|
207
250
|
'unknown codes) as JSON; no conversion')
|
|
@@ -210,6 +253,27 @@ def main(argv=None):
|
|
|
210
253
|
for dest in write_samples(a.samples):
|
|
211
254
|
print(f'-> {dest}', file=sys.stderr)
|
|
212
255
|
return 0
|
|
256
|
+
if a.list_quirks:
|
|
257
|
+
if not a.files:
|
|
258
|
+
print(json.dumps(quirks.list_quirks(), indent=2, ensure_ascii=False,
|
|
259
|
+
sort_keys=True))
|
|
260
|
+
return 0
|
|
261
|
+
status = 0
|
|
262
|
+
for path in a.files:
|
|
263
|
+
try:
|
|
264
|
+
data = open(path, 'rb').read()
|
|
265
|
+
doc = core.parse(data, encoding=a.encoding, variant=a.variant)
|
|
266
|
+
except (OSError, ValueError) as e:
|
|
267
|
+
print(f'ctrl-kd: {path}: {e}', file=sys.stderr)
|
|
268
|
+
status = 1
|
|
269
|
+
continue
|
|
270
|
+
doc = _quirked(ap, doc, a)
|
|
271
|
+
# sort_keys/ensure_ascii chosen to MATCH sr's own JSON writer exactly,
|
|
272
|
+
# so the two CLIs' --list-quirks output is byte-identical.
|
|
273
|
+
print(json.dumps({'file': os.path.basename(path),
|
|
274
|
+
'quirks': quirks.list_quirks(doc)},
|
|
275
|
+
indent=2, ensure_ascii=False, sort_keys=True))
|
|
276
|
+
return status
|
|
213
277
|
if not a.files:
|
|
214
278
|
ap.error('the following arguments are required: files')
|
|
215
279
|
mode_explicit = a.mode is not None
|
|
@@ -284,6 +348,10 @@ def main(argv=None):
|
|
|
284
348
|
f'--variant to force)', file=sys.stderr)
|
|
285
349
|
status = 1
|
|
286
350
|
continue
|
|
351
|
+
# Quirks are resolved ONCE per document, here, before any emitter
|
|
352
|
+
# reads it -- so every requested format renders the same decision and
|
|
353
|
+
# the layout JSON's own report can never disagree with what ran.
|
|
354
|
+
doc = _quirked(ap, doc, a)
|
|
287
355
|
# D5 notice (ruled 2026-08-05): when the user EXPLICITLY asked for
|
|
288
356
|
# modern and the input can only render printed, say so once instead
|
|
289
357
|
# of silently disobeying the flag. The override itself stands -- a
|
|
@@ -5226,6 +5226,26 @@ def _symmetric_blocks(data: bytes, encoding: str, raw_out=None):
|
|
|
5226
5226
|
includes, driver[0], sorted(shift_runs), marks, header,
|
|
5227
5227
|
pcl_programs)
|
|
5228
5228
|
|
|
5229
|
+
# The style attributes that KEEP RUNNING past the paragraph that turned them
|
|
5230
|
+
# on, until a later style's own attrs_OFF word clears them (ruling 2026-09-16,
|
|
5231
|
+
# WordStar-Feature-Decision-Register, "Style-library strikeout runs until a
|
|
5232
|
+
# style clears it").
|
|
5233
|
+
#
|
|
5234
|
+
# WSFORMAT.TXT states the inherit rule for the attribute words as a whole --
|
|
5235
|
+
# "if both corresponding bits are off, then the attribute is inherited from
|
|
5236
|
+
# the current state" -- but the only attribute the WS7 LaserJet captures can
|
|
5237
|
+
# actually DEMONSTRATE it for is strikeout: in every corpus document that
|
|
5238
|
+
# turns bold, underline or italic on in a style, the next style used sets
|
|
5239
|
+
# that same bit in its own attrs_off word, so an inheriting model and a
|
|
5240
|
+
# reset-per-paragraph model produce identical pages and the captures cannot
|
|
5241
|
+
# choose between them. Strikeout is the one bit no corpus style ever clears,
|
|
5242
|
+
# which is exactly why its run is visible (NOVEL.WS: 954 overstrike dash rows
|
|
5243
|
+
# across all 52 captured pages). So the rule is applied to strikeout only,
|
|
5244
|
+
# on measured evidence, and the wider question is left open rather than
|
|
5245
|
+
# guessed. Add a bit here only with a capture that shows it.
|
|
5246
|
+
STICKY_STYLE_ATTRS = ((0x01, 'strike'),)
|
|
5247
|
+
|
|
5248
|
+
|
|
5229
5249
|
def _parse_style_library(raw: bytes, base: int, encoding: str = 'cp437'):
|
|
5230
5250
|
"""The paragraph style library at file-absolute offset `base`.
|
|
5231
5251
|
|
|
@@ -5809,6 +5829,163 @@ def parse_ws(data: bytes, encoding: str = 'cp437') -> Document:
|
|
|
5809
5829
|
style_font_cache[fs] = idx
|
|
5810
5830
|
return idx
|
|
5811
5831
|
|
|
5832
|
+
def _select_style(_w0, _rel, font_sink):
|
|
5833
|
+
"""Apply one 0x11 paragraph-style SELECTION, in the stream order it
|
|
5834
|
+
occurs. Returns True if it closed a block (the caller's own
|
|
5835
|
+
`style_closed_here`).
|
|
5836
|
+
|
|
5837
|
+
Extracted from the per-line mark loop (which is still the only
|
|
5838
|
+
caller for a selection that lands on a CONTENT line) so the
|
|
5839
|
+
dot-command branch can apply one too. A selection whose bytes sit at
|
|
5840
|
+
the very END of a line lands at offset 0 of whatever comes next, and
|
|
5841
|
+
when that is a dot-command line the mark used to die with it -- the
|
|
5842
|
+
same hazard `carried_marks` already names for colour and font, but
|
|
5843
|
+
worse, because ORDER matters here: `.lh` and a style's own line
|
|
5844
|
+
height are the same WordStar setting, so deferring the selection past
|
|
5845
|
+
the dot line would let the style silently win a contest the file says
|
|
5846
|
+
the dot command wins. See the dot-command branch's own call.
|
|
5847
|
+
"""
|
|
5848
|
+
_closed = False
|
|
5849
|
+
# Resolve the handle against the file's own library. Pool tag
|
|
5850
|
+
# 0x02 = this file; anything else (0x03xx editing temps) is
|
|
5851
|
+
# unresolvable BY DESIGN and left unstyled rather than guessed.
|
|
5852
|
+
# Heading level comes from the RESOLVED NAME -- the corpus
|
|
5853
|
+
# proved slot numbers carry none (see the 0x11 parse site).
|
|
5854
|
+
# The selection PERSISTS: style_fmt stays in force for every
|
|
5855
|
+
# following block until the next 0x11. A recordless entry
|
|
5856
|
+
# (the inherit-everything base, e.g. 'WordStar Defaults')
|
|
5857
|
+
# resets formatting to the dot-command state by construction,
|
|
5858
|
+
# since it contributes no record fields.
|
|
5859
|
+
w0 = _w0
|
|
5860
|
+
if (w0 >> 8) == 0x02:
|
|
5861
|
+
slot = w0 & 0xFF
|
|
5862
|
+
entry = style_slots.get(slot)
|
|
5863
|
+
# #236 (WORDSTAR.WS/REF/wordstar-file-format.ws title
|
|
5864
|
+
# blocks): a style record's line_height_vmi of -1 means
|
|
5865
|
+
# INHERIT, already folded to None by `_parse_style_library`
|
|
5866
|
+
# (sword_none). Measured against WORDSTAR.WS's own capture
|
|
5867
|
+
# (Double-Indented Quote, a style with NO font of its own
|
|
5868
|
+
# AND an inherited line height): its body renders at
|
|
5869
|
+
# 16.0pt leading, not the document's 12pt default -- the
|
|
5870
|
+
# SAME 16.0pt the immediately preceding paragraph style
|
|
5871
|
+
# (MS Body Copy, explicit vmi=320) was already using.
|
|
5872
|
+
# 'Inherit' means carry the AMBIENT (previously active)
|
|
5873
|
+
# value forward, not reset to nothing -- style_fmt.clear()
|
|
5874
|
+
# below must not lose it.
|
|
5875
|
+
# 'style_vmi_ambient' is the last line height a STYLE declared,
|
|
5876
|
+
# kept apart from the ACTIVE one because a `.lh` supersedes the
|
|
5877
|
+
# active value (see the dot-command branch) without erasing what
|
|
5878
|
+
# the NEXT style's own 'inherit' should inherit. Measured on
|
|
5879
|
+
# `sawyer/RTF-RJS/NOVEL.WS` page 20 (ws7-prints/v4): inside a
|
|
5880
|
+
# book-list section running at `.lh 12pt`, the entering advance to
|
|
5881
|
+
# each sub-heading -- H3/H2, both of which inherit their line
|
|
5882
|
+
# height -- is 24.0pt, the vmi 'MS Body Copy' declared, and not
|
|
5883
|
+
# the 12pt `.lh` in force. Falls back to the active value, so a
|
|
5884
|
+
# document that never writes a `.lh` behaves exactly as before
|
|
5885
|
+
# (the #236 WORDSTAR.WS measurement above is unmoved).
|
|
5886
|
+
_prev_vmi = style_fmt.get('style_vmi_ambient',
|
|
5887
|
+
style_fmt.get('line_height_vmi'))
|
|
5888
|
+
# Ruling 2026-09-16 ("Style-library strikeout runs until a
|
|
5889
|
+
# style clears it"): an attribute a style turns ON stays on
|
|
5890
|
+
# past that paragraph. WSFORMAT.TXT's own rule for the two
|
|
5891
|
+
# attribute words is "if both corresponding bits are off,
|
|
5892
|
+
# then the attribute is inherited from the current state" --
|
|
5893
|
+
# so only a later style that sets the bit in its attrs_OFF
|
|
5894
|
+
# word ends the run. Real WS7 agrees: NOVEL.WS's H3 sets
|
|
5895
|
+
# strikeout on the title page and nothing in that file's
|
|
5896
|
+
# library ever clears it, and the LaserJet capture prints
|
|
5897
|
+
# the overstrike dash row on all 52 pages.
|
|
5898
|
+
_prev_sticky = style_fmt.get('sticky_attrs', frozenset())
|
|
5899
|
+
style_fmt.clear()
|
|
5900
|
+
if _prev_vmi is not None:
|
|
5901
|
+
style_fmt['style_vmi_ambient'] = _prev_vmi
|
|
5902
|
+
style_fmt['style_id'] = slot
|
|
5903
|
+
_sticky = set(_prev_sticky)
|
|
5904
|
+
if entry is not None:
|
|
5905
|
+
_on, _off = entry.get('attrs_on') or 0, entry.get('attrs_off') or 0
|
|
5906
|
+
for _bit, _tag in STICKY_STYLE_ATTRS:
|
|
5907
|
+
if _on & _bit:
|
|
5908
|
+
_sticky.add(_tag)
|
|
5909
|
+
elif _off & _bit:
|
|
5910
|
+
_sticky.discard(_tag)
|
|
5911
|
+
style_fmt['sticky_attrs'] = frozenset(_sticky)
|
|
5912
|
+
if _sticky:
|
|
5913
|
+
# Also for an UNRESOLVABLE handle (a 0x03xx editing-temp
|
|
5914
|
+
# pool entry, or a slot this file's library doesn't
|
|
5915
|
+
# carry): it declares neither word, so every attribute
|
|
5916
|
+
# inherits, including the running one.
|
|
5917
|
+
style_fmt['attrs'] = frozenset(_sticky)
|
|
5918
|
+
if entry is not None:
|
|
5919
|
+
style_fmt['style_name'] = entry['name']
|
|
5920
|
+
style_fmt['heading'] = _style_heading_level(entry['name'])
|
|
5921
|
+
if entry.get('justification') in ('left', 'justify',
|
|
5922
|
+
'center', 'right'):
|
|
5923
|
+
# 'left' means EXPLICIT no-justification -- it
|
|
5924
|
+
# overrides a running .oj, so it must occupy the
|
|
5925
|
+
# align slot rather than fall through
|
|
5926
|
+
style_fmt['align'] = entry['justification']
|
|
5927
|
+
if entry.get('word_wrap') is not None:
|
|
5928
|
+
style_fmt['wrap'] = entry['word_wrap']
|
|
5929
|
+
for src_k, dst_k in (('left_margin_hmi', 'left_margin'),
|
|
5930
|
+
('right_margin_hmi', 'right_margin'),
|
|
5931
|
+
('para_margin_hmi', 'para_margin')):
|
|
5932
|
+
hmi = entry.get(src_k)
|
|
5933
|
+
if hmi is not None:
|
|
5934
|
+
# HMI 1/1800in -> print columns at 10 CPI,
|
|
5935
|
+
# the unit .lm/.rm already use (180 = 1 col)
|
|
5936
|
+
style_fmt[dst_k] = round(hmi / 180)
|
|
5937
|
+
if entry.get('attrs') or _sticky:
|
|
5938
|
+
# The style's own ON bits, plus every sticky
|
|
5939
|
+
# attribute still running from an earlier style.
|
|
5940
|
+
style_fmt['attrs'] = frozenset(
|
|
5941
|
+
entry.get('attrs') or ()) | style_fmt['sticky_attrs']
|
|
5942
|
+
# Register C5: the style's own declared colour index
|
|
5943
|
+
# (0-15, WSFORMAT's fixed CGA/EGA palette -- same
|
|
5944
|
+
# space as an inline type-1 colour change). `is not
|
|
5945
|
+
# None` mirrors line_height_vmi just below: 0 is a
|
|
5946
|
+
# real, explicit "Black" distinct from "the style
|
|
5947
|
+
# never set one", even though nothing downstream
|
|
5948
|
+
# currently treats 0 differently from unset.
|
|
5949
|
+
if entry.get('colour') is not None:
|
|
5950
|
+
style_fmt['style_colour'] = entry['colour']
|
|
5951
|
+
# line_height_vmi: -2 = auto (the only value the
|
|
5952
|
+
# measured oracle carries), a positive count =
|
|
5953
|
+
# explicit VMI (WSFORMAT.WS: same 1/1440in unit as a
|
|
5954
|
+
# font's own height word). sword_none already folded
|
|
5955
|
+
# -1 (inherit) to None at parse time, so a bare
|
|
5956
|
+
# `is not None` is the right test here -- 'inherit'
|
|
5957
|
+
# never reaches this branch.
|
|
5958
|
+
if entry.get('line_height_vmi') is not None:
|
|
5959
|
+
style_fmt['line_height_vmi'] = entry['line_height_vmi']
|
|
5960
|
+
style_fmt['style_vmi_ambient'] = entry['line_height_vmi']
|
|
5961
|
+
elif _prev_vmi is not None:
|
|
5962
|
+
# -1/inherit: carry the ambient value forward
|
|
5963
|
+
# (see the #236 comment above this block).
|
|
5964
|
+
style_fmt['line_height_vmi'] = _prev_vmi
|
|
5965
|
+
style_fmt['style_vmi_ambient'] = _prev_vmi
|
|
5966
|
+
# an all-zero triple records NO font (OLDTIMES's
|
|
5967
|
+
# 'Double-Indented Quote'), distinct from the -1
|
|
5968
|
+
# inherit sentinel only in never having been set
|
|
5969
|
+
if entry.get('font') and any(entry['font']):
|
|
5970
|
+
font_sink.append((_rel, _style_font(entry['font'])))
|
|
5971
|
+
# The style's own declared size, in points --
|
|
5972
|
+
# captured on the BLOCK (not just the span-level
|
|
5973
|
+
# font_at mark) so a spanless blank line, which
|
|
5974
|
+
# carries no font tag of its own, can still
|
|
5975
|
+
# resolve its style's auto/explicit leading.
|
|
5976
|
+
style_fmt['style_font_pt'] = entry['font'][1] / 20.0
|
|
5977
|
+
# style_fmt is updated BEFORE this close: the previous
|
|
5978
|
+
# block keeps its old style, the fresh block picks the
|
|
5979
|
+
# new one up from _new_block()
|
|
5980
|
+
close_block()
|
|
5981
|
+
_closed = True
|
|
5982
|
+
else:
|
|
5983
|
+
# 0x03xx temp-pool handle: unresolvable by design, but a
|
|
5984
|
+
# selection is still a block boundary in the file
|
|
5985
|
+
close_block()
|
|
5986
|
+
_closed = True
|
|
5987
|
+
return _closed
|
|
5988
|
+
|
|
5812
5989
|
def _new_block():
|
|
5813
5990
|
return Block('para',
|
|
5814
5991
|
align=style_fmt.get('align') or _align_now(fmt),
|
|
@@ -6073,6 +6250,36 @@ def parse_ws(data: bytes, encoding: str = 'cp437') -> Document:
|
|
|
6073
6250
|
# this line would otherwise have mutated is withheld.
|
|
6074
6251
|
if if_active and not all(if_active):
|
|
6075
6252
|
continue
|
|
6253
|
+
# A style SELECTION whose bytes sit at the very END of the
|
|
6254
|
+
# PREVIOUS line lands at offset 0 of this one (lines_pass's
|
|
6255
|
+
# `a <= off < a + ln` cannot place it inside the line it
|
|
6256
|
+
# follows). A dot-command line is CONSUMED below, so the mark
|
|
6257
|
+
# used to die with it -- the same hazard the `carried_marks`
|
|
6258
|
+
# note further down names for colour and font. It is NOT enough
|
|
6259
|
+
# to carry it forward, because ORDER decides the outcome: `.lh`
|
|
6260
|
+
# and a paragraph style's own line height are the same WordStar
|
|
6261
|
+
# setting, and a style applied AFTER the `.lh` line would undo a
|
|
6262
|
+
# dot command the file puts second. So it is applied HERE, before
|
|
6263
|
+
# this line's own command runs -- which is exactly where the
|
|
6264
|
+
# bytes are.
|
|
6265
|
+
#
|
|
6266
|
+
# rel == 0 is what distinguishes the two cases, and it is exact:
|
|
6267
|
+
# a dot line's own first byte is its '.', so nothing of this
|
|
6268
|
+
# line's can mark offset 0, while a style block written INSIDE a
|
|
6269
|
+
# `.h1`/`.f1` argument (or a commented-out `..f1`) always sits
|
|
6270
|
+
# past the command letters and marks rel >= 4. Those belong to
|
|
6271
|
+
# the running head, which reads its own mark (`hf_style_w0`
|
|
6272
|
+
# below), and must never reach the body -- `sawyer/RTF-RJS/
|
|
6273
|
+
# NOVEL.WS` page 1 renders its whole front-matter block in the
|
|
6274
|
+
# FOOTER style's 6pt Univers if they do.
|
|
6275
|
+
_dot_style_fonts = []
|
|
6276
|
+
for _srel, _sm in line_marks:
|
|
6277
|
+
if _sm[0] == 'style' and _srel == 0:
|
|
6278
|
+
_select_style(_sm[1], 0, _dot_style_fonts)
|
|
6279
|
+
if _dot_style_fonts:
|
|
6280
|
+
# The selection's own font reaches the next CONTENT line the
|
|
6281
|
+
# way every other carried state does -- at its offset 0.
|
|
6282
|
+
carried_marks.extend(('font', _idx) for _r, _idx in _dot_style_fonts)
|
|
6076
6283
|
# '..' and '.ig' are COMMENT lines (ruling 2026-08-06): both
|
|
6077
6284
|
# WordStar comment syntaxes unify into Note(kind='comment'),
|
|
6078
6285
|
# each emitting a reference mark at its own position -- the
|
|
@@ -6277,6 +6484,35 @@ def parse_ws(data: bytes, encoding: str = 'cp437') -> Document:
|
|
|
6277
6484
|
# single block cannot hold both.
|
|
6278
6485
|
before = _block_format(fmt)
|
|
6279
6486
|
_parse_format_dot(cmd, fmt)
|
|
6487
|
+
# `.lh` and a paragraph STYLE's own `line_height_vmi` are the
|
|
6488
|
+
# SAME WordStar setting -- a line height -- reached two ways, so
|
|
6489
|
+
# the one that comes LATER in the file wins. `_style_lead_pt`
|
|
6490
|
+
# gives the style's value precedence over the generic `.lh`,
|
|
6491
|
+
# which is right for every document that selects a style and then
|
|
6492
|
+
# leaves the leading alone (LYING.WS, WARPRAYR.WS: no `.lh` at
|
|
6493
|
+
# all). It is wrong the moment a file writes a real `.lh` AFTER
|
|
6494
|
+
# the selection, which is what `sawyer/RTF-RJS/NOVEL.WS` does at
|
|
6495
|
+
# each of its five book-list sections: select 'MS Body Copy'
|
|
6496
|
+
# (vmi 480 = 24pt), then `.lh 12pt`. Real WS7 prints those lists
|
|
6497
|
+
# single-spaced at 12pt (ws7-prints/v4, page 20: baselines 144,
|
|
6498
|
+
# 156, 168 ... exactly 12.0pt apart); the style's 24pt made the
|
|
6499
|
+
# section overflow onto a 52nd page the real print does not have.
|
|
6500
|
+
# Dropping the style's value here hands the following blocks back
|
|
6501
|
+
# to `Line.lead_48`, which is where a `.lh` already lives; the
|
|
6502
|
+
# next selection re-establishes its own.
|
|
6503
|
+
_lhm = _DOT_CMD_RE.match(cmd)
|
|
6504
|
+
if (_lhm and _lhm.group(1).upper() == b'LH'
|
|
6505
|
+
and style_fmt.get('line_height_vmi') is not None
|
|
6506
|
+
and _lhm.group(2).strip().lower()[:1] != b'a'):
|
|
6507
|
+
_lhn = _dot_num_match(_lhm.group(2))
|
|
6508
|
+
if _lhn:
|
|
6509
|
+
try:
|
|
6510
|
+
_lhv = float(_lhn.group(1))
|
|
6511
|
+
except (TypeError, ValueError):
|
|
6512
|
+
_lhv = None
|
|
6513
|
+
if _lhv is not None and _resolve_lh_arg(_lhv, _lhn.group(2)):
|
|
6514
|
+
style_fmt.pop('line_height_vmi', None)
|
|
6515
|
+
close_block()
|
|
6280
6516
|
# #241 remainder: a same-line `.RR` ruler IMAGE (`.RR--!...R`)
|
|
6281
6517
|
# updates left_margin/para_margin/right_margin exactly like an
|
|
6282
6518
|
# explicit `.lm`/`.pm`/`.rm` would -- see `_apply_ruler_margins`'s
|
|
@@ -6474,98 +6710,7 @@ def parse_ws(data: bytes, encoding: str = 'cp437') -> Document:
|
|
|
6474
6710
|
# for the measurement.
|
|
6475
6711
|
cur_line.softpage = True
|
|
6476
6712
|
elif m[0] == 'style':
|
|
6477
|
-
|
|
6478
|
-
# 0x02 = this file; anything else (0x03xx editing temps) is
|
|
6479
|
-
# unresolvable BY DESIGN and left unstyled rather than guessed.
|
|
6480
|
-
# Heading level comes from the RESOLVED NAME -- the corpus
|
|
6481
|
-
# proved slot numbers carry none (see the 0x11 parse site).
|
|
6482
|
-
# The selection PERSISTS: style_fmt stays in force for every
|
|
6483
|
-
# following block until the next 0x11. A recordless entry
|
|
6484
|
-
# (the inherit-everything base, e.g. 'WordStar Defaults')
|
|
6485
|
-
# resets formatting to the dot-command state by construction,
|
|
6486
|
-
# since it contributes no record fields.
|
|
6487
|
-
w0 = m[1]
|
|
6488
|
-
if (w0 >> 8) == 0x02:
|
|
6489
|
-
slot = w0 & 0xFF
|
|
6490
|
-
entry = style_slots.get(slot)
|
|
6491
|
-
# #236 (WORDSTAR.WS/REF/wordstar-file-format.ws title
|
|
6492
|
-
# blocks): a style record's line_height_vmi of -1 means
|
|
6493
|
-
# INHERIT, already folded to None by `_parse_style_library`
|
|
6494
|
-
# (sword_none). Measured against WORDSTAR.WS's own capture
|
|
6495
|
-
# (Double-Indented Quote, a style with NO font of its own
|
|
6496
|
-
# AND an inherited line height): its body renders at
|
|
6497
|
-
# 16.0pt leading, not the document's 12pt default -- the
|
|
6498
|
-
# SAME 16.0pt the immediately preceding paragraph style
|
|
6499
|
-
# (MS Body Copy, explicit vmi=320) was already using.
|
|
6500
|
-
# 'Inherit' means carry the AMBIENT (previously active)
|
|
6501
|
-
# value forward, not reset to nothing -- style_fmt.clear()
|
|
6502
|
-
# below must not lose it.
|
|
6503
|
-
_prev_vmi = style_fmt.get('line_height_vmi')
|
|
6504
|
-
style_fmt.clear()
|
|
6505
|
-
style_fmt['style_id'] = slot
|
|
6506
|
-
if entry is not None:
|
|
6507
|
-
style_fmt['style_name'] = entry['name']
|
|
6508
|
-
style_fmt['heading'] = _style_heading_level(entry['name'])
|
|
6509
|
-
if entry.get('justification') in ('left', 'justify',
|
|
6510
|
-
'center', 'right'):
|
|
6511
|
-
# 'left' means EXPLICIT no-justification -- it
|
|
6512
|
-
# overrides a running .oj, so it must occupy the
|
|
6513
|
-
# align slot rather than fall through
|
|
6514
|
-
style_fmt['align'] = entry['justification']
|
|
6515
|
-
if entry.get('word_wrap') is not None:
|
|
6516
|
-
style_fmt['wrap'] = entry['word_wrap']
|
|
6517
|
-
for src_k, dst_k in (('left_margin_hmi', 'left_margin'),
|
|
6518
|
-
('right_margin_hmi', 'right_margin'),
|
|
6519
|
-
('para_margin_hmi', 'para_margin')):
|
|
6520
|
-
hmi = entry.get(src_k)
|
|
6521
|
-
if hmi is not None:
|
|
6522
|
-
# HMI 1/1800in -> print columns at 10 CPI,
|
|
6523
|
-
# the unit .lm/.rm already use (180 = 1 col)
|
|
6524
|
-
style_fmt[dst_k] = round(hmi / 180)
|
|
6525
|
-
if entry.get('attrs'):
|
|
6526
|
-
style_fmt['attrs'] = entry['attrs']
|
|
6527
|
-
# Register C5: the style's own declared colour index
|
|
6528
|
-
# (0-15, WSFORMAT's fixed CGA/EGA palette -- same
|
|
6529
|
-
# space as an inline type-1 colour change). `is not
|
|
6530
|
-
# None` mirrors line_height_vmi just below: 0 is a
|
|
6531
|
-
# real, explicit "Black" distinct from "the style
|
|
6532
|
-
# never set one", even though nothing downstream
|
|
6533
|
-
# currently treats 0 differently from unset.
|
|
6534
|
-
if entry.get('colour') is not None:
|
|
6535
|
-
style_fmt['style_colour'] = entry['colour']
|
|
6536
|
-
# line_height_vmi: -2 = auto (the only value the
|
|
6537
|
-
# measured oracle carries), a positive count =
|
|
6538
|
-
# explicit VMI (WSFORMAT.WS: same 1/1440in unit as a
|
|
6539
|
-
# font's own height word). sword_none already folded
|
|
6540
|
-
# -1 (inherit) to None at parse time, so a bare
|
|
6541
|
-
# `is not None` is the right test here -- 'inherit'
|
|
6542
|
-
# never reaches this branch.
|
|
6543
|
-
if entry.get('line_height_vmi') is not None:
|
|
6544
|
-
style_fmt['line_height_vmi'] = entry['line_height_vmi']
|
|
6545
|
-
elif _prev_vmi is not None:
|
|
6546
|
-
# -1/inherit: carry the ambient value forward
|
|
6547
|
-
# (see the #236 comment above this block).
|
|
6548
|
-
style_fmt['line_height_vmi'] = _prev_vmi
|
|
6549
|
-
# an all-zero triple records NO font (OLDTIMES's
|
|
6550
|
-
# 'Double-Indented Quote'), distinct from the -1
|
|
6551
|
-
# inherit sentinel only in never having been set
|
|
6552
|
-
if entry.get('font') and any(entry['font']):
|
|
6553
|
-
font_at.append((rel, _style_font(entry['font'])))
|
|
6554
|
-
# The style's own declared size, in points --
|
|
6555
|
-
# captured on the BLOCK (not just the span-level
|
|
6556
|
-
# font_at mark) so a spanless blank line, which
|
|
6557
|
-
# carries no font tag of its own, can still
|
|
6558
|
-
# resolve its style's auto/explicit leading.
|
|
6559
|
-
style_fmt['style_font_pt'] = entry['font'][1] / 20.0
|
|
6560
|
-
# style_fmt is updated BEFORE this close: the previous
|
|
6561
|
-
# block keeps its old style, the fresh block picks the
|
|
6562
|
-
# new one up from _new_block()
|
|
6563
|
-
close_block()
|
|
6564
|
-
style_closed_here = True
|
|
6565
|
-
else:
|
|
6566
|
-
# 0x03xx temp-pool handle: unresolvable by design, but a
|
|
6567
|
-
# selection is still a block boundary in the file
|
|
6568
|
-
close_block()
|
|
6713
|
+
if _select_style(m[1], rel, font_at):
|
|
6569
6714
|
style_closed_here = True
|
|
6570
6715
|
# every other mark kind is SPAN-LEVEL and was collected in the
|
|
6571
6716
|
# pass above this one, before the form-feed branch that needs it
|