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 +21 -0
- ctrl_kd-1.1.2/PKG-INFO +120 -0
- ctrl_kd-1.1.2/README.md +101 -0
- ctrl_kd-1.1.2/pyproject.toml +30 -0
- ctrl_kd-1.1.2/setup.cfg +4 -0
- ctrl_kd-1.1.2/src/ctrl_kd.egg-info/PKG-INFO +120 -0
- ctrl_kd-1.1.2/src/ctrl_kd.egg-info/SOURCES.txt +14 -0
- ctrl_kd-1.1.2/src/ctrl_kd.egg-info/dependency_links.txt +1 -0
- ctrl_kd-1.1.2/src/ctrl_kd.egg-info/entry_points.txt +2 -0
- ctrl_kd-1.1.2/src/ctrl_kd.egg-info/top_level.txt +1 -0
- ctrl_kd-1.1.2/src/ctrlkd/__init__.py +12 -0
- ctrl_kd-1.1.2/src/ctrlkd/cli.py +90 -0
- ctrl_kd-1.1.2/src/ctrlkd/core.py +392 -0
- ctrl_kd-1.1.2/src/ctrlkd/emit.py +236 -0
- ctrl_kd-1.1.2/src/ctrlkd/pdf.py +200 -0
- ctrl_kd-1.1.2/tests/test_ctrlkd.py +278 -0
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
|
ctrl_kd-1.1.2/README.md
ADDED
|
@@ -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"]
|
ctrl_kd-1.1.2/setup.cfg
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,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 @@
|
|
|
1
|
+
|
|
@@ -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())
|