ellipt 0.2.1__py3-none-any.whl
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/__init__.py +30 -0
- ellipt/__main__.py +9 -0
- ellipt/_cli.py +186 -0
- ellipt/_core.py +127 -0
- ellipt/_cut.py +83 -0
- ellipt/_graphemes.py +103 -0
- ellipt/_measure.py +69 -0
- ellipt/_path.py +90 -0
- ellipt/_props.py +61 -0
- ellipt/_shorten.py +46 -0
- ellipt/_ucd_data.py +31 -0
- ellipt/_ucd_part1.py +166 -0
- ellipt/_width.py +50 -0
- ellipt/py.typed +0 -0
- ellipt-0.2.1.dist-info/METADATA +134 -0
- ellipt-0.2.1.dist-info/RECORD +20 -0
- ellipt-0.2.1.dist-info/WHEEL +4 -0
- ellipt-0.2.1.dist-info/entry_points.txt +2 -0
- ellipt-0.2.1.dist-info/licenses/LICENSE +21 -0
- ellipt-0.2.1.dist-info/licenses/LICENSE-UNICODE +39 -0
ellipt/__init__.py
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"""Truncate strings with an ellipsis at the start, middle or end.
|
|
2
|
+
|
|
3
|
+
Limits can count code points, grapheme clusters, terminal columns or UTF-8 bytes, and
|
|
4
|
+
the cut never splits a grapheme cluster (UAX #29, Unicode 18.0.0 tables embedded).
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from ._core import ELLIPSIS, Truncation, Where, truncate
|
|
10
|
+
from ._graphemes import iter_graphemes
|
|
11
|
+
from ._measure import UNITS, Unit, measure
|
|
12
|
+
from ._path import shorten_path
|
|
13
|
+
from ._props import UNICODE_VERSION
|
|
14
|
+
from ._shorten import shorten
|
|
15
|
+
|
|
16
|
+
__version__ = "0.2.1"
|
|
17
|
+
|
|
18
|
+
__all__ = [
|
|
19
|
+
"ELLIPSIS",
|
|
20
|
+
"UNICODE_VERSION",
|
|
21
|
+
"UNITS",
|
|
22
|
+
"Truncation",
|
|
23
|
+
"Unit",
|
|
24
|
+
"Where",
|
|
25
|
+
"iter_graphemes",
|
|
26
|
+
"measure",
|
|
27
|
+
"shorten",
|
|
28
|
+
"shorten_path",
|
|
29
|
+
"truncate",
|
|
30
|
+
]
|
ellipt/__main__.py
ADDED
ellipt/_cli.py
ADDED
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
"""Command line tool: truncate every line of standard input or of the given files."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import contextlib
|
|
7
|
+
import errno
|
|
8
|
+
import os
|
|
9
|
+
import sys
|
|
10
|
+
from collections.abc import Iterator, Sequence
|
|
11
|
+
from typing import IO, NoReturn
|
|
12
|
+
|
|
13
|
+
from . import __version__
|
|
14
|
+
from ._core import ELLIPSIS, truncate
|
|
15
|
+
from ._measure import UNITS
|
|
16
|
+
from ._path import shorten_path
|
|
17
|
+
|
|
18
|
+
EXAMPLES = """\
|
|
19
|
+
examples:
|
|
20
|
+
ls | ellipt 20 end ellipsis at 20 columns
|
|
21
|
+
ellipt 30 --where middle notes.txt keep both ends of each line
|
|
22
|
+
git ls-files | ellipt 28 --path keep the root and the file name
|
|
23
|
+
ellipt 255 --unit bytes --marker "" < titles.txt
|
|
24
|
+
fit a 255-byte UTF-8 column
|
|
25
|
+
tail -f app.log | ellipt 80 --where start --ellipsis
|
|
26
|
+
|
|
27
|
+
Lines are read as UTF-8 (a leading byte order mark is dropped) and written as UTF-8,
|
|
28
|
+
keeping each LF or CRLF line ending. The limit counts the marker, and no grapheme
|
|
29
|
+
cluster (accent, flag, emoji sequence) is ever split. With --unit columns, tabs are
|
|
30
|
+
expanded to spaces first (--tabsize); ANSI escape sequences are not recognised, so
|
|
31
|
+
strip colours before piping. Errors go to standard error with exit status 1.
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class _PipeClosed(Exception):
|
|
36
|
+
"""The reader of standard output went away."""
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class _Parser(argparse.ArgumentParser):
|
|
40
|
+
def error(self, message: str) -> NoReturn:
|
|
41
|
+
self.print_usage(sys.stderr)
|
|
42
|
+
self.exit(1, f"{self.prog}: error: {message}\n")
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _non_negative(value: str) -> int:
|
|
46
|
+
try:
|
|
47
|
+
number = int(value)
|
|
48
|
+
except ValueError:
|
|
49
|
+
raise argparse.ArgumentTypeError(f"not a whole number: {value!r}") from None
|
|
50
|
+
if number < 0:
|
|
51
|
+
raise argparse.ArgumentTypeError(f"must be non-negative: {number}")
|
|
52
|
+
return number
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
56
|
+
"""Return the argument parser of the ``ellipt`` command."""
|
|
57
|
+
parser = _Parser(
|
|
58
|
+
prog="ellipt",
|
|
59
|
+
description="Truncate each input line to LIMIT with an ellipsis at the start, "
|
|
60
|
+
"middle or end.",
|
|
61
|
+
epilog=EXAMPLES,
|
|
62
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
63
|
+
)
|
|
64
|
+
parser.add_argument("limit", type=_non_negative, metavar="LIMIT", help="maximum size")
|
|
65
|
+
parser.add_argument("files", nargs="*", metavar="FILE", help="input files (default: stdin)")
|
|
66
|
+
parser.add_argument(
|
|
67
|
+
"-w", "--where", choices=("start", "middle", "end"), help="where to cut (default: end)"
|
|
68
|
+
)
|
|
69
|
+
parser.add_argument(
|
|
70
|
+
"-u",
|
|
71
|
+
"--unit",
|
|
72
|
+
choices=UNITS,
|
|
73
|
+
default="columns",
|
|
74
|
+
help="what LIMIT counts (default: columns, terminal cells)",
|
|
75
|
+
)
|
|
76
|
+
marker = parser.add_mutually_exclusive_group()
|
|
77
|
+
marker.add_argument("-m", "--marker", default="...", help='replacement text (default: "...")')
|
|
78
|
+
marker.add_argument(
|
|
79
|
+
"-e",
|
|
80
|
+
"--ellipsis",
|
|
81
|
+
dest="marker",
|
|
82
|
+
action="store_const",
|
|
83
|
+
const=ELLIPSIS,
|
|
84
|
+
help="use the single character U+2026 HORIZONTAL ELLIPSIS as the marker",
|
|
85
|
+
)
|
|
86
|
+
parser.add_argument("--words", action="store_true", help="cut at whitespace when possible")
|
|
87
|
+
parser.add_argument(
|
|
88
|
+
"--path", action="store_true", help="treat lines as paths: drop middle directories"
|
|
89
|
+
)
|
|
90
|
+
parser.add_argument(
|
|
91
|
+
"--ambiguous",
|
|
92
|
+
type=int,
|
|
93
|
+
choices=(1, 2),
|
|
94
|
+
default=1,
|
|
95
|
+
help="columns of East Asian Ambiguous characters (default: 1)",
|
|
96
|
+
)
|
|
97
|
+
parser.add_argument(
|
|
98
|
+
"--tabsize",
|
|
99
|
+
type=_non_negative,
|
|
100
|
+
default=8,
|
|
101
|
+
help="with --unit columns, expand tabs to this tab stop first; 0 keeps them as "
|
|
102
|
+
"zero-width characters (default: 8)",
|
|
103
|
+
)
|
|
104
|
+
parser.add_argument("-V", "--version", action="version", version=f"ellipt {__version__}")
|
|
105
|
+
return parser
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def _lines(names: Sequence[str]) -> Iterator[tuple[str, int, bytes]]:
|
|
109
|
+
"""Yield ``(source name, line number, raw line)`` with any UTF-8 BOM removed."""
|
|
110
|
+
if not names:
|
|
111
|
+
yield from _numbered("<stdin>", sys.stdin.buffer)
|
|
112
|
+
return
|
|
113
|
+
for name in names:
|
|
114
|
+
with open(name, "rb") as handle:
|
|
115
|
+
yield from _numbered(name, handle)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def _numbered(name: str, handle: IO[bytes]) -> Iterator[tuple[str, int, bytes]]:
|
|
119
|
+
for number, raw in enumerate(handle, 1):
|
|
120
|
+
yield name, number, raw[3:] if number == 1 and raw.startswith(b"\xef\xbb\xbf") else raw
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def _write(out: IO[bytes], data: bytes) -> None:
|
|
124
|
+
try:
|
|
125
|
+
out.write(data)
|
|
126
|
+
except OSError as exc:
|
|
127
|
+
# Windows reports a closed pipe as EINVAL rather than EPIPE.
|
|
128
|
+
if isinstance(exc, BrokenPipeError) or exc.errno in (errno.EPIPE, errno.EINVAL):
|
|
129
|
+
raise _PipeClosed from None
|
|
130
|
+
raise
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def _split_ending(line: str) -> tuple[str, str]:
|
|
134
|
+
for ending in ("\r\n", "\n", "\r"):
|
|
135
|
+
if line.endswith(ending):
|
|
136
|
+
return line[: -len(ending)], ending
|
|
137
|
+
return line, ""
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def _run(args: argparse.Namespace, out: IO[bytes]) -> None:
|
|
141
|
+
for name, number, raw in _lines(args.files):
|
|
142
|
+
try:
|
|
143
|
+
line = raw.decode("utf-8")
|
|
144
|
+
except UnicodeDecodeError as exc:
|
|
145
|
+
raise ValueError(f"{name}: line {number} is not valid UTF-8: {exc.reason}") from None
|
|
146
|
+
body, ending = _split_ending(line)
|
|
147
|
+
if args.unit == "columns" and args.tabsize and "\t" in body:
|
|
148
|
+
body = body.expandtabs(args.tabsize)
|
|
149
|
+
if args.path:
|
|
150
|
+
short = shorten_path(
|
|
151
|
+
body, args.limit, unit=args.unit, marker=args.marker, ambiguous=args.ambiguous
|
|
152
|
+
)
|
|
153
|
+
else:
|
|
154
|
+
short = truncate(
|
|
155
|
+
body,
|
|
156
|
+
args.limit,
|
|
157
|
+
where=args.where or "end",
|
|
158
|
+
unit=args.unit,
|
|
159
|
+
marker=args.marker,
|
|
160
|
+
words=args.words,
|
|
161
|
+
ambiguous=args.ambiguous,
|
|
162
|
+
).text
|
|
163
|
+
_write(out, (short + ending).encode("utf-8"))
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def main(argv: Sequence[str] | None = None) -> int:
|
|
167
|
+
"""Run the command line tool and return its exit status."""
|
|
168
|
+
parser = build_parser()
|
|
169
|
+
args = parser.parse_args(argv)
|
|
170
|
+
if args.path and (args.where not in (None, "middle") or args.words):
|
|
171
|
+
parser.error("--path always cuts in the middle and does not take --words")
|
|
172
|
+
try:
|
|
173
|
+
_run(args, sys.stdout.buffer)
|
|
174
|
+
_write(sys.stdout.buffer, b"")
|
|
175
|
+
sys.stdout.flush()
|
|
176
|
+
except (_PipeClosed, BrokenPipeError):
|
|
177
|
+
# Point stdout at the null device so the flush at exit cannot fail again.
|
|
178
|
+
with contextlib.suppress(OSError, ValueError, AttributeError):
|
|
179
|
+
null = os.open(os.devnull, os.O_WRONLY)
|
|
180
|
+
os.dup2(null, sys.stdout.fileno())
|
|
181
|
+
os.close(null)
|
|
182
|
+
return 0
|
|
183
|
+
except (OSError, ValueError) as exc:
|
|
184
|
+
print(f"ellipt: error: {exc}", file=sys.stderr)
|
|
185
|
+
return 1
|
|
186
|
+
return 0
|
ellipt/_core.py
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
"""The truncation engine and its typed result."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from typing import Literal
|
|
7
|
+
|
|
8
|
+
from ._cut import cut
|
|
9
|
+
from ._measure import Unit, check_unit, cluster_size, clusters, size_of
|
|
10
|
+
from ._width import check_ambiguous
|
|
11
|
+
|
|
12
|
+
Where = Literal["start", "middle", "end"]
|
|
13
|
+
ELLIPSIS = "\u2026"
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass(frozen=True)
|
|
17
|
+
class Truncation:
|
|
18
|
+
"""The outcome of ``truncate``: the shortened text and how it was built.
|
|
19
|
+
|
|
20
|
+
``text == head + marker + tail`` always holds. ``head`` is a prefix and ``tail`` a
|
|
21
|
+
suffix of ``original``, both ending on grapheme cluster boundaries. When the text
|
|
22
|
+
already fit, ``head`` is the whole original and ``marker`` and ``tail`` are empty.
|
|
23
|
+
``size`` is the size of ``text`` in ``unit`` and never exceeds ``limit``.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
text: str
|
|
27
|
+
head: str
|
|
28
|
+
marker: str
|
|
29
|
+
tail: str
|
|
30
|
+
original: str
|
|
31
|
+
unit: Unit
|
|
32
|
+
limit: int
|
|
33
|
+
size: int
|
|
34
|
+
|
|
35
|
+
@property
|
|
36
|
+
def truncated(self) -> bool:
|
|
37
|
+
"""True when part of the original was removed."""
|
|
38
|
+
return len(self.head) + len(self.tail) < len(self.original)
|
|
39
|
+
|
|
40
|
+
@property
|
|
41
|
+
def removed_span(self) -> tuple[int, int]:
|
|
42
|
+
"""``(start, stop)`` code point indices of the removed part of ``original``."""
|
|
43
|
+
return len(self.head), len(self.original) - len(self.tail)
|
|
44
|
+
|
|
45
|
+
@property
|
|
46
|
+
def removed(self) -> str:
|
|
47
|
+
"""The part of ``original`` that the marker replaces (empty when it fit)."""
|
|
48
|
+
start, stop = self.removed_span
|
|
49
|
+
return self.original[start:stop]
|
|
50
|
+
|
|
51
|
+
def __str__(self) -> str:
|
|
52
|
+
return self.text
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _check(text: str, limit: int, where: str, marker: str) -> None:
|
|
56
|
+
if not isinstance(text, str):
|
|
57
|
+
raise TypeError(f"text must be str, not {type(text).__name__}")
|
|
58
|
+
if not isinstance(marker, str):
|
|
59
|
+
raise TypeError(f"marker must be str, not {type(marker).__name__}")
|
|
60
|
+
if isinstance(limit, bool) or not isinstance(limit, int):
|
|
61
|
+
raise TypeError(f"limit must be int, not {type(limit).__name__}")
|
|
62
|
+
if where not in ("start", "middle", "end"):
|
|
63
|
+
raise ValueError(f"where must be 'start', 'middle' or 'end', not {where!r}")
|
|
64
|
+
if limit < 0:
|
|
65
|
+
raise ValueError(f"limit must be non-negative, not {limit}")
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def truncate(
|
|
69
|
+
text: str,
|
|
70
|
+
limit: int,
|
|
71
|
+
*,
|
|
72
|
+
where: Where = "end",
|
|
73
|
+
unit: Unit = "chars",
|
|
74
|
+
marker: str = "...",
|
|
75
|
+
words: bool = False,
|
|
76
|
+
ambiguous: int = 1,
|
|
77
|
+
) -> Truncation:
|
|
78
|
+
"""Shorten ``text`` to at most ``limit`` and report exactly what happened.
|
|
79
|
+
|
|
80
|
+
``where`` puts the marker at the ``"end"`` (like CSS ``text-overflow: ellipsis``),
|
|
81
|
+
the ``"start"`` (keep the tail, for ids and log lines) or the ``"middle"`` (keep
|
|
82
|
+
both ends, for paths and hashes). ``unit`` is what ``limit`` counts: ``"chars"``
|
|
83
|
+
(code points), ``"graphemes"``, ``"columns"`` (terminal cells) or ``"bytes"``
|
|
84
|
+
(UTF-8); see ``measure``. The marker is measured in the same unit and counted inside
|
|
85
|
+
the limit. The cut always falls on an extended grapheme cluster boundary (UAX #29),
|
|
86
|
+
so accents, flags and emoji ZWJ sequences are kept whole or dropped whole.
|
|
87
|
+
|
|
88
|
+
``words=True`` moves each cut to whitespace and trims the whitespace next to the
|
|
89
|
+
marker, unless a single word fills the budget, in which case the cut stays hard.
|
|
90
|
+
Tabs and other controls are zero columns; expand tabs first if they matter.
|
|
91
|
+
``ambiguous`` (1 or 2) is the width of East Asian Ambiguous characters in columns.
|
|
92
|
+
|
|
93
|
+
Text that already fits is returned whole. A marker larger than ``limit``, a negative
|
|
94
|
+
limit or an unknown ``where`` or ``unit`` raises ``ValueError``.
|
|
95
|
+
|
|
96
|
+
>>> truncate("hello world", 8).text
|
|
97
|
+
'hello...'
|
|
98
|
+
>>> r = truncate("hello brave new world", 15, marker=ELLIPSIS, words=True)
|
|
99
|
+
>>> r.text, r.removed
|
|
100
|
+
('hello brave\u2026', ' new world')
|
|
101
|
+
>>> truncate("Espa\\U0001f1ea\\U0001f1f8a!", 7, unit="chars", marker="..").text
|
|
102
|
+
'Espa..'
|
|
103
|
+
"""
|
|
104
|
+
_check(text, limit, where, marker)
|
|
105
|
+
unit = check_unit(unit)
|
|
106
|
+
ambiguous = check_ambiguous(ambiguous)
|
|
107
|
+
size = size_of(text, unit, ambiguous)
|
|
108
|
+
if size <= limit:
|
|
109
|
+
return Truncation(text, text, "", "", text, unit, limit, size)
|
|
110
|
+
marker_size = size_of(marker, unit, ambiguous)
|
|
111
|
+
if marker_size > limit:
|
|
112
|
+
raise ValueError(f"marker of size {marker_size} does not fit in limit {limit} ({unit})")
|
|
113
|
+
# Cluster boundaries depend only on what precedes them, so an end cut counted in
|
|
114
|
+
# chars or bytes (every cluster at least 1) only needs the first limit + 1 code points.
|
|
115
|
+
window = text[: limit + 1] if where == "end" and unit in ("chars", "bytes") else text
|
|
116
|
+
parts = clusters(window)
|
|
117
|
+
sizes = [cluster_size(c, unit, ambiguous) for c in parts]
|
|
118
|
+
spaces = [c.isspace() for c in parts] if words else None
|
|
119
|
+
for budget in range(limit - marker_size, -1, -1):
|
|
120
|
+
left, right = cut(sizes, budget, where, spaces)
|
|
121
|
+
head, tail = "".join(parts[:left]), "".join(parts[right:])
|
|
122
|
+
result = head + marker + tail
|
|
123
|
+
# A marker that joins a neighbouring cluster can change its size; re-measure.
|
|
124
|
+
size = size_of(result, unit, ambiguous)
|
|
125
|
+
if size <= limit:
|
|
126
|
+
return Truncation(result, head, marker, tail, text, unit, limit, size)
|
|
127
|
+
return Truncation(marker, "", marker, "", text, unit, limit, marker_size)
|
ellipt/_cut.py
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
"""Choose which grapheme clusters to keep on each side of the cut.
|
|
2
|
+
|
|
3
|
+
Every function works on a list of cluster sizes and returns ``(left, right)``: keep the
|
|
4
|
+
first ``left`` clusters and the clusters from index ``right`` onwards. ``left <= right``
|
|
5
|
+
always holds, so the kept head and tail never overlap.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def _take_left(sizes: list[int], budget: int, start: int, stop: int) -> tuple[int, int]:
|
|
12
|
+
"""Greedily extend a prefix from ``start`` up to ``stop``; return (end, used)."""
|
|
13
|
+
used = 0
|
|
14
|
+
end = start
|
|
15
|
+
while end < stop and used + sizes[end] <= budget:
|
|
16
|
+
used += sizes[end]
|
|
17
|
+
end += 1
|
|
18
|
+
return end, used
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _take_right(sizes: list[int], budget: int, stop: int) -> tuple[int, int]:
|
|
22
|
+
"""Greedily extend a suffix down to index ``stop``; return (start, used)."""
|
|
23
|
+
used = 0
|
|
24
|
+
start = len(sizes)
|
|
25
|
+
while start > stop and used + sizes[start - 1] <= budget:
|
|
26
|
+
used += sizes[start - 1]
|
|
27
|
+
start -= 1
|
|
28
|
+
return start, used
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def cut(
|
|
32
|
+
sizes: list[int], budget: int, where: str, spaces: list[bool] | None = None
|
|
33
|
+
) -> tuple[int, int]:
|
|
34
|
+
"""Return ``(left, right)`` keeping at most ``budget`` in total size.
|
|
35
|
+
|
|
36
|
+
With ``spaces`` (one flag per cluster: is it whitespace) every cut is moved to a word
|
|
37
|
+
boundary and the whitespace next to the marker is dropped. If a single word fills
|
|
38
|
+
the whole budget, the hard cut is used instead of returning nothing.
|
|
39
|
+
"""
|
|
40
|
+
count = len(sizes)
|
|
41
|
+
if where == "end":
|
|
42
|
+
left = _take_left(sizes, budget, 0, count)[0]
|
|
43
|
+
return (snap_left(spaces, left, False) if spaces else left), count
|
|
44
|
+
if where == "start":
|
|
45
|
+
right = _take_right(sizes, budget, 0)[0]
|
|
46
|
+
return 0, (snap_right(spaces, right, False) if spaces else right)
|
|
47
|
+
left, used_left = _take_left(sizes, budget - budget // 2, 0, count)
|
|
48
|
+
if spaces:
|
|
49
|
+
left = snap_left(spaces, left, True)
|
|
50
|
+
used_left = sum(sizes[:left])
|
|
51
|
+
right, used_right = _take_right(sizes, budget - used_left, left)
|
|
52
|
+
if spaces:
|
|
53
|
+
right = snap_right(spaces, right, True)
|
|
54
|
+
used_right = sum(sizes[right:])
|
|
55
|
+
# A wide cluster or a word snap may have left budget unused: hand it back.
|
|
56
|
+
more, _ = _take_left(sizes, budget - used_left - used_right, left, right)
|
|
57
|
+
left = max(left, snap_left(spaces, more, True)) if spaces else more
|
|
58
|
+
if spaces and left == 0 and right == count:
|
|
59
|
+
return cut(sizes, budget, where)
|
|
60
|
+
return left, right
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def snap_left(spaces: list[bool], left: int, allow_empty: bool) -> int:
|
|
64
|
+
"""Move a head end back to a word boundary and drop the whitespace before it."""
|
|
65
|
+
end = left
|
|
66
|
+
if left < len(spaces) and left > 0 and not spaces[left] and not spaces[left - 1]:
|
|
67
|
+
while end > 0 and not spaces[end - 1]:
|
|
68
|
+
end -= 1
|
|
69
|
+
while end > 0 and spaces[end - 1]:
|
|
70
|
+
end -= 1
|
|
71
|
+
return end if end > 0 or allow_empty else left
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def snap_right(spaces: list[bool], right: int, allow_empty: bool) -> int:
|
|
75
|
+
"""Move a tail start forward to a word boundary and drop the whitespace after it."""
|
|
76
|
+
count = len(spaces)
|
|
77
|
+
start = right
|
|
78
|
+
if 0 < right < count and not spaces[right] and not spaces[right - 1]:
|
|
79
|
+
while start < count and not spaces[start]:
|
|
80
|
+
start += 1
|
|
81
|
+
while start < count and spaces[start]:
|
|
82
|
+
start += 1
|
|
83
|
+
return start if start < count or allow_empty else right
|
ellipt/_graphemes.py
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
"""Extended grapheme cluster segmentation per UAX #29, using embedded UCD tables."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Iterator
|
|
6
|
+
|
|
7
|
+
from ._props import EXT_PICT_BIT, GCB, INCB_SHIFT, prop
|
|
8
|
+
|
|
9
|
+
_CR = GCB["CR"]
|
|
10
|
+
_LF = GCB["LF"]
|
|
11
|
+
_CONTROL = GCB["Control"]
|
|
12
|
+
_EXTEND = GCB["Extend"]
|
|
13
|
+
_ZWJ = GCB["ZWJ"]
|
|
14
|
+
_RI = GCB["Regional_Indicator"]
|
|
15
|
+
_PREPEND = GCB["Prepend"]
|
|
16
|
+
_SPACING = GCB["SpacingMark"]
|
|
17
|
+
_L = GCB["L"]
|
|
18
|
+
_V = GCB["V"]
|
|
19
|
+
_T = GCB["T"]
|
|
20
|
+
_LV = GCB["LV"]
|
|
21
|
+
_LVT = GCB["LVT"]
|
|
22
|
+
|
|
23
|
+
_BREAKERS = frozenset({_CR, _LF, _CONTROL})
|
|
24
|
+
_AFTER_L = frozenset({_L, _V, _LV, _LVT})
|
|
25
|
+
_AFTER_LV_V = frozenset({_V, _T})
|
|
26
|
+
_AFTER_LVT_T = frozenset({_T})
|
|
27
|
+
_INCB_LINKER = 1
|
|
28
|
+
_INCB_CONSONANT = 2
|
|
29
|
+
_INCB_EXTEND = 3
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def grapheme_ends(s: str) -> Iterator[int]:
|
|
33
|
+
"""Yield the end index (exclusive) of every extended grapheme cluster in ``s``.
|
|
34
|
+
|
|
35
|
+
The last value yielded is ``len(s)``; nothing is yielded for the empty string.
|
|
36
|
+
"""
|
|
37
|
+
if not s:
|
|
38
|
+
return
|
|
39
|
+
code = prop(ord(s[0]))
|
|
40
|
+
prev = code & 0x0F
|
|
41
|
+
ri_run = 1 if prev == _RI else 0
|
|
42
|
+
incb = (code >> INCB_SHIFT) & 0x03
|
|
43
|
+
after_linker = incb == _INCB_LINKER
|
|
44
|
+
emoji = 1 if code & EXT_PICT_BIT else 0 # 1: pictograph Extend*, 2: then ZWJ
|
|
45
|
+
for index in range(1, len(s)):
|
|
46
|
+
code = prop(ord(s[index]))
|
|
47
|
+
cur = code & 0x0F
|
|
48
|
+
incb = (code >> INCB_SHIFT) & 0x03
|
|
49
|
+
pict = code & EXT_PICT_BIT
|
|
50
|
+
if not _joins(prev, cur, incb, pict, after_linker, emoji, ri_run):
|
|
51
|
+
yield index
|
|
52
|
+
after_linker = incb == _INCB_LINKER or (after_linker and incb == _INCB_EXTEND)
|
|
53
|
+
if pict or (emoji == 1 and cur == _EXTEND):
|
|
54
|
+
emoji = 1
|
|
55
|
+
elif emoji == 1 and cur == _ZWJ:
|
|
56
|
+
emoji = 2
|
|
57
|
+
else:
|
|
58
|
+
emoji = 0
|
|
59
|
+
ri_run = ri_run + 1 if cur == _RI else 0
|
|
60
|
+
prev = cur
|
|
61
|
+
yield len(s)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _joins(
|
|
65
|
+
prev: int, cur: int, incb: int, pict: int, after_linker: bool, emoji: int, ri_run: int
|
|
66
|
+
) -> bool:
|
|
67
|
+
"""Return True when there is no boundary between ``prev`` and ``cur``."""
|
|
68
|
+
if prev == _CR and cur == _LF: # GB3
|
|
69
|
+
return True
|
|
70
|
+
if prev in _BREAKERS or cur in _BREAKERS: # GB4, GB5
|
|
71
|
+
return False
|
|
72
|
+
if prev == _L and cur in _AFTER_L: # GB6
|
|
73
|
+
return True
|
|
74
|
+
if prev in (_LV, _V) and cur in _AFTER_LV_V: # GB7
|
|
75
|
+
return True
|
|
76
|
+
if prev in (_LVT, _T) and cur in _AFTER_LVT_T: # GB8
|
|
77
|
+
return True
|
|
78
|
+
if cur in (_EXTEND, _ZWJ, _SPACING) or prev == _PREPEND: # GB9, GB9a, GB9b
|
|
79
|
+
return True
|
|
80
|
+
if incb == _INCB_CONSONANT and after_linker: # GB9c
|
|
81
|
+
return True
|
|
82
|
+
if pict and prev == _ZWJ and emoji == 2: # GB11
|
|
83
|
+
return True
|
|
84
|
+
return prev == _RI and cur == _RI and ri_run % 2 == 1 # GB12, GB13
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def iter_graphemes(s: str) -> Iterator[str]:
|
|
88
|
+
"""Yield the extended grapheme clusters of ``s`` (UAX #29), left to right.
|
|
89
|
+
|
|
90
|
+
A grapheme cluster is what a reader perceives as one character: a base letter with
|
|
91
|
+
its combining marks, a Hangul syllable built from jamo, an emoji ZWJ sequence, a
|
|
92
|
+
flag made of two regional indicators, an Indic conjunct, or a CR LF pair. The
|
|
93
|
+
clusters concatenate back to ``s`` exactly.
|
|
94
|
+
|
|
95
|
+
>>> [len(g) for g in iter_graphemes("e\\u0301\\U0001f1ea\\U0001f1f8")]
|
|
96
|
+
[2, 2]
|
|
97
|
+
"""
|
|
98
|
+
if not isinstance(s, str):
|
|
99
|
+
raise TypeError(f"iter_graphemes expects str, not {type(s).__name__}")
|
|
100
|
+
start = 0
|
|
101
|
+
for end in grapheme_ends(s):
|
|
102
|
+
yield s[start:end]
|
|
103
|
+
start = end
|
ellipt/_measure.py
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"""Units of measure for a limit and the cluster sizes they assign."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Literal
|
|
6
|
+
|
|
7
|
+
from ._graphemes import grapheme_ends
|
|
8
|
+
from ._width import check_ambiguous, cluster_width
|
|
9
|
+
|
|
10
|
+
Unit = Literal["chars", "graphemes", "columns", "bytes"]
|
|
11
|
+
UNITS: tuple[Unit, ...] = ("chars", "graphemes", "columns", "bytes")
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def check_unit(unit: str) -> Unit:
|
|
15
|
+
"""Return ``unit`` after checking it names a supported unit."""
|
|
16
|
+
for known in UNITS:
|
|
17
|
+
if unit == known:
|
|
18
|
+
return known
|
|
19
|
+
raise ValueError(f"unit must be one of {', '.join(map(repr, UNITS))}, not {unit!r}")
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def clusters(text: str) -> list[str]:
|
|
23
|
+
"""Split ``text`` into extended grapheme clusters (UAX #29)."""
|
|
24
|
+
found: list[str] = []
|
|
25
|
+
start = 0
|
|
26
|
+
for end in grapheme_ends(text):
|
|
27
|
+
found.append(text[start:end])
|
|
28
|
+
start = end
|
|
29
|
+
return found
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def cluster_size(cluster: str, unit: Unit, ambiguous: int) -> int:
|
|
33
|
+
"""Size of one grapheme cluster in ``unit``."""
|
|
34
|
+
if unit == "chars":
|
|
35
|
+
return len(cluster)
|
|
36
|
+
if unit == "graphemes":
|
|
37
|
+
return 1
|
|
38
|
+
if unit == "columns":
|
|
39
|
+
return cluster_width(cluster, ambiguous)
|
|
40
|
+
return len(cluster.encode("utf-8", "surrogatepass"))
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def size_of(text: str, unit: Unit, ambiguous: int) -> int:
|
|
44
|
+
"""Size of ``text`` in ``unit`` without validating the arguments."""
|
|
45
|
+
if unit == "chars":
|
|
46
|
+
return len(text)
|
|
47
|
+
if unit == "bytes":
|
|
48
|
+
return len(text.encode("utf-8", "surrogatepass"))
|
|
49
|
+
return sum(cluster_size(c, unit, ambiguous) for c in clusters(text))
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def measure(text: str, unit: Unit = "chars", *, ambiguous: int = 1) -> int:
|
|
53
|
+
"""Return the size of ``text`` in ``unit``, the way ``truncate`` measures it.
|
|
54
|
+
|
|
55
|
+
``"chars"`` counts code points (``len``), ``"graphemes"`` counts extended grapheme
|
|
56
|
+
clusters (UAX #29, what a reader sees as one character), ``"columns"`` counts
|
|
57
|
+
terminal display cells (UAX #11 East Asian Width, emoji sequences as one wide
|
|
58
|
+
glyph, East Asian Ambiguous characters as ``ambiguous`` cells) and ``"bytes"``
|
|
59
|
+
counts UTF-8 bytes.
|
|
60
|
+
|
|
61
|
+
>>> flag = "\\U0001f1ea\\U0001f1f8"
|
|
62
|
+
>>> [measure(flag, u) for u in ("chars", "graphemes", "columns", "bytes")]
|
|
63
|
+
[2, 1, 2, 8]
|
|
64
|
+
>>> measure("\\u2026", "bytes"), measure("...", "bytes")
|
|
65
|
+
(3, 3)
|
|
66
|
+
"""
|
|
67
|
+
if not isinstance(text, str):
|
|
68
|
+
raise TypeError(f"text must be str, not {type(text).__name__}")
|
|
69
|
+
return size_of(text, check_unit(unit), check_ambiguous(ambiguous))
|
ellipt/_path.py
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
"""Middle truncation of file system paths that keeps the root and the file name."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from ._core import truncate
|
|
6
|
+
from ._graphemes import grapheme_ends
|
|
7
|
+
from ._measure import Unit, check_unit, size_of
|
|
8
|
+
from ._width import check_ambiguous
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def _separators(path: str, sep: str | None) -> list[int]:
|
|
12
|
+
"""Indices of separators that sit on grapheme cluster boundaries on both sides."""
|
|
13
|
+
chars = "/\\" if sep is None else sep
|
|
14
|
+
bounds = {0, *grapheme_ends(path)}
|
|
15
|
+
return [i for i, ch in enumerate(path) if ch in chars and i in bounds and i + 1 in bounds]
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _shorten_name(name: str, limit: int, unit: Unit, marker: str, ambiguous: int) -> str:
|
|
19
|
+
"""Shorten a file name in the middle of its stem, keeping the extension if it can."""
|
|
20
|
+
dot = name.rfind(".")
|
|
21
|
+
if name[:dot].lower().endswith(".tar"): # keep .tar.gz, .tar.xz and friends together
|
|
22
|
+
dot -= 4
|
|
23
|
+
if 0 < dot < len(name) - 1 and dot in {0, *grapheme_ends(name)}:
|
|
24
|
+
stem, ext = name[:dot], name[dot:]
|
|
25
|
+
room = limit - size_of(ext, unit, ambiguous)
|
|
26
|
+
if room > size_of(marker, unit, ambiguous):
|
|
27
|
+
short = truncate(
|
|
28
|
+
stem, room, where="middle", unit=unit, marker=marker, ambiguous=ambiguous
|
|
29
|
+
)
|
|
30
|
+
candidate = short.text + ext
|
|
31
|
+
if size_of(candidate, unit, ambiguous) <= limit:
|
|
32
|
+
return candidate
|
|
33
|
+
return truncate(name, limit, where="middle", unit=unit, marker=marker, ambiguous=ambiguous).text
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def shorten_path(
|
|
37
|
+
path: str,
|
|
38
|
+
limit: int,
|
|
39
|
+
*,
|
|
40
|
+
unit: Unit = "chars",
|
|
41
|
+
marker: str = "...",
|
|
42
|
+
sep: str | None = None,
|
|
43
|
+
ambiguous: int = 1,
|
|
44
|
+
) -> str:
|
|
45
|
+
r"""Shorten a path to at most ``limit`` by dropping whole directories from the middle.
|
|
46
|
+
|
|
47
|
+
The first component (``/``, ``C:\``, ``~/``, ``\\server\share\`` or a relative
|
|
48
|
+
top directory) and the file name are kept, and as many directories next to the file
|
|
49
|
+
name as fit, like the middle truncation of macOS Finder and editor tab titles. When
|
|
50
|
+
only ``marker/name`` fits the root goes too, and when even that is too long the file
|
|
51
|
+
name alone is shown, shortened in the middle of its stem with the extension (``.gz``
|
|
52
|
+
or a compound ``.tar.gz``) kept. ``sep`` is the
|
|
53
|
+
separator; by default both ``/`` and ``\`` count. ``unit``, ``marker`` and
|
|
54
|
+
``ambiguous`` mean the same as in ``truncate``; an empty marker is rejected because
|
|
55
|
+
the result would read as a different, real path, and the result never exceeds
|
|
56
|
+
``limit`` in ``unit``.
|
|
57
|
+
|
|
58
|
+
>>> shorten_path("/home/me/projects/app/src/main.py", 24)
|
|
59
|
+
'/.../app/src/main.py'
|
|
60
|
+
>>> shorten_path(r"C:\Users\me\Documents\report.pdf", 27)
|
|
61
|
+
'C:\\...\\Documents\\report.pdf'
|
|
62
|
+
>>> shorten_path("photos/2026/an_extremely_long_holiday_name.jpeg", 20)
|
|
63
|
+
'an_ext...y_name.jpeg'
|
|
64
|
+
"""
|
|
65
|
+
if sep is not None and (not isinstance(sep, str) or len(sep) != 1):
|
|
66
|
+
raise ValueError(f"sep must be a single character, not {sep!r}")
|
|
67
|
+
if marker == "":
|
|
68
|
+
raise ValueError("marker must not be empty: the result would look like a real path")
|
|
69
|
+
result = truncate(path, limit, where="middle", unit=unit, marker=marker, ambiguous=ambiguous)
|
|
70
|
+
if not result.truncated:
|
|
71
|
+
return path
|
|
72
|
+
unit = check_unit(unit)
|
|
73
|
+
ambiguous = check_ambiguous(ambiguous)
|
|
74
|
+
stops = _separators(path.rstrip("/\\" if sep is None else sep), sep)
|
|
75
|
+
name_start = stops[-1] + 1 if stops else 0
|
|
76
|
+
# The root is the first component; a UNC or //host root also keeps host and share.
|
|
77
|
+
first = 3 if len(stops) > 4 and stops[:2] == [0, 1] else 0
|
|
78
|
+
candidates: list[str] = []
|
|
79
|
+
if len(stops) > first + 1:
|
|
80
|
+
head = path[: stops[first] + 1]
|
|
81
|
+
candidates += [head + marker + path[i:] for i in stops[first + 1 :] if i > stops[first] + 1]
|
|
82
|
+
if stops:
|
|
83
|
+
candidates.append(marker + path[name_start - 1 :])
|
|
84
|
+
for candidate in candidates:
|
|
85
|
+
if size_of(candidate, unit, ambiguous) <= limit:
|
|
86
|
+
return candidate
|
|
87
|
+
name = path[name_start:]
|
|
88
|
+
if size_of(name, unit, ambiguous) <= limit:
|
|
89
|
+
return name
|
|
90
|
+
return _shorten_name(name, limit, unit, marker, ambiguous)
|
ellipt/_props.py
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"""Lookup of the embedded Unicode properties: width class, grapheme and emoji data."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from bisect import bisect_right
|
|
6
|
+
|
|
7
|
+
from ._ucd_data import (
|
|
8
|
+
EMOJI_BIT,
|
|
9
|
+
EXT_PICT_BIT,
|
|
10
|
+
GCB_NAMES,
|
|
11
|
+
INCB_SHIFT,
|
|
12
|
+
MODIFIER_BIT,
|
|
13
|
+
RUNS,
|
|
14
|
+
UNICODE_VERSION,
|
|
15
|
+
WIDTH_SHIFT,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
__all__ = [
|
|
19
|
+
"AMBIGUOUS",
|
|
20
|
+
"EMOJI_BIT",
|
|
21
|
+
"EXT_PICT_BIT",
|
|
22
|
+
"GCB",
|
|
23
|
+
"INCB_SHIFT",
|
|
24
|
+
"MODIFIER_BIT",
|
|
25
|
+
"UNICODE_VERSION",
|
|
26
|
+
"WIDTH_SHIFT",
|
|
27
|
+
"prop",
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
GCB = {name: index for index, name in enumerate(GCB_NAMES)}
|
|
31
|
+
AMBIGUOUS = 3
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _decode(runs: str) -> tuple[list[int], list[int]]:
|
|
35
|
+
"""Expand the run tokens into parallel lists of run starts and property codes."""
|
|
36
|
+
starts: list[int] = []
|
|
37
|
+
codes: list[int] = []
|
|
38
|
+
position = 0
|
|
39
|
+
for token in runs.split():
|
|
40
|
+
starts.append(position)
|
|
41
|
+
codes.append(int(token[:3], 36))
|
|
42
|
+
position += int(token[3:], 36)
|
|
43
|
+
if position != 0x110000:
|
|
44
|
+
raise RuntimeError("embedded Unicode tables do not cover the code space")
|
|
45
|
+
return starts, codes
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
_STARTS, _CODES = _decode(RUNS)
|
|
49
|
+
_LATIN = tuple(_CODES[bisect_right(_STARTS, cp) - 1] for cp in range(0x300))
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def prop(cp: int) -> int:
|
|
53
|
+
"""Return the packed property code of code point ``cp``.
|
|
54
|
+
|
|
55
|
+
Bits 0-3 hold Grapheme_Cluster_Break (an index into ``GCB_NAMES``), bit 4 is
|
|
56
|
+
Extended_Pictographic, bits 5-6 Indic_Conjunct_Break, bit 7 Emoji, bit 8
|
|
57
|
+
Emoji_Modifier and bits 9-10 the width class (0 zero, 1 narrow, 2 wide, 3 ambiguous).
|
|
58
|
+
"""
|
|
59
|
+
if cp < 0x300:
|
|
60
|
+
return _LATIN[cp]
|
|
61
|
+
return _CODES[bisect_right(_STARTS, cp) - 1]
|
ellipt/_shorten.py
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"""Truncate strings with an ellipsis at the start, middle or end."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from ._core import Where, truncate
|
|
6
|
+
from ._measure import Unit
|
|
7
|
+
|
|
8
|
+
__all__ = ["Where", "shorten"]
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def shorten(
|
|
12
|
+
text: str,
|
|
13
|
+
width: int,
|
|
14
|
+
*,
|
|
15
|
+
where: Where = "end",
|
|
16
|
+
marker: str = "...",
|
|
17
|
+
width_aware: bool = False,
|
|
18
|
+
unit: Unit | None = None,
|
|
19
|
+
words: bool = False,
|
|
20
|
+
ambiguous: int = 1,
|
|
21
|
+
) -> str:
|
|
22
|
+
"""Return text clipped to width, with marker marking where it was cut.
|
|
23
|
+
|
|
24
|
+
The result never exceeds width, measured in ``unit``: ``"chars"`` (code points, the
|
|
25
|
+
default), ``"graphemes"``, ``"columns"`` (display cells) or ``"bytes"`` (UTF-8).
|
|
26
|
+
``width_aware=True`` is a shorthand for ``unit="columns"``. The marker counts inside
|
|
27
|
+
the width, and the cut never splits a grapheme cluster. ``words=True`` cuts at
|
|
28
|
+
whitespace when it can. Text that already fits is returned unchanged. A width that
|
|
29
|
+
cannot even hold the marker, or an unknown position or unit, raises ValueError.
|
|
30
|
+
Use ``truncate`` for a typed result that also says what was removed.
|
|
31
|
+
|
|
32
|
+
>>> shorten("hello world", 8)
|
|
33
|
+
'hello...'
|
|
34
|
+
>>> shorten("/var/log/app/service.log", 16, where="middle")
|
|
35
|
+
'/var/lo...ce.log'
|
|
36
|
+
>>> shorten("\\u5e7f\\u5e7f\\u5e7fx", 5, marker="..", width_aware=True)
|
|
37
|
+
'\u5e7f..'
|
|
38
|
+
"""
|
|
39
|
+
if unit is None:
|
|
40
|
+
unit = "columns" if width_aware else "chars"
|
|
41
|
+
elif width_aware and unit != "columns":
|
|
42
|
+
raise ValueError(f"width_aware=True conflicts with unit={unit!r}")
|
|
43
|
+
result = truncate(
|
|
44
|
+
text, width, where=where, unit=unit, marker=marker, words=words, ambiguous=ambiguous
|
|
45
|
+
)
|
|
46
|
+
return result.text
|
ellipt/_ucd_data.py
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""Index of the generated Unicode property data."""
|
|
2
|
+
|
|
3
|
+
# Generated by scripts/gen_ucd_tables.py from data/ucd/. Do not edit.
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
from ._ucd_part1 import RUNS as _R0
|
|
8
|
+
|
|
9
|
+
UNICODE_VERSION = "18.0.0"
|
|
10
|
+
GCB_NAMES = (
|
|
11
|
+
"Other",
|
|
12
|
+
"CR",
|
|
13
|
+
"LF",
|
|
14
|
+
"Control",
|
|
15
|
+
"Extend",
|
|
16
|
+
"ZWJ",
|
|
17
|
+
"Regional_Indicator",
|
|
18
|
+
"Prepend",
|
|
19
|
+
"SpacingMark",
|
|
20
|
+
"L",
|
|
21
|
+
"V",
|
|
22
|
+
"T",
|
|
23
|
+
"LV",
|
|
24
|
+
"LVT",
|
|
25
|
+
)
|
|
26
|
+
EXT_PICT_BIT = 16
|
|
27
|
+
INCB_SHIFT = 5
|
|
28
|
+
EMOJI_BIT = 128
|
|
29
|
+
MODIFIER_BIT = 256
|
|
30
|
+
WIDTH_SHIFT = 9
|
|
31
|
+
RUNS = _R0
|
ellipt/_ucd_part1.py
ADDED
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
"""Run-length property data, part 1."""
|
|
2
|
+
|
|
3
|
+
# Generated by scripts/gen_ucd_tables.py from data/ucd/. Do not edit.
|
|
4
|
+
|
|
5
|
+
RUNS = (
|
|
6
|
+
"003a 0021 0032 0011 003i 0e83 0hs1 0e86 0hs1 0e85 0hsa 0e81x 003x 0e81 16o1 0e82 16o1 "
|
|
7
|
+
"0e82 16o2 0i81 16o1 0e82 16r1 1ao1 0e81 16o5 0e81 16o5 0e81 16o4 0e86 16o1 0e89 16o1 "
|
|
8
|
+
"0e86 16o2 0e85 16o4 0e84 16o1 0e81 16o3 0e81 16o2 0e82 16o1 0e81 16o2 0e83 16o4 0e81 "
|
|
9
|
+
"16o1 0e81 16o1 0e82 16o1 0e8f 16o1 0e81 16o1 0e87 16o1 0e8a 16o2 0e83 16o1 0e85 16o3 "
|
|
10
|
+
"0e84 16o1 0e86 16o4 0e81 16o1 0e83 16o4 0e81 16o1 0e84 16o2 0e8i 16o2 0e83 16o1 0e82q "
|
|
11
|
+
"16o1 0e81 16o1 0e81 16o1 0e81 16o1 0e81 16o1 0e81 16o1 0e81 16o1 0e81 16o1 0e838 16o1 "
|
|
12
|
+
"0e8f 16o1 0e82q 16o1 0e82 16o1 0e81 16o3 0e81 16o1 0e82 16o1 0e87 16o4 0e81 16o1 0e81 "
|
|
13
|
+
"16o1 0e8w 02s34 0e8x 16oh 0e81 16o7 0e87 16oh 0e81 16o7 0e81j 16o1 0e8e 16o1s 0e81 16o1 "
|
|
14
|
+
"0e81d 02s7 0e87b 02s19 0e81 02s1 0e81 02s2 0e81 02s2 0e81 02s3 0e81i 0076 0e8a 02sb "
|
|
15
|
+
"0e81 0031 0e81a 02sl 0e8g 02s1 0e82t 02s7 0071 0e81 02s6 0e82 02s2 0e81 02s4 0e8x 0071 "
|
|
16
|
+
"0e81 02s1 0e8u 02sr 0e82j 02sb 0e81m 02s9 0e89 02s1 0e8o 02s4 0e81 02s9 0e81 02s3 0e81 "
|
|
17
|
+
"02s5 0e817 02s3 0e81g 0072 0e85 02s9 0e816 02so 0071 02sw 0eg1 0e8h 0g011 02s1 0eg1 "
|
|
18
|
+
"02s1 0e81 0eg3 02s8 0eg4 0101 0eg2 0e81 02s7 0g08 0e82 02s2 0e8k 0g08 0e81 02s1 0eg2 "
|
|
19
|
+
"0e8h 0g0k 0e81 0g07 0e81 0g01 0e83 0g04 0e82 02s1 0e81 0h01 0eg2 02s4 0e82 0eg2 0e82 "
|
|
20
|
+
"0eg2 0101 0e89 0h01 0e84 0g02 0e81 0g01 0e82 02s2 0e8c 0g02 0e8c 02s1 0e82 02s2 0eg1 "
|
|
21
|
+
"0e81k 02s1 0e81 0eg3 02s2 0e84 02s2 0e82 02s3 0e83 02s1 0e8u 02s2 0e83 02s1 0e8b 02s2 "
|
|
22
|
+
"0eg1 0e8h 0g0k 0e81 0g07 0e81 0g02 0e81 0g05 0e82 02s1 0e81 0eg3 02s5 0e81 02s2 0eg1 "
|
|
23
|
+
"0e81 0eg2 0101 0e8k 02s2 0e8l 0g01 02s6 0e81 02s1 0eg2 0e8h 0g0k 0e81 0g07 0e81 0g02 "
|
|
24
|
+
"0e81 0g05 0e82 02s1 0e81 0h01 02s1 0eg1 02s4 0e82 0eg2 0e82 0eg2 0101 0e85 02s4 0h01 "
|
|
25
|
+
"0e84 0g02 0e81 0g01 0e82 02s2 0e8d 0g01 0e8g 02s1 0e81n 0h01 0eg1 02s1 0eg2 0e83 0eg3 "
|
|
26
|
+
"0e81 0eg3 02s1 0e89 0h01 0e814 02s1 0eg3 02s1 0e8g 0g0k 0e81 0g0g 0e82 02s1 0e81 02s3 "
|
|
27
|
+
"0eg4 0e81 02s3 0e81 02s3 0101 0e87 02s2 0e81 0g03 0e87 02s2 0e8t 02s1 0eg2 0e81k 02s1 "
|
|
28
|
+
"0e81 0eg1 02s1 0h01 0eg1 0h01 0eg2 0e81 02s1 0h02 0e81 0h02 02s2 0e87 0h02 0e8b 02s2 "
|
|
29
|
+
"0e8f 0eg1 0e8c 02s2 0eg2 0e8h 0g012 02s2 0e81 0h01 0eg2 02s4 0e81 0eg3 0e81 0eg3 0101 "
|
|
30
|
+
"0ef1 0e88 0h01 0e8a 02s2 0e8t 02s1 0eg2 0e81y 02s1 0e84 0h01 0eg2 02s3 0e81 02s1 0e81 "
|
|
31
|
+
"0eg7 0h01 0e8i 0eg2 0e81p 02s1 0e81 0eg1 02s7 0e8c 02s8 0e82q 02s1 0e81 0eg1 02s9 0e8b "
|
|
32
|
+
"02s7 0e821 02s2 0e8r 02s1 0e81 02s1 0e81 02s1 0e84 0eg2 0e81d 02se 0eg1 02s5 0e81 02s2 "
|
|
33
|
+
"0e85 02sb 0e81 02s10 0e89 02s1 0e81l 0g017 0e82 02s4 0eg1 02s6 0e81 0101 02s1 0eg2 02s2 "
|
|
34
|
+
"0g01 0e8g 0g06 0eg2 02s2 0g04 02s3 0g01 0e83 0g02 0e87 0g03 02s4 0g0d 02s1 0e81 0eg1 "
|
|
35
|
+
"02s2 0e86 02s1 0g01 0e8e 02s1 0e82q 0sp2o 00a20 00b2g 0e89p 02s3 0e8qa 02s3 0h01 0e8s "
|
|
36
|
+
"02s2 0h01 0e8t 02s2 0e8u 02s2 0e8c 0g01g 02s2 0eg1 02s7 0eg8 02s1 0eg2 02s9 0101 02s1 "
|
|
37
|
+
"0e89 02s1 0e819 02s3 0031 02s1 0e839 02s2 0e8y 02s1 0e83a 02s3 0eg4 02s2 0eg3 0e84 0eg2 "
|
|
38
|
+
"02s1 0eg6 02s3 0e863 02s2 0eg2 02s1 0e84 0g01h 0eg1 02s1 0eg1 02s7 0e81 0101 0e81 02s1 "
|
|
39
|
+
"0e82 02s8 0eg6 02sa 0e82 02s1 0e81c 02s1t 0e8f 02s4 0eg1 0e86 0g02 0e86 0g0x 02s1 0h01 "
|
|
40
|
+
"02s5 0h01 02s1 0h01 0eg4 02s1 0h01 0f81 0g08 0e8u 02s9 0e8c 02s2 0eg1 0g0u 0eg1 02s4 "
|
|
41
|
+
"0eg2 02s2 0h01 0101 02s2 0g02 0e8b 0g03 0e814 02s1 0eg1 02s2 0eg3 02s1 0eg1 02s3 0h02 "
|
|
42
|
+
"0e81c 0eg8 02s8 0eg2 02s2 0e848 02s3 0e81 02sd 0eg1 02s7 0e84 02s1 0e86 02s1 0f42 0eg1 "
|
|
43
|
+
"02s2 0e85i 02s1s 0e8ej 0031 0041 02t1 0032 16o1 0e82 16o4 0e81 16o2 0e82 16o2 0e82 16o3 "
|
|
44
|
+
"0e81 16o4 0eb2 0035 0e81 16o1 0e81 16o2 0e81 16o1 0e85 16o1 0i81 0e81 16o1 0e8a 0i81 "
|
|
45
|
+
"0e8m 0035 0eb1 003a 0e84 16o1 0e8a 16o1 0e81 16o4 0e813 16o1 0e8z 02sx 0e8i 16o1 0e81 "
|
|
46
|
+
"16o1 0e83 16o1 0e89 16o1 0e82 16o1 0e8a 16o1 1ao1 0e83 16o1 0e84 16o1 0e8d 0i81 0e8p "
|
|
47
|
+
"16o2 0e86 16o4 0e81 16oc 0e84 16oa 0e8f 16o1 0e86 16o4 1ao6 0e8f 0i82 0e8d 16o2 0e8o "
|
|
48
|
+
"16o1 0e81 16o1 0e8i 16o1 0e8o 16o1 0e81 16o2 0e83 16o2 0e82 16o1 0e83 16o1 0e81 16o1 "
|
|
49
|
+
"0e83 16o1 0e84 16o1 0e82 16o4 0e82 16o1 0e81 16o1 0e81 16o6 0e81 16o1 0e85 16o4 0e84 "
|
|
50
|
+
"16o2 0e8a 16o1 0e83 16o1 0e85 16o1 0e8d 16o2 0e82 16o4 0e82 16o2 0e82 16o2 0e8i 16o2 "
|
|
51
|
+
"0e82 16o2 0e8d 16o1 0e83 16o1 0e8b 16o1 0e8p 16o1 0e82a 16o1 0e87 0wg2 0e8c 0i81 0sg2 "
|
|
52
|
+
"0e84k 0i81 0e8p 0wg4 0i83 0wg1 0i82 0wg1 0e84 0i83 0e82t 16o2q 1ao1 16o13 0e81 16o2p "
|
|
53
|
+
"0e84 16o10 0e8c 16og 0e82 16o4 0e8a 16o2 0e81 16o7 0i82 0e86 16o2 0e82 1ao1 16o1 0e84 "
|
|
54
|
+
"16o2 0e82 1ao1 16o1 0e84 16o3 0e82 16o1 0e82 16o4 0e8g 16o4 0e89 16o1 0e8b 0i82 0wg2 "
|
|
55
|
+
"0e81 0i85 16o2 0e82 16o1 0e84 1ao1 16o1 0e81 0i81 0e82 0wg2 0e82 0i81 0e83 16o1 0i81 "
|
|
56
|
+
"16o1 0e81 0i81 0e81 0i82 0e82 0i81 0e83 0i81 0e83 0i82 0sg8 0i83 0e85 1ao1 0e81 1ao1 "
|
|
57
|
+
"0e85 0wgc 0e8b 0i81 1ao1 16o1 0e81 1ao1 16o1 1ao1 0i81 16o1 1ao1 16o2 0e81 16o2 0e81 "
|
|
58
|
+
"16o1 0e8b 0i81 0e82 0i81 0wg1 0e8a 0sg6 0e82 0i81 0wg1 0i84 0e81 0i81 0e81 0i82 0e81 "
|
|
59
|
+
"16o2 0i81 0wg1 0e85 0i81 0e82 0wg2 0e84 0i82 0e8b 0wg2 16o1 0e84 0wg2 16o2 1ao1 16o5 "
|
|
60
|
+
"0wg1 1ao1 16o1 1ao1 16o1 1ao1 0wg1 16od 0e81 16o1 0e84 16o1 1ao1 0wg1 16o5 1ao2 0wg2 "
|
|
61
|
+
"1ao1 0wg1 16o1 1ao3 0wg1 16o2 0wg1 16o2 0e82 0i81 0e82 0wg1 0e82 0i82 0wg2 0i82 0e81 "
|
|
62
|
+
"0i81 0e82 0i81 0e81 0i81 0e81 0i81 0e86 0i81 0e83 0i81 0e86 0wg1 0e8a 0i82 0e88 16o1 "
|
|
63
|
+
"0e86 0i81 0e82 0i81 0e84 0wg1 0e81 0wg1 0e84 0wg3 0e81 0wg1 0e8b 0i82 0e8h 16oa 0e8l "
|
|
64
|
+
"0wg3 0e89 0i81 0e8e 0wg1 0e8e 0wg1 0e8ac 0i82 0e8cv 0i83 0e8j 0wg2 0e81f 0wg1 0e84 0wg1 "
|
|
65
|
+
"16o4 0e8b9 02s3 0e83x 02s1 0e82o 02sw 0e83k 0sgq 0e81 0sg2h 0e8c 0sg5y 0e8q 0sg1m 02s4 "
|
|
66
|
+
"0v82 0wg1 0sgc 0wg1 0sg1 0e82 0sg2e 0e82 02s2 0sg2t 0e85 0sg17 0e81 0sg2m 0e81 0sg2e "
|
|
67
|
+
"0e89 0sg1c 0e81 0sg14 16o8 0sg1z 0wg1 0sg1 0wg1 0sgmib 0e83 0sg1j 0e8bs 02s4 0e81 02sa "
|
|
68
|
+
"0e8w 02s2 0e828 02s2 0e87k 02s1 0e83 02s1 0e84 02s1 0e8n 0eg2 02s2 0eg1 0e84 02s1 0e82b "
|
|
69
|
+
"0eg2 0e81e 0egg 02s2 0e8q 02si 0e8d 02s1 0e812 02s8 0e8p 02sb 0eg1 0h01 0e8c 0spt 0e83 "
|
|
70
|
+
"02s3 0eg1 0e85 0g03 0e83 0g010 02s1 0eg2 02s4 0eg2 02s2 0eg2 0f81 0e8v 0g05 02s1 0e81 "
|
|
71
|
+
"0g09 0e8a 0g05 0e816 02s6 0eg2 02s2 0eg2 02s2 0e8c 02s1 0e88 02s1 0eg1 0e8i 0g0g 0e81 "
|
|
72
|
+
"0g03 0e86 0g01 0e81 02s1 0e81 0g02 0e81c 02s1 0e81 02s3 0e82 02s2 0e85 02s2 0e81 02s1 "
|
|
73
|
+
"0e8u 0g0b 0eg1 02s2 0eg2 0e85 0eg1 0101 0e85l 0g0r 0e88 0eg2 02s1 0eg2 02s1 0eg2 0e81 "
|
|
74
|
+
"0eg1 02s1 0e8i 0ss1 0str 0ss1 0str 0ss1 0str 0ss1 0str 0ss1 0str 0ss1 0str 0ss1 0str "
|
|
75
|
+
"0ss1 0str 0ss1 0str 0ss1 0str 0ss1 0str 0ss1 0str 0ss1 0str 0ss1 0str 0ss1 0str 0ss1 "
|
|
76
|
+
"0str 0ss1 0str 0ss1 0str 0ss1 0str 0ss1 0str 0ss1 0str 0ss1 0str 0ss1 0str 0ss1 0str "
|
|
77
|
+
"0ss1 0str 0ss1 0str 0ss1 0str 0ss1 0str 0ss1 0str 0ss1 0str 0ss1 0str 0ss1 0str 0ss1 "
|
|
78
|
+
"0str 0ss1 0str 0ss1 0str 0ss1 0str 0ss1 0str 0ss1 0str 0ss1 0str 0ss1 0str 0ss1 0str "
|
|
79
|
+
"0ss1 0str 0ss1 0str 0ss1 0str 0ss1 0str 0ss1 0str 0ss1 0str 0ss1 0str 0ss1 0str 0ss1 "
|
|
80
|
+
"0str 0ss1 0str 0ss1 0str 0ss1 0str 0ss1 0str 0ss1 0str 0ss1 0str 0ss1 0str 0ss1 0str "
|
|
81
|
+
"0ss1 0str 0ss1 0str 0ss1 0str 0ss1 0str 0ss1 0str 0ss1 0str 0ss1 0str 0ss1 0str 0ss1 "
|
|
82
|
+
"0str 0ss1 0str 0ss1 0str 0ss1 0str 0ss1 0str 0ss1 0str 0ss1 0str 0ss1 0str 0ss1 0str "
|
|
83
|
+
"0ss1 0str 0ss1 0str 0ss1 0str 0ss1 0str 0ss1 0str 0ss1 0str 0ss1 0str 0ss1 0str 0ss1 "
|
|
84
|
+
"0str 0ss1 0str 0ss1 0str 0ss1 0str 0ss1 0str 0ss1 0str 0ss1 0str 0ss1 0str 0ss1 0str "
|
|
85
|
+
"0ss1 0str 0ss1 0str 0ss1 0str 0ss1 0str 0ss1 0str 0ss1 0str 0ss1 0str 0ss1 0str 0ss1 "
|
|
86
|
+
"0str 0ss1 0str 0ss1 0str 0ss1 0str 0ss1 0str 0ss1 0str 0ss1 0str 0ss1 0str 0ss1 0str "
|
|
87
|
+
"0ss1 0str 0ss1 0str 0ss1 0str 0ss1 0str 0ss1 0str 0ss1 0str 0ss1 0str 0ss1 0str 0ss1 "
|
|
88
|
+
"0str 0ss1 0str 0ss1 0str 0ss1 0str 0ss1 0str 0ss1 0str 0ss1 0str 0ss1 0str 0ss1 0str "
|
|
89
|
+
"0ss1 0str 0ss1 0str 0ss1 0str 0ss1 0str 0ss1 0str 0ss1 0str 0ss1 0str 0ss1 0str 0ss1 "
|
|
90
|
+
"0str 0ss1 0str 0ss1 0str 0ss1 0str 0ss1 0str 0ss1 0str 0ss1 0str 0ss1 0str 0ss1 0str "
|
|
91
|
+
"0ss1 0str 0ss1 0str 0ss1 0str 0ss1 0str 0ss1 0str 0ss1 0str 0ss1 0str 0ss1 0str 0ss1 "
|
|
92
|
+
"0str 0ss1 0str 0ss1 0str 0ss1 0str 0ss1 0str 0ss1 0str 0ss1 0str 0ss1 0str 0ss1 0str "
|
|
93
|
+
"0ss1 0str 0ss1 0str 0ss1 0str 0ss1 0str 0ss1 0str 0ss1 0str 0ss1 0str 0ss1 0str 0ss1 "
|
|
94
|
+
"0str 0ss1 0str 0ss1 0str 0ss1 0str 0ss1 0str 0ss1 0str 0ss1 0str 0ss1 0str 0ss1 0str "
|
|
95
|
+
"0ss1 0str 0ss1 0str 0ss1 0str 0ss1 0str 0ss1 0str 0ss1 0str 0ss1 0str 0ss1 0str 0ss1 "
|
|
96
|
+
"0str 0ss1 0str 0ss1 0str 0ss1 0str 0ss1 0str 0ss1 0str 0ss1 0str 0ss1 0str 0ss1 0str "
|
|
97
|
+
"0ss1 0str 0ss1 0str 0ss1 0str 0ss1 0str 0ss1 0str 0ss1 0str 0ss1 0str 0ss1 0str 0ss1 "
|
|
98
|
+
"0str 0ss1 0str 0ss1 0str 0ss1 0str 0ss1 0str 0ss1 0str 0ss1 0str 0ss1 0str 0ss1 0str "
|
|
99
|
+
"0ss1 0str 0ss1 0str 0ss1 0str 0ss1 0str 0ss1 0str 0ss1 0str 0ss1 0str 0ss1 0str 0ss1 "
|
|
100
|
+
"0str 0ss1 0str 0ss1 0str 0ss1 0str 0ss1 0str 0ss1 0str 0ss1 0str 0ss1 0str 0ss1 0str "
|
|
101
|
+
"0ss1 0str 0ss1 0str 0ss1 0str 0ss1 0str 0ss1 0str 0ss1 0str 0ss1 0str 0ss1 0str 0ss1 "
|
|
102
|
+
"0str 0ss1 0str 0ss1 0str 0ss1 0str 0ss1 0str 0ss1 0str 0ss1 0str 0ss1 0str 0ss1 0str "
|
|
103
|
+
"0ss1 0str 0ss1 0str 0ss1 0str 0ss1 0str 0ss1 0str 0ss1 0str 0ss1 0str 0ss1 0str 0ss1 "
|
|
104
|
+
"0str 0ss1 0str 0ss1 0str 0ss1 0str 0ss1 0str 0ss1 0str 0ss1 0str 0ss1 0str 0ss1 0str "
|
|
105
|
+
"0ss1 0str 0ss1 0str 0ss1 0str 0ss1 0str 0ss1 0str 0ss1 0str 0ss1 0str 0ss1 0str 0ss1 "
|
|
106
|
+
"0str 0ss1 0str 0ss1 0str 0ss1 0str 0ss1 0str 0ss1 0str 0ss1 0str 0ss1 0str 0ss1 0str "
|
|
107
|
+
"0ss1 0str 0ss1 0str 0ss1 0str 0ss1 0str 0ss1 0str 0ss1 0str 0ss1 0str 0ss1 0str 0ss1 "
|
|
108
|
+
"0str 0ss1 0str 0ss1 0str 0ss1 0str 0ss1 0str 0ss1 0str 0ss1 0str 0ss1 0str 0ss1 0str "
|
|
109
|
+
"0ss1 0str 0ss1 0str 0ss1 0str 0ss1 0str 0ss1 0str 0ss1 0str 0ss1 0str 0ss1 0str 0ss1 "
|
|
110
|
+
"0str 0ss1 0str 0ss1 0str 0ss1 0str 0ss1 0str 0ss1 0str 0ss1 0str 0ss1 0str 0ss1 0str "
|
|
111
|
+
"0ss1 0str 0ss1 0str 0ss1 0str 0ss1 0str 0ss1 0str 0ss1 0str 0ss1 0str 0ss1 0str 0ss1 "
|
|
112
|
+
"0str 0ss1 0str 0ss1 0str 0ss1 0str 0ss1 0str 0ss1 0str 0ss1 0str 0ss1 0str 0ss1 0str "
|
|
113
|
+
"0ss1 0str 0ss1 0str 0ss1 0str 0ss1 0str 0ss1 0str 0ss1 0str 0ss1 0str 0ss1 0str 0ss1 "
|
|
114
|
+
"0str 0ss1 0str 0ss1 0str 0ss1 0str 0ss1 0str 0ss1 0str 0ss1 0str 0ss1 0str 0ss1 0str "
|
|
115
|
+
"0ss1 0str 0ss1 0str 0ss1 0str 0ss1 0str 0ss1 0str 0ss1 0str 0ss1 0str 0ss1 0str 0ss1 "
|
|
116
|
+
"0str 0ss1 0str 0ss1 0str 0ss1 0str 0ss1 0str 0ss1 0str 0ss1 0str 0ss1 0str 0ss1 0str "
|
|
117
|
+
"0ss1 0str 0ss1 0str 0ss1 0str 0ss1 0str 0ss1 0str 0ss1 0str 0ss1 0str 0ss1 0str 0ss1 "
|
|
118
|
+
"0str 0ss1 0str 0ss1 0str 0ss1 0str 0ss1 0str 0ss1 0str 0ss1 0str 0ss1 0str 0ss1 0str "
|
|
119
|
+
"0ss1 0str 0ss1 0str 0ss1 0str 0ss1 0str 0ss1 0str 0ss1 0str 0ss1 0str 0ss1 0str 0ss1 "
|
|
120
|
+
"0str 0ss1 0str 0ss1 0str 0ss1 0str 0ss1 0str 0ss1 0str 0ss1 0str 0ss1 0str 0ss1 0str "
|
|
121
|
+
"0ss1 0str 0e8c 00an 0004 00b1d 0004 0e81kw 16o4xs 0sge8 0e8u 02s1 0e8kh 02sg 0sga 0e86 "
|
|
122
|
+
"02sg 0sgz 0e81 0sgj 0e81 0sg4 0e843 0031 0e81 0sg2o 0e81p 0h02 0e81s 0sg7 0e89 0eb9 "
|
|
123
|
+
"0033 0e81 16o1 0e8e7 02s1 0e86a 02s1 0e845 02s5 0e81ad 0g01 02s3 0e81 02s2 0e85 02s4 "
|
|
124
|
+
"0g04 0e81 0g03 0e81 0g0t 0e82 02s3 0e84 0101 0e84l 02s2 0e8fx 02s4 0e81t 02s5 0e88t "
|
|
125
|
+
"02s2 0e8u 02s5 0e8w 02sg 0e81y 02sb 0e81d 02s4 0e83e 0eg1 02s1 0eg1 0e81h 02sf 0e815 "
|
|
126
|
+
"02s1 0e82 02s2 0e8a 02s3 0eg1 0e819 0eg3 02s4 0eg2 02s2 0e82 0071 0e84 02s1 0e8a 0071 "
|
|
127
|
+
"0e81e 02s3 0g010 02s5 0eg1 02s6 0101 02s1 0e8f 0g01 0eg2 0g01 0e817 02s1 0e8c 02s2 0eg1 "
|
|
128
|
+
"0e81c 0eg3 02s9 0eg1 0h01 0e81 0ef2 0e85 02s4 0e81 0eg1 02s1 0e82k 0eg3 02s3 0eg2 02s1 "
|
|
129
|
+
"0h01 02s2 0e86 02s1 0e82 02s1 0e84d 02s1 0eg3 02s8 0e8l 02s2 0eg2 0e81j 02s2 0e81 0h01 "
|
|
130
|
+
"0eg1 02s1 0eg4 0e82 0eg2 0e82 0eg2 0h01 0e89 0h01 0e8a 0eg2 0e82 02s7 0e83 02s5 0e8b "
|
|
131
|
+
"0g0a 0e81 0g01 0e82 0g01 0e81 0g012 0e82 0h01 0eg2 02s6 0e81 0h01 0e82 0h01 0e81 0h03 "
|
|
132
|
+
"0eg1 0e81 0eg2 02s1 0h01 0101 0ef1 02s1 0e8e 02s2 0e82a 0eg3 02s8 0eg2 02s3 0eg1 02s1 "
|
|
133
|
+
"0e8n 02s1 0e829 0h01 0eg2 02s6 0eg1 02s1 0eg2 0h01 0eg1 02s2 0eg1 02s2 0e86j 0h01 0eg2 "
|
|
134
|
+
"02s4 0e82 0eg4 02s2 0eg1 02s2 0e8r 02s2 0e82a 0eg3 02s8 0eg2 02s1 0eg1 02s2 0e82y 02s1 "
|
|
135
|
+
"0eg1 02s1 0eg2 02s6 0h01 02s1 0e82t 02s1 0eg1 02s1 0e82 02s4 0eg1 02s5 0e874 0eg3 02s9 "
|
|
136
|
+
"0eg1 02s2 0e85h 0g07 0e82 0g01 0e82 0g08 0e81 0g02 0e81 0g0o 0h01 0eg5 0e81 0eg2 0e82 "
|
|
137
|
+
"02s2 0h01 0101 0ef1 0eg1 0ef1 0eg1 02s1 0e83x 0eg3 02s4 0e82 02s2 0eg4 02s1 0e83 0eg1 "
|
|
138
|
+
"0e8r 0g01 02sa 0g014 02s6 0eg1 0f41 02s4 0e88 0101 0e88 0g01 02s6 0eg2 02s3 0g014 0ef6 "
|
|
139
|
+
"02sd 0eg1 02s1 0101 0e834 0g01 0e82d 02s1 0eg1 02s3 0eg1 02s1 0eg1 0e85j 0eg1 02s7 0e81 "
|
|
140
|
+
"02s6 0eg1 02s1 0e82a 02sm 0e81 0eg1 02s7 0eg1 02s2 0eg1 02s2 0e83e 02s6 0e83 02s1 0e81 "
|
|
141
|
+
"02s2 0e81 02s7 0ef1 02s1 0e81u 0eg5 0e81 02s2 0e81 0eg2 02s1 0eg1 02s1 0e82g 02s1 0g01 "
|
|
142
|
+
"0e875 02s2 0eg2 0e89 02s2 0ef1 0eg1 0g0d 0e81 0g0y 0eg2 02s5 0e83 0eg2 02s1 0h01 0101 "
|
|
143
|
+
"0e8n 02s1 0e8445 003g 02s1 0e86 02sf 0e88ug 02sc 0eg3 02s3 0e81xc 02s5 0e81n 02s7 0e8fg "
|
|
144
|
+
"0ei1 0e83 0ei4 0e8dg 02s1 0e81 0eg1j 0e87 02s4 0e825 0sg4 02s1 0e8b 0v82 0sg5 0e89 "
|
|
145
|
+
"0sg5p7 0e810 0sgy 0e82n 0sg37 0e8d 0sgpe 0e8e 0sg1f 0e85y5 0sg4 0e81 0sg7 0e81 0sg2 "
|
|
146
|
+
"0e81 0sg89 0e89 0sg1 0e8t 0sg3 0e82 0sg1 0e8e 0sg5 0e87 0sgb0 0e81wh 02s2 0e81 0034 "
|
|
147
|
+
"0e83mk 02s1a 0e82 02sn 0e8dc 02s2 0e81o 0h02 02s3 0e83 0h06 0038 02s8 0e82 02s7 0e8u "
|
|
148
|
+
"02s4 0e844 02s3 0e8b 0h03 0e88 02s2 0e82 0h01 0e8w 0h02 0e83i 0sg2f 0e89 0sgn 0e81ah "
|
|
149
|
+
"02s1j 0e84 02s1e 0e88 02s1 0e8e 02s1 0e8m 02s5 0e81 02sf 0e811s 02s7 0e81 02sh 0e82 "
|
|
150
|
+
"02s7 0e81 02s2 0e81 02s5 0e82s 02s1 0e84g 02s7 0e8af 02s1 0e81p 02s4 0e8e4 02s4 0e872 "
|
|
151
|
+
"02s2 0e86r 02s1 0e82 02s1 0e87 02s2 0e85 02s1 0e8d6 02s7 0e831 02s7 0e81bt 0wg1 0e813 "
|
|
152
|
+
"0eo4 0e82s 0eoc 0e8f 0eo2 0e8f 0eo1 0e8e 0wg1 0eo1 0e811 0eoa 16ob 0e85 16ou 0e82 16o1m "
|
|
153
|
+
"0e86 1ao2 16oc 1ao2 16oe 0wg1 16o2 0wga 16oi 0e81 0sg1 0eo1j 0hyq 0sg1 0wg2 0eod 0sga "
|
|
154
|
+
"0wg1 0sgk 0wg1 0sg2 0wg9 0sg1 0eo4 0sg9 0eo7 0wg2 0eoe 0sg6 0eo4a 0wgx 0i81 0e82 0i89 "
|
|
155
|
+
"0wg9 0i81 0wg1y 0i81 0wgm 0e82 0i82 0e81 0i83 0e82 0i82 0wg17 0i84 0wg5 0i8c 0wgh 0e82 "
|
|
156
|
+
"0i81 0wg1 0i81 0e81 0i81 0wg3 15w5 0wg1r 0i81 0wg1 0i81 0wg57 0i81 0e81 0wg1r 0e8b 0i82 "
|
|
157
|
+
"0wg4 0e81 0wgo 0e87 0i82 0e82 0i87 0wg1 0e8c 0i81 0e82 0i84 0e82 0i81 0e84 0wg2 0e8d "
|
|
158
|
+
"0wg1 0i81 0e82 0i81 0e88 0i82 0e89 0i81 0e85 0i83 0e8c 0i83 0e88 0i83 0e82 0i81 0e81 "
|
|
159
|
+
"0i81 0e84 0i81 0e86 0i81 0e83 0i81 0e86 0i81 0wg2d 0e81c 0wg1y 0e85 0i81 0wg1 0i83 0wg3 "
|
|
160
|
+
"0e82 0wg5 0eo2 0wg4 0i86 0e83 0i81 0e81 0wg2 0eo3 0i81 0e82 0i81 0wg9 0eo3 0e862 0sg1 "
|
|
161
|
+
"0e81 0eo4 0wgc 0eo4 0wg1 0e8r 0eo4 0e81k 0eo8 0e8a 0eo6 0e814 0eo8 0e8u 0eo2 0e8c 0eo4 "
|
|
162
|
+
"0e82 0eoe 0e89 0eo13 0e8c 0wg1b 0e81 0wga 0e81 0wg55 0e82g 0eo8 0e8e 0eo2 0wgd 0eo3 "
|
|
163
|
+
"0wg1z 0eo1 0wg1 0eo3 0wgi 0eo1 0wgd 0eo3 0wgc 0eo5 0e874 0eose 0e82 0sg1eke 0e82 "
|
|
164
|
+
"0sg1eke 0e8e1oi 0eb1 0031 0ebu 02s2o 0eb3k 02s6o 0eb2s0 0e81beo 16o1eke 0e82 16o1eke "
|
|
165
|
+
"0e82 "
|
|
166
|
+
)
|
ellipt/_width.py
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"""Display width (UAX #11 East Asian Width plus emoji presentation) of clusters and text."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from ._props import AMBIGUOUS, EMOJI_BIT, EXT_PICT_BIT, GCB, MODIFIER_BIT, WIDTH_SHIFT, prop
|
|
6
|
+
|
|
7
|
+
_RI = GCB["Regional_Indicator"]
|
|
8
|
+
_VS15 = "\ufe0e"
|
|
9
|
+
_VS16 = "\ufe0f"
|
|
10
|
+
_ZWJ = "\u200d"
|
|
11
|
+
_TAGS = range(0xE0020, 0xE0080)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def check_ambiguous(ambiguous: int) -> int:
|
|
15
|
+
"""Return ``ambiguous`` after checking it is 1 or 2."""
|
|
16
|
+
if isinstance(ambiguous, bool) or ambiguous not in (1, 2):
|
|
17
|
+
raise ValueError(f"ambiguous must be 1 or 2, not {ambiguous!r}")
|
|
18
|
+
return ambiguous
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _code_width(code: int, ambiguous: int) -> int:
|
|
22
|
+
width = code >> WIDTH_SHIFT
|
|
23
|
+
return ambiguous if width == AMBIGUOUS else width
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def cluster_width(cluster: str, ambiguous: int = 1) -> int:
|
|
27
|
+
"""Return the display width of one extended grapheme cluster.
|
|
28
|
+
|
|
29
|
+
Emoji sequences draw as one glyph: an emoji with VS16, a skin tone modifier, a ZWJ
|
|
30
|
+
sequence, a tag sequence or a regional indicator pair (a flag) is two cells and an
|
|
31
|
+
emoji with VS15 is one. Any other cluster is the sum of its code points.
|
|
32
|
+
"""
|
|
33
|
+
code = prop(ord(cluster[0]))
|
|
34
|
+
if len(cluster) == 1:
|
|
35
|
+
return _code_width(code, ambiguous)
|
|
36
|
+
rest = cluster[1:]
|
|
37
|
+
if code & EMOJI_BIT and not code & MODIFIER_BIT:
|
|
38
|
+
if _VS15 in rest:
|
|
39
|
+
return 1
|
|
40
|
+
if _VS16 in rest:
|
|
41
|
+
return 2
|
|
42
|
+
if (
|
|
43
|
+
code & (EMOJI_BIT | EXT_PICT_BIT)
|
|
44
|
+
and not code & MODIFIER_BIT
|
|
45
|
+
and (_ZWJ in rest or any(prop(ord(ch)) & MODIFIER_BIT or ord(ch) in _TAGS for ch in rest))
|
|
46
|
+
):
|
|
47
|
+
return 2
|
|
48
|
+
if code & 0x0F == _RI and prop(ord(rest[0])) & 0x0F == _RI:
|
|
49
|
+
return 2
|
|
50
|
+
return sum(_code_width(prop(ord(ch)), ambiguous) for ch in cluster)
|
ellipt/py.typed
ADDED
|
File without changes
|
|
@@ -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/
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
ellipt/__init__.py,sha256=n7BOVntpg0yfH2JisRQjrUoQHe6XUb4OYXGwJCmXtkI,735
|
|
2
|
+
ellipt/__main__.py,sha256=K2Z-jKrpRnWHwy828VLCc_qzA1AG_3dSyXHNsGkHiWg,124
|
|
3
|
+
ellipt/_cli.py,sha256=ONO44Q0TYjOk2HV9uRi4Bv8kjNLyU71NDy4sJyqXVyc,6775
|
|
4
|
+
ellipt/_core.py,sha256=eW16PucnoQAtWo9xoLuHJaVMs9Vf4vaEYpEuYV7oqJg,5284
|
|
5
|
+
ellipt/_cut.py,sha256=WApqSK9HhiIIJ9bcr8_jyJDPNJuuj58AG_RKoGYdSEY,3362
|
|
6
|
+
ellipt/_graphemes.py,sha256=ZQgxcpf3iOGwopjEflpX8SzZyECokjRvT-di45XzLWA,3409
|
|
7
|
+
ellipt/_measure.py,sha256=hO376PMBXY1ae3FwmRyKCEgskoyVY69QvbxUhqpFYZo,2480
|
|
8
|
+
ellipt/_path.py,sha256=oMH-Y8CgCGv8DJAHCZ6EDbsQMnqzfQaAMQDX3ceFeWQ,4126
|
|
9
|
+
ellipt/_props.py,sha256=NrdqRp33hAv7Escf7APSSoWUb0ZEkWyRyNBAu3PkCEY,1604
|
|
10
|
+
ellipt/_shorten.py,sha256=dQ-jOfbBnPVTvgPXNI3H4TC3dczDEtZo_soAa27jEis,1663
|
|
11
|
+
ellipt/_ucd_data.py,sha256=dSWD1BP9fsV9zYR-fehbMt4WBzUQWTGLmEDLZGFBNi0,516
|
|
12
|
+
ellipt/_ucd_part1.py,sha256=CpJg26U_HgS3_dMtQrBeRzZrgDNd58WZ8cvhE3BziYg,14858
|
|
13
|
+
ellipt/_width.py,sha256=PPqlKtbm-TKD67WSnGXQMj_ydzv7-EmG2r7xCHtrros,1764
|
|
14
|
+
ellipt/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
15
|
+
ellipt-0.2.1.dist-info/METADATA,sha256=JHfIrRAIPVcd-9U9yoOmaF7St_sfeds8OhvIrGmi5Hs,4899
|
|
16
|
+
ellipt-0.2.1.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
|
|
17
|
+
ellipt-0.2.1.dist-info/entry_points.txt,sha256=10m7FYfoLaGt9SPlgisutoFwLOPLnK_BnsfSZW6QPm4,44
|
|
18
|
+
ellipt-0.2.1.dist-info/licenses/LICENSE,sha256=_WmABlQG__oqlx4Rzp-qDJ10xND5KCLScftbhXbLC_c,1070
|
|
19
|
+
ellipt-0.2.1.dist-info/licenses/LICENSE-UNICODE,sha256=56k7AJVlz85VkZo4FDesTbiD6dohJvoouR0ScyvFPZY,1995
|
|
20
|
+
ellipt-0.2.1.dist-info/RECORD,,
|
|
@@ -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.
|