ctok 1.0.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,54 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+
8
+ permissions:
9
+ contents: read
10
+
11
+ jobs:
12
+ test:
13
+ runs-on: ubuntu-latest
14
+ permissions:
15
+ contents: read
16
+ pull-requests: write # to post the gate report as a PR comment
17
+ strategy:
18
+ fail-fast: false
19
+ matrix:
20
+ python-version: ["3.12", "3.13"]
21
+
22
+ steps:
23
+ - uses: actions/checkout@v5
24
+
25
+ - uses: astral-sh/setup-uv@v6
26
+ with:
27
+ enable-cache: true
28
+
29
+ - name: Install
30
+ run: uv sync --dev --python ${{ matrix.python-version }}
31
+
32
+ - name: Test
33
+ run: uv run --python ${{ matrix.python-version }} pytest -q
34
+
35
+ - name: Report gates, vocabulary and witness coverage
36
+ if: matrix.python-version == '3.12'
37
+ run: |
38
+ uv run --python ${{ matrix.python-version }} python tests/gates.py \
39
+ | tee gates.md >> "$GITHUB_STEP_SUMMARY"
40
+ cat gates.md
41
+
42
+ # Same-repo pull requests only: a fork's GITHUB_TOKEN is read-only and cannot comment.
43
+ # The step summary above is always written, so a fork PR still reports, just not inline.
44
+ - name: Comment gates and witness coverage on the pull request
45
+ if: >-
46
+ matrix.python-version == '3.12'
47
+ && github.event_name == 'pull_request'
48
+ && github.event.pull_request.head.repo.full_name == github.repository
49
+ env:
50
+ GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
51
+ PR: ${{ github.event.pull_request.number }}
52
+ run: |
53
+ gh pr comment "$PR" --body-file gates.md --edit-last --create-if-none \
54
+ || gh pr comment "$PR" --body-file gates.md
@@ -0,0 +1,51 @@
1
+ name: Publish to PyPI
2
+
3
+ on:
4
+ release:
5
+ types: [published]
6
+
7
+ permissions:
8
+ contents: read
9
+
10
+ jobs:
11
+ build:
12
+ runs-on: ubuntu-latest
13
+ steps:
14
+ - uses: actions/checkout@v5
15
+
16
+ - uses: astral-sh/setup-uv@v6
17
+ with:
18
+ enable-cache: true
19
+
20
+ - name: Test
21
+ run: |
22
+ uv sync --dev
23
+ uv run pytest -q
24
+
25
+ - name: Build
26
+ run: uv build
27
+
28
+ - name: Check metadata
29
+ run: uvx twine check dist/*
30
+
31
+ - uses: actions/upload-artifact@v4
32
+ with:
33
+ name: dist
34
+ path: dist/
35
+
36
+ publish:
37
+ needs: build
38
+ runs-on: ubuntu-latest
39
+ environment:
40
+ name: pypi
41
+ url: https://pypi.org/p/ctok
42
+ permissions:
43
+ # Required for PyPI trusted publishing (OIDC); no API token is stored.
44
+ id-token: write
45
+ steps:
46
+ - uses: actions/download-artifact@v5
47
+ with:
48
+ name: dist
49
+ path: dist/
50
+
51
+ - uses: pypa/gh-action-pypi-publish@release/v1
ctok-1.0.0/.gitignore ADDED
@@ -0,0 +1,15 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ *.egg-info/
4
+
5
+ .coverage
6
+ .pytest_cache/
7
+ .ruff_cache/
8
+ .venv/
9
+
10
+ build/
11
+ dist/
12
+
13
+ .DS_Store
14
+ .env
15
+ .envrc
ctok-1.0.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Sander Land
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.
ctok-1.0.0/PKG-INFO ADDED
@@ -0,0 +1,133 @@
1
+ Metadata-Version: 2.5
2
+ Name: ctok
3
+ Version: 1.0.0
4
+ Summary: Offline reconstruction of Claude's tokenizer token counts
5
+ Project-URL: Repository, https://github.com/sanderland/ctok
6
+ Project-URL: Issues, https://github.com/sanderland/ctok/issues
7
+ Project-URL: Research, https://tokencontributions.substack.com/p/on-the-biology-of-claudes-tokenizer
8
+ Author: Sander Land
9
+ License-Expression: MIT
10
+ License-File: LICENSE
11
+ Keywords: anthropic,claude,nlp,token-count,tokenizer
12
+ Classifier: Development Status :: 5 - Production/Stable
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Intended Audience :: Science/Research
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Programming Language :: Python :: 3.13
18
+ Classifier: Topic :: Text Processing :: Linguistic
19
+ Requires-Python: >=3.12
20
+ Description-Content-Type: text/markdown
21
+
22
+ # ctok
23
+
24
+ `ctok` reconstructs Claude token counts offline, with no API call, network access, or runtime
25
+ dependencies. It is unofficial and is not affiliated with Anthropic.
26
+
27
+ The reconstruction targets counts. Claude does not expose token boundaries, so `tokenize()` returns
28
+ one valid minimum-cost tiling, not a claim about Anthropic's exact segmentation. The research behind
29
+ the model is described in [On the biology of Claude's tokenizer](https://tokencontributions.substack.com/p/on-the-biology-of-claudes-tokenizer).
30
+
31
+ ## Install
32
+
33
+ ```bash
34
+ pip install ctok
35
+ ```
36
+
37
+ ## Quick start
38
+
39
+ ```python
40
+ from ctok import token_count, tokenize
41
+
42
+ token_count("hello, world") # 10, using the v3 family
43
+ token_count("hello, world", "4.7") # 15
44
+ token_count("hello, world", "5.0") # 10
45
+
46
+ tokens = tokenize("NASA likes tokenizers")
47
+ assert len(tokens) == token_count("NASA likes tokenizers")
48
+ ```
49
+
50
+ The command-line interface prints the marked stream and its tiling:
51
+
52
+ ```bash
53
+ ctok "hello, world"
54
+ ```
55
+
56
+ ## Supported families
57
+
58
+ `version` is a string such as `"4.7"`. Components are compared as integers, so `"4.10"` sorts
59
+ after `"4.9"`. Python reads the float literal `4.10` as `4.1`, so passing a non-string version
60
+ raises `TypeError`.
61
+
62
+ | requested version | family | model generation |
63
+ |---|---|---|
64
+ | `"3.0" <= version < "4.7"` | v3, the default | Claude 3 through Opus 4.6 |
65
+ | `"4.7" <= version < "5.0"` | v4.7 | Opus 4.7 through 4.9 |
66
+ | `version >= "5.0"` | v5 | Opus 5 and Sonnet 5 |
67
+
68
+ v5 uses the v4.7 vocabulary with a different message frame.
69
+
70
+ ## How it works
71
+
72
+ For one user message, `ctok`:
73
+
74
+ 1. normalizes the text, including NFC and family-specific quote folding;
75
+ 2. rewrites it into a stream with word, case, and byte markers;
76
+ 3. finds a minimum-cost tiling over the measured vocabulary and UTF-8 byte fallback;
77
+ 4. adds the measured message frame.
78
+
79
+ `token_count(text)` is `len(tokenize(text))`. The output notation makes internal structure visible:
80
+
81
+ | notation | meaning |
82
+ |---|---|
83
+ | `⟨bow⟩`, `⟨eow⟩` | word boundaries |
84
+ | `⟨shift⟩`, `⟨caps⟩` | case rewrites |
85
+ | `⟨0xNN⟩` | a byte-fallback token |
86
+ | `⟨pad⟩` | part of the single-message frame |
87
+
88
+ ## Measured accuracy
89
+
90
+ These results compare `ctok` with recorded `count_tokens` responses:
91
+
92
+ | corpus | role | v3 exact | v4.7 exact |
93
+ |---|---|---:|---:|
94
+ | Goldfish, 350 languages and 350,000 rows | mining | 350,000 | 350,000 |
95
+ | MultiPL-E, 22 programming languages | held out | 22 | 22 |
96
+ | Rosetta Code, 1,741 documents | mining | 1,741 | 1,741 |
97
+ | Rosetta Code, separate 250 documents | held out | 250 | 250 |
98
+ | UDHR, 501 languages | mining (in-sample since 2026-08-12) | 501 | 501 |
99
+
100
+ v5 is omitted from the table because its deviation from recorded counts matches v4.7's on every
101
+ gated document. Separate API tests cover its message-frame rules.
102
+
103
+ The stored measurement sets contain no under-counts: 0 of 1,664,940 v3 texts and 0 of 1,722,961
104
+ v4.7 texts. This does not guarantee the result for arbitrary input. Goldfish, the main Rosetta
105
+ sample, and UDHR informed piece selection. MultiPL-E and the separate Rosetta sample did not.
106
+
107
+ Run the public gates with:
108
+
109
+ ```bash
110
+ uv run pytest
111
+ uv run python tests/gates.py
112
+ ```
113
+
114
+ ## Vocabulary evidence
115
+
116
+ The two vocabulary files contain 48,645 v3 pieces and 15,240 v4.7 pieces. Every entry has a fixed
117
+ membership witness or is one of the structural marker atoms checked by the test suite.
118
+
119
+ ```python
120
+ from ctok import pieces, witness
121
+
122
+ len(pieces("4.7")) # 15240
123
+ witness("⟨bow⟩the⟨eow⟩", "4.7")
124
+ # {'probe': 'the', 'raw': 12, 'kind': 'raw'}
125
+ ```
126
+
127
+ A witness says that one marked piece costs one token in a calibrated probe. It does not prove the
128
+ encoder rewrite or resolve ties between equal-cost tilings. `tests/test_witness.py` checks every
129
+ published witness and requires complete witnessed-or-special coverage.
130
+
131
+ ## License
132
+
133
+ MIT
ctok-1.0.0/README.md ADDED
@@ -0,0 +1,112 @@
1
+ # ctok
2
+
3
+ `ctok` reconstructs Claude token counts offline, with no API call, network access, or runtime
4
+ dependencies. It is unofficial and is not affiliated with Anthropic.
5
+
6
+ The reconstruction targets counts. Claude does not expose token boundaries, so `tokenize()` returns
7
+ one valid minimum-cost tiling, not a claim about Anthropic's exact segmentation. The research behind
8
+ the model is described in [On the biology of Claude's tokenizer](https://tokencontributions.substack.com/p/on-the-biology-of-claudes-tokenizer).
9
+
10
+ ## Install
11
+
12
+ ```bash
13
+ pip install ctok
14
+ ```
15
+
16
+ ## Quick start
17
+
18
+ ```python
19
+ from ctok import token_count, tokenize
20
+
21
+ token_count("hello, world") # 10, using the v3 family
22
+ token_count("hello, world", "4.7") # 15
23
+ token_count("hello, world", "5.0") # 10
24
+
25
+ tokens = tokenize("NASA likes tokenizers")
26
+ assert len(tokens) == token_count("NASA likes tokenizers")
27
+ ```
28
+
29
+ The command-line interface prints the marked stream and its tiling:
30
+
31
+ ```bash
32
+ ctok "hello, world"
33
+ ```
34
+
35
+ ## Supported families
36
+
37
+ `version` is a string such as `"4.7"`. Components are compared as integers, so `"4.10"` sorts
38
+ after `"4.9"`. Python reads the float literal `4.10` as `4.1`, so passing a non-string version
39
+ raises `TypeError`.
40
+
41
+ | requested version | family | model generation |
42
+ |---|---|---|
43
+ | `"3.0" <= version < "4.7"` | v3, the default | Claude 3 through Opus 4.6 |
44
+ | `"4.7" <= version < "5.0"` | v4.7 | Opus 4.7 through 4.9 |
45
+ | `version >= "5.0"` | v5 | Opus 5 and Sonnet 5 |
46
+
47
+ v5 uses the v4.7 vocabulary with a different message frame.
48
+
49
+ ## How it works
50
+
51
+ For one user message, `ctok`:
52
+
53
+ 1. normalizes the text, including NFC and family-specific quote folding;
54
+ 2. rewrites it into a stream with word, case, and byte markers;
55
+ 3. finds a minimum-cost tiling over the measured vocabulary and UTF-8 byte fallback;
56
+ 4. adds the measured message frame.
57
+
58
+ `token_count(text)` is `len(tokenize(text))`. The output notation makes internal structure visible:
59
+
60
+ | notation | meaning |
61
+ |---|---|
62
+ | `⟨bow⟩`, `⟨eow⟩` | word boundaries |
63
+ | `⟨shift⟩`, `⟨caps⟩` | case rewrites |
64
+ | `⟨0xNN⟩` | a byte-fallback token |
65
+ | `⟨pad⟩` | part of the single-message frame |
66
+
67
+ ## Measured accuracy
68
+
69
+ These results compare `ctok` with recorded `count_tokens` responses:
70
+
71
+ | corpus | role | v3 exact | v4.7 exact |
72
+ |---|---|---:|---:|
73
+ | Goldfish, 350 languages and 350,000 rows | mining | 350,000 | 350,000 |
74
+ | MultiPL-E, 22 programming languages | held out | 22 | 22 |
75
+ | Rosetta Code, 1,741 documents | mining | 1,741 | 1,741 |
76
+ | Rosetta Code, separate 250 documents | held out | 250 | 250 |
77
+ | UDHR, 501 languages | mining (in-sample since 2026-08-12) | 501 | 501 |
78
+
79
+ v5 is omitted from the table because its deviation from recorded counts matches v4.7's on every
80
+ gated document. Separate API tests cover its message-frame rules.
81
+
82
+ The stored measurement sets contain no under-counts: 0 of 1,664,940 v3 texts and 0 of 1,722,961
83
+ v4.7 texts. This does not guarantee the result for arbitrary input. Goldfish, the main Rosetta
84
+ sample, and UDHR informed piece selection. MultiPL-E and the separate Rosetta sample did not.
85
+
86
+ Run the public gates with:
87
+
88
+ ```bash
89
+ uv run pytest
90
+ uv run python tests/gates.py
91
+ ```
92
+
93
+ ## Vocabulary evidence
94
+
95
+ The two vocabulary files contain 48,645 v3 pieces and 15,240 v4.7 pieces. Every entry has a fixed
96
+ membership witness or is one of the structural marker atoms checked by the test suite.
97
+
98
+ ```python
99
+ from ctok import pieces, witness
100
+
101
+ len(pieces("4.7")) # 15240
102
+ witness("⟨bow⟩the⟨eow⟩", "4.7")
103
+ # {'probe': 'the', 'raw': 12, 'kind': 'raw'}
104
+ ```
105
+
106
+ A witness says that one marked piece costs one token in a calibrated probe. It does not prove the
107
+ encoder rewrite or resolve ties between equal-cost tilings. `tests/test_witness.py` checks every
108
+ published witness and requires complete witnessed-or-special coverage.
109
+
110
+ ## License
111
+
112
+ MIT
@@ -0,0 +1,5 @@
1
+ """Offline reconstruction of Claude's tokenizer token counts."""
2
+
3
+ from .main import marked_stream, normalize, pieces, token_count, tokenize, witness
4
+
5
+ __all__ = ["token_count", "tokenize", "normalize", "marked_stream", "pieces", "witness"]
@@ -0,0 +1,224 @@
1
+ """The marker notation and the measured character tables.
2
+
3
+ The tables were measured against ``count_tokens``. Tables marked as enumerated must stay
4
+ enumerated: no Unicode-category rule reproduces them.
5
+ """
6
+
7
+ import re
8
+
9
+ # ---- marker notation ----------------------------------------------------------------------------
10
+
11
+ # The engine computes over marked strings in which each structural marker is one codepoint.
12
+ BOW_G, EOW_G, PAD_G = "\ufdd0", "\ufdd1", "\ufdd2"
13
+ SHIFT_G, CAPS_G = "\ufdd3", "\ufdd4"
14
+
15
+ MARKER_GLYPHS = frozenset({BOW_G, EOW_G, SHIFT_G, CAPS_G})
16
+
17
+ # The public form: named atoms in mathematical angle brackets, practically absent from real text.
18
+ # Literal brackets in text are byte-escaped at render time, so a rendered token always parses back
19
+ # unambiguously.
20
+ L, R = "⟨", "⟩"
21
+ BOW, EOW, SHIFT, CAPS = f"{L}bow{R}", f"{L}eow{R}", f"{L}shift{R}", f"{L}caps{R}"
22
+ PAD = f"{L}pad{R}"
23
+
24
+ GLYPH_TO_ATOM = {BOW_G: BOW, EOW_G: EOW, PAD_G: PAD, SHIFT_G: SHIFT, CAPS_G: CAPS}
25
+ ATOM_TO_GLYPH = {BOW: BOW_G, EOW: EOW_G, SHIFT: SHIFT_G, CAPS: CAPS_G}
26
+
27
+ # Scraped text can contain the noncharacters used above. NFC strips BMP private-use input, so these
28
+ # five private-use codepoints are safe internal escapes inserted after NFC. The byte floor and
29
+ # renderer map them back to the original noncharacters.
30
+ LITERAL_MARKER_ESCAPES = {glyph: chr(0xE000 + i) for i, glyph in enumerate(GLYPH_TO_ATOM)}
31
+ ESCAPED_MARKER_LITERALS = {escape: glyph for glyph, escape in LITERAL_MARKER_ESCAPES.items()}
32
+ LITERAL_MARKER_ESCAPE_TABLE = str.maketrans(LITERAL_MARKER_ESCAPES)
33
+
34
+ # ---- character classes --------------------------------------------------------------------------
35
+
36
+ WORDY, HARD, DIGIT, PUNCT, SPACE = "wordy", "hard", "digit", "punct", "space"
37
+
38
+ # Non-ASCII symbols/punctuation that tile over the punct vocabulary rather than standing alone.
39
+ # Enumerated: the behaviour splits per codepoint with no categorical rule (`(` is punct but `)`
40
+ # is hard). Anything unlisted stays HARD.
41
+ PUNCT_SYMS = frozenset("—»«•°„–−£§€…√→(№†└│།·─═█")
42
+
43
+ # Symbol-letters measured to take the full word model (⟨bow⟩ and ⟨eow⟩ flanks, seam absorption, case
44
+ # markers) exactly like Latin letters. Enumerated blocks: category Nl/So/Lu splits both ways, and
45
+ # the Hangzhou numerals (also Nl) measured markerless, so it is the block that predicts, not the
46
+ # category.
47
+ SYMBOL_LETTERS = (
48
+ (0x16EE, 0x16F0), # Runic golden numbers (Nl, caseless)
49
+ (0x2160, 0x2188), # Roman numerals (Nl/Lu/Ll, cased pairs)
50
+ (0x24B6, 0x24E9), # circled letters (So, cased)
51
+ (0xA6E6, 0xA6EF), # Bamum number-letters (Nl, caseless)
52
+ )
53
+
54
+ # Variation selectors are gc=Mn but take no word model: each costs one more than its base
55
+ # character, where ⟨bow⟩ and ⟨eow⟩ flanks would read three more. The supplementary selectors
56
+ # (U+E0100 to U+E01EF) are astral and already HARD.
57
+ VARIATION_SELECTORS = (0xFE00, 0xFE0F)
58
+
59
+ # The one canonical-combining-class-9 character that does not separate word runs: measured over
60
+ # as a separator on its own script's consonants and under wherever no letter precedes it. See
61
+ # :func:`normalize.is_separator` for the grid.
62
+ NON_SEPARATORS = frozenset("ฺ") # THAI CHARACTER PHINTHU
63
+
64
+ # Marks that separate word runs the way a virama does, but without combining class 9. Measured
65
+ # one character at a time on byte-floor consonant hosts; enumerated, because the rule is
66
+ # orthographic (the neighbouring codepoint is usually a vowel sign that does not split). The
67
+ # U+0300 block is handled as a range instead. See :data:`SEPARATOR_MARKS`.
68
+ SEPARATOR_SIGNS = frozenset((
69
+ "\u0740", # SYRIAC FEMININE DOT
70
+ "\u0741", # SYRIAC QUSHSHAYA
71
+ "\u0742", # SYRIAC RUKKAKHA
72
+ "\u0743", # SYRIAC TWO VERTICAL DOTS ABOVE
73
+ "\u0744", # SYRIAC TWO VERTICAL DOTS BELOW
74
+ "\u0745", # SYRIAC THREE DOTS ABOVE
75
+ "\u0746", # SYRIAC THREE DOTS BELOW
76
+ "\u0747", # SYRIAC OBLIQUE LINE ABOVE
77
+ "\u0748", # SYRIAC OBLIQUE LINE BELOW
78
+ "\u0749", # SYRIAC MUSIC
79
+ "\u074a", # SYRIAC BARREKH
80
+ "\u093c", # DEVANAGARI SIGN NUKTA
81
+ "\u0951", # DEVANAGARI STRESS SIGN UDATTA
82
+ "\u0952", # DEVANAGARI STRESS SIGN ANUDATTA
83
+ "\u0953", # DEVANAGARI GRAVE ACCENT
84
+ "\u0954", # DEVANAGARI ACUTE ACCENT
85
+ "\u09bc", # BENGALI SIGN NUKTA
86
+ "\u09fe", # BENGALI SANDHI MARK
87
+ "\u0a3c", # GURMUKHI SIGN NUKTA
88
+ "\u0abc", # GUJARATI SIGN NUKTA
89
+ "\u0afd", # GUJARATI SIGN THREE-DOT NUKTA ABOVE
90
+ "\u0afe", # GUJARATI SIGN CIRCLE NUKTA ABOVE
91
+ "\u0aff", # GUJARATI SIGN TWO-CIRCLE NUKTA ABOVE
92
+ "\u0b3c", # ORIYA SIGN NUKTA
93
+ "\u0b55", # ORIYA SIGN OVERLINE
94
+ # U+0C03 TELUGU SIGN VISARGA is deliberately not here: it is a spacing mark (Mc, ccc 0) that
95
+ # measures as plain word material, closing a word with a `ః⟨eow⟩` piece rather than splitting it.
96
+ "\u0c3c", # TELUGU SIGN NUKTA
97
+ "\u0cbc", # KANNADA SIGN NUKTA
98
+ "\u0e47", # THAI CHARACTER MAITAIKHU
99
+ "\u0e48", # THAI CHARACTER MAI EK
100
+ "\u0e49", # THAI CHARACTER MAI THO
101
+ "\u0e4a", # THAI CHARACTER MAI TRI
102
+ "\u0e4b", # THAI CHARACTER MAI CHATTAWA
103
+ "\u0e4c", # THAI CHARACTER THANTHAKHAT
104
+ "\u0e4e", # THAI CHARACTER YAMAKKAN
105
+ "\u0ec8", # LAO TONE MAI EK
106
+ "\u0ec9", # LAO TONE MAI THO
107
+ "\u0eca", # LAO TONE MAI TI
108
+ "\u0ecb", # LAO TONE MAI CATAWA
109
+ "\u0ecc", # LAO CANCELLATION MARK
110
+ "\u0ece", # LAO YAMAKKAN
111
+ "\u0f18", # TIBETAN ASTROLOGICAL SIGN -KHYUD PA
112
+ "\u0f19", # TIBETAN ASTROLOGICAL SIGN SDONG TSHUGS
113
+ "\u0f35", # TIBETAN MARK NGAS BZUNG NYI ZLA
114
+ "\u0f37", # TIBETAN MARK NGAS BZUNG SGOR RTAGS
115
+ "\u0f39", # TIBETAN MARK TSA -PHRU
116
+ "\u0f3e", # TIBETAN SIGN YAR TSHES
117
+ "\u0f3f", # TIBETAN SIGN MAR TSHES
118
+ "\u0f86", # TIBETAN SIGN LCI RTAGS
119
+ "\u0f87", # TIBETAN SIGN YANG RTAGS
120
+ "\u0fc6", # TIBETAN SYMBOL PADMA GDAN
121
+ "\u1037", # MYANMAR SIGN DOT BELOW
122
+ "\u17b4", # KHMER VOWEL INHERENT AQ
123
+ "\u17b5", # KHMER VOWEL INHERENT AA
124
+ "\u17c9", # KHMER SIGN MUUSIKATOAN
125
+ "\u17ca", # KHMER SIGN TRIISAP
126
+ "\u17cb", # KHMER SIGN BANTOC
127
+ "\u17cc", # KHMER SIGN ROBAT
128
+ "\u17cd", # KHMER SIGN TOANDAKHIAT
129
+ "\u17ce", # KHMER SIGN KAKABAT
130
+ "\u17cf", # KHMER SIGN AHSDA
131
+ "\u17d0", # KHMER SIGN SAMYOK SANNYA
132
+ "\u17d1", # KHMER SIGN VIRIAM
133
+ "\u17d3", # KHMER SIGN BATHAMASAT
134
+ "\u17dd", # KHMER SIGN ATTHACAN
135
+ "\u1a75", # TAI THAM SIGN TONE-1
136
+ "\u1a76", # TAI THAM SIGN TONE-2
137
+ "\u1a77", # TAI THAM SIGN KHUEN TONE-3
138
+ "\u1a78", # TAI THAM SIGN KHUEN TONE-4
139
+ "\u1a79", # TAI THAM SIGN KHUEN TONE-5
140
+ "\u1a7a", # TAI THAM SIGN RA HAAM
141
+ "\u1a7b", # TAI THAM SIGN MAI SAM
142
+ "\u1a7c", # TAI THAM SIGN KHUEN-LUE KARAN
143
+ "\u1a7f", # TAI THAM COMBINING CRYPTOGRAMMIC DOT
144
+ "\u1b34", # BALINESE SIGN REREKAN
145
+ "\u1b6b", # BALINESE MUSICAL SYMBOL COMBINING TEGEH
146
+ "\u1b6c", # BALINESE MUSICAL SYMBOL COMBINING ENDEP
147
+ "\u1b6d", # BALINESE MUSICAL SYMBOL COMBINING KEMPUL
148
+ "\u1b6e", # BALINESE MUSICAL SYMBOL COMBINING KEMPLI
149
+ "\u1b6f", # BALINESE MUSICAL SYMBOL COMBINING JEGOGAN
150
+ "\u1b70", # BALINESE MUSICAL SYMBOL COMBINING KEMPUL WITH JEGOGAN
151
+ "\u1b71", # BALINESE MUSICAL SYMBOL COMBINING KEMPLI WITH JEGOGAN
152
+ "\u1b72", # BALINESE MUSICAL SYMBOL COMBINING BENDE
153
+ "\u1b73", # BALINESE MUSICAL SYMBOL COMBINING GONG
154
+ "\ua9b3", # JAVANESE SIGN CECAK TELU
155
+ ))
156
+
157
+ # ---- normalization ------------------------------------------------------------------------------
158
+
159
+ # C0/C1 controls the API strips before tokenizing (cost 0), i.e. every gc=Cc except TAB, LF and NUL.
160
+ STRIP_CONTROL = re.compile("[\x01-\x08\x0B-\x1F\x7F\x80-\x9F]")
161
+
162
+ # BMP private use is stripped the same way, and its two neighbours join into one word. Unassigned
163
+ # codepoints survive and pay their bytes; the supplementary private-use planes are unprobed and
164
+ # deliberately left out.
165
+ STRIP_PRIVATE = re.compile("[-]")
166
+
167
+ # Lone surrogates are valid in a str but not UTF-8-encodable, so they would crash the byte floor.
168
+ # They can never be genuine tokenizer input; fold each to U+FFFD, which prices as an ordinary
169
+ # 3-byte HARD character. This is what keeps token_count total on any str.
170
+ SURROGATE = re.compile(r"[\ud800-\udfff]")
171
+
172
+ # Space separators the tokenizer treats identically to U+0020: all Zs except U+3000 (ideographic
173
+ # space), plus Zl/Zp. NB U+3000, TAB and LF each have their own cost. NUL is not stripped but
174
+ # tokenizes exactly like a space, so it is folded here too.
175
+ FUNNY_SPACE = re.compile("[\u00A0\u1680\u2000-\u200A\u2028\u2029\u202F\u205F]")
176
+
177
+ # The four standard curly quotes fold to their ASCII forms (v3 only; NFC does not do this). The
178
+ # low-9 mark U+201E is a different token and is deliberately not folded.
179
+ QUOTE_FOLD = str.maketrans({"\u2018": "'", "\u2019": "'", "\u201C": '"', "\u201D": '"'})
180
+
181
+ # The suffixes an apostrophe binds into the word ahead of it, deleting that word's ⟨bow⟩. This is the
182
+ # standard English contraction set, lowercase and whole-word only. Measured per member; other
183
+ # suffixes pay a full boundary even when their own piece exists, so no piece-based rule reproduces
184
+ # this. Case-sensitive and whole-word (`x'S`, `x'LL`, `x'llo`, `x'lls` all pay).
185
+ CONTRACTION_SUFFIXES = frozenset({"s", "t", "d", "m", "ll", "re", "ve"})
186
+
187
+ # ---- the seam law -------------------------------------------------------------------------------
188
+
189
+ # ⟨eow⟩ ' ' [case markers] ⟨bow⟩ -> ⟨eow⟩ [case markers] ⟨bow⟩: a single space between two marked
190
+ # spans is the seam and is not written as a character.
191
+ SEAM_RE = re.compile("(.)" + EOW_G + " " + "([" + SHIFT_G + CAPS_G + "]*)" + BOW_G)
192
+
193
+ # ---- marks that stand outside the word -----------------------------------------------------------
194
+
195
+ # Combining marks that close the word before themselves, exactly as a virama does
196
+ # (`normalize.is_separator` folds this range in): `q́z` = `⟨bow⟩q⟨eow⟩` + mark + `⟨bow⟩z⟨eow⟩`, so an
197
+ # accent makes two words out of one. Two oracle frames on one-token hosts distinguish the
198
+ # spellings. A sweep covered U+0300 to U+036F on 21 hosts across 13 scripts and both families, with
199
+ # no dissent. U+0345 and the combining Latin letters U+0363 to U+036F pin both ends of the range
200
+ # from outside by reading "inside the word". U+0340, U+0341, U+0343, and U+0344 are NFC-folded away
201
+ # and inside the range for that reason alone.
202
+ SEPARATOR_MARKS = re.compile("[\u0300-\u0344\u0346-\u0362]")
203
+
204
+ # The same question asked of every other combining block of the BMP: 418 marks swept on the same
205
+ # two frames, both families, no dissent. The split is orthographic, as for Thai:
206
+ #
207
+ # OUTSIDE the word accents, tone marks, cantillation and annotation
208
+ # INSIDE the word vowel points and combining letters (niqqud, harakat, the Syriac vowels,
209
+ # Thaana, Samaritan, the combining Latin/Cyrillic letter blocks)
210
+ #
211
+ # Most ranges are pinned on both sides by an inside-the-word neighbour in the same block; where a
212
+ # range abuts unassigned codepoints, it is written to the marks actually probed. The N'Ko and
213
+ # Mandaic marks have no in-script one-token host and rest on `q` and `б` alone.
214
+ SEPARATOR_ANNOTATIONS = re.compile(
215
+ "[\u0483-\u0489\u0591-\u05af\u0658\u06df-\u06e0\u06ea-\u06ec"
216
+ "\u07eb-\u07f3\u07fd\u0859-\u085b\u0898-\u089f\u08ca-\u08d3\u08e0-\u08e1"
217
+ "\u0818-\u0819\u082d\u08ea-\u08ef\u135d-\u135f"
218
+ "\u180b-\u180d\u180f\u1939-\u193b"
219
+ "\u1ab0-\u1abe\u1ac1-\u1acb\u1cd0-\u1ce8\u1ced\u1cf4\u1cf8-\u1cf9"
220
+ "\u1be6\u1c37\u1cf7"
221
+ "\u1dc0-\u1dd2\u1df5-\u1dff\u20d0-\u20f0\ua66f-\ua672\ua67c-\ua67d"
222
+ "\u2cef-\u2cf1\u302a-\u302f\u3099-\u309a"
223
+ "\ua6f0-\ua6f1\ua8e0-\ua8f1\ua92b-\ua92d\uaabf\uaac1\uabec"
224
+ "\ufe20-\ufe2f]")