unidash 0.2.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.
@@ -0,0 +1 @@
1
+ * text=auto eol=lf
@@ -0,0 +1 @@
1
+ buy_me_a_coffee: jlianesglrs
@@ -0,0 +1,16 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ .venv/
4
+ venv/
5
+ build/
6
+ dist/
7
+ *.egg-info/
8
+ .pytest_cache/
9
+ .mypy_cache/
10
+ .ruff_cache/
11
+ .coverage
12
+ htmlcov/
13
+
14
+ # Third-party data, fetched by scripts/fetch_vectors.py (see its MANIFEST.txt)
15
+ tests/vectors/unicode/*
16
+ !tests/vectors/unicode/MANIFEST.txt
@@ -0,0 +1,42 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project are documented here. The format follows
4
+ [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and the project uses
5
+ [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
+
7
+ ## [Unreleased]
8
+
9
+ ## [0.2.0]
10
+
11
+ ### Added
12
+
13
+ - `fold_dashes(text, form="ascii")` with three forms: `ascii` (every dash to one `-`, wavy
14
+ dashes to `~`), `tex` (en dash `--`, em dash `---`) and `typographic` (real dashes kept,
15
+ small, vertical and fullwidth forms replaced by their standard counterparts).
16
+ - Every code point with the Unicode 18.0 Dash property is classified: general dashes fold by
17
+ default, script hyphens only with `script_hyphens=True`, and the superscript and subscript
18
+ minus are never folded.
19
+ - Soft hyphens are removed unless `keep_soft_hyphens=True`.
20
+ - `find_dashes` and `DashHit` report line, column and name of each character a fold would
21
+ change; `is_dash`, `DASHES`, `dash_table` and the streaming `iter_fold_dashes`.
22
+ - Command line tool `unidash` (also `python -m unidash`) with `--check` for pre-commit use.
23
+ - Tests against the Unicode data files and the UTS #39 confusables, fetched by
24
+ `scripts/fetch_vectors.py` and pinned by size and SHA-256, plus an installed-wheel test.
25
+
26
+ ### Changed
27
+
28
+ - `to_ascii_punct` now folds every general dash (fullwidth, small, vertical, two-em and
29
+ three-em dashes, wave dashes to `~`) and removes soft hyphens.
30
+ - Type errors name the type that was passed.
31
+ - `to_ascii_punct` folds fullwidth punctuation (U+FF01 to U+FF5E, not letters or digits) to
32
+ ASCII, so the Japanese wave dash from CP932 text (U+FF5E) becomes `~`.
33
+
34
+ ## [0.1.0]
35
+
36
+ - First release.
37
+ - `to_ascii_punct(text)` folds Unicode punctuation to ASCII: curly quotes to `'` and `"`,
38
+ en and em dashes and the minus sign to `-`, the ellipsis to `...`, non-breaking and
39
+ other spaces to a normal space, and bullets to `*`.
40
+ - Letters, including accented ones, pass through untouched, unlike `unidecode` and
41
+ `anyascii`, which transliterate every character.
42
+ - Zero runtime dependencies, with the mapping vendored in.
@@ -0,0 +1,19 @@
1
+ # Contributing
2
+
3
+ Thanks for taking the time. This project keeps a small, predictable shape.
4
+
5
+ ## Setup
6
+
7
+ ```
8
+ pip install -r requirements-dev.txt
9
+ ```
10
+
11
+ ## Before you open a pull request
12
+
13
+ - Keep public API in `src/unidash/__init__.py`; internals live in `_`-prefixed modules.
14
+ - Add a test for the behaviour you change. Tests inject time, disk and network; they
15
+ never touch the real thing.
16
+ - Run `scripts/check.ps1` (format, lint, types, tests) and make sure it passes.
17
+ - One logical change per commit, imperative subject line.
18
+
19
+ Open an issue first if the change is large or shifts the public API.
@@ -0,0 +1,125 @@
1
+ # unidash improvement round
2
+
3
+ Progress:
4
+ - [x] Brainstorm ranked improvements (this file)
5
+ - [x] Dash classification from the Unicode data: general dashes, script hyphens, kept marks
6
+ - [x] `fold_dashes` with the `ascii`, `tex` and `typographic` forms and soft hyphen control
7
+ - [x] `to_ascii_punct` covers every general dash and drops soft hyphens
8
+ - [x] `find_dashes`, `is_dash`, `DASHES`, `dash_table`, streaming `iter_fold_dashes`
9
+ - [x] Command line tool (`unidash`, `python -m unidash`) with `--check`
10
+ - [x] Contracts: UCD and UTS #39 data, interpreter scan, typing, README, wheel e2e
11
+ - [x] Mutation run (guards removed one at a time, each must fail the suite)
12
+ - [x] Docs: README, CHANGELOG, docstrings, packaging metadata
13
+ - [x] Gates: ruff format, ruff check, mypy --strict, pytest
14
+ - [x] Independent review findings fixed with regression tests
15
+ - [x] Pull request opened (#2), guard clean
16
+
17
+ ## Starting point
18
+
19
+ `0.1.0` (not on PyPI) ships one function, `to_ascii_punct`, which folds a hand-picked set of
20
+ quotes, seven dashes, bullets, spaces and the ellipsis. Read as a user who cleans text pasted
21
+ from word processors, PDFs, CJK input methods and web pages:
22
+
23
+ - Only U+2010..U+2015 and U+2212 fold. The fullwidth hyphen-minus from a Japanese keyboard,
24
+ the small and vertical presentation forms, the two-em and three-em dashes and the wave dash
25
+ pass through, so "ASCII punctuation" output still holds non-ASCII dashes.
26
+ - The soft hyphen (U+00AD) is kept. It is invisible, breaks search and diffing, and is the most
27
+ common stray character in text copied from PDFs and hyphenating renderers.
28
+ - There is no choice of target. Everything becomes one `-`, which loses the en and em dash
29
+ distinction that Markdown and TeX users keep as `--` and `---`, and there is no way to only
30
+ tidy lookalike forms while keeping real typographic dashes.
31
+ - Nothing says which characters were chosen or why; nothing is checked against the Unicode
32
+ data, so the next Unicode version silently adds dashes the table never heard of.
33
+ - No way to find dashes (a lint or a pre-commit check), no streaming, no command line tool.
34
+ - Packaging: README example is not executed, no Python classifiers, no wheel test.
35
+
36
+ ## Sources (fetched 2026-10-13)
37
+
38
+ - UnicodeData.txt 18.0.0, https://www.unicode.org/Public/18.0.0/ucd/UnicodeData.txt: 27 code
39
+ points have General_Category=Pd (U+002D, U+058A, U+05BE, U+1400, U+1806, U+2010..U+2015,
40
+ U+2E17, U+2E1A, U+2E3A, U+2E3B, U+2E40, U+2E5D, U+301C, U+3030, U+30A0, U+FE31, U+FE32,
41
+ U+FE58, U+FE63, U+FF0D, U+10D6E, U+10EAD).
42
+ - PropList.txt 18.0.0, https://www.unicode.org/Public/18.0.0/ucd/PropList.txt: the Dash
43
+ property is every Pd code point plus U+2053 SWUNG DASH (Po) and three math signs, U+207B,
44
+ U+208B and U+2212 (Sm).
45
+ - confusables.txt 18.0.0 (UTS #39), https://www.unicode.org/Public/18.0.0/security/confusables.txt:
46
+ maps to U+002D the hyphen, non-breaking hyphen, figure dash, en dash, minus sign, small em
47
+ dash and small hyphen-minus, and also non-dashes (letters in Coptic, Buhid and Myanmar, a
48
+ modifier letter, the Arabic full stop, the hyphen bullet, an emoji, a Roman weight sign).
49
+ The em dash, horizontal bar and fullwidth hyphen-minus map to U+30FC instead; the wave dash
50
+ and swung dash to `~`; the double hyphens to `=`.
51
+ - The three files are third-party and not committed: `scripts/fetch_vectors.py` fetches them
52
+ into a gitignored folder checked against a tracked MANIFEST; the tests that read them skip
53
+ when the folder is absent. The running interpreter's `unicodedata` gives a second, always-on
54
+ check for General_Category and NFKC.
55
+
56
+ ## Ranked improvements
57
+
58
+ 1. **Every dash is classified, on purpose.** Three tables built from code points, each with a
59
+ reason: general dashes (fold by default), script hyphens (Armenian, Hebrew maqaf, Canadian
60
+ syllabics, Mongolian, katakana double hyphen, Garay, Yezidi: they belong to a script, like
61
+ letters, so they fold only on request) and kept marks (superscript and subscript minus,
62
+ which sit next to superscript digits that stay). A contract checks that every Pd and Dash
63
+ code point in the fetched data lands in exactly one table, and every UTS #39 confusable of
64
+ `-` is either folded or excluded for a stated reason (letter, emoji, bullet, sentence stop).
65
+ 2. **`fold_dashes(text, form=...)`.** `ascii` (default): every general dash becomes `-`, the
66
+ wave and swung dashes become `~`. `tex`: en dash `--`, em dash and horizontal bar `---`,
67
+ two-em and three-em dashes six and nine hyphens, the rest as `ascii`. `typographic`: real
68
+ dashes stay; only compatibility forms (small, vertical, fullwidth) become their NFKC
69
+ counterparts. `script_hyphens=True` folds the script hyphens too.
70
+ 3. **Soft and non-breaking hyphens.** The soft hyphen is dropped by default (it is a line
71
+ break hint, not a character), `keep_soft_hyphens=True` keeps it. The non-breaking hyphen
72
+ folds to `-` in the ASCII forms and stays in `typographic`, where it carries meaning.
73
+ 4. **Numbers and ranges preserved.** A minus sign is always a single `-` in the ASCII forms, so
74
+ `-5` stays a negative number; the TeX form never touches characters that are already ASCII,
75
+ so `1-2` and `--` written by hand come out unchanged.
76
+ 5. **`to_ascii_punct` follows suit.** It folds every general dash and drops soft hyphens, so
77
+ its output is ASCII for every dash the Unicode data knows.
78
+ 6. **Find, test and build.** `find_dashes(text)` yields position, character and Unicode name
79
+ for each non-ASCII dash (for linting); `is_dash(char)`; `DASHES` is the Dash property as a
80
+ frozenset; `dash_table(...)` returns the `str.translate` table for callers with their own
81
+ pipeline.
82
+ 7. **Streaming.** `iter_fold_dashes(chunks, ...)` folds an iterable of strings lazily. The fold
83
+ is per code point, so a chunked result equals the whole-text result (property test).
84
+ 8. **Command line tool.** `unidash [FILE...]` writes folded text to stdout (`-` is stdin),
85
+ `--form`, `--script-hyphens`, `--keep-soft-hyphens`, `--punct` for the full punctuation fold,
86
+ and `--check` lists `file:line:col: U+2014 EM DASH` and exits 1 (pre-commit style). Exit 2
87
+ on unreadable or non UTF-8 input.
88
+ 9. **Contracts that bite.** UCD and UTS #39 data checks, an interpreter scan, chunking
89
+ properties, a mypy snippet over the typed API, executed README examples, an installed-wheel
90
+ e2e and a mutation run.
91
+ 10. **Docs and packaging.** README with runnable examples and the full classification, CHANGELOG
92
+ 0.2.0, classifiers and keywords.
93
+
94
+ Left out on purpose: turning ASCII hyphens into typographic dashes (the opposite direction,
95
+ `smartypants` owns it), context rules such as "en dash between digits" (guesswork that breaks
96
+ code and identifiers), in-place file rewriting (shell redirection and editors already do this
97
+ safely), and folding letters that look like hyphens (the library never touches letters).
98
+
99
+ ## Verification
100
+
101
+ - Gates: `ruff format --check .`, `ruff check .`, `mypy --strict src` (also `src tests`) and
102
+ `pytest` exit 0 on Windows with Python 3.13 (126 passed, 2 skipped: the wheel e2e and the
103
+ POSIX file name test). On the second machine with Python 3.14 and `UNIDASH_E2E=1` the suite
104
+ passes (127 passed, 1 skipped: the POSIX test): it builds the wheel and the sdist, checks
105
+ that no third-party data or tests ship in them, installs the wheel in a fresh venv,
106
+ drives the API from an empty folder, and runs the installed `unidash` script and
107
+ `python -m unidash`.
108
+ - Unicode data: the classes equal the Dash property of PropList.txt 18.0.0 exactly, cover all
109
+ 27 Pd code points of UnicodeData.txt 18.0.0, and the stored names match it. Every UTS #39
110
+ confusable of `-` either folds to `-` or is a letter or one of five listed non-dashes. The
111
+ running interpreter's Pd set and NFKC mappings are checked on every run, data or not.
112
+ - Mutations: 53 mutants (classes, tables, option checks, TeX widths, compatibility targets,
113
+ soft and script hyphen switches, table copying, position counting, name lookup, streaming
114
+ type checks, the punctuation and fullwidth tables, LF, CR and CRLF line counting, the byte
115
+ order mark column, the broken pipe exit, and every command line flag, label, encoding and
116
+ exit code rule), applied one at a time with the bytecode cache disabled and purged before each
117
+ run: all killed. One first draft of an output-encoding mutant was equivalent and was
118
+ replaced. The baseline was green before and after.
119
+ - Independent review: 9 findings, all addressed with regression tests: lone CR now ends a
120
+ line for positions; a leading byte order mark takes no column; a closed output pipe stops
121
+ quietly; undecodable POSIX file names are printed as given; the typographic minus claim and
122
+ the scope of minus-like characters are stated exactly in the README, with the reason double
123
+ hyphens become `-`; `to_ascii_punct` folds fullwidth punctuation, including the CP932 wave
124
+ dash; every README table cell is checked against the fold; the sdist is checked for data
125
+ files; `is_dash` raises `ValueError` for a string of the wrong length.
unidash-0.2.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Javier Lianes
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.
unidash-0.2.0/PKG-INFO ADDED
@@ -0,0 +1,223 @@
1
+ Metadata-Version: 2.5
2
+ Name: unidash
3
+ Version: 0.2.0
4
+ Summary: Fold Unicode dashes and punctuation to plain ASCII while leaving letters untouched
5
+ Project-URL: Homepage, https://lianes8studio.com
6
+ Project-URL: Repository, https://github.com/JaviLianes8/unidash
7
+ Project-URL: Issues, https://github.com/JaviLianes8/unidash/issues
8
+ Project-URL: Changelog, https://github.com/JaviLianes8/unidash/blob/main/CHANGELOG.md
9
+ Project-URL: LinkedIn, https://www.linkedin.com/in/jlianes/
10
+ Project-URL: Funding, https://buymeacoffee.com/jlianesglrs
11
+ Author-email: Javier Lianes <javier@lianes8studio.com>
12
+ License-Expression: MIT
13
+ License-File: LICENSE
14
+ Keywords: ascii,dash,em-dash,en-dash,hyphen,minus,normalize,punctuation,quotes,soft-hyphen,text,unicode
15
+ Classifier: Development Status :: 4 - Beta
16
+ Classifier: Intended Audience :: Developers
17
+ Classifier: Operating System :: OS Independent
18
+ Classifier: Programming Language :: Python :: 3
19
+ Classifier: Programming Language :: Python :: 3 :: Only
20
+ Classifier: Programming Language :: Python :: 3.10
21
+ Classifier: Programming Language :: Python :: 3.11
22
+ Classifier: Programming Language :: Python :: 3.12
23
+ Classifier: Programming Language :: Python :: 3.13
24
+ Classifier: Programming Language :: Python :: 3.14
25
+ Classifier: Topic :: Text Processing :: Filters
26
+ Classifier: Typing :: Typed
27
+ Requires-Python: >=3.10
28
+ Provides-Extra: dev
29
+ Requires-Dist: build; extra == 'dev'
30
+ Requires-Dist: mypy; extra == 'dev'
31
+ Requires-Dist: pytest; extra == 'dev'
32
+ Requires-Dist: ruff; extra == 'dev'
33
+ Requires-Dist: twine; extra == 'dev'
34
+ Description-Content-Type: text/markdown
35
+
36
+ # unidash
37
+
38
+ Fold Unicode dashes and punctuation to plain ASCII while leaving letters untouched.
39
+
40
+ Text pasted from word processors, PDFs, CJK input methods and web pages is full of
41
+ characters that look like `-` but are not: en and em dashes, the minus sign, the
42
+ non-breaking hyphen, fullwidth and small forms, and the invisible soft hyphen. They break
43
+ search, diffs, command lines and parsers. `unidash` knows every character with the Unicode
44
+ Dash property, plus the soft hyphen, and folds them the way you choose.
45
+
46
+ ## Install
47
+
48
+ ```
49
+ pip install unidash
50
+ ```
51
+
52
+ Python 3.10 or newer, no dependencies.
53
+
54
+ ## Dashes
55
+
56
+ `fold_dashes(text, form="ascii")` has three forms. `ascii` turns every dash into one `-`
57
+ (wavy dashes into `~`). `tex` keeps the en and em dash apart the way TeX and Markdown do, as
58
+ `--` and `---`. `typographic` keeps the real dashes and only replaces the small, vertical and
59
+ fullwidth compatibility forms with their standard counterparts.
60
+
61
+ ```python
62
+ from unidash import fold_dashes
63
+
64
+ text = "Pages 10\u201320 \u2014 see \u22125\uff0d6"
65
+ print(fold_dashes(text))
66
+ print(fold_dashes(text, "tex"))
67
+ print(ascii(fold_dashes("a\ufe58b\uff0dc", "typographic")))
68
+ # Pages 10-20 - see -5-6
69
+ # Pages 10--20 --- see -5-6
70
+ # 'a\u2014b-c'
71
+ ```
72
+
73
+ A minus sign is always a single `-` in the `ascii` and `tex` forms (`typographic` keeps
74
+ U+2212), so negative numbers stay negative, and characters that are already ASCII are never
75
+ touched: `1-2` and a hand-written `--` come out as they went in.
76
+
77
+ ## Soft hyphens, non-breaking hyphens and script hyphens
78
+
79
+ The soft hyphen (U+00AD) is a hint for where a word may break, not a character, so it is
80
+ removed unless you pass `keep_soft_hyphens=True`. The non-breaking hyphen (U+2011) becomes
81
+ `-` in the ASCII forms and stays in `typographic`, where its promise matters.
82
+
83
+ Hyphens that belong to one script (Armenian, the Hebrew maqaf, Canadian syllabics,
84
+ Mongolian, the katakana double hyphen, Garay and Yezidi) are left alone, like that script's
85
+ letters, unless you pass `script_hyphens=True`.
86
+
87
+ ```python
88
+ from unidash import fold_dashes
89
+
90
+ print(fold_dashes("co\u00adop\u00aderate"))
91
+ print(ascii(fold_dashes("co\u00adop", keep_soft_hyphens=True)))
92
+ print(ascii(fold_dashes("\u05e2\u05be\u05d1")))
93
+ print(ascii(fold_dashes("\u05e2\u05be\u05d1", script_hyphens=True)))
94
+ # cooperate
95
+ # 'co\xadop'
96
+ # '\u05e2\u05be\u05d1'
97
+ # '\u05e2-\u05d1'
98
+ ```
99
+
100
+ ## Finding dashes
101
+
102
+ `find_dashes` yields a `DashHit` (offset, line, column, character and Unicode name) for
103
+ every character `fold_dashes` would change with the same options, which makes a lint or a
104
+ pre-commit check a one-liner. Lines end at LF, CR or CRLF, columns count code points from 1,
105
+ and a byte order mark at the start of the text takes no column. `is_dash(char)` tells whether a character has the Unicode Dash
106
+ property and `DASHES` is that set. `dash_table(...)` returns the `str.translate` table, and
107
+ `iter_fold_dashes(chunks, ...)` folds an iterable of strings lazily, for large inputs.
108
+
109
+ ```python
110
+ from unidash import find_dashes, is_dash, iter_fold_dashes
111
+
112
+ for hit in find_dashes("first line\nsecond\u2014line\u00ad"):
113
+ print(hit.line, hit.column, hit.codepoint, hit.name)
114
+ print(is_dash("\u2212"), is_dash("\u00ad"))
115
+ print("".join(iter_fold_dashes(["a\u2013", "b"])))
116
+ # 2 7 U+2014 EM DASH
117
+ # 2 12 U+00AD SOFT HYPHEN
118
+ # True False
119
+ # a-b
120
+ ```
121
+
122
+ ## All punctuation
123
+
124
+ `to_ascii_punct(text)` folds the rest of the typographic punctuation too: curly quotes become
125
+ `'` and `"`, the ellipsis becomes `...`, non-breaking and other Unicode spaces become a normal
126
+ space, bullets become `*`, fullwidth punctuation from CJK input (U+FF01 to U+FF5E, letters and
127
+ digits excepted) becomes its ASCII character, and dashes follow the `ascii` form. Accented
128
+ letters pass through.
129
+
130
+ ```python
131
+ from unidash import to_ascii_punct
132
+
133
+ print(to_ascii_punct("\u201cCaf\u00e9\u201d\u2014it\u2019s 100\u00a0%\u2026"))
134
+ # "Café"-it's 100 %...
135
+ ```
136
+
137
+ ## Command line
138
+
139
+ `unidash FILE...` writes the folded text to stdout (`-` or no file reads stdin). It takes
140
+ `--form`, `--script-hyphens`, `--keep-soft-hyphens`, and `--punct` for the full punctuation
141
+ fold. `--check` lists what would change and exits 1, for pre-commit hooks. Input must be
142
+ UTF-8; unreadable files exit 2.
143
+
144
+ ```
145
+ $ unidash --check notes.md
146
+ notes.md:2:6: U+2014 EM DASH
147
+ notes.md:3:3: U+00AD SOFT HYPHEN
148
+ $ unidash --form tex notes.md
149
+ Draft
150
+ Done --- ship it
151
+ coop
152
+ ```
153
+
154
+ ## What counts as a dash
155
+
156
+ The classes follow the Unicode 18.0 Dash property and General_Category=Pd, and the tests
157
+ check them against the Unicode data files and the UTS #39 confusables list.
158
+
159
+ | Code point | Name | `ascii` | `tex` | `typographic` |
160
+ | --- | --- | --- | --- | --- |
161
+ | U+2010 | HYPHEN | `-` | `-` | kept |
162
+ | U+2011 | NON-BREAKING HYPHEN | `-` | `-` | kept |
163
+ | U+2012 | FIGURE DASH | `-` | `-` | kept |
164
+ | U+2013 | EN DASH | `-` | `--` | kept |
165
+ | U+2014 | EM DASH | `-` | `---` | kept |
166
+ | U+2015 | HORIZONTAL BAR | `-` | `---` | kept |
167
+ | U+2212 | MINUS SIGN | `-` | `-` | kept |
168
+ | U+2E17 | DOUBLE OBLIQUE HYPHEN | `-` | `-` | kept |
169
+ | U+2E1A | HYPHEN WITH DIAERESIS | `-` | `-` | kept |
170
+ | U+2E3A | TWO-EM DASH | `-` | `------` | kept |
171
+ | U+2E3B | THREE-EM DASH | `-` | `---------` | kept |
172
+ | U+2E40 | DOUBLE HYPHEN | `-` | `-` | kept |
173
+ | U+2E5D | OBLIQUE HYPHEN | `-` | `-` | kept |
174
+ | U+FE31 | PRESENTATION FORM FOR VERTICAL EM DASH | `-` | `---` | U+2014 |
175
+ | U+FE32 | PRESENTATION FORM FOR VERTICAL EN DASH | `-` | `--` | U+2013 |
176
+ | U+FE58 | SMALL EM DASH | `-` | `---` | U+2014 |
177
+ | U+FE63 | SMALL HYPHEN-MINUS | `-` | `-` | `-` |
178
+ | U+FF0D | FULLWIDTH HYPHEN-MINUS | `-` | `-` | `-` |
179
+ | U+2053 | SWUNG DASH | `~` | `~` | kept |
180
+ | U+301C | WAVE DASH | `~` | `~` | kept |
181
+ | U+3030 | WAVY DASH | `~` | `~` | kept |
182
+ | U+00AD | SOFT HYPHEN | removed | removed | removed |
183
+
184
+ Script hyphens, folded only with `script_hyphens=True` (to `-`, or to U+2010 in
185
+ `typographic`): U+058A ARMENIAN HYPHEN, U+05BE HEBREW PUNCTUATION MAQAF, U+1400 CANADIAN
186
+ SYLLABICS HYPHEN, U+1806 MONGOLIAN TODO SOFT HYPHEN, U+30A0 KATAKANA-HIRAGANA DOUBLE HYPHEN,
187
+ U+10D6E GARAY HYPHEN and U+10EAD YEZIDI HYPHENATION MARK.
188
+
189
+ The double hyphens (U+2E40, and U+1400 and U+30A0 among the script hyphens) become `-`
190
+ although UTS #39 lists them as confusable with `=`: they are hyphens in use, and `=` would
191
+ change the meaning of the text.
192
+
193
+ Never folded: U+002D HYPHEN-MINUS, and U+207B SUPERSCRIPT MINUS and U+208B SUBSCRIPT MINUS,
194
+ which sit next to superscript and subscript digits that stay as they are. Characters that
195
+ only look like a hyphen are not dashes and are left alone: letters in other scripts, the Arabic
196
+ full stop, the hyphen bullet, U+02D7 MODIFIER LETTER MINUS SIGN (a tone mark), and minus-like
197
+ symbols without the Dash property such as U+2052 COMMERCIAL MINUS SIGN, U+2796 HEAVY MINUS
198
+ SIGN (an emoji), U+2E43 DASH WITH LEFT UPTURN and U+10D8F GARAY MINUS SIGN.
199
+
200
+ ## Why
201
+
202
+ Tools like `unidecode` and `anyascii` transliterate every character, so `café` turns into
203
+ `cafe`. That is the wrong tool when you only want to tidy punctuation and keep the words.
204
+ `smartypants` goes the opposite way, turning ASCII into typographic dashes. `unidash` folds
205
+ punctuation to ASCII, knows the whole Unicode dash family rather than the three most common
206
+ members, and leaves letters alone. It is zero dependency and typed.
207
+
208
+ ## Development
209
+
210
+ The Unicode data files are not part of the repository. `python scripts/fetch_vectors.py`
211
+ downloads them into `tests/vectors/unicode/` and checks each against the size and SHA-256 in
212
+ `MANIFEST.txt`; without them the data tests skip. The installed-wheel test runs with
213
+ `UNIDASH_E2E=1`.
214
+
215
+ ## License
216
+
217
+ MIT. See [LICENSE](LICENSE).
218
+
219
+ ## Links
220
+
221
+ - Website: https://lianes8studio.com
222
+ - GitHub: https://github.com/JaviLianes8
223
+ - LinkedIn: https://www.linkedin.com/in/jlianes/
@@ -0,0 +1,188 @@
1
+ # unidash
2
+
3
+ Fold Unicode dashes and punctuation to plain ASCII while leaving letters untouched.
4
+
5
+ Text pasted from word processors, PDFs, CJK input methods and web pages is full of
6
+ characters that look like `-` but are not: en and em dashes, the minus sign, the
7
+ non-breaking hyphen, fullwidth and small forms, and the invisible soft hyphen. They break
8
+ search, diffs, command lines and parsers. `unidash` knows every character with the Unicode
9
+ Dash property, plus the soft hyphen, and folds them the way you choose.
10
+
11
+ ## Install
12
+
13
+ ```
14
+ pip install unidash
15
+ ```
16
+
17
+ Python 3.10 or newer, no dependencies.
18
+
19
+ ## Dashes
20
+
21
+ `fold_dashes(text, form="ascii")` has three forms. `ascii` turns every dash into one `-`
22
+ (wavy dashes into `~`). `tex` keeps the en and em dash apart the way TeX and Markdown do, as
23
+ `--` and `---`. `typographic` keeps the real dashes and only replaces the small, vertical and
24
+ fullwidth compatibility forms with their standard counterparts.
25
+
26
+ ```python
27
+ from unidash import fold_dashes
28
+
29
+ text = "Pages 10\u201320 \u2014 see \u22125\uff0d6"
30
+ print(fold_dashes(text))
31
+ print(fold_dashes(text, "tex"))
32
+ print(ascii(fold_dashes("a\ufe58b\uff0dc", "typographic")))
33
+ # Pages 10-20 - see -5-6
34
+ # Pages 10--20 --- see -5-6
35
+ # 'a\u2014b-c'
36
+ ```
37
+
38
+ A minus sign is always a single `-` in the `ascii` and `tex` forms (`typographic` keeps
39
+ U+2212), so negative numbers stay negative, and characters that are already ASCII are never
40
+ touched: `1-2` and a hand-written `--` come out as they went in.
41
+
42
+ ## Soft hyphens, non-breaking hyphens and script hyphens
43
+
44
+ The soft hyphen (U+00AD) is a hint for where a word may break, not a character, so it is
45
+ removed unless you pass `keep_soft_hyphens=True`. The non-breaking hyphen (U+2011) becomes
46
+ `-` in the ASCII forms and stays in `typographic`, where its promise matters.
47
+
48
+ Hyphens that belong to one script (Armenian, the Hebrew maqaf, Canadian syllabics,
49
+ Mongolian, the katakana double hyphen, Garay and Yezidi) are left alone, like that script's
50
+ letters, unless you pass `script_hyphens=True`.
51
+
52
+ ```python
53
+ from unidash import fold_dashes
54
+
55
+ print(fold_dashes("co\u00adop\u00aderate"))
56
+ print(ascii(fold_dashes("co\u00adop", keep_soft_hyphens=True)))
57
+ print(ascii(fold_dashes("\u05e2\u05be\u05d1")))
58
+ print(ascii(fold_dashes("\u05e2\u05be\u05d1", script_hyphens=True)))
59
+ # cooperate
60
+ # 'co\xadop'
61
+ # '\u05e2\u05be\u05d1'
62
+ # '\u05e2-\u05d1'
63
+ ```
64
+
65
+ ## Finding dashes
66
+
67
+ `find_dashes` yields a `DashHit` (offset, line, column, character and Unicode name) for
68
+ every character `fold_dashes` would change with the same options, which makes a lint or a
69
+ pre-commit check a one-liner. Lines end at LF, CR or CRLF, columns count code points from 1,
70
+ and a byte order mark at the start of the text takes no column. `is_dash(char)` tells whether a character has the Unicode Dash
71
+ property and `DASHES` is that set. `dash_table(...)` returns the `str.translate` table, and
72
+ `iter_fold_dashes(chunks, ...)` folds an iterable of strings lazily, for large inputs.
73
+
74
+ ```python
75
+ from unidash import find_dashes, is_dash, iter_fold_dashes
76
+
77
+ for hit in find_dashes("first line\nsecond\u2014line\u00ad"):
78
+ print(hit.line, hit.column, hit.codepoint, hit.name)
79
+ print(is_dash("\u2212"), is_dash("\u00ad"))
80
+ print("".join(iter_fold_dashes(["a\u2013", "b"])))
81
+ # 2 7 U+2014 EM DASH
82
+ # 2 12 U+00AD SOFT HYPHEN
83
+ # True False
84
+ # a-b
85
+ ```
86
+
87
+ ## All punctuation
88
+
89
+ `to_ascii_punct(text)` folds the rest of the typographic punctuation too: curly quotes become
90
+ `'` and `"`, the ellipsis becomes `...`, non-breaking and other Unicode spaces become a normal
91
+ space, bullets become `*`, fullwidth punctuation from CJK input (U+FF01 to U+FF5E, letters and
92
+ digits excepted) becomes its ASCII character, and dashes follow the `ascii` form. Accented
93
+ letters pass through.
94
+
95
+ ```python
96
+ from unidash import to_ascii_punct
97
+
98
+ print(to_ascii_punct("\u201cCaf\u00e9\u201d\u2014it\u2019s 100\u00a0%\u2026"))
99
+ # "Café"-it's 100 %...
100
+ ```
101
+
102
+ ## Command line
103
+
104
+ `unidash FILE...` writes the folded text to stdout (`-` or no file reads stdin). It takes
105
+ `--form`, `--script-hyphens`, `--keep-soft-hyphens`, and `--punct` for the full punctuation
106
+ fold. `--check` lists what would change and exits 1, for pre-commit hooks. Input must be
107
+ UTF-8; unreadable files exit 2.
108
+
109
+ ```
110
+ $ unidash --check notes.md
111
+ notes.md:2:6: U+2014 EM DASH
112
+ notes.md:3:3: U+00AD SOFT HYPHEN
113
+ $ unidash --form tex notes.md
114
+ Draft
115
+ Done --- ship it
116
+ coop
117
+ ```
118
+
119
+ ## What counts as a dash
120
+
121
+ The classes follow the Unicode 18.0 Dash property and General_Category=Pd, and the tests
122
+ check them against the Unicode data files and the UTS #39 confusables list.
123
+
124
+ | Code point | Name | `ascii` | `tex` | `typographic` |
125
+ | --- | --- | --- | --- | --- |
126
+ | U+2010 | HYPHEN | `-` | `-` | kept |
127
+ | U+2011 | NON-BREAKING HYPHEN | `-` | `-` | kept |
128
+ | U+2012 | FIGURE DASH | `-` | `-` | kept |
129
+ | U+2013 | EN DASH | `-` | `--` | kept |
130
+ | U+2014 | EM DASH | `-` | `---` | kept |
131
+ | U+2015 | HORIZONTAL BAR | `-` | `---` | kept |
132
+ | U+2212 | MINUS SIGN | `-` | `-` | kept |
133
+ | U+2E17 | DOUBLE OBLIQUE HYPHEN | `-` | `-` | kept |
134
+ | U+2E1A | HYPHEN WITH DIAERESIS | `-` | `-` | kept |
135
+ | U+2E3A | TWO-EM DASH | `-` | `------` | kept |
136
+ | U+2E3B | THREE-EM DASH | `-` | `---------` | kept |
137
+ | U+2E40 | DOUBLE HYPHEN | `-` | `-` | kept |
138
+ | U+2E5D | OBLIQUE HYPHEN | `-` | `-` | kept |
139
+ | U+FE31 | PRESENTATION FORM FOR VERTICAL EM DASH | `-` | `---` | U+2014 |
140
+ | U+FE32 | PRESENTATION FORM FOR VERTICAL EN DASH | `-` | `--` | U+2013 |
141
+ | U+FE58 | SMALL EM DASH | `-` | `---` | U+2014 |
142
+ | U+FE63 | SMALL HYPHEN-MINUS | `-` | `-` | `-` |
143
+ | U+FF0D | FULLWIDTH HYPHEN-MINUS | `-` | `-` | `-` |
144
+ | U+2053 | SWUNG DASH | `~` | `~` | kept |
145
+ | U+301C | WAVE DASH | `~` | `~` | kept |
146
+ | U+3030 | WAVY DASH | `~` | `~` | kept |
147
+ | U+00AD | SOFT HYPHEN | removed | removed | removed |
148
+
149
+ Script hyphens, folded only with `script_hyphens=True` (to `-`, or to U+2010 in
150
+ `typographic`): U+058A ARMENIAN HYPHEN, U+05BE HEBREW PUNCTUATION MAQAF, U+1400 CANADIAN
151
+ SYLLABICS HYPHEN, U+1806 MONGOLIAN TODO SOFT HYPHEN, U+30A0 KATAKANA-HIRAGANA DOUBLE HYPHEN,
152
+ U+10D6E GARAY HYPHEN and U+10EAD YEZIDI HYPHENATION MARK.
153
+
154
+ The double hyphens (U+2E40, and U+1400 and U+30A0 among the script hyphens) become `-`
155
+ although UTS #39 lists them as confusable with `=`: they are hyphens in use, and `=` would
156
+ change the meaning of the text.
157
+
158
+ Never folded: U+002D HYPHEN-MINUS, and U+207B SUPERSCRIPT MINUS and U+208B SUBSCRIPT MINUS,
159
+ which sit next to superscript and subscript digits that stay as they are. Characters that
160
+ only look like a hyphen are not dashes and are left alone: letters in other scripts, the Arabic
161
+ full stop, the hyphen bullet, U+02D7 MODIFIER LETTER MINUS SIGN (a tone mark), and minus-like
162
+ symbols without the Dash property such as U+2052 COMMERCIAL MINUS SIGN, U+2796 HEAVY MINUS
163
+ SIGN (an emoji), U+2E43 DASH WITH LEFT UPTURN and U+10D8F GARAY MINUS SIGN.
164
+
165
+ ## Why
166
+
167
+ Tools like `unidecode` and `anyascii` transliterate every character, so `café` turns into
168
+ `cafe`. That is the wrong tool when you only want to tidy punctuation and keep the words.
169
+ `smartypants` goes the opposite way, turning ASCII into typographic dashes. `unidash` folds
170
+ punctuation to ASCII, knows the whole Unicode dash family rather than the three most common
171
+ members, and leaves letters alone. It is zero dependency and typed.
172
+
173
+ ## Development
174
+
175
+ The Unicode data files are not part of the repository. `python scripts/fetch_vectors.py`
176
+ downloads them into `tests/vectors/unicode/` and checks each against the size and SHA-256 in
177
+ `MANIFEST.txt`; without them the data tests skip. The installed-wheel test runs with
178
+ `UNIDASH_E2E=1`.
179
+
180
+ ## License
181
+
182
+ MIT. See [LICENSE](LICENSE).
183
+
184
+ ## Links
185
+
186
+ - Website: https://lianes8studio.com
187
+ - GitHub: https://github.com/JaviLianes8
188
+ - LinkedIn: https://www.linkedin.com/in/jlianes/