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.
- ellipt-0.2.1/.gitignore +16 -0
- ellipt-0.2.1/CHANGELOG.md +53 -0
- ellipt-0.2.1/CONTRIBUTING.md +25 -0
- ellipt-0.2.1/GUIDE.md +142 -0
- ellipt-0.2.1/LICENSE +21 -0
- ellipt-0.2.1/LICENSE-UNICODE +39 -0
- ellipt-0.2.1/PKG-INFO +134 -0
- ellipt-0.2.1/README.md +101 -0
- ellipt-0.2.1/data/ucd/MANIFEST.json +56 -0
- ellipt-0.2.1/pyproject.toml +77 -0
- ellipt-0.2.1/scripts/fetch_upstream.py +40 -0
- ellipt-0.2.1/scripts/gen_ucd_tables.py +202 -0
- ellipt-0.2.1/src/ellipt/__init__.py +30 -0
- ellipt-0.2.1/src/ellipt/__main__.py +9 -0
- ellipt-0.2.1/src/ellipt/_cli.py +186 -0
- ellipt-0.2.1/src/ellipt/_core.py +127 -0
- ellipt-0.2.1/src/ellipt/_cut.py +83 -0
- ellipt-0.2.1/src/ellipt/_graphemes.py +103 -0
- ellipt-0.2.1/src/ellipt/_measure.py +69 -0
- ellipt-0.2.1/src/ellipt/_path.py +90 -0
- ellipt-0.2.1/src/ellipt/_props.py +61 -0
- ellipt-0.2.1/src/ellipt/_shorten.py +46 -0
- ellipt-0.2.1/src/ellipt/_ucd_data.py +31 -0
- ellipt-0.2.1/src/ellipt/_ucd_part1.py +166 -0
- ellipt-0.2.1/src/ellipt/_width.py +50 -0
- ellipt-0.2.1/src/ellipt/py.typed +0 -0
- ellipt-0.2.1/tests/conftest.py +28 -0
- ellipt-0.2.1/tests/test_cli.py +167 -0
- ellipt-0.2.1/tests/test_contract.py +91 -0
- ellipt-0.2.1/tests/test_docs_examples.py +32 -0
- ellipt-0.2.1/tests/test_doctests.py +23 -0
- ellipt-0.2.1/tests/test_e2e_wheel.py +72 -0
- ellipt-0.2.1/tests/test_fuzz.py +83 -0
- ellipt-0.2.1/tests/test_grapheme_conformance.py +70 -0
- ellipt-0.2.1/tests/test_path.py +80 -0
- ellipt-0.2.1/tests/test_shorten.py +66 -0
- ellipt-0.2.1/tests/test_truncate.py +140 -0
- ellipt-0.2.1/tests/test_ucd_tables.py +79 -0
ellipt-0.2.1/.gitignore
ADDED
|
@@ -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"
|