ellipt 0.2.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. ellipt-0.2.1/.gitignore +16 -0
  2. ellipt-0.2.1/CHANGELOG.md +53 -0
  3. ellipt-0.2.1/CONTRIBUTING.md +25 -0
  4. ellipt-0.2.1/GUIDE.md +142 -0
  5. ellipt-0.2.1/LICENSE +21 -0
  6. ellipt-0.2.1/LICENSE-UNICODE +39 -0
  7. ellipt-0.2.1/PKG-INFO +134 -0
  8. ellipt-0.2.1/README.md +101 -0
  9. ellipt-0.2.1/data/ucd/MANIFEST.json +56 -0
  10. ellipt-0.2.1/pyproject.toml +77 -0
  11. ellipt-0.2.1/scripts/fetch_upstream.py +40 -0
  12. ellipt-0.2.1/scripts/gen_ucd_tables.py +202 -0
  13. ellipt-0.2.1/src/ellipt/__init__.py +30 -0
  14. ellipt-0.2.1/src/ellipt/__main__.py +9 -0
  15. ellipt-0.2.1/src/ellipt/_cli.py +186 -0
  16. ellipt-0.2.1/src/ellipt/_core.py +127 -0
  17. ellipt-0.2.1/src/ellipt/_cut.py +83 -0
  18. ellipt-0.2.1/src/ellipt/_graphemes.py +103 -0
  19. ellipt-0.2.1/src/ellipt/_measure.py +69 -0
  20. ellipt-0.2.1/src/ellipt/_path.py +90 -0
  21. ellipt-0.2.1/src/ellipt/_props.py +61 -0
  22. ellipt-0.2.1/src/ellipt/_shorten.py +46 -0
  23. ellipt-0.2.1/src/ellipt/_ucd_data.py +31 -0
  24. ellipt-0.2.1/src/ellipt/_ucd_part1.py +166 -0
  25. ellipt-0.2.1/src/ellipt/_width.py +50 -0
  26. ellipt-0.2.1/src/ellipt/py.typed +0 -0
  27. ellipt-0.2.1/tests/conftest.py +28 -0
  28. ellipt-0.2.1/tests/test_cli.py +167 -0
  29. ellipt-0.2.1/tests/test_contract.py +91 -0
  30. ellipt-0.2.1/tests/test_docs_examples.py +32 -0
  31. ellipt-0.2.1/tests/test_doctests.py +23 -0
  32. ellipt-0.2.1/tests/test_e2e_wheel.py +72 -0
  33. ellipt-0.2.1/tests/test_fuzz.py +83 -0
  34. ellipt-0.2.1/tests/test_grapheme_conformance.py +70 -0
  35. ellipt-0.2.1/tests/test_path.py +80 -0
  36. ellipt-0.2.1/tests/test_shorten.py +66 -0
  37. ellipt-0.2.1/tests/test_truncate.py +140 -0
  38. ellipt-0.2.1/tests/test_ucd_tables.py +79 -0
@@ -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
+ # Unicode data files, fetched locally by scripts/fetch_upstream.py; only the manifest is tracked.
15
+ data/ucd/*
16
+ !data/ucd/MANIFEST.json
@@ -0,0 +1,53 @@
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.1]
10
+
11
+ ### Changed
12
+
13
+ - The Unicode Character Database files are no longer committed. `data/ucd/MANIFEST.json`
14
+ pins them to Unicode 18.0.0 (URL, size, sha256, licence) and `scripts/fetch_upstream.py`
15
+ fetches them; the generator check and the conformance tests skip until then. The
16
+ generated tables stay in the package and the Unicode licence notice ships in the wheel.
17
+ - The sdist ships only the sources, tests, scripts and docs.
18
+
19
+ ## [0.2.0]
20
+
21
+ ### Added
22
+
23
+ - `truncate` returns a frozen `Truncation` with `text`, `head`, `marker`, `tail`,
24
+ `original`, `unit`, `limit` and `size`, plus `truncated`, `removed` and
25
+ `removed_span`.
26
+ - `unit=` on `shorten` and `truncate`: `"chars"`, `"graphemes"`, `"columns"` or
27
+ `"bytes"` (UTF-8). `measure(text, unit)` exposes the same measurement.
28
+ - `words=True` cuts at whitespace when it can.
29
+ - `shorten_path` drops whole directories from the middle and keeps the root and the
30
+ file name, then shortens the stem and keeps the extension.
31
+ - `ambiguous=` (1 or 2) sets the width of East Asian Ambiguous characters.
32
+ - `ELLIPSIS` (U+2026), `UNITS`, `UNICODE_VERSION` and `iter_graphemes`.
33
+ - The `ellipt` command truncates every line of standard input or files.
34
+ - Unicode 18.0.0 tables generated from a committed UCD snapshot, with the official
35
+ GraphemeBreakTest.txt in the test suite.
36
+
37
+ ### Changed
38
+
39
+ - The cut never splits an extended grapheme cluster (UAX #29): accents, flags, emoji
40
+ ZWJ sequences and CR LF stay whole.
41
+ - `width_aware=True` now measures grapheme clusters, so emoji sequences count as one
42
+ two-column glyph and the width data follows Unicode 18.0.0.
43
+ - In the middle position, budget the left side cannot use goes to the right side.
44
+ - Wrong argument types now raise `TypeError`: a non-str `text` or `marker`, and a
45
+ `width` that is not an int (a float or a bool).
46
+ - Error messages say "limit" instead of "width" and name the unit.
47
+ - With `width_aware=True`, control characters such as tab count as zero columns
48
+ (0.1.0 counted them as one), following wcwidth. Expand tabs first if they matter.
49
+
50
+ ## [0.1.0]
51
+
52
+ - First release: `shorten` truncates a string with an ellipsis at the start,
53
+ middle or end, by character count or display width, with a configurable marker.
@@ -0,0 +1,25 @@
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
+ python scripts/fetch_upstream.py
10
+ ```
11
+
12
+ The fetch step is optional: it downloads the Unicode data files listed in
13
+ `data/ucd/MANIFEST.json` (checked against their sha256) into `data/ucd/`, which
14
+ git ignores. The generator check and the tests that need them skip until they are there.
15
+
16
+ ## Before you open a pull request
17
+
18
+ - Keep public API in `src/ellipt/__init__.py`; internals live in `_`-prefixed modules.
19
+ - Add a test for the behaviour you change.
20
+ - Never edit `src/ellipt/_ucd_*.py` by hand: they are generated from the fetched `data/ucd/` files by
21
+ `python scripts/gen_ucd_tables.py` (see GUIDE.md, "Unicode data").
22
+ - Run `scripts/check.ps1` (format, lint, types, tests) and make sure it passes.
23
+ - One logical change per commit, imperative subject line.
24
+
25
+ Open an issue first if the change is large or shifts the public API.
ellipt-0.2.1/GUIDE.md ADDED
@@ -0,0 +1,142 @@
1
+ # ellipt guide
2
+
3
+ This guide states the exact rules `ellipt` follows, where its Unicode data comes from,
4
+ and how it behaves at the edges.
5
+
6
+ ## The promise
7
+
8
+ For `truncate(text, limit, where=..., unit=..., marker=..., words=..., ambiguous=...)`:
9
+
10
+ 1. If `text` already fits in `limit`, it comes back whole and `truncated` is false.
11
+ 2. Otherwise the result is `head + marker + tail`, where `head` is a prefix and `tail`
12
+ a suffix of `text`. With `where="end"` the tail is empty, with `where="start"` the
13
+ head is empty, and with `where="middle"` both ends are kept.
14
+ 3. `head` and `tail` end on extended grapheme cluster boundaries of `text` (UAX #29).
15
+ 4. The size of the result, measured in `unit`, never exceeds `limit`. The marker is
16
+ measured in the same unit and counted inside the limit.
17
+ 5. A marker larger than the limit raises `ValueError`, as do a negative limit and an
18
+ unknown `where`, `unit` or `ambiguous`. Wrong types raise `TypeError`.
19
+
20
+ `shorten` returns `truncate(...).text`. `shorten_path` keeps promises 1, 3 (at
21
+ separators and in the file name) and 4.
22
+
23
+ ## Units
24
+
25
+ | unit | counts | use it for |
26
+ | --- | --- | --- |
27
+ | `chars` | code points, `len(s)` | the 0.1.0 default, Python-side limits |
28
+ | `graphemes` | extended grapheme clusters | "characters" as a reader counts them |
29
+ | `columns` | terminal display cells | tables, status lines, fixed-width output |
30
+ | `bytes` | UTF-8 bytes | database columns, file names, protocol fields |
31
+
32
+ Columns follow UAX #11 East Asian Width with Markus Kuhn's wcwidth rules: controls,
33
+ nonspacing and enclosing marks, format characters (except SOFT HYPHEN) and Hangul medial
34
+ vowels and final consonants are zero; Wide and Fullwidth are two; East Asian Ambiguous
35
+ characters are `ambiguous` cells (1 by default, 2 for CJK terminals); everything else is
36
+ one. A grapheme cluster that draws as one emoji (VS16, a skin tone, a ZWJ sequence, a
37
+ tag sequence or a flag) is two cells, and an emoji with VS15 is one. These are the same
38
+ rules and the same data as the sibling `cellstr` library.
39
+
40
+ ```python
41
+ from ellipt import ELLIPSIS, measure, truncate
42
+
43
+ measure("\u2500\u2500", "columns") # -> 2
44
+ measure("\u2500\u2500", "columns", ambiguous=2) # -> 4
45
+ measure("\u2764\ufe0f", "columns") # -> 2
46
+ measure("e\u0301", "graphemes") # -> 1
47
+ ```
48
+
49
+ ## Choosing a marker
50
+
51
+ U+2026 HORIZONTAL ELLIPSIS is one code point, one grapheme and one column, so it leaves
52
+ two more units of text than `"..."` in those units. In UTF-8 both are three bytes. CSS
53
+ `text-overflow: ellipsis` renders U+2026. The default stays `"..."` because it is plain
54
+ ASCII and safe in every encoding and font; pass `marker=ELLIPSIS` for typographic output.
55
+ Any string works, including `""` for a hard cut and `" [...] "` for prose.
56
+
57
+ ```python
58
+ truncate("abcdefgh", 5).text # -> 'ab...'
59
+ truncate("abcdefgh", 5, marker=ELLIPSIS).text # -> 'abcd\u2026'
60
+ ```
61
+
62
+ ## Where to cut
63
+
64
+ - `end` keeps the start, like CSS `text-overflow: ellipsis`. Good for titles and
65
+ previews.
66
+ - `start` keeps the end. Good for log lines, long ids and anything whose tail matters.
67
+ - `middle` keeps both ends, the way macOS Finder and editors shorten file names. The
68
+ left side gets the larger half; when a wide cluster stops it early, the spare budget
69
+ goes to the right side, then back to the left.
70
+
71
+ ```python
72
+ truncate("a\u4f60bcdefgh", 5, where="middle", unit="columns", marker=".").text # -> 'a.fgh'
73
+ ```
74
+
75
+ ## Words
76
+
77
+ `words=True` moves each cut to whitespace (any character for which `str.isspace` is
78
+ true) and drops the whitespace next to the marker. In the middle position the budget a
79
+ snap frees on one side is offered to the other side, again on word boundaries. It does
80
+ not use dictionary-based word segmentation, so text without spaces (Chinese, Japanese,
81
+ Thai) gets a hard cut at a grapheme boundary. If a single word fills the whole budget,
82
+ the cut is hard too, rather than returning only the marker.
83
+
84
+ ```python
85
+ truncate("Supercalifragilistic", 8, words=True).text # -> 'Super...'
86
+ truncate("one two three", 11, where="start", words=True).text # -> '...three'
87
+ truncate("a b c d e f", 7, where="middle", words=True).text # -> 'a...e f'
88
+ ```
89
+
90
+ ## Paths
91
+
92
+ `shorten_path` keeps the first component (`/`, `C:\`, `~/`, `\\server\share\`, or the top
93
+ directory of a relative path) and the file name, and keeps as many directories next to the file name as
94
+ fit. The order of fallbacks is:
95
+
96
+ 1. `root + marker + /dir/.../name`, dropping directories from the root side first;
97
+ 2. `marker + /name`;
98
+ 3. `name` alone, when it fits;
99
+ 4. the name shortened in the middle of its stem with the extension kept (`.tar.gz`,
100
+ `.tar.xz` and other `.tar.*` extensions stay together);
101
+ 5. the name shortened in the middle.
102
+
103
+ Both `/` and `\` count as separators unless `sep` names one. A trailing separator stays
104
+ with the last directory. An empty marker is rejected, because `a/b/c` cut to `a//c`
105
+ would read as a different, real path.
106
+
107
+ ```python
108
+ from ellipt import shorten_path
109
+
110
+ shorten_path(r"C:\Users\me\AppData\Local\Temp\build.log", 24) # -> 'C:\\...\\Temp\\build.log'
111
+ shorten_path("/srv/www/site/public/", 16) # -> '/.../public/'
112
+ ```
113
+
114
+ ## Edge cases
115
+
116
+ - A marker that would merge with the neighbouring cluster (for example one that starts
117
+ with U+FE0F or a combining mark) can change the measured size. The result is measured
118
+ again after joining and the cut backs off until it fits.
119
+ - Lone surrogates are counted as three bytes (the `surrogatepass` encoding) so strings
120
+ from `os.fsdecode` do not crash the byte unit.
121
+ - CR LF is a single grapheme cluster and is never split.
122
+ - Tabs and other control characters are zero columns, as in wcwidth. The library does
123
+ not know your tab stops, so expand tabs first (`s.expandtabs()`); the command line tool
124
+ does this for you with `--tabsize`.
125
+ - ANSI escape sequences are measured as ordinary text. Strip them first.
126
+ - Long inputs: an end cut in `chars` or `bytes` only segments the first `limit + 1` code
127
+ points. Other cuts segment the whole string once, which is linear in its length.
128
+
129
+ ## Unicode data
130
+
131
+ The tables in `src/ellipt/_ucd_*.py` are generated by `scripts/gen_ucd_tables.py` from
132
+ the Unicode 18.0.0 data files. Those files are not committed: `data/ucd/MANIFEST.json`
133
+ pins their URLs, sizes and SHA-256 checksums, and `python scripts/fetch_upstream.py`
134
+ fetches them into `data/ucd/`. With them in place the test suite regenerates the tables
135
+ and compares them byte for byte, checks the checksums, and runs all 853 cases of the
136
+ official `GraphemeBreakTest.txt` through both the segmenter and the truncation engine;
137
+ without them those tests skip.
138
+
139
+ To move to a new Unicode version, update the URLs, sizes and checksums in
140
+ `data/ucd/MANIFEST.json`, run `python scripts/fetch_upstream.py` and
141
+ `python scripts/gen_ucd_tables.py`, and update the expected case count in
142
+ `tests/test_grapheme_conformance.py`.
ellipt-0.2.1/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.
@@ -0,0 +1,39 @@
1
+ UNICODE LICENSE V3
2
+
3
+ COPYRIGHT AND PERMISSION NOTICE
4
+
5
+ Copyright © 1991-2026 Unicode, Inc.
6
+
7
+ NOTICE TO USER: Carefully read the following legal agreement. BY
8
+ DOWNLOADING, INSTALLING, COPYING OR OTHERWISE USING DATA FILES, AND/OR
9
+ SOFTWARE, YOU UNEQUIVOCALLY ACCEPT, AND AGREE TO BE BOUND BY, ALL OF THE
10
+ TERMS AND CONDITIONS OF THIS AGREEMENT. IF YOU DO NOT AGREE, DO NOT
11
+ DOWNLOAD, INSTALL, COPY, DISTRIBUTE OR USE THE DATA FILES OR SOFTWARE.
12
+
13
+ Permission is hereby granted, free of charge, to any person obtaining a
14
+ copy of data files and any associated documentation (the "Data Files") or
15
+ software and any associated documentation (the "Software") to deal in the
16
+ Data Files or Software without restriction, including without limitation
17
+ the rights to use, copy, modify, merge, publish, distribute, and/or sell
18
+ copies of the Data Files or Software, and to permit persons to whom the
19
+ Data Files or Software are furnished to do so, provided that either (a)
20
+ this copyright and permission notice appear with all copies of the Data
21
+ Files or Software, or (b) this copyright and permission notice appear in
22
+ associated Documentation.
23
+
24
+ THE DATA FILES AND SOFTWARE ARE PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
25
+ KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
26
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF
27
+ THIRD PARTY RIGHTS.
28
+
29
+ IN NO EVENT SHALL THE COPYRIGHT HOLDER OR HOLDERS INCLUDED IN THIS NOTICE
30
+ BE LIABLE FOR ANY CLAIM, OR ANY SPECIAL INDIRECT OR CONSEQUENTIAL DAMAGES,
31
+ OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS,
32
+ WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION,
33
+ ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THE DATA
34
+ FILES OR SOFTWARE.
35
+
36
+ Except as contained in this notice, the name of a copyright holder shall
37
+ not be used in advertising or otherwise to promote the sale, use or other
38
+ dealings in these Data Files or Software without prior written
39
+ authorization of the copyright holder.
ellipt-0.2.1/PKG-INFO ADDED
@@ -0,0 +1,134 @@
1
+ Metadata-Version: 2.5
2
+ Name: ellipt
3
+ Version: 0.2.1
4
+ Summary: Truncate strings with an ellipsis at the start, middle or end, by characters, grapheme clusters, display columns or UTF-8 bytes, without splitting emoji or accents
5
+ Project-URL: Homepage, https://lianes8studio.com
6
+ Project-URL: Repository, https://github.com/JaviLianes8/ellipt
7
+ Project-URL: Issues, https://github.com/JaviLianes8/ellipt/issues
8
+ Project-URL: Changelog, https://github.com/JaviLianes8/ellipt/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
+ License-File: LICENSE-UNICODE
15
+ Keywords: cli,display-width,ellipsis,grapheme,path,shorten,string,truncate,unicode
16
+ Classifier: Development Status :: 4 - Beta
17
+ Classifier: Environment :: Console
18
+ Classifier: Intended Audience :: Developers
19
+ Classifier: Operating System :: OS Independent
20
+ Classifier: Programming Language :: Python :: 3
21
+ Classifier: Programming Language :: Python :: 3 :: Only
22
+ Classifier: Topic :: Software Development :: Libraries
23
+ Classifier: Topic :: Text Processing
24
+ Classifier: Typing :: Typed
25
+ Requires-Python: >=3.10
26
+ Provides-Extra: dev
27
+ Requires-Dist: build; extra == 'dev'
28
+ Requires-Dist: mypy; extra == 'dev'
29
+ Requires-Dist: pytest; extra == 'dev'
30
+ Requires-Dist: ruff; extra == 'dev'
31
+ Requires-Dist: twine; extra == 'dev'
32
+ Description-Content-Type: text/markdown
33
+
34
+ # ellipt
35
+
36
+ Truncate strings with an ellipsis at the start, middle or end. The limit can count code
37
+ points, grapheme clusters, terminal columns or UTF-8 bytes, the marker is counted inside
38
+ the limit, and the cut never splits an accent, a flag or an emoji sequence. Typed, zero
39
+ dependencies, Unicode 18.0.0 tables embedded.
40
+
41
+ ## Install
42
+
43
+ ```
44
+ pip install ellipt
45
+ ```
46
+
47
+ ## Usage
48
+
49
+ ```python
50
+ from ellipt import ELLIPSIS, measure, shorten, shorten_path, truncate
51
+
52
+ shorten("hello world", 8) # -> 'hello...'
53
+ shorten("hello world", 8, where="start") # -> '...world'
54
+ shorten("hello world", 9, where="middle") # -> 'hel...rld'
55
+ shorten("hello world", 6, marker=ELLIPSIS) # -> 'hello…'
56
+
57
+ # Text that already fits is returned unchanged.
58
+ shorten("short", 20) # -> 'short'
59
+ ```
60
+
61
+ ### Pick the unit of the limit
62
+
63
+ ```python
64
+ family = "\U0001f468\u200d\U0001f469\u200d\U0001f467" # one emoji, five code points
65
+ text = "ab" + family + "cd"
66
+
67
+ measure(text, "chars") # -> 9
68
+ measure(text, "graphemes") # -> 5
69
+ measure(text, "columns") # -> 6
70
+ measure(text, "bytes") # -> 22
71
+
72
+ # The family is kept whole or dropped whole, never cut into pieces.
73
+ shorten(text, 6, marker=ELLIPSIS) # -> 'ab…'
74
+ shorten(text, 4, unit="graphemes", marker=ELLIPSIS) # -> 'ab👨\u200d👩\u200d👧…'
75
+ shorten("你好世界", 5, unit="columns", marker=ELLIPSIS) # -> '你好…'
76
+ shorten("café crème", 8, unit="bytes", marker="") # -> 'café cr'
77
+ ```
78
+
79
+ `width_aware=True` from 0.1.0 still works and means `unit="columns"`.
80
+
81
+ ### Know what was removed
82
+
83
+ ```python
84
+ r = truncate("The quick brown fox jumps", 14, words=True, marker=ELLIPSIS)
85
+ r.text # -> 'The quick…'
86
+ r.removed # -> ' brown fox jumps'
87
+ r.truncated # -> True
88
+ r.size <= r.limit # -> True
89
+ ```
90
+
91
+ `words=True` backs the cut off to whitespace; with one long word it cuts hard instead.
92
+
93
+ ### Shorten paths the way file browsers do
94
+
95
+ ```python
96
+ shorten_path("/home/me/projects/app/src/main.py", 20) # -> '/.../app/src/main.py'
97
+ shorten_path("/home/me/projects/app/src/main.py", 11) # -> '.../main.py'
98
+ shorten_path("photos/an_extremely_long_holiday_name.jpeg", 20) # -> 'an_ext...y_name.jpeg'
99
+ ```
100
+
101
+ ## Command line
102
+
103
+ ```
104
+ $ printf 'hello world\n' | ellipt 8
105
+ hello...
106
+ $ git ls-files | ellipt 28 --path
107
+ $ ellipt 30 --where middle --ellipsis notes.txt
108
+ $ ellipt 255 --unit bytes --marker "" < titles.txt
109
+ ```
110
+
111
+ The command reads standard input (or the files given) as UTF-8, keeps each LF or CRLF
112
+ line ending, and counts terminal columns by default, expanding tabs to 8-column stops
113
+ first (`--tabsize`). `--ellipsis` uses U+2026 as the marker without having to type it.
114
+ `ellipt --help` lists every option. Errors go to standard error with exit status 1, and
115
+ a reader that stops early (`| head`) is not an error.
116
+
117
+ ## Why
118
+
119
+ `textwrap.shorten` only cuts at the end, collapses whitespace and always cuts on words.
120
+ Plain slicing (`s[:n] + "..."`) splits emoji and accents, overflows a terminal with wide
121
+ characters and can exceed a byte limit. `ellipt` cuts where you ask, in the unit you
122
+ need, and guarantees the result is never longer than the limit. See [GUIDE.md](GUIDE.md)
123
+ for the exact rules, the Unicode sources and the edge cases.
124
+
125
+ ## License
126
+
127
+ MIT. See [LICENSE](LICENSE). The embedded Unicode data is covered by the Unicode
128
+ License v3, see [LICENSE-UNICODE](LICENSE-UNICODE).
129
+
130
+ ## Links
131
+
132
+ - Website: https://lianes8studio.com
133
+ - GitHub: https://github.com/JaviLianes8
134
+ - LinkedIn: https://www.linkedin.com/in/jlianes/
ellipt-0.2.1/README.md ADDED
@@ -0,0 +1,101 @@
1
+ # ellipt
2
+
3
+ Truncate strings with an ellipsis at the start, middle or end. The limit can count code
4
+ points, grapheme clusters, terminal columns or UTF-8 bytes, the marker is counted inside
5
+ the limit, and the cut never splits an accent, a flag or an emoji sequence. Typed, zero
6
+ dependencies, Unicode 18.0.0 tables embedded.
7
+
8
+ ## Install
9
+
10
+ ```
11
+ pip install ellipt
12
+ ```
13
+
14
+ ## Usage
15
+
16
+ ```python
17
+ from ellipt import ELLIPSIS, measure, shorten, shorten_path, truncate
18
+
19
+ shorten("hello world", 8) # -> 'hello...'
20
+ shorten("hello world", 8, where="start") # -> '...world'
21
+ shorten("hello world", 9, where="middle") # -> 'hel...rld'
22
+ shorten("hello world", 6, marker=ELLIPSIS) # -> 'hello…'
23
+
24
+ # Text that already fits is returned unchanged.
25
+ shorten("short", 20) # -> 'short'
26
+ ```
27
+
28
+ ### Pick the unit of the limit
29
+
30
+ ```python
31
+ family = "\U0001f468\u200d\U0001f469\u200d\U0001f467" # one emoji, five code points
32
+ text = "ab" + family + "cd"
33
+
34
+ measure(text, "chars") # -> 9
35
+ measure(text, "graphemes") # -> 5
36
+ measure(text, "columns") # -> 6
37
+ measure(text, "bytes") # -> 22
38
+
39
+ # The family is kept whole or dropped whole, never cut into pieces.
40
+ shorten(text, 6, marker=ELLIPSIS) # -> 'ab…'
41
+ shorten(text, 4, unit="graphemes", marker=ELLIPSIS) # -> 'ab👨\u200d👩\u200d👧…'
42
+ shorten("你好世界", 5, unit="columns", marker=ELLIPSIS) # -> '你好…'
43
+ shorten("café crème", 8, unit="bytes", marker="") # -> 'café cr'
44
+ ```
45
+
46
+ `width_aware=True` from 0.1.0 still works and means `unit="columns"`.
47
+
48
+ ### Know what was removed
49
+
50
+ ```python
51
+ r = truncate("The quick brown fox jumps", 14, words=True, marker=ELLIPSIS)
52
+ r.text # -> 'The quick…'
53
+ r.removed # -> ' brown fox jumps'
54
+ r.truncated # -> True
55
+ r.size <= r.limit # -> True
56
+ ```
57
+
58
+ `words=True` backs the cut off to whitespace; with one long word it cuts hard instead.
59
+
60
+ ### Shorten paths the way file browsers do
61
+
62
+ ```python
63
+ shorten_path("/home/me/projects/app/src/main.py", 20) # -> '/.../app/src/main.py'
64
+ shorten_path("/home/me/projects/app/src/main.py", 11) # -> '.../main.py'
65
+ shorten_path("photos/an_extremely_long_holiday_name.jpeg", 20) # -> 'an_ext...y_name.jpeg'
66
+ ```
67
+
68
+ ## Command line
69
+
70
+ ```
71
+ $ printf 'hello world\n' | ellipt 8
72
+ hello...
73
+ $ git ls-files | ellipt 28 --path
74
+ $ ellipt 30 --where middle --ellipsis notes.txt
75
+ $ ellipt 255 --unit bytes --marker "" < titles.txt
76
+ ```
77
+
78
+ The command reads standard input (or the files given) as UTF-8, keeps each LF or CRLF
79
+ line ending, and counts terminal columns by default, expanding tabs to 8-column stops
80
+ first (`--tabsize`). `--ellipsis` uses U+2026 as the marker without having to type it.
81
+ `ellipt --help` lists every option. Errors go to standard error with exit status 1, and
82
+ a reader that stops early (`| head`) is not an error.
83
+
84
+ ## Why
85
+
86
+ `textwrap.shorten` only cuts at the end, collapses whitespace and always cuts on words.
87
+ Plain slicing (`s[:n] + "..."`) splits emoji and accents, overflows a terminal with wide
88
+ characters and can exceed a byte limit. `ellipt` cuts where you ask, in the unit you
89
+ need, and guarantees the result is never longer than the limit. See [GUIDE.md](GUIDE.md)
90
+ for the exact rules, the Unicode sources and the edge cases.
91
+
92
+ ## License
93
+
94
+ MIT. See [LICENSE](LICENSE). The embedded Unicode data is covered by the Unicode
95
+ License v3, see [LICENSE-UNICODE](LICENSE-UNICODE).
96
+
97
+ ## Links
98
+
99
+ - Website: https://lianes8studio.com
100
+ - GitHub: https://github.com/JaviLianes8
101
+ - LinkedIn: https://www.linkedin.com/in/jlianes/
@@ -0,0 +1,56 @@
1
+ {
2
+ "DerivedCoreProperties.txt": {
3
+ "title": "Unicode Character Database 18.0.0, DerivedCoreProperties.txt",
4
+ "url": "https://www.unicode.org/Public/18.0.0/ucd/DerivedCoreProperties.txt",
5
+ "revision": "Unicode 18.0.0",
6
+ "fetched": "2026-10-13",
7
+ "size": 1159889,
8
+ "sha256": "09c928886a178fcafd93c29e4bd59073a058e5a100b716d425cb563ab50f68c9",
9
+ "licence": "Unicode License v3 (https://www.unicode.org/license.txt), see LICENSE-UNICODE"
10
+ },
11
+ "DerivedGeneralCategory.txt": {
12
+ "title": "Unicode Character Database 18.0.0, DerivedGeneralCategory.txt",
13
+ "url": "https://www.unicode.org/Public/18.0.0/ucd/extracted/DerivedGeneralCategory.txt",
14
+ "revision": "Unicode 18.0.0",
15
+ "fetched": "2026-10-13",
16
+ "size": 282143,
17
+ "sha256": "d6b151d2d40ee9b1876d26f417980f45ffae47b6055ccf7203cb31f07a030f94",
18
+ "licence": "Unicode License v3 (https://www.unicode.org/license.txt), see LICENSE-UNICODE"
19
+ },
20
+ "EastAsianWidth.txt": {
21
+ "title": "Unicode Character Database 18.0.0, EastAsianWidth.txt",
22
+ "url": "https://www.unicode.org/Public/18.0.0/ucd/EastAsianWidth.txt",
23
+ "revision": "Unicode 18.0.0",
24
+ "fetched": "2026-10-13",
25
+ "size": 204705,
26
+ "sha256": "a0cf29eacd00cfcaec4381c6b7c281685f18dbb4e7ff82b4076ccb342ca839aa",
27
+ "licence": "Unicode License v3 (https://www.unicode.org/license.txt), see LICENSE-UNICODE"
28
+ },
29
+ "GraphemeBreakProperty.txt": {
30
+ "title": "Unicode Character Database 18.0.0, GraphemeBreakProperty.txt",
31
+ "url": "https://www.unicode.org/Public/18.0.0/ucd/auxiliary/GraphemeBreakProperty.txt",
32
+ "revision": "Unicode 18.0.0",
33
+ "fetched": "2026-10-13",
34
+ "size": 100009,
35
+ "sha256": "0839dcb79e4ac639ecd538b1abf7c9d22e3f9dd265b7e182d33627aa4d75b45a",
36
+ "licence": "Unicode License v3 (https://www.unicode.org/license.txt), see LICENSE-UNICODE"
37
+ },
38
+ "GraphemeBreakTest.txt": {
39
+ "title": "Unicode Character Database 18.0.0, GraphemeBreakTest.txt",
40
+ "url": "https://www.unicode.org/Public/18.0.0/ucd/auxiliary/GraphemeBreakTest.txt",
41
+ "revision": "Unicode 18.0.0",
42
+ "fetched": "2026-10-13",
43
+ "size": 137090,
44
+ "sha256": "b0cf047ee94485bbdc846de2b902f5f8a815f6b674f9d04223cddadd91c9df31",
45
+ "licence": "Unicode License v3 (https://www.unicode.org/license.txt), see LICENSE-UNICODE"
46
+ },
47
+ "emoji-data.txt": {
48
+ "title": "Unicode Character Database 18.0.0, emoji-data.txt",
49
+ "url": "https://www.unicode.org/Public/18.0.0/ucd/emoji/emoji-data.txt",
50
+ "revision": "Unicode 18.0.0",
51
+ "fetched": "2026-10-13",
52
+ "size": 108719,
53
+ "sha256": "80d00f8e616a0ef27fd6b8de3b758c06383b5d917e2977709578e68baf733bf1",
54
+ "licence": "Unicode License v3 (https://www.unicode.org/license.txt), see LICENSE-UNICODE"
55
+ }
56
+ }
@@ -0,0 +1,77 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "ellipt"
7
+ version = "0.2.1"
8
+ description = "Truncate strings with an ellipsis at the start, middle or end, by characters, grapheme clusters, display columns or UTF-8 bytes, without splitting emoji or accents"
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ license = "MIT"
12
+ license-files = ["LICENSE", "LICENSE-UNICODE"]
13
+ authors = [{ name = "Javier Lianes", email = "javier@lianes8studio.com" }]
14
+ keywords = ["ellipsis", "truncate", "shorten", "string", "unicode", "grapheme", "display-width", "path", "cli"]
15
+ classifiers = [
16
+ "Development Status :: 4 - Beta",
17
+ "Intended Audience :: Developers",
18
+ "Programming Language :: Python :: 3",
19
+ "Programming Language :: Python :: 3 :: Only",
20
+ "Environment :: Console",
21
+ "Operating System :: OS Independent",
22
+ "Topic :: Text Processing",
23
+ "Topic :: Software Development :: Libraries",
24
+ "Typing :: Typed",
25
+ ]
26
+ dependencies = []
27
+
28
+ [project.scripts]
29
+ ellipt = "ellipt._cli:main"
30
+
31
+ [project.optional-dependencies]
32
+ dev = ["ruff", "mypy", "pytest", "build", "twine"]
33
+
34
+ [project.urls]
35
+ Homepage = "https://lianes8studio.com"
36
+ Repository = "https://github.com/JaviLianes8/ellipt"
37
+ Issues = "https://github.com/JaviLianes8/ellipt/issues"
38
+ Changelog = "https://github.com/JaviLianes8/ellipt/blob/main/CHANGELOG.md"
39
+ LinkedIn = "https://www.linkedin.com/in/jlianes/"
40
+ Funding = "https://buymeacoffee.com/jlianesglrs"
41
+
42
+ [tool.hatch.build.targets.sdist]
43
+ # The Unicode data files are fetched locally and never shipped; only their manifest is.
44
+ include = [
45
+ "src",
46
+ "tests",
47
+ "scripts/gen_ucd_tables.py",
48
+ "scripts/fetch_upstream.py",
49
+ "data/ucd/MANIFEST.json",
50
+ "CHANGELOG.md",
51
+ "CONTRIBUTING.md",
52
+ "GUIDE.md",
53
+ ]
54
+ exclude = ["**/__pycache__"]
55
+
56
+ [tool.hatch.build.targets.wheel]
57
+ packages = ["src/ellipt"]
58
+
59
+ [tool.ruff]
60
+ line-length = 100
61
+ target-version = "py310"
62
+
63
+ [tool.ruff.lint]
64
+ select = ["E", "F", "I", "UP", "B", "SIM", "RUF"]
65
+
66
+ [tool.ruff.lint.per-file-ignores]
67
+ # Tests use look-alike characters (fullwidth, ellipsis, accents) on purpose.
68
+ "tests/*" = ["RUF001", "RUF003"]
69
+
70
+ [tool.mypy]
71
+ python_version = "3.10"
72
+ strict = true
73
+
74
+ [tool.pytest.ini_options]
75
+ testpaths = ["tests"]
76
+ pythonpath = ["src"]
77
+ addopts = "-q"