ctrl-kd 4.0.0__tar.gz → 4.5.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (75) hide show
  1. ctrl_kd-4.5.0/PKG-INFO +106 -0
  2. ctrl_kd-4.5.0/README.md +87 -0
  3. ctrl_kd-4.5.0/pyproject.toml +61 -0
  4. ctrl_kd-4.5.0/src/ctrl_kd.egg-info/PKG-INFO +106 -0
  5. ctrl_kd-4.5.0/src/ctrl_kd.egg-info/SOURCES.txt +63 -0
  6. {ctrl_kd-4.0.0 → ctrl_kd-4.5.0}/src/ctrlkd/__init__.py +1 -1
  7. {ctrl_kd-4.0.0 → ctrl_kd-4.5.0}/src/ctrlkd/cli.py +150 -6
  8. {ctrl_kd-4.0.0 → ctrl_kd-4.5.0}/src/ctrlkd/core.py +1642 -92
  9. ctrl_kd-4.5.0/src/ctrlkd/emit.py +3002 -0
  10. {ctrl_kd-4.0.0 → ctrl_kd-4.5.0}/src/ctrlkd/fontmap.py +98 -8
  11. ctrl_kd-4.5.0/src/ctrlkd/info.py +176 -0
  12. ctrl_kd-4.5.0/src/ctrlkd/layout.py +652 -0
  13. ctrl_kd-4.5.0/src/ctrlkd/pdf.py +5700 -0
  14. ctrl_kd-4.5.0/src/ctrlkd/pictures.py +180 -0
  15. ctrl_kd-4.5.0/src/ctrlkd/pix.py +600 -0
  16. ctrl_kd-4.5.0/src/ctrlkd/piximg.py +194 -0
  17. ctrl_kd-4.5.0/src/ctrlkd/samples/LYING.WS +0 -0
  18. ctrl_kd-4.5.0/src/ctrlkd/samples/OCAPTAIN.WS +38 -0
  19. ctrl_kd-4.5.0/src/ctrlkd/samples/TWAINLET.WS +56 -0
  20. ctrl_kd-4.5.0/src/ctrlkd/samples/WARPRAYR.WS +0 -0
  21. {ctrl_kd-4.0.0 → ctrl_kd-4.5.0}/src/ctrlkd/wschange.py +1 -2
  22. {ctrl_kd-4.0.0 → ctrl_kd-4.5.0}/tests/test_ctrlkd.py +1826 -98
  23. ctrl_kd-4.5.0/tests/test_endnote_leading_gap.py +107 -0
  24. ctrl_kd-4.5.0/tests/test_fidelity_gate.py +231 -0
  25. ctrl_kd-4.5.0/tests/test_flags_toc_inline.py +307 -0
  26. ctrl_kd-4.5.0/tests/test_glyph_aspect.py +186 -0
  27. ctrl_kd-4.5.0/tests/test_layout_marks.py +221 -0
  28. ctrl_kd-4.5.0/tests/test_lj6dtp_bullet_glyph.py +79 -0
  29. ctrl_kd-4.5.0/tests/test_lj6dtp_char_substitution.py +184 -0
  30. ctrl_kd-4.5.0/tests/test_lj6dtp_colour_restore.py +77 -0
  31. ctrl_kd-4.5.0/tests/test_lj6dtp_heading_face.py +276 -0
  32. ctrl_kd-4.5.0/tests/test_lj6dtp_hp_patterns.py +115 -0
  33. ctrl_kd-4.5.0/tests/test_lj6dtp_legend_line_spacing.py +68 -0
  34. ctrl_kd-4.5.0/tests/test_lj6dtp_pcl_rectangles.py +221 -0
  35. ctrl_kd-4.5.0/tests/test_lj6dtp_shading_table_rules.py +73 -0
  36. ctrl_kd-4.5.0/tests/test_lj6dtp_table_rule_weight.py +82 -0
  37. ctrl_kd-4.5.0/tests/test_modern_box_regions.py +197 -0
  38. ctrl_kd-4.5.0/tests/test_modern_line_spacing.py +234 -0
  39. ctrl_kd-4.5.0/tests/test_modern_lint.py +1857 -0
  40. ctrl_kd-4.5.0/tests/test_note_rulings_20260824.py +266 -0
  41. ctrl_kd-4.5.0/tests/test_pictures.py +812 -0
  42. ctrl_kd-4.5.0/tests/test_pix.py +583 -0
  43. ctrl_kd-4.5.0/tests/test_polarity_gate.py +197 -0
  44. ctrl_kd-4.5.0/tests/test_printed_fidelity.py +824 -0
  45. ctrl_kd-4.5.0/tests/test_samples.py +117 -0
  46. ctrl_kd-4.5.0/tests/test_sawyer_corpus.py +209 -0
  47. ctrl_kd-4.5.0/tests/test_screenplay_detection.py +191 -0
  48. ctrl_kd-4.5.0/tests/test_screenplay_pdf.py +232 -0
  49. ctrl_kd-4.5.0/tests/test_screenplay_rendering.py +145 -0
  50. ctrl_kd-4.5.0/tests/test_sentence_spacing_n9.py +299 -0
  51. ctrl_kd-4.5.0/tests/test_style_leading.py +451 -0
  52. ctrl_kd-4.5.0/tests/test_verse_quote_couplet.py +109 -0
  53. ctrl_kd-4.5.0/tests/test_verse_spacing.py +95 -0
  54. {ctrl_kd-4.0.0 → ctrl_kd-4.5.0}/tests/test_writer.py +6 -78
  55. ctrl_kd-4.5.0/tests/test_wschange.py +77 -0
  56. ctrl_kd-4.0.0/PKG-INFO +0 -170
  57. ctrl_kd-4.0.0/README.md +0 -151
  58. ctrl_kd-4.0.0/pyproject.toml +0 -36
  59. ctrl_kd-4.0.0/src/ctrl_kd.egg-info/PKG-INFO +0 -170
  60. ctrl_kd-4.0.0/src/ctrl_kd.egg-info/SOURCES.txt +0 -25
  61. ctrl_kd-4.0.0/src/ctrlkd/emit.py +0 -1085
  62. ctrl_kd-4.0.0/src/ctrlkd/info.py +0 -59
  63. ctrl_kd-4.0.0/src/ctrlkd/layout.py +0 -341
  64. ctrl_kd-4.0.0/src/ctrlkd/pdf.py +0 -1938
  65. ctrl_kd-4.0.0/tests/test_wschange.py +0 -194
  66. {ctrl_kd-4.0.0 → ctrl_kd-4.5.0}/LICENSE +0 -0
  67. {ctrl_kd-4.0.0 → ctrl_kd-4.5.0}/setup.cfg +0 -0
  68. {ctrl_kd-4.0.0 → ctrl_kd-4.5.0}/src/ctrl_kd.egg-info/dependency_links.txt +0 -0
  69. {ctrl_kd-4.0.0 → ctrl_kd-4.5.0}/src/ctrl_kd.egg-info/entry_points.txt +0 -0
  70. {ctrl_kd-4.0.0 → ctrl_kd-4.5.0}/src/ctrl_kd.egg-info/top_level.txt +0 -0
  71. {ctrl_kd-4.0.0 → ctrl_kd-4.5.0}/src/ctrlkd/afm.py +0 -0
  72. {ctrl_kd-4.0.0 → ctrl_kd-4.5.0}/src/ctrlkd/convert.py +0 -0
  73. {ctrl_kd-4.0.0 → ctrl_kd-4.5.0}/src/ctrlkd/symbolmap.py +0 -0
  74. {ctrl_kd-4.0.0 → ctrl_kd-4.5.0}/src/ctrlkd/typestyles.py +0 -0
  75. {ctrl_kd-4.0.0 → ctrl_kd-4.5.0}/src/ctrlkd/writer.py +0 -0
ctrl_kd-4.5.0/PKG-INFO ADDED
@@ -0,0 +1,106 @@
1
+ Metadata-Version: 2.4
2
+ Name: ctrl-kd
3
+ Version: 4.5.0
4
+ Summary: Convert WordStar 4-7 documents and print-to-disk files to text, Markdown, HTML, RTF, or PDF. ^KD: save and done.
5
+ Author: Jon Michaels
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/jonmichaels/ctrl-kd
8
+ Keywords: wordstar,converter,retrocomputing,archive,dos
9
+ Classifier: Development Status :: 5 - Production/Stable
10
+ Classifier: Environment :: Console
11
+ Classifier: License :: OSI Approved :: MIT License
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Topic :: Text Processing :: Filters
14
+ Classifier: Topic :: System :: Archiving
15
+ Requires-Python: >=3.9
16
+ Description-Content-Type: text/markdown
17
+ License-File: LICENSE
18
+ Dynamic: license-file
19
+
20
+ ```
21
+ __ __ __ __
22
+ _____/ /______/ / / /______/ /
23
+ / ___/ __/ ___/ /_____/ //_/ __ /
24
+ / /__/ /_/ / / /_____/ ,< / /_/ /
25
+ \___/\__/_/ /_/ /_/|_|\__,_/
26
+ ```
27
+ # ctrl-kd
28
+
29
+ Convert WordStar for DOS v4-v7 files to modern formats. **^KD: save and done.**
30
+
31
+ `ctrl-kd` reads WordStar for DOS documents, and WordStar print stream files,
32
+ and writes plain text, Markdown, HTML, RTF, or PDF (set on a viewer's
33
+ built-in base-14 fonts — no dependencies, nothing embedded, the page as it
34
+ would have printed: printed mode follows the document's own font blocks and
35
+ its own layout arithmetic, while modern mode is the same document reflowed
36
+ for today: its fonts, headers, and footnotes all carried.
37
+
38
+ ```console
39
+ $ ctrl-kd ESSAY.WS # -> ESSAY.rtf: modern reflow, the
40
+ # document's own fonts carried
41
+ $ ctrl-kd --mode printed LETTER.WS # -> LETTER.pdf: the 1990 facsimile
42
+ $ ctrl-kd ESSAY.WS -t md # modern markdown instead
43
+ $ ctrl-kd ESSAY.WS -t html -t rtf # multiple formats
44
+ $ ctrl-kd --page-settings sawyer X.WS # a known machine's page defaults
45
+ $ ctrl-kd --diagnose MYSTERY.FIL # what IS this file?
46
+ $ ctrl-kd --comments MEMO.WS # include the author's hidden comments
47
+ $ ctrl-kd --no-notes PAPER.WS # body text only, no notes
48
+ $ ctrl-kd --samples DIR # write 4 bundled public-domain sample .WS files into DIR
49
+ ```
50
+
51
+ ## Modes
52
+
53
+ * `--mode modern` (default; bare runs produce RTF): the document brought to a
54
+ modern audience — reflowed, its own fonts and styles carried, footnotes at
55
+ the page bottom, gaps the file never specified filled with today's
56
+ conventions (a comfortable serif at reading size, one-inch margins).
57
+ * `--mode printed` (bare runs produce PDF): every line as laid out, on the
58
+ era's own page — how it came off the printer. Gaps are filled with 1990's
59
+ conventions instead; `--page-settings` supplies a particular machine's.
60
+
61
+ More information: **[FAQ.md](FAQ.md)**, and **[ERAS.md](ERAS.md)**.
62
+
63
+ ## Install
64
+
65
+ ```console
66
+ $ brew install jonmichaels/tap/ctrl-kd # macOS / Linuxbrew
67
+ $ pipx install ctrl-kd # or: pip install ctrl-kd
68
+ ```
69
+
70
+ Python ≥ 3.9, no dependencies. Library API: `ctrlkd.convert(data, to='html')`.
71
+
72
+ ## Adding an output format
73
+
74
+ An output format is one function over the parsed document — register it with the
75
+ `@ctrlkd.emitter` decorator, or ship it as a pip-installable plugin via the
76
+ `ctrlkd.emitters` entry-point group and it appears in the CLI automatically.
77
+ **[EXTENDING.md](EXTENDING.md)** has the IR contract, a complete worked example
78
+ (BBCode in ~40 lines), and a checklist.
79
+
80
+ ## Siblings
81
+
82
+ **[Soft Return](https://github.com/jonmichaels/soft-return)** — macOS viewer and converter plus QuickLook extension.
83
+ Includes a Swift command line utility.
84
+
85
+ ## Lineage
86
+
87
+ I wanted to be able to see the 70-some WordStar 4 files I had from junior high
88
+ and high school. In about an hour and half my agent had my files looking
89
+ pretty good. And then I fell down the research rabbit hole...
90
+
91
+ `ctrl-kd` wouldn't have been possible without the tools and documentation that
92
+ kept WordStar readable: Yohanes Nugroho's WS-CON, Michael Petrie's English port,
93
+ the `wsconvert`project, Robert J. Sawyer's WordStar archive, and the WordStar
94
+ format documentation community.
95
+
96
+ My own test files are personal and are not distributed — this repo's tests use
97
+ synthetic fixtures and some public domain docs I retyped in WordStar 4 and
98
+ WordStar 7 in DOSBox-X, plus Robert J. Sawyer's public WS7 archive (opt-in,
99
+ `pytest -m sawyer`; see `tests/SAWYER-CORPUS.md`) you can run your own tests
100
+ against that if you have a copy.
101
+
102
+ ## Credits
103
+
104
+ Written by Jon Michaels — whose 1987–1992 WordStar files, and the need to read
105
+ them again, are the reason this exists — with Athena (Claude, Anthropic) as
106
+ co-author.
@@ -0,0 +1,87 @@
1
+ ```
2
+ __ __ __ __
3
+ _____/ /______/ / / /______/ /
4
+ / ___/ __/ ___/ /_____/ //_/ __ /
5
+ / /__/ /_/ / / /_____/ ,< / /_/ /
6
+ \___/\__/_/ /_/ /_/|_|\__,_/
7
+ ```
8
+ # ctrl-kd
9
+
10
+ Convert WordStar for DOS v4-v7 files to modern formats. **^KD: save and done.**
11
+
12
+ `ctrl-kd` reads WordStar for DOS documents, and WordStar print stream files,
13
+ and writes plain text, Markdown, HTML, RTF, or PDF (set on a viewer's
14
+ built-in base-14 fonts — no dependencies, nothing embedded, the page as it
15
+ would have printed: printed mode follows the document's own font blocks and
16
+ its own layout arithmetic, while modern mode is the same document reflowed
17
+ for today: its fonts, headers, and footnotes all carried.
18
+
19
+ ```console
20
+ $ ctrl-kd ESSAY.WS # -> ESSAY.rtf: modern reflow, the
21
+ # document's own fonts carried
22
+ $ ctrl-kd --mode printed LETTER.WS # -> LETTER.pdf: the 1990 facsimile
23
+ $ ctrl-kd ESSAY.WS -t md # modern markdown instead
24
+ $ ctrl-kd ESSAY.WS -t html -t rtf # multiple formats
25
+ $ ctrl-kd --page-settings sawyer X.WS # a known machine's page defaults
26
+ $ ctrl-kd --diagnose MYSTERY.FIL # what IS this file?
27
+ $ ctrl-kd --comments MEMO.WS # include the author's hidden comments
28
+ $ ctrl-kd --no-notes PAPER.WS # body text only, no notes
29
+ $ ctrl-kd --samples DIR # write 4 bundled public-domain sample .WS files into DIR
30
+ ```
31
+
32
+ ## Modes
33
+
34
+ * `--mode modern` (default; bare runs produce RTF): the document brought to a
35
+ modern audience — reflowed, its own fonts and styles carried, footnotes at
36
+ the page bottom, gaps the file never specified filled with today's
37
+ conventions (a comfortable serif at reading size, one-inch margins).
38
+ * `--mode printed` (bare runs produce PDF): every line as laid out, on the
39
+ era's own page — how it came off the printer. Gaps are filled with 1990's
40
+ conventions instead; `--page-settings` supplies a particular machine's.
41
+
42
+ More information: **[FAQ.md](FAQ.md)**, and **[ERAS.md](ERAS.md)**.
43
+
44
+ ## Install
45
+
46
+ ```console
47
+ $ brew install jonmichaels/tap/ctrl-kd # macOS / Linuxbrew
48
+ $ pipx install ctrl-kd # or: pip install ctrl-kd
49
+ ```
50
+
51
+ Python ≥ 3.9, no dependencies. Library API: `ctrlkd.convert(data, to='html')`.
52
+
53
+ ## Adding an output format
54
+
55
+ An output format is one function over the parsed document — register it with the
56
+ `@ctrlkd.emitter` decorator, or ship it as a pip-installable plugin via the
57
+ `ctrlkd.emitters` entry-point group and it appears in the CLI automatically.
58
+ **[EXTENDING.md](EXTENDING.md)** has the IR contract, a complete worked example
59
+ (BBCode in ~40 lines), and a checklist.
60
+
61
+ ## Siblings
62
+
63
+ **[Soft Return](https://github.com/jonmichaels/soft-return)** — macOS viewer and converter plus QuickLook extension.
64
+ Includes a Swift command line utility.
65
+
66
+ ## Lineage
67
+
68
+ I wanted to be able to see the 70-some WordStar 4 files I had from junior high
69
+ and high school. In about an hour and half my agent had my files looking
70
+ pretty good. And then I fell down the research rabbit hole...
71
+
72
+ `ctrl-kd` wouldn't have been possible without the tools and documentation that
73
+ kept WordStar readable: Yohanes Nugroho's WS-CON, Michael Petrie's English port,
74
+ the `wsconvert`project, Robert J. Sawyer's WordStar archive, and the WordStar
75
+ format documentation community.
76
+
77
+ My own test files are personal and are not distributed — this repo's tests use
78
+ synthetic fixtures and some public domain docs I retyped in WordStar 4 and
79
+ WordStar 7 in DOSBox-X, plus Robert J. Sawyer's public WS7 archive (opt-in,
80
+ `pytest -m sawyer`; see `tests/SAWYER-CORPUS.md`) you can run your own tests
81
+ against that if you have a copy.
82
+
83
+ ## Credits
84
+
85
+ Written by Jon Michaels — whose 1987–1992 WordStar files, and the need to read
86
+ them again, are the reason this exists — with Athena (Claude, Anthropic) as
87
+ co-author.
@@ -0,0 +1,61 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "ctrl-kd"
7
+ dynamic = ["version"]
8
+ description = "Convert WordStar 4-7 documents and print-to-disk files to text, Markdown, HTML, RTF, or PDF. ^KD: save and done."
9
+ readme = "README.md"
10
+ license = {text = "MIT"}
11
+ authors = [{name = "Jon Michaels"}]
12
+ requires-python = ">=3.9"
13
+ keywords = ["wordstar", "converter", "retrocomputing", "archive", "dos"]
14
+ classifiers = [
15
+ "Development Status :: 5 - Production/Stable",
16
+ "Environment :: Console",
17
+ "License :: OSI Approved :: MIT License",
18
+ "Programming Language :: Python :: 3",
19
+ "Topic :: Text Processing :: Filters",
20
+ "Topic :: System :: Archiving",
21
+ ]
22
+
23
+ [project.urls]
24
+ Homepage = "https://github.com/jonmichaels/ctrl-kd"
25
+
26
+ [project.scripts]
27
+ ctrl-kd = "ctrlkd.cli:main"
28
+
29
+ [tool.setuptools.packages.find]
30
+ where = ["src"]
31
+
32
+ [tool.setuptools.dynamic]
33
+ # single source of truth: src/ctrlkd/__init__.py __version__ -- the 4.0.0
34
+ # release shipped with pyproject still saying 3.0.0 because the version
35
+ # lived in two places and the bump hit one (2026-08-06); never again
36
+ version = {attr = "ctrlkd.__version__"}
37
+
38
+ [tool.setuptools.package-data]
39
+ # The four bundled public-domain sample documents (see samples/README.md and
40
+ # `ctrl-kd --samples DIR`) travel with the wheel/sdist as package data.
41
+ ctrlkd = ["samples/*.WS"]
42
+
43
+ [tool.pytest.ini_options]
44
+ # Two-tier test architecture (K1, 2026-08-26; tier 3/private relocated out
45
+ # of this public repo entirely -- see README). Bare `pytest` must run ONLY
46
+ # tier 1 (tests/test_samples.py and every other test with no marker below)
47
+ # -- green, complete, zero skips, no environment required. Tier 2 is
48
+ # DESELECTED here, not skipped: it never even collects into the report, so
49
+ # a bare run's summary line is an honest denominator.
50
+ #
51
+ # Arming (see tests/sawyer_fixture.py, conftest.py):
52
+ # sawyer CTRLKD_SAWYER_ARCHIVE=/path (legacy alias: CTRLKD_CORPUS_SOURCE)
53
+ #
54
+ # tools/run-full-suite.sh overrides this filter (`-o addopts=""`) for an
55
+ # armed run, where an unarmed gate FAILS instead of silently not running --
56
+ # see that script's own header for why.
57
+ testpaths = ["tests"]
58
+ markers = [
59
+ "sawyer: opt-in tier 2 -- an explicit, committed list of documents from Robert J. Sawyer's public WordStar 7 archive (arm with CTRLKD_SAWYER_ARCHIVE)",
60
+ ]
61
+ addopts = "-m \"not sawyer\""
@@ -0,0 +1,106 @@
1
+ Metadata-Version: 2.4
2
+ Name: ctrl-kd
3
+ Version: 4.5.0
4
+ Summary: Convert WordStar 4-7 documents and print-to-disk files to text, Markdown, HTML, RTF, or PDF. ^KD: save and done.
5
+ Author: Jon Michaels
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/jonmichaels/ctrl-kd
8
+ Keywords: wordstar,converter,retrocomputing,archive,dos
9
+ Classifier: Development Status :: 5 - Production/Stable
10
+ Classifier: Environment :: Console
11
+ Classifier: License :: OSI Approved :: MIT License
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Topic :: Text Processing :: Filters
14
+ Classifier: Topic :: System :: Archiving
15
+ Requires-Python: >=3.9
16
+ Description-Content-Type: text/markdown
17
+ License-File: LICENSE
18
+ Dynamic: license-file
19
+
20
+ ```
21
+ __ __ __ __
22
+ _____/ /______/ / / /______/ /
23
+ / ___/ __/ ___/ /_____/ //_/ __ /
24
+ / /__/ /_/ / / /_____/ ,< / /_/ /
25
+ \___/\__/_/ /_/ /_/|_|\__,_/
26
+ ```
27
+ # ctrl-kd
28
+
29
+ Convert WordStar for DOS v4-v7 files to modern formats. **^KD: save and done.**
30
+
31
+ `ctrl-kd` reads WordStar for DOS documents, and WordStar print stream files,
32
+ and writes plain text, Markdown, HTML, RTF, or PDF (set on a viewer's
33
+ built-in base-14 fonts — no dependencies, nothing embedded, the page as it
34
+ would have printed: printed mode follows the document's own font blocks and
35
+ its own layout arithmetic, while modern mode is the same document reflowed
36
+ for today: its fonts, headers, and footnotes all carried.
37
+
38
+ ```console
39
+ $ ctrl-kd ESSAY.WS # -> ESSAY.rtf: modern reflow, the
40
+ # document's own fonts carried
41
+ $ ctrl-kd --mode printed LETTER.WS # -> LETTER.pdf: the 1990 facsimile
42
+ $ ctrl-kd ESSAY.WS -t md # modern markdown instead
43
+ $ ctrl-kd ESSAY.WS -t html -t rtf # multiple formats
44
+ $ ctrl-kd --page-settings sawyer X.WS # a known machine's page defaults
45
+ $ ctrl-kd --diagnose MYSTERY.FIL # what IS this file?
46
+ $ ctrl-kd --comments MEMO.WS # include the author's hidden comments
47
+ $ ctrl-kd --no-notes PAPER.WS # body text only, no notes
48
+ $ ctrl-kd --samples DIR # write 4 bundled public-domain sample .WS files into DIR
49
+ ```
50
+
51
+ ## Modes
52
+
53
+ * `--mode modern` (default; bare runs produce RTF): the document brought to a
54
+ modern audience — reflowed, its own fonts and styles carried, footnotes at
55
+ the page bottom, gaps the file never specified filled with today's
56
+ conventions (a comfortable serif at reading size, one-inch margins).
57
+ * `--mode printed` (bare runs produce PDF): every line as laid out, on the
58
+ era's own page — how it came off the printer. Gaps are filled with 1990's
59
+ conventions instead; `--page-settings` supplies a particular machine's.
60
+
61
+ More information: **[FAQ.md](FAQ.md)**, and **[ERAS.md](ERAS.md)**.
62
+
63
+ ## Install
64
+
65
+ ```console
66
+ $ brew install jonmichaels/tap/ctrl-kd # macOS / Linuxbrew
67
+ $ pipx install ctrl-kd # or: pip install ctrl-kd
68
+ ```
69
+
70
+ Python ≥ 3.9, no dependencies. Library API: `ctrlkd.convert(data, to='html')`.
71
+
72
+ ## Adding an output format
73
+
74
+ An output format is one function over the parsed document — register it with the
75
+ `@ctrlkd.emitter` decorator, or ship it as a pip-installable plugin via the
76
+ `ctrlkd.emitters` entry-point group and it appears in the CLI automatically.
77
+ **[EXTENDING.md](EXTENDING.md)** has the IR contract, a complete worked example
78
+ (BBCode in ~40 lines), and a checklist.
79
+
80
+ ## Siblings
81
+
82
+ **[Soft Return](https://github.com/jonmichaels/soft-return)** — macOS viewer and converter plus QuickLook extension.
83
+ Includes a Swift command line utility.
84
+
85
+ ## Lineage
86
+
87
+ I wanted to be able to see the 70-some WordStar 4 files I had from junior high
88
+ and high school. In about an hour and half my agent had my files looking
89
+ pretty good. And then I fell down the research rabbit hole...
90
+
91
+ `ctrl-kd` wouldn't have been possible without the tools and documentation that
92
+ kept WordStar readable: Yohanes Nugroho's WS-CON, Michael Petrie's English port,
93
+ the `wsconvert`project, Robert J. Sawyer's WordStar archive, and the WordStar
94
+ format documentation community.
95
+
96
+ My own test files are personal and are not distributed — this repo's tests use
97
+ synthetic fixtures and some public domain docs I retyped in WordStar 4 and
98
+ WordStar 7 in DOSBox-X, plus Robert J. Sawyer's public WS7 archive (opt-in,
99
+ `pytest -m sawyer`; see `tests/SAWYER-CORPUS.md`) you can run your own tests
100
+ against that if you have a copy.
101
+
102
+ ## Credits
103
+
104
+ Written by Jon Michaels — whose 1987–1992 WordStar files, and the need to read
105
+ them again, are the reason this exists — with Athena (Claude, Anthropic) as
106
+ co-author.
@@ -0,0 +1,63 @@
1
+ LICENSE
2
+ README.md
3
+ pyproject.toml
4
+ src/ctrl_kd.egg-info/PKG-INFO
5
+ src/ctrl_kd.egg-info/SOURCES.txt
6
+ src/ctrl_kd.egg-info/dependency_links.txt
7
+ src/ctrl_kd.egg-info/entry_points.txt
8
+ src/ctrl_kd.egg-info/top_level.txt
9
+ src/ctrlkd/__init__.py
10
+ src/ctrlkd/afm.py
11
+ src/ctrlkd/cli.py
12
+ src/ctrlkd/convert.py
13
+ src/ctrlkd/core.py
14
+ src/ctrlkd/emit.py
15
+ src/ctrlkd/fontmap.py
16
+ src/ctrlkd/info.py
17
+ src/ctrlkd/layout.py
18
+ src/ctrlkd/pdf.py
19
+ src/ctrlkd/pictures.py
20
+ src/ctrlkd/pix.py
21
+ src/ctrlkd/piximg.py
22
+ src/ctrlkd/symbolmap.py
23
+ src/ctrlkd/typestyles.py
24
+ src/ctrlkd/writer.py
25
+ src/ctrlkd/wschange.py
26
+ src/ctrlkd/samples/LYING.WS
27
+ src/ctrlkd/samples/OCAPTAIN.WS
28
+ src/ctrlkd/samples/TWAINLET.WS
29
+ src/ctrlkd/samples/WARPRAYR.WS
30
+ tests/test_ctrlkd.py
31
+ tests/test_endnote_leading_gap.py
32
+ tests/test_fidelity_gate.py
33
+ tests/test_flags_toc_inline.py
34
+ tests/test_glyph_aspect.py
35
+ tests/test_layout_marks.py
36
+ tests/test_lj6dtp_bullet_glyph.py
37
+ tests/test_lj6dtp_char_substitution.py
38
+ tests/test_lj6dtp_colour_restore.py
39
+ tests/test_lj6dtp_heading_face.py
40
+ tests/test_lj6dtp_hp_patterns.py
41
+ tests/test_lj6dtp_legend_line_spacing.py
42
+ tests/test_lj6dtp_pcl_rectangles.py
43
+ tests/test_lj6dtp_shading_table_rules.py
44
+ tests/test_lj6dtp_table_rule_weight.py
45
+ tests/test_modern_box_regions.py
46
+ tests/test_modern_line_spacing.py
47
+ tests/test_modern_lint.py
48
+ tests/test_note_rulings_20260824.py
49
+ tests/test_pictures.py
50
+ tests/test_pix.py
51
+ tests/test_polarity_gate.py
52
+ tests/test_printed_fidelity.py
53
+ tests/test_samples.py
54
+ tests/test_sawyer_corpus.py
55
+ tests/test_screenplay_detection.py
56
+ tests/test_screenplay_pdf.py
57
+ tests/test_screenplay_rendering.py
58
+ tests/test_sentence_spacing_n9.py
59
+ tests/test_style_leading.py
60
+ tests/test_verse_quote_couplet.py
61
+ tests/test_verse_spacing.py
62
+ tests/test_writer.py
63
+ tests/test_wschange.py
@@ -8,4 +8,4 @@ from .pdf import emit_pdf # registers the 'pdf' format
8
8
  from .convert import convert, select_notes, DEFAULT_NOTE_KINDS, ALL_NOTE_KINDS
9
9
  from .info import document_info
10
10
 
11
- __version__ = '4.0.0'
11
+ __version__ = '4.5.0'
@@ -7,8 +7,9 @@
7
7
  ctrl-kd --page-settings sawyer X.WS # a known machine's page defaults
8
8
  ctrl-kd --diagnose MYSTERY.FIL # what IS this file?
9
9
  ctrl-kd -t text -t html -d out/ *.WS # batch, multiple formats
10
+ ctrl-kd --samples DIR # write the 4 bundled sample .WS files into DIR
10
11
  """
11
- import argparse, json, os, sys
12
+ import argparse, importlib.resources, json, os, shutil, sys
12
13
 
13
14
  BANNER = r""" __ __ __ __
14
15
  _____/ /______/ / / /______/ /
@@ -21,7 +22,7 @@ BANNER = r""" __ __ __ __
21
22
  # because the name never changes; no .flf machinery needed.
22
23
 
23
24
 
24
- from . import core, emit
25
+ from . import core, emit, pictures
25
26
  from .convert import DEFAULT_NOTE_KINDS # module attr, not the re-exported convert()
26
27
 
27
28
  def diagnose(path, data):
@@ -30,6 +31,26 @@ def diagnose(path, data):
30
31
  from .info import document_info
31
32
  return document_info(data, path=path)
32
33
 
34
+ # The four public-domain WordStar sample documents (see samples/README.md at
35
+ # the repo root for credits) -- shipped as package data (src/ctrlkd/samples/,
36
+ # [tool.setuptools.package-data] in pyproject.toml) so `--samples` works from
37
+ # an installed wheel, not just a checkout.
38
+ SAMPLE_NAMES = ['LYING.WS', 'OCAPTAIN.WS', 'TWAINLET.WS', 'WARPRAYR.WS']
39
+
40
+
41
+ def write_samples(outdir):
42
+ """Write the four bundled sample documents into outdir. Returns the list
43
+ of paths written."""
44
+ os.makedirs(outdir, exist_ok=True)
45
+ written = []
46
+ samples_dir = importlib.resources.files('ctrlkd').joinpath('samples')
47
+ for name in SAMPLE_NAMES:
48
+ dest = os.path.join(outdir, name)
49
+ with importlib.resources.as_file(samples_dir.joinpath(name)) as src:
50
+ shutil.copyfile(src, dest)
51
+ written.append(dest)
52
+ return written
53
+
33
54
  def main(argv=None):
34
55
  emit.load_plugins() # third-party emitters (ctrlkd.emitters entry points)
35
56
  ap = argparse.ArgumentParser(
@@ -41,7 +62,12 @@ def main(argv=None):
41
62
  from . import __version__
42
63
  ap.add_argument('--version', action='version',
43
64
  version=BANNER + f'\nctrl-kd {__version__}')
44
- ap.add_argument('files', nargs='+', help='input file(s)')
65
+ ap.add_argument('files', nargs='*', help='input file(s) (not required '
66
+ 'with --samples)')
67
+ ap.add_argument('--samples', metavar='DIR',
68
+ help='write the four bundled public-domain WordStar '
69
+ 'sample documents into DIR (see samples/README.md); '
70
+ 'no conversion, files are not required')
45
71
  ap.add_argument('-t', '--to', action='append', choices=emit.formats(),
46
72
  help='output format (repeatable). Default follows the '
47
73
  'mode: modern -> rtf (the full-fidelity living '
@@ -103,6 +129,70 @@ def main(argv=None):
103
129
  ap.add_argument('--no-styles', action='store_true',
104
130
  help='omit paragraph-style pass-through (HTML classes + '
105
131
  'generated CSS, RTF stylesheet) from the output')
132
+ ap.add_argument('--headers', choices=('on', 'off'), default='on',
133
+ help='headers, footers, and page numbers in the paged '
134
+ 'surfaces (Printed/Native PDF and RTF). Default: on')
135
+ ap.add_argument('--page-numbers', choices=('auto', 'on', 'off'),
136
+ default='auto',
137
+ help="WordStar's own AUTOMATIC page number -- the one "
138
+ '.pc positions, a separate mechanism from a # the '
139
+ 'author placed inside a real .he/.fo (that always '
140
+ 'prints, unaffected by this flag). Printed PDF '
141
+ 'only. auto (DEFAULT): the document\'s own dot '
142
+ 'commands decide -- .pn/.pg turn it on, .op turns '
143
+ 'it off, exactly like real WordStar; a document '
144
+ 'that never touches any of the four gets no '
145
+ 'number, byte-identical to before this flag '
146
+ 'existed. on: force stock default numbering '
147
+ '(bottom-center; .pc repositions it) even on a '
148
+ 'silent document. off: suppress it unconditionally. '
149
+ 'A declared footer always pre-empts it, in every '
150
+ 'mode (WSFORMAT.WS: "active only when the footers '
151
+ 'are not in use"). --headers off also suppresses '
152
+ 'it, per --headers\' own documented scope.')
153
+ ap.add_argument('--line-numbers', choices=('on', 'off'), default='on',
154
+ help="the document's own .l# line-number gutter in the "
155
+ 'paged surfaces (Printed/Native PDF and RTF); no '
156
+ 'effect on a document that never set .l#. Default: on')
157
+ ap.add_argument('--sentence-spacing', choices=('auto', 'keep', 'single'),
158
+ default='auto',
159
+ help='the typewriter double space after a sentence-'
160
+ "ending '.', '?', or '!'. auto (DEFAULT): follows "
161
+ '--mode -- modern converts it to a single space '
162
+ '(the modern typographic convention); printed '
163
+ 'keeps the document exactly as authored (period '
164
+ 'fidelity). keep/single force that choice '
165
+ 'regardless of mode. A simple textual rule, no '
166
+ 'abbreviation detection: "e.g. x" collapses like '
167
+ 'a real sentence end. Markdown never emits a '
168
+ 'trailing double space from this either way (that '
169
+ "is CommonMark's own hard-break marker) -- unrelated "
170
+ 'to this flag, always on.')
171
+ ap.add_argument('--toc', choices=('on', 'off'), default='off',
172
+ help='compile a Table of Contents (.tc) and Index (.ix) '
173
+ 'section at the document end, in every format; the '
174
+ 'two paged surfaces (Printed PDF and RTF) resolve '
175
+ 'each entry to a real page number, every other '
176
+ 'format lists entries without one. Default: off')
177
+ ap.add_argument('--inline-styling', choices=('on', 'off'), default='on',
178
+ help='inline colour (^A) and font-size (^B... a symmetric '
179
+ 'type-2 font block) changes the author placed mid-'
180
+ 'text -- RTF gets \\cf from a 16-colour screen '
181
+ 'palette and \\fsN; HTML gets a span with color/'
182
+ 'font-size. Default: on')
183
+ ap.add_argument('--pictures', choices=('off', 'embed', 'export'), default='embed',
184
+ help='WS5+ PIX image references (register, "PIX images '
185
+ 'RULED IN"). embed (DEFAULT): RTF/PDF native '
186
+ 'embedding, HTML data URI, MD exports files + a '
187
+ 'one-line stderr note (MD has no true embed). '
188
+ 'export: PNG files under <docname>-images/ beside '
189
+ 'the output, relative links from HTML/MD; RTF/PDF '
190
+ 'still embed (no portable reference mechanism) AND '
191
+ 'the PNGs are also written. off: the plain '
192
+ '[image: NAME] placeholder, as before this flag '
193
+ 'existed. A missing/unreadable .PIX is reported on '
194
+ 'stderr (name + probed locations) and never fails '
195
+ 'the conversion; the placeholder is kept either way.')
106
196
  ap.add_argument('--no-notes', action='store_true',
107
197
  help='omit footnotes, endnotes and annotations from the output')
108
198
  ap.add_argument('--comments', action='store_true',
@@ -112,6 +202,12 @@ def main(argv=None):
112
202
  help='report what the file is (variant, margin, dot commands, '
113
203
  'unknown codes) as JSON; no conversion')
114
204
  a = ap.parse_args(argv)
205
+ if a.samples:
206
+ for dest in write_samples(a.samples):
207
+ print(f'-> {dest}', file=sys.stderr)
208
+ return 0
209
+ if not a.files:
210
+ ap.error('the following arguments are required: files')
115
211
  mode_explicit = a.mode is not None
116
212
  a.mode = a.mode or 'modern'
117
213
  # The default format follows the mode (ruling 2026-08-05): bare modern =
@@ -223,11 +319,21 @@ def main(argv=None):
223
319
  doc.meta['page'] = core.effective_page(doc.meta['page'],
224
320
  page_settings)
225
321
  base = os.path.splitext(os.path.basename(path))[0]
322
+ # Round 19 (PIX images RULED IN, ledger PIX row): resolved/decoded
323
+ # ONCE per document, reused across every requested format (each
324
+ # PIX file is read and decoded at most once regardless of how
325
+ # many -t flags this run carries). `path` is the real filesystem
326
+ # location resolution searches near -- the CLI always has one; a
327
+ # library caller without a real path passes None (every tag then
328
+ # reports 'unresolved', ctrlkd.pictures' own documented behavior).
329
+ pix_results = (pictures.resolve_document_pictures(doc, path)
330
+ if doc.graphics else [])
331
+ if pix_results and a.pictures != 'off':
332
+ # 'off' means the user doesn't want the feature at all -- no
333
+ # point reporting misses for pictures nothing will render.
334
+ pictures.report_misses(pix_results, path, path)
226
335
  for fmt in formats:
227
336
  reg = emit.get_emitter(fmt)
228
- out = reg['fn'](doc, a.mode, title=base, notes=notes,
229
- styles=not a.no_styles, fonts_target=a.fonts,
230
- note_refs=a.note_refs)
231
337
  if a.output:
232
338
  dest = a.output
233
339
  else:
@@ -235,6 +341,44 @@ def main(argv=None):
235
341
  base + reg['ext'])
236
342
  if a.outdir:
237
343
  os.makedirs(a.outdir, exist_ok=True)
344
+ # --pictures export writes PNG files beside the output for
345
+ # every applicable format; MD's own embed mode degrades to
346
+ # the same export-and-link behavior (ruled: MD has no true
347
+ # embed) -- ONE stderr note for that degradation, here, not
348
+ # inside emit_markdown (library functions stay silent, same
349
+ # convention as every other emit_* degradation this session).
350
+ image_links = None
351
+ # MD's embed-degrades-to-export only matters in MODERN mode --
352
+ # a Printed-mode MD body is emit_text's fenced facsimile
353
+ # (never consults image_links at all, by design: a fence is
354
+ # the emitter's own "verbatim" promise), so exporting files
355
+ # and printing the degradation note for it would be pure
356
+ # waste, not a correctness issue but a confusing one.
357
+ need_export = (a.pictures == 'export'
358
+ or (fmt == 'md' and a.pictures == 'embed'
359
+ and a.mode == 'modern'))
360
+ if need_export and any(r.ok for r in pix_results):
361
+ images_dir = os.path.join(os.path.dirname(dest) or '.',
362
+ base + '-images')
363
+ written = pictures.write_export_images(pix_results, images_dir)
364
+ image_links = {i: f'{base}-images/{name}'
365
+ for i, name in written.items()}
366
+ if fmt == 'md' and a.pictures == 'embed' and written:
367
+ print(f'ctrl-kd: {path}: Markdown has no true image '
368
+ f'embedding -- exporting {len(written)} PNG '
369
+ f"file(s) to {os.path.basename(images_dir)}/ "
370
+ f'and linking to them instead (degradation per '
371
+ f'the pictures flag ruling)', file=sys.stderr)
372
+ out = reg['fn'](doc, a.mode, title=base, notes=notes,
373
+ styles=not a.no_styles, fonts_target=a.fonts,
374
+ note_refs=a.note_refs, headers=a.headers == 'on',
375
+ line_numbers=a.line_numbers == 'on',
376
+ toc=a.toc == 'on',
377
+ inline_styling=a.inline_styling == 'on',
378
+ pictures=a.pictures, pix_results=pix_results,
379
+ image_links=image_links,
380
+ page_numbers=a.page_numbers,
381
+ sentence_spacing=a.sentence_spacing)
238
382
  if isinstance(out, bytes): # binary formats (e.g. pdf)
239
383
  with open(dest, 'wb') as f:
240
384
  f.write(out)