axorn 0.1.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.
axorn-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Korakot Chaovavanich
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.
axorn-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,116 @@
1
+ Metadata-Version: 2.4
2
+ Name: axorn
3
+ Version: 0.1.0
4
+ Summary: Pāli script converter, roman ↔ Thai, for Python and JS from one shared table
5
+ License-Expression: MIT
6
+ Project-URL: Repository, https://github.com/korakot/axorn
7
+ Requires-Python: >=3.9
8
+ Description-Content-Type: text/markdown
9
+ License-File: LICENSE
10
+ Dynamic: license-file
11
+
12
+ # axorn
13
+
14
+ **English** | [ไทย](README.th.md)
15
+
16
+ Pāli script converter, roman ↔ Thai (อักษร), for Python and JS from one shared table.
17
+
18
+ Pāli has no script of its own, so the same text is written in roman (*evaṃ me sutaṃ*) and in Thai script (เอวํ เม สุตํ). axorn converts between the two, both ways, and gives exactly the same output in Python (for backends) and JS (for web pages). It follows the conventions of the VRI Tipiṭaka (tipitaka.org), and is tested against VRI's own files.
19
+
20
+ ## Use
21
+
22
+ **Try it in the browser:** <https://korakot.github.io/axorn/> — paste roman or Thai and the other follows as you type. It runs the same `axorn.js`, entirely in your browser.
23
+
24
+ Python:
25
+
26
+ ```python
27
+ from axorn import roman_to_thai, thai_to_roman
28
+
29
+ roman_to_thai("Evaṃ me sutaṃ.") # 'เอวํ เม สุตํฯ'
30
+ thai_to_roman("เอวํ เม สุตํฯ") # 'evaṃ me sutaṃ.'
31
+ ```
32
+
33
+ JavaScript (ES module, Node 22+ or a recent browser):
34
+
35
+ ```js
36
+ import { romanToThai, thaiToRoman } from "./axorn/axorn.js";
37
+
38
+ romanToThai("Evaṃ me sutaṃ."); // "เอวํ เม สุตํฯ"
39
+ thaiToRoman("เอวํ เม สุตํฯ"); // "evaṃ me sutaṃ."
40
+ ```
41
+
42
+ Command line: `python3 -m axorn thai < roman.txt > thai.txt` (or `roman` for the other way).
43
+
44
+ Options:
45
+
46
+ - `ue` (roman → Thai): write -iṃ as ึ, as many Thai prints do (กึ), instead of VRI's ิํ (กิํ).
47
+ - `punct` (both ways, on by default): convert digits (12 ↔ ๑๒) and full stops (. ↔ ฯ). Turn it off to convert letters only.
48
+
49
+ ## Conventions
50
+
51
+ Thai-script Pāli is written in more than one way. axorn writes the VRI way and reads all the common ones:
52
+
53
+ | | axorn writes (VRI) | axorn also reads |
54
+ |---|---|---|
55
+ | -iṃ, e.g. *kiṃ* | กิํ | กึ |
56
+ | e/o before a consonant cluster, e.g. *tumhe* | ตุมฺเห (before the last consonant) | ตุเมฺห (before the whole cluster) |
57
+ | *ṭ* | ฏ | ฎ |
58
+ | virama (the dot under a consonant) | ฺ (phinthu) | ๎ (yamakkan) |
59
+ | niggahita | ṃ | ṁ, ŋ in roman input |
60
+
61
+ Two Pāli facts the converter relies on:
62
+
63
+ - **a + i and a + u are two vowels.** Pāli has no *ai* or *au* diphthong, so *sauddesaṃ* (sa-uddesaṃ) is สอุทฺเทสํ, not เสาทฺเทสํ.
64
+ - **A vowel that starts a syllable takes อ**, e.g. *okkāka* → โอกฺกาก.
65
+
66
+ **Full stops.** VRI's master text is in Devanagari, which has two marks where roman has one dot. The full stop (।) is ฯ in Thai, while the abbreviation sign (॰) stays "." in Thai, as in *dī. ni.* (ที. นิ.). Going roman → Thai, axorn treats a dot as an abbreviation when it:
67
+
68
+ - touches a digit ("12.", "1.7");
69
+ - sits inside parentheses;
70
+ - comes before ")" or ",";
71
+ - is followed by a number or another short abbreviation;
72
+ - or ends a chain of them ("sī. pī. potthakesu").
73
+
74
+ Every other dot is a full stop. The elision mark *…pe…* is written …เป.… in Thai.
75
+
76
+ Thai has no capital letters, so Thai → roman returns lower case.
77
+
78
+ ## Tested on
79
+
80
+ `tests/vri_check.py` converts every paragraph of a VRI book roman → Thai and Thai → roman, compares each with VRI's file in the other script, and checks that Python and JS agree. On 14 Oct 2026:
81
+
82
+ | Book | Paragraphs | Words | Mismatches (of 2 × paragraphs) |
83
+ |---|---:|---:|---:|
84
+ | Dīgha Nikāya 1 (s0101m) | 1,046 | 45,891 | 2 |
85
+ | DN 1 commentary (s0101a) | 1,477 | 61,232 | 4 |
86
+ | Jātaka verses 1 (s0513m) | 10,473 | 53,074 | 6 |
87
+ | Majjhima Nikāya 1 (s0201m) | 1,785 | 85,305 | 5 |
88
+ | Dhammasaṅgaṇī (abh01m) | 2,436 | 53,189 | 2 |
89
+ | Vinaya, Pārājika (vin01m) | 2,465 | 72,253 | 4 |
90
+
91
+ Python and JS gave identical output on all of them, and every letter matched. The 23 mismatches are all punctuation and spacing, most of them inconsistencies inside VRI's own files. For example, the Thai file has a ฯ before the *Namo tassa* title that the roman file lacks, and *…pe…* is sometimes written …เป… without the dot.
92
+
93
+ Running the tests:
94
+
95
+ ```
96
+ python3 tests/test_cases.py # 18 fixed cases, Python and JS
97
+ git clone --depth 1 --filter=blob:none --sparse https://github.com/VipassanaTech/tipitaka-xml vri
98
+ git -C vri sparse-checkout set --no-cone /romn/s0101m.mul.xml /thai/s0101m.mul.xml
99
+ python3 tests/vri_check.py vri s0101m.mul
100
+ ```
101
+
102
+ ## Design
103
+
104
+ `axorn/thai.json` holds the letters: consonants, vowels (with their position before or after the consonant), virama, niggahita, digits and punctuation. `__init__.py` and `axorn.js` both read it and mirror each other line for line, so a fix to the table reaches both languages at once.
105
+
106
+ Roman is the hub. Other scripts (Myanmar, Sinhala, …) can be added later as one table each, and any script → any script goes through roman. The reasoning, history and plan are in [wiki/](wiki/index.md).
107
+
108
+ ## Credits
109
+
110
+ axorn was written by Claude (Anthropic) for Korakot Chaovavanich in 2026. Its design builds on Korakot's 2018 notebook, a Python port of the [pitaka.lk](https://pitaka.lk/converter.html) JavaScript converter. Two ideas carry over from it: spelling each Thai syllable from a consonant + vowel pair, with เ/โ placed before the consonant, and the consonant table itself. Korakot's notebook already matched VRI exactly on DN 1, letter for letter.
111
+
112
+ [Aksharamukha](https://github.com/virtualvinodh/aksharamukha-python) served as a cross-check during development.
113
+
114
+ ## License
115
+
116
+ MIT
axorn-0.1.0/README.md ADDED
@@ -0,0 +1,105 @@
1
+ # axorn
2
+
3
+ **English** | [ไทย](README.th.md)
4
+
5
+ Pāli script converter, roman ↔ Thai (อักษร), for Python and JS from one shared table.
6
+
7
+ Pāli has no script of its own, so the same text is written in roman (*evaṃ me sutaṃ*) and in Thai script (เอวํ เม สุตํ). axorn converts between the two, both ways, and gives exactly the same output in Python (for backends) and JS (for web pages). It follows the conventions of the VRI Tipiṭaka (tipitaka.org), and is tested against VRI's own files.
8
+
9
+ ## Use
10
+
11
+ **Try it in the browser:** <https://korakot.github.io/axorn/> — paste roman or Thai and the other follows as you type. It runs the same `axorn.js`, entirely in your browser.
12
+
13
+ Python:
14
+
15
+ ```python
16
+ from axorn import roman_to_thai, thai_to_roman
17
+
18
+ roman_to_thai("Evaṃ me sutaṃ.") # 'เอวํ เม สุตํฯ'
19
+ thai_to_roman("เอวํ เม สุตํฯ") # 'evaṃ me sutaṃ.'
20
+ ```
21
+
22
+ JavaScript (ES module, Node 22+ or a recent browser):
23
+
24
+ ```js
25
+ import { romanToThai, thaiToRoman } from "./axorn/axorn.js";
26
+
27
+ romanToThai("Evaṃ me sutaṃ."); // "เอวํ เม สุตํฯ"
28
+ thaiToRoman("เอวํ เม สุตํฯ"); // "evaṃ me sutaṃ."
29
+ ```
30
+
31
+ Command line: `python3 -m axorn thai < roman.txt > thai.txt` (or `roman` for the other way).
32
+
33
+ Options:
34
+
35
+ - `ue` (roman → Thai): write -iṃ as ึ, as many Thai prints do (กึ), instead of VRI's ิํ (กิํ).
36
+ - `punct` (both ways, on by default): convert digits (12 ↔ ๑๒) and full stops (. ↔ ฯ). Turn it off to convert letters only.
37
+
38
+ ## Conventions
39
+
40
+ Thai-script Pāli is written in more than one way. axorn writes the VRI way and reads all the common ones:
41
+
42
+ | | axorn writes (VRI) | axorn also reads |
43
+ |---|---|---|
44
+ | -iṃ, e.g. *kiṃ* | กิํ | กึ |
45
+ | e/o before a consonant cluster, e.g. *tumhe* | ตุมฺเห (before the last consonant) | ตุเมฺห (before the whole cluster) |
46
+ | *ṭ* | ฏ | ฎ |
47
+ | virama (the dot under a consonant) | ฺ (phinthu) | ๎ (yamakkan) |
48
+ | niggahita | ṃ | ṁ, ŋ in roman input |
49
+
50
+ Two Pāli facts the converter relies on:
51
+
52
+ - **a + i and a + u are two vowels.** Pāli has no *ai* or *au* diphthong, so *sauddesaṃ* (sa-uddesaṃ) is สอุทฺเทสํ, not เสาทฺเทสํ.
53
+ - **A vowel that starts a syllable takes อ**, e.g. *okkāka* → โอกฺกาก.
54
+
55
+ **Full stops.** VRI's master text is in Devanagari, which has two marks where roman has one dot. The full stop (।) is ฯ in Thai, while the abbreviation sign (॰) stays "." in Thai, as in *dī. ni.* (ที. นิ.). Going roman → Thai, axorn treats a dot as an abbreviation when it:
56
+
57
+ - touches a digit ("12.", "1.7");
58
+ - sits inside parentheses;
59
+ - comes before ")" or ",";
60
+ - is followed by a number or another short abbreviation;
61
+ - or ends a chain of them ("sī. pī. potthakesu").
62
+
63
+ Every other dot is a full stop. The elision mark *…pe…* is written …เป.… in Thai.
64
+
65
+ Thai has no capital letters, so Thai → roman returns lower case.
66
+
67
+ ## Tested on
68
+
69
+ `tests/vri_check.py` converts every paragraph of a VRI book roman → Thai and Thai → roman, compares each with VRI's file in the other script, and checks that Python and JS agree. On 14 Oct 2026:
70
+
71
+ | Book | Paragraphs | Words | Mismatches (of 2 × paragraphs) |
72
+ |---|---:|---:|---:|
73
+ | Dīgha Nikāya 1 (s0101m) | 1,046 | 45,891 | 2 |
74
+ | DN 1 commentary (s0101a) | 1,477 | 61,232 | 4 |
75
+ | Jātaka verses 1 (s0513m) | 10,473 | 53,074 | 6 |
76
+ | Majjhima Nikāya 1 (s0201m) | 1,785 | 85,305 | 5 |
77
+ | Dhammasaṅgaṇī (abh01m) | 2,436 | 53,189 | 2 |
78
+ | Vinaya, Pārājika (vin01m) | 2,465 | 72,253 | 4 |
79
+
80
+ Python and JS gave identical output on all of them, and every letter matched. The 23 mismatches are all punctuation and spacing, most of them inconsistencies inside VRI's own files. For example, the Thai file has a ฯ before the *Namo tassa* title that the roman file lacks, and *…pe…* is sometimes written …เป… without the dot.
81
+
82
+ Running the tests:
83
+
84
+ ```
85
+ python3 tests/test_cases.py # 18 fixed cases, Python and JS
86
+ git clone --depth 1 --filter=blob:none --sparse https://github.com/VipassanaTech/tipitaka-xml vri
87
+ git -C vri sparse-checkout set --no-cone /romn/s0101m.mul.xml /thai/s0101m.mul.xml
88
+ python3 tests/vri_check.py vri s0101m.mul
89
+ ```
90
+
91
+ ## Design
92
+
93
+ `axorn/thai.json` holds the letters: consonants, vowels (with their position before or after the consonant), virama, niggahita, digits and punctuation. `__init__.py` and `axorn.js` both read it and mirror each other line for line, so a fix to the table reaches both languages at once.
94
+
95
+ Roman is the hub. Other scripts (Myanmar, Sinhala, …) can be added later as one table each, and any script → any script goes through roman. The reasoning, history and plan are in [wiki/](wiki/index.md).
96
+
97
+ ## Credits
98
+
99
+ axorn was written by Claude (Anthropic) for Korakot Chaovavanich in 2026. Its design builds on Korakot's 2018 notebook, a Python port of the [pitaka.lk](https://pitaka.lk/converter.html) JavaScript converter. Two ideas carry over from it: spelling each Thai syllable from a consonant + vowel pair, with เ/โ placed before the consonant, and the consonant table itself. Korakot's notebook already matched VRI exactly on DN 1, letter for letter.
100
+
101
+ [Aksharamukha](https://github.com/virtualvinodh/aksharamukha-python) served as a cross-check during development.
102
+
103
+ ## License
104
+
105
+ MIT
@@ -0,0 +1,164 @@
1
+ """axorn — Pāli script converter, roman ↔ Thai.
2
+
3
+ >>> from axorn import roman_to_thai, thai_to_roman
4
+ >>> roman_to_thai("Evaṃ me sutaṃ.")
5
+ 'เอวํ เม สุตํฯ'
6
+ >>> thai_to_roman("เอวํ เม สุตํฯ")
7
+ 'evaṃ me sutaṃ.'
8
+
9
+ Both directions read the same table (thai.json), which axorn.js also
10
+ reads, so the Python and JS versions give the same output.
11
+ The defaults follow the VRI (tipitaka.org) Thai edition; see README.md.
12
+ """
13
+ import json
14
+ import re
15
+ import unicodedata
16
+ from pathlib import Path
17
+
18
+ __all__ = ["roman_to_thai", "thai_to_roman"]
19
+
20
+ _T = json.loads((Path(__file__).with_name("thai.json")).read_text(encoding="utf-8"))
21
+
22
+ _R2T_CONS = {r: t for r, t in _T["consonants"]}
23
+ _T2R_CONS = {t: r for r, t in _T["consonants"]} | _T["consonants_thai_alt"]
24
+ _R2T_VOW = {r: (pre, post) for r, pre, post in _T["vowels"]}
25
+ _PRE = {pre: r for r, pre, post in _T["vowels"] if pre} # เ -> e
26
+ _POST = {post: r for r, pre, post in _T["vowels"] if post} # า -> ā
27
+ _CARRIER = _T["vowel_carrier"]
28
+ _VIRAMA = _T["virama"]
29
+ _VIRAMAS = {_VIRAMA, *_T["virama_thai_alt"]}
30
+ _NIG_R, _NIG_T = _T["niggahita"]
31
+ _NIG_R_ALL = [_NIG_R, *_T["niggahita_roman_alt"]]
32
+ _UE = _T["i_niggahita_ligature"]
33
+ _DIG_R, _DIG_T = _T["digits"]
34
+ _STOP_R, _STOP_T = _T["full_stop"]
35
+ _PE_R, _PE_T = _T["peyyala"] # the elision mark "…pe…", also "… pe…"
36
+ _PE_RE_R = re.compile(r"(…\s?)" + re.escape(_PE_R))
37
+ _PE_RE_T = re.compile(r"(…\s?)" + re.escape(_PE_T))
38
+
39
+ # Longest letters first, so "kh" wins over "k".
40
+ _letters = sorted([*_R2T_CONS, *_R2T_VOW, *_NIG_R_ALL], key=len, reverse=True)
41
+ _R_TOKEN = re.compile("|".join(map(re.escape, _letters)) + "|.", re.S)
42
+
43
+
44
+ def roman_to_thai(text: str, *, ue: bool = False, punct: bool = True) -> str:
45
+ """Roman Pāli -> Thai script.
46
+
47
+ ue -- write -iṃ as ึ (common in Thai prints) instead of ิํ (VRI).
48
+ punct -- digits -> Thai digits, and a sentence-ending full stop -> ฯ
49
+ (abbreviation dots stay "."; see _thai_punct).
50
+ """
51
+ text = unicodedata.normalize("NFC", text).lower()
52
+ if punct:
53
+ text = _thai_punct(text)
54
+ toks = _R_TOKEN.findall(text)
55
+ out = []
56
+ i = 0
57
+ while i < len(toks):
58
+ tok = toks[i]
59
+ if tok in _R2T_CONS:
60
+ nxt = toks[i + 1] if i + 1 < len(toks) else ""
61
+ if nxt in _R2T_VOW:
62
+ pre, post = _R2T_VOW[nxt]
63
+ out.append(pre + _R2T_CONS[tok] + post)
64
+ i += 1
65
+ else:
66
+ out.append(_R2T_CONS[tok] + _VIRAMA)
67
+ elif tok in _R2T_VOW:
68
+ pre, post = _R2T_VOW[tok]
69
+ out.append(pre + _CARRIER + post)
70
+ elif tok in _NIG_R_ALL:
71
+ if ue and out and out[-1].endswith("ิ"):
72
+ out[-1] = out[-1][:-1] + _UE
73
+ else:
74
+ out.append(_NIG_T)
75
+ else:
76
+ out.append(tok)
77
+ i += 1
78
+ return "".join(out)
79
+
80
+
81
+ _NEXT_IS_ABBREV = re.compile(r"\s*\d|\s+[^\W\d_]{1,4}\.")
82
+ _PREV_IS_ABBREV = re.compile(r"[\s(][^\W\d_]{1,4}\.\s+[^\W\d_]{1,4}$")
83
+
84
+
85
+ def _thai_punct(s: str) -> str:
86
+ """Roman "." is two marks in VRI's Devanagari master: a full stop (।),
87
+ written ฯ in Thai, and an abbreviation sign (॰), kept as "." in Thai.
88
+ A dot counts as an abbreviation when it touches a digit ("12.", "1.7"),
89
+ sits inside parentheses ("(sī. syā.)"), comes before ")" or ",", or is
90
+ followed by a number or another short abbreviation ("dī. ni. 1.1"),
91
+ or ends a chain of them ("sī. pī. potthakesu").
92
+ Every other dot is a full stop."""
93
+ out = []
94
+ depth = 0
95
+ for i, c in enumerate(s):
96
+ if c == "(":
97
+ depth += 1
98
+ elif c == ")":
99
+ depth = max(depth - 1, 0)
100
+ if c in _DIG_R:
101
+ out.append(_DIG_T[_DIG_R.index(c)])
102
+ elif c == _STOP_R:
103
+ prev = s[i - 1] if i else ""
104
+ nxt = s[i + 1] if i + 1 < len(s) else ""
105
+ abbrev = (prev.isdigit() or nxt.isdigit() or (nxt != "" and nxt in "),")
106
+ or depth > 0 or _NEXT_IS_ABBREV.match(s, i + 1)
107
+ or _PREV_IS_ABBREV.search((" " if i < 30 else "") + s[max(0, i - 30):i]))
108
+ out.append(_STOP_R if abbrev else _STOP_T)
109
+ else:
110
+ out.append(c)
111
+ return _PE_RE_R.sub(lambda m: m.group(1) + _PE_T, "".join(out))
112
+
113
+
114
+ def thai_to_roman(text: str, *, punct: bool = True) -> str:
115
+ """Thai-script Pāli -> roman (lower case; Thai has no capitals).
116
+
117
+ Reads both conventions for -iṃ (ิํ and ึ) and both placements of
118
+ เ/โ in a cluster (ตุมฺเห and ตุเมฺห).
119
+ punct -- Thai digits -> 0-9, and ฯ -> full stop.
120
+ """
121
+ if punct:
122
+ text = _PE_RE_T.sub(lambda m: m.group(1) + _PE_R, text)
123
+ out = []
124
+ pending = "" # a prefix vowel (เ/โ) waiting for its consonant
125
+ n = len(text)
126
+ i = 0
127
+ while i < n:
128
+ c = text[i]
129
+ if c in _PRE:
130
+ pending = _PRE[c]
131
+ elif c in _T2R_CONS or c == _CARRIER:
132
+ base = _T2R_CONS.get(c, "")
133
+ nxt = text[i + 1] if i + 1 < n else ""
134
+ if nxt in _VIRAMAS:
135
+ out.append(base) # pending vowel belongs to a later consonant
136
+ i += 1
137
+ elif nxt in _POST:
138
+ out.append(base + _POST[nxt])
139
+ pending = ""
140
+ i += 1
141
+ elif nxt == _UE:
142
+ out.append(base + "i" + _NIG_R)
143
+ pending = ""
144
+ i += 1
145
+ else:
146
+ out.append(base + (pending or "a"))
147
+ pending = ""
148
+ else:
149
+ if pending: # a stray เ/โ with no consonant after it
150
+ out.append(pending)
151
+ pending = ""
152
+ if c == _NIG_T:
153
+ out.append(_NIG_R)
154
+ elif punct and c in _DIG_T:
155
+ out.append(_DIG_R[_DIG_T.index(c)])
156
+ elif punct and c == _STOP_T:
157
+ out.append(_STOP_R)
158
+ else:
159
+ out.append(c)
160
+ i += 1
161
+ if pending:
162
+ out.append(pending)
163
+ return "".join(out)
164
+
@@ -0,0 +1,6 @@
1
+ """python3 -m axorn thai < roman.txt > thai.txt (or: roman)"""
2
+ import sys
3
+ from . import roman_to_thai, thai_to_roman
4
+
5
+ f = {"thai": roman_to_thai, "roman": thai_to_roman}[sys.argv[1]]
6
+ sys.stdout.write(f(sys.stdin.read()))
@@ -0,0 +1,25 @@
1
+ {
2
+ "about": "Pāli letters in roman and Thai script. The single source of truth for __init__.py and axorn.js. Roman is the hub: one table per script.",
3
+ "consonants": [
4
+ ["k", "ก"], ["kh", "ข"], ["g", "ค"], ["gh", "ฆ"], ["ṅ", "ง"],
5
+ ["c", "จ"], ["ch", "ฉ"], ["j", "ช"], ["jh", "ฌ"], ["ñ", "ญ"],
6
+ ["ṭ", "ฏ"], ["ṭh", "ฐ"], ["ḍ", "ฑ"], ["ḍh", "ฒ"], ["ṇ", "ณ"],
7
+ ["t", "ต"], ["th", "ถ"], ["d", "ท"], ["dh", "ธ"], ["n", "น"],
8
+ ["p", "ป"], ["ph", "ผ"], ["b", "พ"], ["bh", "ภ"], ["m", "ม"],
9
+ ["y", "ย"], ["r", "ร"], ["l", "ล"], ["v", "ว"], ["s", "ส"], ["h", "ห"], ["ḷ", "ฬ"]
10
+ ],
11
+ "consonants_thai_alt": {"ฎ": "ṭ"},
12
+ "vowels": [
13
+ ["a", "", ""], ["ā", "", "า"], ["i", "", "ิ"], ["ī", "", "ี"],
14
+ ["u", "", "ุ"], ["ū", "", "ู"], ["e", "เ", ""], ["o", "โ", ""]
15
+ ],
16
+ "vowel_carrier": "อ",
17
+ "virama": "ฺ",
18
+ "virama_thai_alt": ["๎"],
19
+ "niggahita": ["ṃ", "ํ"],
20
+ "niggahita_roman_alt": ["ṁ", "ŋ"],
21
+ "i_niggahita_ligature": "ึ",
22
+ "digits": ["0123456789", "๐๑๒๓๔๕๖๗๘๙"],
23
+ "full_stop": [".", "ฯ"],
24
+ "peyyala": ["pe…", "เป.…"]
25
+ }
@@ -0,0 +1,116 @@
1
+ Metadata-Version: 2.4
2
+ Name: axorn
3
+ Version: 0.1.0
4
+ Summary: Pāli script converter, roman ↔ Thai, for Python and JS from one shared table
5
+ License-Expression: MIT
6
+ Project-URL: Repository, https://github.com/korakot/axorn
7
+ Requires-Python: >=3.9
8
+ Description-Content-Type: text/markdown
9
+ License-File: LICENSE
10
+ Dynamic: license-file
11
+
12
+ # axorn
13
+
14
+ **English** | [ไทย](README.th.md)
15
+
16
+ Pāli script converter, roman ↔ Thai (อักษร), for Python and JS from one shared table.
17
+
18
+ Pāli has no script of its own, so the same text is written in roman (*evaṃ me sutaṃ*) and in Thai script (เอวํ เม สุตํ). axorn converts between the two, both ways, and gives exactly the same output in Python (for backends) and JS (for web pages). It follows the conventions of the VRI Tipiṭaka (tipitaka.org), and is tested against VRI's own files.
19
+
20
+ ## Use
21
+
22
+ **Try it in the browser:** <https://korakot.github.io/axorn/> — paste roman or Thai and the other follows as you type. It runs the same `axorn.js`, entirely in your browser.
23
+
24
+ Python:
25
+
26
+ ```python
27
+ from axorn import roman_to_thai, thai_to_roman
28
+
29
+ roman_to_thai("Evaṃ me sutaṃ.") # 'เอวํ เม สุตํฯ'
30
+ thai_to_roman("เอวํ เม สุตํฯ") # 'evaṃ me sutaṃ.'
31
+ ```
32
+
33
+ JavaScript (ES module, Node 22+ or a recent browser):
34
+
35
+ ```js
36
+ import { romanToThai, thaiToRoman } from "./axorn/axorn.js";
37
+
38
+ romanToThai("Evaṃ me sutaṃ."); // "เอวํ เม สุตํฯ"
39
+ thaiToRoman("เอวํ เม สุตํฯ"); // "evaṃ me sutaṃ."
40
+ ```
41
+
42
+ Command line: `python3 -m axorn thai < roman.txt > thai.txt` (or `roman` for the other way).
43
+
44
+ Options:
45
+
46
+ - `ue` (roman → Thai): write -iṃ as ึ, as many Thai prints do (กึ), instead of VRI's ิํ (กิํ).
47
+ - `punct` (both ways, on by default): convert digits (12 ↔ ๑๒) and full stops (. ↔ ฯ). Turn it off to convert letters only.
48
+
49
+ ## Conventions
50
+
51
+ Thai-script Pāli is written in more than one way. axorn writes the VRI way and reads all the common ones:
52
+
53
+ | | axorn writes (VRI) | axorn also reads |
54
+ |---|---|---|
55
+ | -iṃ, e.g. *kiṃ* | กิํ | กึ |
56
+ | e/o before a consonant cluster, e.g. *tumhe* | ตุมฺเห (before the last consonant) | ตุเมฺห (before the whole cluster) |
57
+ | *ṭ* | ฏ | ฎ |
58
+ | virama (the dot under a consonant) | ฺ (phinthu) | ๎ (yamakkan) |
59
+ | niggahita | ṃ | ṁ, ŋ in roman input |
60
+
61
+ Two Pāli facts the converter relies on:
62
+
63
+ - **a + i and a + u are two vowels.** Pāli has no *ai* or *au* diphthong, so *sauddesaṃ* (sa-uddesaṃ) is สอุทฺเทสํ, not เสาทฺเทสํ.
64
+ - **A vowel that starts a syllable takes อ**, e.g. *okkāka* → โอกฺกาก.
65
+
66
+ **Full stops.** VRI's master text is in Devanagari, which has two marks where roman has one dot. The full stop (।) is ฯ in Thai, while the abbreviation sign (॰) stays "." in Thai, as in *dī. ni.* (ที. นิ.). Going roman → Thai, axorn treats a dot as an abbreviation when it:
67
+
68
+ - touches a digit ("12.", "1.7");
69
+ - sits inside parentheses;
70
+ - comes before ")" or ",";
71
+ - is followed by a number or another short abbreviation;
72
+ - or ends a chain of them ("sī. pī. potthakesu").
73
+
74
+ Every other dot is a full stop. The elision mark *…pe…* is written …เป.… in Thai.
75
+
76
+ Thai has no capital letters, so Thai → roman returns lower case.
77
+
78
+ ## Tested on
79
+
80
+ `tests/vri_check.py` converts every paragraph of a VRI book roman → Thai and Thai → roman, compares each with VRI's file in the other script, and checks that Python and JS agree. On 14 Oct 2026:
81
+
82
+ | Book | Paragraphs | Words | Mismatches (of 2 × paragraphs) |
83
+ |---|---:|---:|---:|
84
+ | Dīgha Nikāya 1 (s0101m) | 1,046 | 45,891 | 2 |
85
+ | DN 1 commentary (s0101a) | 1,477 | 61,232 | 4 |
86
+ | Jātaka verses 1 (s0513m) | 10,473 | 53,074 | 6 |
87
+ | Majjhima Nikāya 1 (s0201m) | 1,785 | 85,305 | 5 |
88
+ | Dhammasaṅgaṇī (abh01m) | 2,436 | 53,189 | 2 |
89
+ | Vinaya, Pārājika (vin01m) | 2,465 | 72,253 | 4 |
90
+
91
+ Python and JS gave identical output on all of them, and every letter matched. The 23 mismatches are all punctuation and spacing, most of them inconsistencies inside VRI's own files. For example, the Thai file has a ฯ before the *Namo tassa* title that the roman file lacks, and *…pe…* is sometimes written …เป… without the dot.
92
+
93
+ Running the tests:
94
+
95
+ ```
96
+ python3 tests/test_cases.py # 18 fixed cases, Python and JS
97
+ git clone --depth 1 --filter=blob:none --sparse https://github.com/VipassanaTech/tipitaka-xml vri
98
+ git -C vri sparse-checkout set --no-cone /romn/s0101m.mul.xml /thai/s0101m.mul.xml
99
+ python3 tests/vri_check.py vri s0101m.mul
100
+ ```
101
+
102
+ ## Design
103
+
104
+ `axorn/thai.json` holds the letters: consonants, vowels (with their position before or after the consonant), virama, niggahita, digits and punctuation. `__init__.py` and `axorn.js` both read it and mirror each other line for line, so a fix to the table reaches both languages at once.
105
+
106
+ Roman is the hub. Other scripts (Myanmar, Sinhala, …) can be added later as one table each, and any script → any script goes through roman. The reasoning, history and plan are in [wiki/](wiki/index.md).
107
+
108
+ ## Credits
109
+
110
+ axorn was written by Claude (Anthropic) for Korakot Chaovavanich in 2026. Its design builds on Korakot's 2018 notebook, a Python port of the [pitaka.lk](https://pitaka.lk/converter.html) JavaScript converter. Two ideas carry over from it: spelling each Thai syllable from a consonant + vowel pair, with เ/โ placed before the consonant, and the consonant table itself. Korakot's notebook already matched VRI exactly on DN 1, letter for letter.
111
+
112
+ [Aksharamukha](https://github.com/virtualvinodh/aksharamukha-python) served as a cross-check during development.
113
+
114
+ ## License
115
+
116
+ MIT
@@ -0,0 +1,11 @@
1
+ LICENSE
2
+ README.md
3
+ pyproject.toml
4
+ axorn/__init__.py
5
+ axorn/__main__.py
6
+ axorn/thai.json
7
+ axorn.egg-info/PKG-INFO
8
+ axorn.egg-info/SOURCES.txt
9
+ axorn.egg-info/dependency_links.txt
10
+ axorn.egg-info/top_level.txt
11
+ tests/test_cases.py
@@ -0,0 +1 @@
1
+ axorn
@@ -0,0 +1,20 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "axorn"
7
+ version = "0.1.0"
8
+ description = "Pāli script converter, roman ↔ Thai, for Python and JS from one shared table"
9
+ readme = "README.md"
10
+ license = "MIT"
11
+ requires-python = ">=3.9"
12
+
13
+ [project.urls]
14
+ Repository = "https://github.com/korakot/axorn"
15
+
16
+ [tool.setuptools]
17
+ packages = ["axorn"]
18
+
19
+ [tool.setuptools.package-data]
20
+ axorn = ["thai.json"]
axorn-0.1.0/setup.cfg ADDED
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,37 @@
1
+ """Fixed cases from tests/cases.json, run on both Python and JS. python3 tests/test_cases.py"""
2
+ import json, subprocess, sys
3
+ from pathlib import Path
4
+ HERE = Path(__file__).resolve().parent
5
+ sys.path.insert(0, str(HERE.parent))
6
+ from axorn import roman_to_thai, thai_to_roman
7
+
8
+ JS = """
9
+ import { romanToThai, thaiToRoman } from "../axorn/axorn.js";
10
+ import cases from "./cases.json" with { type: "json" };
11
+ console.log(JSON.stringify(cases.map(c => [
12
+ romanToThai(c.roman, { ue: !!c.ue }), thaiToRoman(c.thai)])));
13
+ """
14
+
15
+ def main():
16
+ cases = json.loads((HERE / "cases.json").read_text(encoding="utf-8"))
17
+ tmp = HERE / "_cases_run.mjs"
18
+ tmp.write_text(JS, encoding="utf-8")
19
+ try:
20
+ js = json.loads(subprocess.run(["node", str(tmp)], capture_output=True, text=True, check=True).stdout)
21
+ finally:
22
+ tmp.unlink()
23
+ fails = 0
24
+ for c, (js_t, js_r) in zip(cases, js):
25
+ checks = []
26
+ if not c.get("reverse_only"):
27
+ checks += [("py r→t", roman_to_thai(c["roman"], ue=c.get("ue", False)), c["thai"]), ("js r→t", js_t, c["thai"])]
28
+ if not c.get("oneway"):
29
+ checks += [("py t→r", thai_to_roman(c["thai"]), c["roman"]), ("js t→r", js_r, c["roman"])]
30
+ for name, got, want in checks:
31
+ if got != want:
32
+ fails += 1
33
+ print(f"FAIL {name}: {got!r} != {want!r} ({c.get('note', '')})")
34
+ print(f"{len(cases)} cases, {fails} failures")
35
+ sys.exit(1 if fails else 0)
36
+
37
+ main()