ctrl-kd 4.7.1__tar.gz → 4.8.1__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.7.1/src/ctrl_kd.egg-info → ctrl_kd-4.8.1}/PKG-INFO +1 -1
- {ctrl_kd-4.7.1 → ctrl_kd-4.8.1/src/ctrl_kd.egg-info}/PKG-INFO +1 -1
- {ctrl_kd-4.7.1 → ctrl_kd-4.8.1}/src/ctrl_kd.egg-info/SOURCES.txt +6 -0
- {ctrl_kd-4.7.1 → ctrl_kd-4.8.1}/src/ctrlkd/__init__.py +1 -1
- {ctrl_kd-4.7.1 → ctrl_kd-4.8.1}/src/ctrlkd/cli.py +10 -7
- {ctrl_kd-4.7.1 → ctrl_kd-4.8.1}/src/ctrlkd/core.py +132 -4
- {ctrl_kd-4.7.1 → ctrl_kd-4.8.1}/src/ctrlkd/emit.py +189 -54
- {ctrl_kd-4.7.1 → ctrl_kd-4.8.1}/src/ctrlkd/layout.py +97 -8
- {ctrl_kd-4.7.1 → ctrl_kd-4.8.1}/src/ctrlkd/pdf.py +920 -105
- {ctrl_kd-4.7.1 → ctrl_kd-4.8.1}/src/ctrlkd/symbolmap.py +40 -3
- {ctrl_kd-4.7.1 → ctrl_kd-4.8.1}/tests/test_ctrlkd.py +256 -22
- {ctrl_kd-4.7.1 → ctrl_kd-4.8.1}/tests/test_graphic_cells.py +2 -2
- ctrl_kd-4.8.1/tests/test_head_foot_placement_m16.py +146 -0
- {ctrl_kd-4.7.1 → ctrl_kd-4.8.1}/tests/test_head_foot_timing.py +91 -0
- {ctrl_kd-4.7.1 → ctrl_kd-4.8.1}/tests/test_justification.py +1 -1
- {ctrl_kd-4.7.1 → ctrl_kd-4.8.1}/tests/test_merge_page_number_variable.py +8 -1
- {ctrl_kd-4.7.1 → ctrl_kd-4.8.1}/tests/test_modern_centring_clip.py +11 -0
- ctrl_kd-4.8.1/tests/test_modern_columns.py +379 -0
- {ctrl_kd-4.7.1 → ctrl_kd-4.8.1}/tests/test_modern_line_spacing.py +25 -2
- ctrl_kd-4.8.1/tests/test_modern_page_number.py +344 -0
- ctrl_kd-4.8.1/tests/test_modern_sheet_height.py +137 -0
- {ctrl_kd-4.7.1 → ctrl_kd-4.8.1}/tests/test_modern_verse_defrow.py +32 -4
- {ctrl_kd-4.7.1 → ctrl_kd-4.8.1}/tests/test_pcl_tolerance.py +97 -0
- {ctrl_kd-4.7.1 → ctrl_kd-4.8.1}/tests/test_pcl_v4_column_geometry.py +1 -1
- {ctrl_kd-4.7.1 → ctrl_kd-4.8.1}/tests/test_pn_page_timing.py +33 -0
- {ctrl_kd-4.7.1 → ctrl_kd-4.8.1}/tests/test_printed_fidelity.py +143 -8
- {ctrl_kd-4.7.1 → ctrl_kd-4.8.1}/tests/test_rtf_section_spine.py +25 -15
- {ctrl_kd-4.7.1 → ctrl_kd-4.8.1}/tests/test_structural_checks.py +2 -4
- ctrl_kd-4.8.1/tests/test_style_strikeout_runs_until_cleared.py +297 -0
- ctrl_kd-4.8.1/tests/test_style_symbol_map_resolved_face.py +173 -0
- {ctrl_kd-4.7.1 → ctrl_kd-4.8.1}/tests/test_symbol_span_graphic_chars.py +30 -12
- {ctrl_kd-4.7.1 → ctrl_kd-4.8.1}/LICENSE +0 -0
- {ctrl_kd-4.7.1 → ctrl_kd-4.8.1}/README.md +0 -0
- {ctrl_kd-4.7.1 → ctrl_kd-4.8.1}/pyproject.toml +0 -0
- {ctrl_kd-4.7.1 → ctrl_kd-4.8.1}/setup.cfg +0 -0
- {ctrl_kd-4.7.1 → ctrl_kd-4.8.1}/src/ctrl_kd.egg-info/dependency_links.txt +0 -0
- {ctrl_kd-4.7.1 → ctrl_kd-4.8.1}/src/ctrl_kd.egg-info/entry_points.txt +0 -0
- {ctrl_kd-4.7.1 → ctrl_kd-4.8.1}/src/ctrl_kd.egg-info/top_level.txt +0 -0
- {ctrl_kd-4.7.1 → ctrl_kd-4.8.1}/src/ctrlkd/afm.py +0 -0
- {ctrl_kd-4.7.1 → ctrl_kd-4.8.1}/src/ctrlkd/convert.py +0 -0
- {ctrl_kd-4.7.1 → ctrl_kd-4.8.1}/src/ctrlkd/fontmap.py +0 -0
- {ctrl_kd-4.7.1 → ctrl_kd-4.8.1}/src/ctrlkd/info.py +0 -0
- {ctrl_kd-4.7.1 → ctrl_kd-4.8.1}/src/ctrlkd/pictures.py +0 -0
- {ctrl_kd-4.7.1 → ctrl_kd-4.8.1}/src/ctrlkd/pix.py +0 -0
- {ctrl_kd-4.7.1 → ctrl_kd-4.8.1}/src/ctrlkd/piximg.py +0 -0
- {ctrl_kd-4.7.1 → ctrl_kd-4.8.1}/src/ctrlkd/samples/LYING.WS +0 -0
- {ctrl_kd-4.7.1 → ctrl_kd-4.8.1}/src/ctrlkd/samples/OCAPTAIN.WS +0 -0
- {ctrl_kd-4.7.1 → ctrl_kd-4.8.1}/src/ctrlkd/samples/TWAINLET.WS +0 -0
- {ctrl_kd-4.7.1 → ctrl_kd-4.8.1}/src/ctrlkd/samples/WARPRAYR.WS +0 -0
- {ctrl_kd-4.7.1 → ctrl_kd-4.8.1}/src/ctrlkd/typestyles.py +0 -0
- {ctrl_kd-4.7.1 → ctrl_kd-4.8.1}/src/ctrlkd/writer.py +0 -0
- {ctrl_kd-4.7.1 → ctrl_kd-4.8.1}/src/ctrlkd/wschange.py +0 -0
- {ctrl_kd-4.7.1 → ctrl_kd-4.8.1}/tests/test_bare_tab_modulus8.py +0 -0
- {ctrl_kd-4.7.1 → ctrl_kd-4.8.1}/tests/test_dot_comment_prints_nothing.py +0 -0
- {ctrl_kd-4.7.1 → ctrl_kd-4.8.1}/tests/test_driver_substitutions_exports.py +0 -0
- {ctrl_kd-4.7.1 → ctrl_kd-4.8.1}/tests/test_endnote_leading_gap.py +0 -0
- {ctrl_kd-4.7.1 → ctrl_kd-4.8.1}/tests/test_fidelity_gate.py +0 -0
- {ctrl_kd-4.7.1 → ctrl_kd-4.8.1}/tests/test_flags_toc_inline.py +0 -0
- {ctrl_kd-4.7.1 → ctrl_kd-4.8.1}/tests/test_form_feed_line_marks.py +0 -0
- {ctrl_kd-4.7.1 → ctrl_kd-4.8.1}/tests/test_glyph_aspect.py +0 -0
- {ctrl_kd-4.7.1 → ctrl_kd-4.8.1}/tests/test_graphic_cell_ops.py +0 -0
- {ctrl_kd-4.7.1 → ctrl_kd-4.8.1}/tests/test_html_print_stylesheet.py +0 -0
- {ctrl_kd-4.7.1 → ctrl_kd-4.8.1}/tests/test_layout_marks.py +0 -0
- {ctrl_kd-4.7.1 → ctrl_kd-4.8.1}/tests/test_lj6dtp_bullet_glyph.py +0 -0
- {ctrl_kd-4.7.1 → ctrl_kd-4.8.1}/tests/test_lj6dtp_char_substitution.py +0 -0
- {ctrl_kd-4.7.1 → ctrl_kd-4.8.1}/tests/test_lj6dtp_colour_restore.py +0 -0
- {ctrl_kd-4.7.1 → ctrl_kd-4.8.1}/tests/test_lj6dtp_heading_face.py +0 -0
- {ctrl_kd-4.7.1 → ctrl_kd-4.8.1}/tests/test_lj6dtp_hp_patterns.py +0 -0
- {ctrl_kd-4.7.1 → ctrl_kd-4.8.1}/tests/test_lj6dtp_legend_line_spacing.py +0 -0
- {ctrl_kd-4.7.1 → ctrl_kd-4.8.1}/tests/test_lj6dtp_pcl_rectangles.py +0 -0
- {ctrl_kd-4.7.1 → ctrl_kd-4.8.1}/tests/test_lj6dtp_shading_table_rules.py +0 -0
- {ctrl_kd-4.7.1 → ctrl_kd-4.8.1}/tests/test_lj6dtp_table_rule_weight.py +0 -0
- {ctrl_kd-4.7.1 → ctrl_kd-4.8.1}/tests/test_load_plugins.py +0 -0
- {ctrl_kd-4.7.1 → ctrl_kd-4.8.1}/tests/test_mailmerge_data_files.py +0 -0
- {ctrl_kd-4.7.1 → ctrl_kd-4.8.1}/tests/test_modern_blank_after_tightened_line.py +0 -0
- {ctrl_kd-4.7.1 → ctrl_kd-4.8.1}/tests/test_modern_box_regions.py +0 -0
- {ctrl_kd-4.7.1 → ctrl_kd-4.8.1}/tests/test_modern_lint.py +0 -0
- {ctrl_kd-4.7.1 → ctrl_kd-4.8.1}/tests/test_modern_page_baseline.py +0 -0
- {ctrl_kd-4.7.1 → ctrl_kd-4.8.1}/tests/test_modern_rtf_structure_rows.py +0 -0
- {ctrl_kd-4.7.1 → ctrl_kd-4.8.1}/tests/test_note_rulings_20260824.py +0 -0
- {ctrl_kd-4.7.1 → ctrl_kd-4.8.1}/tests/test_page_parity.py +0 -0
- {ctrl_kd-4.7.1 → ctrl_kd-4.8.1}/tests/test_paper_verdicts.py +0 -0
- {ctrl_kd-4.7.1 → ctrl_kd-4.8.1}/tests/test_pcl_fidelity.py +0 -0
- {ctrl_kd-4.7.1 → ctrl_kd-4.8.1}/tests/test_pcl_page_membership.py +0 -0
- {ctrl_kd-4.7.1 → ctrl_kd-4.8.1}/tests/test_pcl_page_size_eject.py +0 -0
- {ctrl_kd-4.7.1 → ctrl_kd-4.8.1}/tests/test_pcl_transparent_print.py +0 -0
- {ctrl_kd-4.7.1 → ctrl_kd-4.8.1}/tests/test_pcl_v4_untriaged_fixes.py +0 -0
- {ctrl_kd-4.7.1 → ctrl_kd-4.8.1}/tests/test_pcl_vertical_wraparound.py +0 -0
- {ctrl_kd-4.7.1 → ctrl_kd-4.8.1}/tests/test_peseta_euro_driver.py +0 -0
- {ctrl_kd-4.7.1 → ctrl_kd-4.8.1}/tests/test_pf_print_reformat.py +0 -0
- {ctrl_kd-4.7.1 → ctrl_kd-4.8.1}/tests/test_pictures.py +0 -0
- {ctrl_kd-4.7.1 → ctrl_kd-4.8.1}/tests/test_pix.py +0 -0
- {ctrl_kd-4.7.1 → ctrl_kd-4.8.1}/tests/test_polarity_gate.py +0 -0
- {ctrl_kd-4.7.1 → ctrl_kd-4.8.1}/tests/test_print_control_labels_invisible.py +0 -0
- {ctrl_kd-4.7.1 → ctrl_kd-4.8.1}/tests/test_rtf_facing_pages.py +0 -0
- {ctrl_kd-4.7.1 → ctrl_kd-4.8.1}/tests/test_rtf_head_style_attrs.py +0 -0
- {ctrl_kd-4.7.1 → ctrl_kd-4.8.1}/tests/test_rtf_keep_with_next.py +0 -0
- {ctrl_kd-4.7.1 → ctrl_kd-4.8.1}/tests/test_rtf_page_numbers.py +0 -0
- {ctrl_kd-4.7.1 → ctrl_kd-4.8.1}/tests/test_samples.py +0 -0
- {ctrl_kd-4.7.1 → ctrl_kd-4.8.1}/tests/test_sawyer_corpus.py +0 -0
- {ctrl_kd-4.7.1 → ctrl_kd-4.8.1}/tests/test_screenplay_detection.py +0 -0
- {ctrl_kd-4.7.1 → ctrl_kd-4.8.1}/tests/test_screenplay_pdf.py +0 -0
- {ctrl_kd-4.7.1 → ctrl_kd-4.8.1}/tests/test_screenplay_rendering.py +0 -0
- {ctrl_kd-4.7.1 → ctrl_kd-4.8.1}/tests/test_sentence_spacing_n9.py +0 -0
- {ctrl_kd-4.7.1 → ctrl_kd-4.8.1}/tests/test_style_leading.py +0 -0
- {ctrl_kd-4.7.1 → ctrl_kd-4.8.1}/tests/test_trailing_pa_break.py +0 -0
- {ctrl_kd-4.7.1 → ctrl_kd-4.8.1}/tests/test_verse_quote_couplet.py +0 -0
- {ctrl_kd-4.7.1 → ctrl_kd-4.8.1}/tests/test_verse_spacing.py +0 -0
- {ctrl_kd-4.7.1 → ctrl_kd-4.8.1}/tests/test_width_and_cp1252_memo.py +0 -0
- {ctrl_kd-4.7.1 → ctrl_kd-4.8.1}/tests/test_writer.py +0 -0
- {ctrl_kd-4.7.1 → ctrl_kd-4.8.1}/tests/test_wschange.py +0 -0
- {ctrl_kd-4.7.1 → ctrl_kd-4.8.1}/tests/test_zero_width_tab.py +0 -0
|
@@ -38,6 +38,7 @@ tests/test_form_feed_line_marks.py
|
|
|
38
38
|
tests/test_glyph_aspect.py
|
|
39
39
|
tests/test_graphic_cell_ops.py
|
|
40
40
|
tests/test_graphic_cells.py
|
|
41
|
+
tests/test_head_foot_placement_m16.py
|
|
41
42
|
tests/test_head_foot_timing.py
|
|
42
43
|
tests/test_html_print_stylesheet.py
|
|
43
44
|
tests/test_justification.py
|
|
@@ -57,10 +58,13 @@ tests/test_merge_page_number_variable.py
|
|
|
57
58
|
tests/test_modern_blank_after_tightened_line.py
|
|
58
59
|
tests/test_modern_box_regions.py
|
|
59
60
|
tests/test_modern_centring_clip.py
|
|
61
|
+
tests/test_modern_columns.py
|
|
60
62
|
tests/test_modern_line_spacing.py
|
|
61
63
|
tests/test_modern_lint.py
|
|
62
64
|
tests/test_modern_page_baseline.py
|
|
65
|
+
tests/test_modern_page_number.py
|
|
63
66
|
tests/test_modern_rtf_structure_rows.py
|
|
67
|
+
tests/test_modern_sheet_height.py
|
|
64
68
|
tests/test_modern_verse_defrow.py
|
|
65
69
|
tests/test_note_rulings_20260824.py
|
|
66
70
|
tests/test_page_parity.py
|
|
@@ -94,6 +98,8 @@ tests/test_screenplay_rendering.py
|
|
|
94
98
|
tests/test_sentence_spacing_n9.py
|
|
95
99
|
tests/test_structural_checks.py
|
|
96
100
|
tests/test_style_leading.py
|
|
101
|
+
tests/test_style_strikeout_runs_until_cleared.py
|
|
102
|
+
tests/test_style_symbol_map_resolved_face.py
|
|
97
103
|
tests/test_symbol_span_graphic_chars.py
|
|
98
104
|
tests/test_trailing_pa_break.py
|
|
99
105
|
tests/test_verse_quote_couplet.py
|
|
@@ -130,15 +130,17 @@ def main(argv=None):
|
|
|
130
130
|
help='omit paragraph-style pass-through (HTML classes + '
|
|
131
131
|
'generated CSS, RTF stylesheet) from the output')
|
|
132
132
|
ap.add_argument('--headers', choices=('on', 'off'), default='on',
|
|
133
|
-
help='
|
|
134
|
-
'surfaces
|
|
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')
|
|
135
137
|
ap.add_argument('--page-numbers', choices=('auto', 'on', 'off'),
|
|
136
138
|
default='auto',
|
|
137
139
|
help="WordStar's own AUTOMATIC page number -- the one "
|
|
138
140
|
'.pc positions, a separate mechanism from a # the '
|
|
139
|
-
'author placed inside a real .he/.fo (that
|
|
140
|
-
'
|
|
141
|
-
'
|
|
141
|
+
'author placed inside a real .he/.fo (that goes '
|
|
142
|
+
'with its head, under --headers, never this flag). '
|
|
143
|
+
'Every paged surface. auto (DEFAULT): the '
|
|
142
144
|
'document\'s own dot '
|
|
143
145
|
'commands decide -- .pn/.pg turn it on, .op turns '
|
|
144
146
|
'it off, exactly like real WordStar; a document '
|
|
@@ -149,8 +151,9 @@ def main(argv=None):
|
|
|
149
151
|
'silent document. off: suppress it unconditionally. '
|
|
150
152
|
'A declared footer always pre-empts it, in every '
|
|
151
153
|
'mode (WSFORMAT.WS: "active only when the footers '
|
|
152
|
-
'are not in use")
|
|
153
|
-
'it,
|
|
154
|
+
'are not in use") -- a property of the document, so '
|
|
155
|
+
'--headers off does not lift it, and does not '
|
|
156
|
+
'suppress the number either.')
|
|
154
157
|
ap.add_argument('--line-numbers', choices=('on', 'off'), default='on',
|
|
155
158
|
help="the document's own .l# line-number gutter in the "
|
|
156
159
|
'paged surfaces (Printed/Native PDF and RTF); no '
|
|
@@ -1427,6 +1427,46 @@ class Document:
|
|
|
1427
1427
|
# can `|` it into a run's own styles unconditionally.
|
|
1428
1428
|
header_style_attrs: dict = field(default_factory=dict)
|
|
1429
1429
|
footer_style_attrs: dict = field(default_factory=dict)
|
|
1430
|
+
# M16 (2026-09-15, WS7 probes B0-B5 against sawyer/REF/BOOKLET.WS): the
|
|
1431
|
+
# RIGHT MARGIN the head/foot line's own style declares, in print
|
|
1432
|
+
# columns at 10 CPI (`right_margin_hmi / 180`), or absent when the
|
|
1433
|
+
# style declares none. `{line: cols}`.
|
|
1434
|
+
#
|
|
1435
|
+
# A right- or centre-aligned running head aligns against THIS, measured
|
|
1436
|
+
# from `.po` -- not against the document's `.rm`, not against the
|
|
1437
|
+
# column grid, and not against the sheet. Measured, five probes printed
|
|
1438
|
+
# by real WS7 under DOSBox-X: turning `.co 2` off (B2) and changing it
|
|
1439
|
+
# to `.co 3` with a `.rm` of 2.50" instead of 4.50" (B3) both leave
|
|
1440
|
+
# BOOKLET.WS's "Header Odd 1" at exactly the x the unmodified document
|
|
1441
|
+
# prints it at, while moving `.po` from 0.2in to 1.2in (B4) moves it by
|
|
1442
|
+
# exactly 1.00in. Its style's own `right_margin_hmi` is 18000 = 100
|
|
1443
|
+
# columns = 10.00in, and `.po` 0.2in + 10.00in = 10.20in is where the
|
|
1444
|
+
# last glyph of "Header Odd 1" ends, to 0.5pt.
|
|
1445
|
+
#
|
|
1446
|
+
# Every other document in the corpus with a right-aligned head declares
|
|
1447
|
+
# a style right margin EQUAL to its own `.rm` (a "Header at Top of
|
|
1448
|
+
# Page" style at 11700 = 6.50in beside a `.rm 65`), which is why the
|
|
1449
|
+
# `.rm` reading was right everywhere else and wrong here.
|
|
1450
|
+
header_style_rm: dict = field(default_factory=dict)
|
|
1451
|
+
footer_style_rm: dict = field(default_factory=dict)
|
|
1452
|
+
# M16: the LINE HEIGHT in force where this head/foot line was DEFINED,
|
|
1453
|
+
# in 48ths of an inch -- the step between successive running-head
|
|
1454
|
+
# lines, and 0 when the document asked for zero. `{line: lh_48}`.
|
|
1455
|
+
#
|
|
1456
|
+
# Measured (probe B1): removing BOOKLET.WS's own `.lh 0` -- which sits
|
|
1457
|
+
# immediately before its `.f1`/`.h1`/`.h2` and is cancelled by a
|
|
1458
|
+
# `.lh12` immediately after them -- makes real WS7 print its two header
|
|
1459
|
+
# lines on two rows 1/6in apart instead of both on one row, with the
|
|
1460
|
+
# columns left in place. Turning the columns off instead (B2) leaves
|
|
1461
|
+
# them on one row. So the collapse is the line height's doing, not the
|
|
1462
|
+
# column grid's, and it is the value in force at the COMMAND (the
|
|
1463
|
+
# `.lh12` that follows governs the body and not these lines).
|
|
1464
|
+
#
|
|
1465
|
+
# The block is anchored on its LAST line either way (probe B5: a
|
|
1466
|
+
# one-line head prints on the same row the second of two occupies), so
|
|
1467
|
+
# a zero step stacks every line onto that row.
|
|
1468
|
+
header_leads: dict = field(default_factory=dict)
|
|
1469
|
+
footer_leads: dict = field(default_factory=dict)
|
|
1430
1470
|
# Planning #250: `.H1E`/`.H1O`/`.F1E`/`.F1O` (WSFORMAT.TXT: ".HE can
|
|
1431
1471
|
# optionally specify even or odd numbered page headers" / the matching
|
|
1432
1472
|
# ".FO" sentence -- confirmed the ONLY two lines that do; `.H2`-`.H5`/
|
|
@@ -3273,7 +3313,18 @@ def _parse_format_dot(cmd: bytes, state: dict) -> None:
|
|
|
3273
3313
|
resolved = _resolve_lh_arg(value, m.group(2))
|
|
3274
3314
|
if resolved is not None: # junk/non-positive: state stands
|
|
3275
3315
|
state['lead_48'] = resolved
|
|
3316
|
+
state['hf_lead_48'] = resolved
|
|
3276
3317
|
state['lh_auto'] = False
|
|
3318
|
+
elif value == 0:
|
|
3319
|
+
# M16 (2026-09-15): `.lh 0` leaves `lead_48` alone --
|
|
3320
|
+
# a zero body leading is not something any renderer or
|
|
3321
|
+
# the pagination arithmetic can use, and no evidence
|
|
3322
|
+
# says WordStar applies it to the body -- but it IS
|
|
3323
|
+
# what a running head's own line step becomes, proved
|
|
3324
|
+
# by probes B0/B1/B2 against real WS7 (see
|
|
3325
|
+
# `Document.header_leads`). Recorded on its own key so
|
|
3326
|
+
# the head can read it and nothing else changes.
|
|
3327
|
+
state['hf_lead_48'] = 0.0
|
|
3277
3328
|
elif name in (b'LM', b'RM', b'PM'): # left / right / paragraph margin
|
|
3278
3329
|
# Print columns at 10 CPI, matching `.po`; a unit suffix converts. The
|
|
3279
3330
|
# archive writes both (`.rm 65` and `.rm 6.5"`).
|
|
@@ -3637,7 +3688,8 @@ def _hf_within(doc, cur, cur_line):
|
|
|
3637
3688
|
|
|
3638
3689
|
def _parse_head_foot(cmd: bytes, doc, encoding: str, anchor=None, font_idx=None,
|
|
3639
3690
|
tab_mark=None, align=None, style_attrs=None,
|
|
3640
|
-
pctl_marks=None, within=None
|
|
3691
|
+
pctl_marks=None, within=None, style_rm_cols=None,
|
|
3692
|
+
lead_48=None):
|
|
3641
3693
|
"""Record `.he`/`.h1`-`.h5` and `.fo`/`.f1`-`.f5` text on the Document.
|
|
3642
3694
|
|
|
3643
3695
|
`.HE` and `.FO` are line 1; the numbered forms select their own line, so a
|
|
@@ -3766,6 +3818,18 @@ def _parse_head_foot(cmd: bytes, doc, encoding: str, anchor=None, font_idx=None,
|
|
|
3766
3818
|
which_style_attrs = (doc.header_style_attrs if kind == 'H'
|
|
3767
3819
|
else doc.footer_style_attrs)
|
|
3768
3820
|
which_style_attrs[line] = style_attrs or frozenset()
|
|
3821
|
+
# M16: see `Document.header_style_rm`/`header_leads` for the probes.
|
|
3822
|
+
which_style_rm = (doc.header_style_rm if kind == 'H'
|
|
3823
|
+
else doc.footer_style_rm)
|
|
3824
|
+
if style_rm_cols is not None:
|
|
3825
|
+
which_style_rm[line] = style_rm_cols
|
|
3826
|
+
else:
|
|
3827
|
+
which_style_rm.pop(line, None)
|
|
3828
|
+
which_leads = doc.header_leads if kind == 'H' else doc.footer_leads
|
|
3829
|
+
if lead_48 is not None:
|
|
3830
|
+
which_leads[line] = lead_48
|
|
3831
|
+
else:
|
|
3832
|
+
which_leads.pop(line, None)
|
|
3769
3833
|
if parity is not None:
|
|
3770
3834
|
which_p = doc.headers_parity if kind == 'H' else doc.footers_parity
|
|
3771
3835
|
which_fonts_p = (doc.header_fonts_parity if kind == 'H'
|
|
@@ -5162,6 +5226,26 @@ def _symmetric_blocks(data: bytes, encoding: str, raw_out=None):
|
|
|
5162
5226
|
includes, driver[0], sorted(shift_runs), marks, header,
|
|
5163
5227
|
pcl_programs)
|
|
5164
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
|
+
|
|
5165
5249
|
def _parse_style_library(raw: bytes, base: int, encoding: str = 'cp437'):
|
|
5166
5250
|
"""The paragraph style library at file-absolute offset `base`.
|
|
5167
5251
|
|
|
@@ -6146,6 +6230,7 @@ def parse_ws(data: bytes, encoding: str = 'cp437') -> Document:
|
|
|
6146
6230
|
hf_align = None
|
|
6147
6231
|
hf_style_font_idx = None
|
|
6148
6232
|
hf_style_attrs = None
|
|
6233
|
+
hf_style_rm = None
|
|
6149
6234
|
if hf_style_w0 is not None and (hf_style_w0 >> 8) == 0x02:
|
|
6150
6235
|
_hf_entry = style_slots.get(hf_style_w0 & 0xFF)
|
|
6151
6236
|
if _hf_entry is not None:
|
|
@@ -6153,6 +6238,11 @@ def parse_ws(data: bytes, encoding: str = 'cp437') -> Document:
|
|
|
6153
6238
|
if _hf_entry.get('font') and any(_hf_entry['font']):
|
|
6154
6239
|
hf_style_font_idx = _style_font(_hf_entry['font'])
|
|
6155
6240
|
hf_style_attrs = _hf_entry.get('attrs')
|
|
6241
|
+
# M16: the same 180-HMI-per-column conversion
|
|
6242
|
+
# `emit.py` already applies to a BODY block's own
|
|
6243
|
+
# style right margin, read here for the head.
|
|
6244
|
+
if _hf_entry.get('right_margin_hmi'):
|
|
6245
|
+
hf_style_rm = _hf_entry['right_margin_hmi'] / 180.0
|
|
6156
6246
|
_parse_head_foot(cmd if strip_hibit else raw.rstrip(), doc,
|
|
6157
6247
|
encoding,
|
|
6158
6248
|
anchor=len(doc.blocks) + (1 if cur.lines or
|
|
@@ -6175,7 +6265,16 @@ def parse_ws(data: bytes, encoding: str = 'cp437') -> Document:
|
|
|
6175
6265
|
else hf_style_font_idx),
|
|
6176
6266
|
tab_mark=hf_tab_mark,
|
|
6177
6267
|
align=hf_align, style_attrs=hf_style_attrs,
|
|
6178
|
-
pctl_marks=hf_pctl_marks
|
|
6268
|
+
pctl_marks=hf_pctl_marks,
|
|
6269
|
+
style_rm_cols=hf_style_rm,
|
|
6270
|
+
# M16: the `.lh` in force RIGHT HERE, which is
|
|
6271
|
+
# what the step between running-head lines
|
|
6272
|
+
# turns out to be -- `hf_lead_48` and not
|
|
6273
|
+
# `lead_48`, because a `.lh 0` leaves the
|
|
6274
|
+
# latter standing (it is not a body leading
|
|
6275
|
+
# any renderer could use) while still being
|
|
6276
|
+
# the head's own answer.
|
|
6277
|
+
lead_48=fmt.get('hf_lead_48'))
|
|
6179
6278
|
# The index of the block this entry POINTS AT -- the one that follows it,
|
|
6180
6279
|
# which is the block still open (if it has content) or the next to open.
|
|
6181
6280
|
# "This heading is in the table of contents" refers forward, not back.
|
|
@@ -6422,8 +6521,34 @@ def parse_ws(data: bytes, encoding: str = 'cp437') -> Document:
|
|
|
6422
6521
|
# value forward, not reset to nothing -- style_fmt.clear()
|
|
6423
6522
|
# below must not lose it.
|
|
6424
6523
|
_prev_vmi = style_fmt.get('line_height_vmi')
|
|
6524
|
+
# Ruling 2026-09-16 ("Style-library strikeout runs until a
|
|
6525
|
+
# style clears it"): an attribute a style turns ON stays on
|
|
6526
|
+
# past that paragraph. WSFORMAT.TXT's own rule for the two
|
|
6527
|
+
# attribute words is "if both corresponding bits are off,
|
|
6528
|
+
# then the attribute is inherited from the current state" --
|
|
6529
|
+
# so only a later style that sets the bit in its attrs_OFF
|
|
6530
|
+
# word ends the run. Real WS7 agrees: NOVEL.WS's H3 sets
|
|
6531
|
+
# strikeout on the title page and nothing in that file's
|
|
6532
|
+
# library ever clears it, and the LaserJet capture prints
|
|
6533
|
+
# the overstrike dash row on all 52 pages.
|
|
6534
|
+
_prev_sticky = style_fmt.get('sticky_attrs', frozenset())
|
|
6425
6535
|
style_fmt.clear()
|
|
6426
6536
|
style_fmt['style_id'] = slot
|
|
6537
|
+
_sticky = set(_prev_sticky)
|
|
6538
|
+
if entry is not None:
|
|
6539
|
+
_on, _off = entry.get('attrs_on') or 0, entry.get('attrs_off') or 0
|
|
6540
|
+
for _bit, _tag in STICKY_STYLE_ATTRS:
|
|
6541
|
+
if _on & _bit:
|
|
6542
|
+
_sticky.add(_tag)
|
|
6543
|
+
elif _off & _bit:
|
|
6544
|
+
_sticky.discard(_tag)
|
|
6545
|
+
style_fmt['sticky_attrs'] = frozenset(_sticky)
|
|
6546
|
+
if _sticky:
|
|
6547
|
+
# Also for an UNRESOLVABLE handle (a 0x03xx editing-temp
|
|
6548
|
+
# pool entry, or a slot this file's library doesn't
|
|
6549
|
+
# carry): it declares neither word, so every attribute
|
|
6550
|
+
# inherits, including the running one.
|
|
6551
|
+
style_fmt['attrs'] = frozenset(_sticky)
|
|
6427
6552
|
if entry is not None:
|
|
6428
6553
|
style_fmt['style_name'] = entry['name']
|
|
6429
6554
|
style_fmt['heading'] = _style_heading_level(entry['name'])
|
|
@@ -6443,8 +6568,11 @@ def parse_ws(data: bytes, encoding: str = 'cp437') -> Document:
|
|
|
6443
6568
|
# HMI 1/1800in -> print columns at 10 CPI,
|
|
6444
6569
|
# the unit .lm/.rm already use (180 = 1 col)
|
|
6445
6570
|
style_fmt[dst_k] = round(hmi / 180)
|
|
6446
|
-
if entry.get('attrs'):
|
|
6447
|
-
|
|
6571
|
+
if entry.get('attrs') or _sticky:
|
|
6572
|
+
# The style's own ON bits, plus every sticky
|
|
6573
|
+
# attribute still running from an earlier style.
|
|
6574
|
+
style_fmt['attrs'] = frozenset(
|
|
6575
|
+
entry.get('attrs') or ()) | style_fmt['sticky_attrs']
|
|
6448
6576
|
# Register C5: the style's own declared colour index
|
|
6449
6577
|
# (0-15, WSFORMAT's fixed CGA/EGA palette -- same
|
|
6450
6578
|
# space as an inline type-1 colour change). `is not
|