ctrl-kd 1.1.2__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-1.1.2/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Jon Michaels
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
ctrl_kd-1.1.2/PKG-INFO ADDED
@@ -0,0 +1,120 @@
1
+ Metadata-Version: 2.4
2
+ Name: ctrl-kd
3
+ Version: 1.1.2
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
+ # ctrl-kd
21
+
22
+ Convert WordStar-era files to modern formats. **^KD: save and done.**
23
+
24
+ `ctrl-kd` reads WordStar 4 documents, WordStar 5–7 documents, and WordStar
25
+ **print-to-disk files** (the printer byte stream, captured to a file — a distinct
26
+ format most converters mangle), and writes plain text, Markdown, HTML, RTF, or PDF (typewriter-set on the
27
+ built-in Courier fonts — no dependencies, the page as it would have printed).
28
+
29
+ ```console
30
+ $ ctrl-kd ESSAY.WS # -> ESSAY.md
31
+ $ ctrl-kd ESSAY.WS -t html -t rtf # multiple formats
32
+ $ ctrl-kd ESSAY.WS -t pdf --mode printed # a facsimile of the 1990 printout
33
+ $ ctrl-kd --mode printed LETTER.WS # line-for-line, as it printed in 1990
34
+ $ ctrl-kd --diagnose MYSTERY.FIL # what IS this file?
35
+ ```
36
+
37
+ ## Why another converter?
38
+
39
+ Existing tools each lose something. Fed a WordStar 4 file, converters written for
40
+ WS7 delete the last letter of every word (WS4 set bit 7 on it). Most delete soft
41
+ returns outright — `Jon Michaels` + `March 6, 1992` becomes
42
+ `Jon MichaelsMarch 6, 1992` — which also destroys every poem, because poem lines
43
+ end in soft returns too. And print-to-disk files aren't WordStar documents at all,
44
+ so feeding them to a WordStar converter produces stray superscripts and garbage.
45
+
46
+ `ctrl-kd` was built by converting a real 1987–1992 corpus (high-school and college
47
+ papers, poems, stories — WordStar 4 on DOS, dot-matrix printer) and verifying
48
+ against surviving period printouts of the same documents. Its rules are empirical:
49
+
50
+ * **Detection by content, never by extension.** WS4 vs WS5+ vs print stream vs
51
+ plain text vs binary, with the evidence shown in `--diagnose`.
52
+ * **The wrap test.** WordStar wrapped only when the next word didn't fit. So a
53
+ soft return where the next word *would* have fit (strictly — WordStar wrapped
54
+ even on an exact-margin fit) is a deliberate break: a poem line, a heading.
55
+ Everything else is word wrap and joins with a space. The margin is estimated
56
+ from the 90th percentile of soft-wrapped line lengths (floor 65, the default).
57
+ * **Break runs.** Soft/hard return runs containing a hard return and a blank line
58
+ are paragraph breaks; a lone hard return is the author's deliberate line break.
59
+ Double-spaced documents (blank soft lines between every line) collapse
60
+ automatically.
61
+ * **Ruler lines mean columns.** A `.rr----!----` dot line defines tab stops; the
62
+ document's alignment is space-built and only survives fixed-width. Such
63
+ documents render `printed` in every mode.
64
+ * **Print streams render verbatim** — they ARE the printed page — with printer
65
+ style codes decoded (superscript/underline/italic/bold pairs; table in
66
+ `core.PRINT_CODES`, derived from a late-80s dot-matrix driver and overridable).
67
+ * **WS5+ symmetric blocks** (`0x1D`: real footnotes/endnotes, headings, page
68
+ breaks — machinery added in WS5) are parsed with their nested structure,
69
+ verified against the 86 WordStar 7 documents in Robert J. Sawyer's public
70
+ WordStar archive: footnotes extract with in-text references (`[^n]` in
71
+ Markdown), paragraph styles become headings, and 82/86 convert with zero
72
+ mojibake. More WS5–7 corpora still welcome.
73
+
74
+ ## Modes
75
+
76
+ * `--mode modern` (default): reflowed paragraphs, semantic markup, deliberate
77
+ line breaks kept.
78
+ * `--mode printed`: every line as laid out, fixed-width, `.pa`/form-feed page
79
+ breaks honored — how it came off the printer.
80
+
81
+ ## Install
82
+
83
+ Straight from GitHub (not yet on PyPI):
84
+
85
+ ```console
86
+ $ pipx install git+https://github.com/jonmichaels/ctrl-kd
87
+ ```
88
+
89
+ or `pip install git+https://github.com/jonmichaels/ctrl-kd` into an environment
90
+ of your choice. Python ≥ 3.9, no dependencies.
91
+ Library API: `ctrlkd.convert(data, to='html')`.
92
+
93
+ ## Adding an output format
94
+
95
+ An output format is one function over the parsed document — register it with the
96
+ `@ctrlkd.emitter` decorator, or ship it as a pip-installable plugin via the
97
+ `ctrlkd.emitters` entry-point group and it appears in the CLI automatically.
98
+ **[EXTENDING.md](EXTENDING.md)** has the IR contract, a complete worked example
99
+ (BBCode in ~40 lines), and a checklist.
100
+
101
+ ## Lineage
102
+
103
+ Standing on the shoulders of the tools and documentation that kept WordStar
104
+ readable: Yohanes Nugroho's WS-CON, Michael Petrie's English port, the `wsconvert`
105
+ project, Robert J. Sawyer's WordStar archive, and the WordStar format
106
+ documentation community. Behaviors were studied and reimplemented; no code was
107
+ copied. The development corpus is personal and is not distributed — tests use
108
+ synthetic fixtures that encode the same behaviors.
109
+
110
+ ## Credits
111
+
112
+ Written by Jon Michaels — whose 1987–1992 WordStar files, and the need to read
113
+ them again, are the reason this exists — with Athena (Claude, Anthropic) as
114
+ co-author: the byte archaeology, the wrap test, and the implementation grew out
115
+ of a joint effort to recover those disks. Every commit carries the co-author
116
+ trailer.
117
+
118
+ ## License
119
+
120
+ MIT © Jon Michaels
@@ -0,0 +1,101 @@
1
+ # ctrl-kd
2
+
3
+ Convert WordStar-era files to modern formats. **^KD: save and done.**
4
+
5
+ `ctrl-kd` reads WordStar 4 documents, WordStar 5–7 documents, and WordStar
6
+ **print-to-disk files** (the printer byte stream, captured to a file — a distinct
7
+ format most converters mangle), and writes plain text, Markdown, HTML, RTF, or PDF (typewriter-set on the
8
+ built-in Courier fonts — no dependencies, the page as it would have printed).
9
+
10
+ ```console
11
+ $ ctrl-kd ESSAY.WS # -> ESSAY.md
12
+ $ ctrl-kd ESSAY.WS -t html -t rtf # multiple formats
13
+ $ ctrl-kd ESSAY.WS -t pdf --mode printed # a facsimile of the 1990 printout
14
+ $ ctrl-kd --mode printed LETTER.WS # line-for-line, as it printed in 1990
15
+ $ ctrl-kd --diagnose MYSTERY.FIL # what IS this file?
16
+ ```
17
+
18
+ ## Why another converter?
19
+
20
+ Existing tools each lose something. Fed a WordStar 4 file, converters written for
21
+ WS7 delete the last letter of every word (WS4 set bit 7 on it). Most delete soft
22
+ returns outright — `Jon Michaels` + `March 6, 1992` becomes
23
+ `Jon MichaelsMarch 6, 1992` — which also destroys every poem, because poem lines
24
+ end in soft returns too. And print-to-disk files aren't WordStar documents at all,
25
+ so feeding them to a WordStar converter produces stray superscripts and garbage.
26
+
27
+ `ctrl-kd` was built by converting a real 1987–1992 corpus (high-school and college
28
+ papers, poems, stories — WordStar 4 on DOS, dot-matrix printer) and verifying
29
+ against surviving period printouts of the same documents. Its rules are empirical:
30
+
31
+ * **Detection by content, never by extension.** WS4 vs WS5+ vs print stream vs
32
+ plain text vs binary, with the evidence shown in `--diagnose`.
33
+ * **The wrap test.** WordStar wrapped only when the next word didn't fit. So a
34
+ soft return where the next word *would* have fit (strictly — WordStar wrapped
35
+ even on an exact-margin fit) is a deliberate break: a poem line, a heading.
36
+ Everything else is word wrap and joins with a space. The margin is estimated
37
+ from the 90th percentile of soft-wrapped line lengths (floor 65, the default).
38
+ * **Break runs.** Soft/hard return runs containing a hard return and a blank line
39
+ are paragraph breaks; a lone hard return is the author's deliberate line break.
40
+ Double-spaced documents (blank soft lines between every line) collapse
41
+ automatically.
42
+ * **Ruler lines mean columns.** A `.rr----!----` dot line defines tab stops; the
43
+ document's alignment is space-built and only survives fixed-width. Such
44
+ documents render `printed` in every mode.
45
+ * **Print streams render verbatim** — they ARE the printed page — with printer
46
+ style codes decoded (superscript/underline/italic/bold pairs; table in
47
+ `core.PRINT_CODES`, derived from a late-80s dot-matrix driver and overridable).
48
+ * **WS5+ symmetric blocks** (`0x1D`: real footnotes/endnotes, headings, page
49
+ breaks — machinery added in WS5) are parsed with their nested structure,
50
+ verified against the 86 WordStar 7 documents in Robert J. Sawyer's public
51
+ WordStar archive: footnotes extract with in-text references (`[^n]` in
52
+ Markdown), paragraph styles become headings, and 82/86 convert with zero
53
+ mojibake. More WS5–7 corpora still welcome.
54
+
55
+ ## Modes
56
+
57
+ * `--mode modern` (default): reflowed paragraphs, semantic markup, deliberate
58
+ line breaks kept.
59
+ * `--mode printed`: every line as laid out, fixed-width, `.pa`/form-feed page
60
+ breaks honored — how it came off the printer.
61
+
62
+ ## Install
63
+
64
+ Straight from GitHub (not yet on PyPI):
65
+
66
+ ```console
67
+ $ pipx install git+https://github.com/jonmichaels/ctrl-kd
68
+ ```
69
+
70
+ or `pip install git+https://github.com/jonmichaels/ctrl-kd` into an environment
71
+ of your choice. Python ≥ 3.9, no dependencies.
72
+ Library API: `ctrlkd.convert(data, to='html')`.
73
+
74
+ ## Adding an output format
75
+
76
+ An output format is one function over the parsed document — register it with the
77
+ `@ctrlkd.emitter` decorator, or ship it as a pip-installable plugin via the
78
+ `ctrlkd.emitters` entry-point group and it appears in the CLI automatically.
79
+ **[EXTENDING.md](EXTENDING.md)** has the IR contract, a complete worked example
80
+ (BBCode in ~40 lines), and a checklist.
81
+
82
+ ## Lineage
83
+
84
+ Standing on the shoulders of the tools and documentation that kept WordStar
85
+ readable: Yohanes Nugroho's WS-CON, Michael Petrie's English port, the `wsconvert`
86
+ project, Robert J. Sawyer's WordStar archive, and the WordStar format
87
+ documentation community. Behaviors were studied and reimplemented; no code was
88
+ copied. The development corpus is personal and is not distributed — tests use
89
+ synthetic fixtures that encode the same behaviors.
90
+
91
+ ## Credits
92
+
93
+ Written by Jon Michaels — whose 1987–1992 WordStar files, and the need to read
94
+ them again, are the reason this exists — with Athena (Claude, Anthropic) as
95
+ co-author: the byte archaeology, the wrap test, and the implementation grew out
96
+ of a joint effort to recover those disks. Every commit carries the co-author
97
+ trailer.
98
+
99
+ ## License
100
+
101
+ MIT © Jon Michaels
@@ -0,0 +1,30 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "ctrl-kd"
7
+ version = "1.1.2"
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"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,120 @@
1
+ Metadata-Version: 2.4
2
+ Name: ctrl-kd
3
+ Version: 1.1.2
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
+ # ctrl-kd
21
+
22
+ Convert WordStar-era files to modern formats. **^KD: save and done.**
23
+
24
+ `ctrl-kd` reads WordStar 4 documents, WordStar 5–7 documents, and WordStar
25
+ **print-to-disk files** (the printer byte stream, captured to a file — a distinct
26
+ format most converters mangle), and writes plain text, Markdown, HTML, RTF, or PDF (typewriter-set on the
27
+ built-in Courier fonts — no dependencies, the page as it would have printed).
28
+
29
+ ```console
30
+ $ ctrl-kd ESSAY.WS # -> ESSAY.md
31
+ $ ctrl-kd ESSAY.WS -t html -t rtf # multiple formats
32
+ $ ctrl-kd ESSAY.WS -t pdf --mode printed # a facsimile of the 1990 printout
33
+ $ ctrl-kd --mode printed LETTER.WS # line-for-line, as it printed in 1990
34
+ $ ctrl-kd --diagnose MYSTERY.FIL # what IS this file?
35
+ ```
36
+
37
+ ## Why another converter?
38
+
39
+ Existing tools each lose something. Fed a WordStar 4 file, converters written for
40
+ WS7 delete the last letter of every word (WS4 set bit 7 on it). Most delete soft
41
+ returns outright — `Jon Michaels` + `March 6, 1992` becomes
42
+ `Jon MichaelsMarch 6, 1992` — which also destroys every poem, because poem lines
43
+ end in soft returns too. And print-to-disk files aren't WordStar documents at all,
44
+ so feeding them to a WordStar converter produces stray superscripts and garbage.
45
+
46
+ `ctrl-kd` was built by converting a real 1987–1992 corpus (high-school and college
47
+ papers, poems, stories — WordStar 4 on DOS, dot-matrix printer) and verifying
48
+ against surviving period printouts of the same documents. Its rules are empirical:
49
+
50
+ * **Detection by content, never by extension.** WS4 vs WS5+ vs print stream vs
51
+ plain text vs binary, with the evidence shown in `--diagnose`.
52
+ * **The wrap test.** WordStar wrapped only when the next word didn't fit. So a
53
+ soft return where the next word *would* have fit (strictly — WordStar wrapped
54
+ even on an exact-margin fit) is a deliberate break: a poem line, a heading.
55
+ Everything else is word wrap and joins with a space. The margin is estimated
56
+ from the 90th percentile of soft-wrapped line lengths (floor 65, the default).
57
+ * **Break runs.** Soft/hard return runs containing a hard return and a blank line
58
+ are paragraph breaks; a lone hard return is the author's deliberate line break.
59
+ Double-spaced documents (blank soft lines between every line) collapse
60
+ automatically.
61
+ * **Ruler lines mean columns.** A `.rr----!----` dot line defines tab stops; the
62
+ document's alignment is space-built and only survives fixed-width. Such
63
+ documents render `printed` in every mode.
64
+ * **Print streams render verbatim** — they ARE the printed page — with printer
65
+ style codes decoded (superscript/underline/italic/bold pairs; table in
66
+ `core.PRINT_CODES`, derived from a late-80s dot-matrix driver and overridable).
67
+ * **WS5+ symmetric blocks** (`0x1D`: real footnotes/endnotes, headings, page
68
+ breaks — machinery added in WS5) are parsed with their nested structure,
69
+ verified against the 86 WordStar 7 documents in Robert J. Sawyer's public
70
+ WordStar archive: footnotes extract with in-text references (`[^n]` in
71
+ Markdown), paragraph styles become headings, and 82/86 convert with zero
72
+ mojibake. More WS5–7 corpora still welcome.
73
+
74
+ ## Modes
75
+
76
+ * `--mode modern` (default): reflowed paragraphs, semantic markup, deliberate
77
+ line breaks kept.
78
+ * `--mode printed`: every line as laid out, fixed-width, `.pa`/form-feed page
79
+ breaks honored — how it came off the printer.
80
+
81
+ ## Install
82
+
83
+ Straight from GitHub (not yet on PyPI):
84
+
85
+ ```console
86
+ $ pipx install git+https://github.com/jonmichaels/ctrl-kd
87
+ ```
88
+
89
+ or `pip install git+https://github.com/jonmichaels/ctrl-kd` into an environment
90
+ of your choice. Python ≥ 3.9, no dependencies.
91
+ Library API: `ctrlkd.convert(data, to='html')`.
92
+
93
+ ## Adding an output format
94
+
95
+ An output format is one function over the parsed document — register it with the
96
+ `@ctrlkd.emitter` decorator, or ship it as a pip-installable plugin via the
97
+ `ctrlkd.emitters` entry-point group and it appears in the CLI automatically.
98
+ **[EXTENDING.md](EXTENDING.md)** has the IR contract, a complete worked example
99
+ (BBCode in ~40 lines), and a checklist.
100
+
101
+ ## Lineage
102
+
103
+ Standing on the shoulders of the tools and documentation that kept WordStar
104
+ readable: Yohanes Nugroho's WS-CON, Michael Petrie's English port, the `wsconvert`
105
+ project, Robert J. Sawyer's WordStar archive, and the WordStar format
106
+ documentation community. Behaviors were studied and reimplemented; no code was
107
+ copied. The development corpus is personal and is not distributed — tests use
108
+ synthetic fixtures that encode the same behaviors.
109
+
110
+ ## Credits
111
+
112
+ Written by Jon Michaels — whose 1987–1992 WordStar files, and the need to read
113
+ them again, are the reason this exists — with Athena (Claude, Anthropic) as
114
+ co-author: the byte archaeology, the wrap test, and the implementation grew out
115
+ of a joint effort to recover those disks. Every commit carries the co-author
116
+ trailer.
117
+
118
+ ## License
119
+
120
+ MIT © Jon Michaels
@@ -0,0 +1,14 @@
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/cli.py
11
+ src/ctrlkd/core.py
12
+ src/ctrlkd/emit.py
13
+ src/ctrlkd/pdf.py
14
+ tests/test_ctrlkd.py
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ ctrl-kd = ctrlkd.cli:main
@@ -0,0 +1 @@
1
+ ctrlkd
@@ -0,0 +1,12 @@
1
+ """ctrl-kd — convert WordStar-era files to modern formats. ^KD: save and done."""
2
+ from .core import detect, parse, parse_ws, parse_printstream, Document, Block, Line, Span
3
+ from .emit import (emit_text, emit_markdown, emit_html, emit_rtf,
4
+ emitter, get_emitter, formats, load_plugins)
5
+ from .pdf import emit_pdf # registers the 'pdf' format
6
+
7
+ __version__ = '1.1.2'
8
+
9
+ def convert(data: bytes, to: str = 'markdown', mode: str = 'modern',
10
+ encoding: str = 'cp437', **options) -> str:
11
+ """One-call API: bytes in, converted string out."""
12
+ return get_emitter(to)['fn'](parse(data, encoding=encoding), mode, **options)
@@ -0,0 +1,90 @@
1
+ """ctrl-kd command line: convert WordStar-era files to modern formats.
2
+
3
+ ctrl-kd PAPER.WS # -> PAPER.md, modern reflow
4
+ ctrl-kd PAPER.WS -t html -o out.html
5
+ ctrl-kd --mode printed LETTER # as it came off the printer
6
+ ctrl-kd --diagnose MYSTERY.FIL # what IS this file?
7
+ ctrl-kd -t text -t html -d out/ *.WS # batch, multiple formats
8
+ """
9
+ import argparse, json, os, sys
10
+ from . import core, emit
11
+
12
+ def diagnose(path, data):
13
+ det = core.detect(data)
14
+ info = {'file': path, **det}
15
+ if det['variant'] in ('ws4', 'ws5+'):
16
+ doc = core.parse_ws(data)
17
+ info.update({k: doc.meta[k] for k in
18
+ ('margin_estimate', 'dot_commands', 'unknown_codes', 'columnar')})
19
+ info['paragraphs'] = sum(1 for b in doc.blocks if b.kind == 'para')
20
+ info['footnotes'] = len(doc.footnotes)
21
+ return info
22
+
23
+ def main(argv=None):
24
+ emit.load_plugins() # third-party emitters (ctrlkd.emitters entry points)
25
+ ap = argparse.ArgumentParser(
26
+ prog='ctrl-kd',
27
+ description='Convert WordStar 4-7 documents and print-to-disk files to '
28
+ 'text, Markdown, HTML, or RTF (extensible: see EXTENDING.md). '
29
+ '^KD: save and done.')
30
+ ap.add_argument('files', nargs='+', help='input file(s)')
31
+ ap.add_argument('-t', '--to', action='append', choices=emit.formats(),
32
+ help='output format (repeatable; default: markdown)')
33
+ ap.add_argument('-o', '--output', help='output file (single input only)')
34
+ ap.add_argument('-d', '--outdir', help='output directory for batch conversion')
35
+ ap.add_argument('--mode', choices=('modern', 'printed'), default='modern',
36
+ help='modern: reflowed paragraphs. printed: line-for-line, '
37
+ 'fixed-width, as it printed in 1990 (default: modern; '
38
+ 'print streams and ruler-line documents always render printed)')
39
+ ap.add_argument('--variant', choices=('ws4', 'ws5+', 'printstream', 'text'),
40
+ help='override detection')
41
+ ap.add_argument('--encoding', default='cp437',
42
+ help='byte encoding of the source (default: cp437)')
43
+ ap.add_argument('--diagnose', action='store_true',
44
+ help='report what the file is (variant, margin, dot commands, '
45
+ 'unknown codes) as JSON; no conversion')
46
+ a = ap.parse_args(argv)
47
+ formats = a.to or ['markdown']
48
+ if a.output and (len(a.files) > 1 or len(formats) > 1):
49
+ ap.error('-o works with a single input and a single format; use -d for batch')
50
+
51
+ status = 0
52
+ for path in a.files:
53
+ try:
54
+ data = open(path, 'rb').read()
55
+ except OSError as e:
56
+ print(f'ctrl-kd: {e}', file=sys.stderr)
57
+ status = 1
58
+ continue
59
+ if a.diagnose:
60
+ print(json.dumps(diagnose(path, data), indent=2))
61
+ continue
62
+ try:
63
+ doc = core.parse(data, encoding=a.encoding, variant=a.variant)
64
+ except ValueError as e:
65
+ print(f'ctrl-kd: {path}: {e} (use --diagnose to inspect, '
66
+ f'--variant to force)', file=sys.stderr)
67
+ status = 1
68
+ continue
69
+ base = os.path.splitext(os.path.basename(path))[0]
70
+ for fmt in formats:
71
+ reg = emit.get_emitter(fmt)
72
+ out = reg['fn'](doc, a.mode, title=base)
73
+ if a.output:
74
+ dest = a.output
75
+ else:
76
+ dest = os.path.join(a.outdir or os.path.dirname(path) or '.',
77
+ base + reg['ext'])
78
+ if a.outdir:
79
+ os.makedirs(a.outdir, exist_ok=True)
80
+ if isinstance(out, bytes): # binary formats (e.g. pdf)
81
+ with open(dest, 'wb') as f:
82
+ f.write(out)
83
+ else:
84
+ with open(dest, 'w', encoding='utf-8', newline='\n') as f:
85
+ f.write(out)
86
+ print(f'{path} -> {dest}')
87
+ return status
88
+
89
+ if __name__ == '__main__':
90
+ sys.exit(main())