arabic-lint 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.
- arabic_lint-0.1.0/LICENSE +21 -0
- arabic_lint-0.1.0/PKG-INFO +120 -0
- arabic_lint-0.1.0/README.md +100 -0
- arabic_lint-0.1.0/pyproject.toml +32 -0
- arabic_lint-0.1.0/setup.cfg +4 -0
- arabic_lint-0.1.0/src/arabic_lint/__init__.py +0 -0
- arabic_lint-0.1.0/src/arabic_lint/cli.py +112 -0
- arabic_lint-0.1.0/src/arabic_lint/detect.py +184 -0
- arabic_lint-0.1.0/src/arabic_lint.egg-info/PKG-INFO +120 -0
- arabic_lint-0.1.0/src/arabic_lint.egg-info/SOURCES.txt +12 -0
- arabic_lint-0.1.0/src/arabic_lint.egg-info/dependency_links.txt +1 -0
- arabic_lint-0.1.0/src/arabic_lint.egg-info/entry_points.txt +2 -0
- arabic_lint-0.1.0/src/arabic_lint.egg-info/top_level.txt +1 -0
- arabic_lint-0.1.0/tests/test_detect.py +105 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Syamjith NK
|
|
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,120 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: arabic-lint
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Find Arabic text corrupted by reshape+bidi before it was stored. Zero dependencies.
|
|
5
|
+
Author-email: Syamjith NK <hello@syamjithnk.com>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/Syamjith-NK/arabic-lint
|
|
8
|
+
Project-URL: Issues, https://github.com/Syamjith-NK/arabic-lint/issues
|
|
9
|
+
Keywords: arabic,unicode,bidi,linter,i18n,rtl,text-shaping
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: Natural Language :: Arabic
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Topic :: Software Development :: Quality Assurance
|
|
15
|
+
Classifier: Topic :: Text Processing :: Linguistic
|
|
16
|
+
Requires-Python: >=3.9
|
|
17
|
+
Description-Content-Type: text/markdown
|
|
18
|
+
License-File: LICENSE
|
|
19
|
+
Dynamic: license-file
|
|
20
|
+
|
|
21
|
+
# arabic-lint
|
|
22
|
+
|
|
23
|
+
Finds Arabic text that was corrupted **before it was stored** — in your JSON, your
|
|
24
|
+
localisation files, your database exports, your source code.
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
pip install arabic-lint
|
|
28
|
+
arabic-lint ./src
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
```
|
|
32
|
+
src/strings.json:3:20: 21 Arabic presentation forms stored [UNSAFE TO AUTO-FIX]
|
|
33
|
+
found : ﺓﺪﺤﺘﻤﻟﺍ ﺔﻴﺑﺮﻌﻟﺍ ﺕﺍﺭﺎﻣﻹﺍ
|
|
34
|
+
would be : اإلمارات العربية المتحدة
|
|
35
|
+
contains a lam-alef ligature; NFKC decomposition reorders the pair, so this
|
|
36
|
+
recovery is wrong even though it looks like Arabic
|
|
37
|
+
|
|
38
|
+
3 corrupted span(s) in 1 file(s); 1 cannot be auto-fixed safely.
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Exit code 1 when anything is found, so it drops into CI unchanged.
|
|
42
|
+
MIT. **Zero dependencies.** Python 3.9+.
|
|
43
|
+
|
|
44
|
+
## What it actually detects
|
|
45
|
+
|
|
46
|
+
The most widely copied recipe for "making Arabic work" in Python is:
|
|
47
|
+
|
|
48
|
+
```python
|
|
49
|
+
text = get_display(arabic_reshaper.reshape(text))
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Those two calls do the job a text engine is supposed to do: substitute each letter
|
|
53
|
+
for its contextual *presentation form*, and reorder the string into visual order.
|
|
54
|
+
If your renderer already does complex text layout — Pillow with Raqm, matplotlib,
|
|
55
|
+
any browser — the work happens twice and the output is wrong.
|
|
56
|
+
|
|
57
|
+
The real damage is when that string gets **written back**: to a config, an export,
|
|
58
|
+
a translation file. Now the corruption is at rest and every downstream reader
|
|
59
|
+
inherits it. It renders as clean-looking Arabic, so nobody who does not read the
|
|
60
|
+
script will ever notice.
|
|
61
|
+
|
|
62
|
+
The signature is unambiguous: Arabic **Presentation Forms-B** codepoints
|
|
63
|
+
(U+FE70–U+FEFF) in stored text. Those exist for legacy-encoding compatibility;
|
|
64
|
+
correctly authored modern Arabic never contains them.
|
|
65
|
+
|
|
66
|
+
## Why it reports instead of fixing
|
|
67
|
+
|
|
68
|
+
You would think you could just undo it: `NFKC` maps every presentation form back
|
|
69
|
+
to its base letter, and reversing undoes the visual reordering. That round-trips
|
|
70
|
+
exactly — **until the span contains a lam-alef ligature**.
|
|
71
|
+
|
|
72
|
+
A lam-alef ligature (`لا`, `لأ`, `لإ`, `لآ`) is *one* codepoint standing for *two*
|
|
73
|
+
letters. NFKC expands it in logical order while the text around it is still in
|
|
74
|
+
visual order, so the pair comes out reversed relative to its neighbours:
|
|
75
|
+
|
|
76
|
+
| original | naive "fix" | |
|
|
77
|
+
|---|---|---|
|
|
78
|
+
| `الإمارات` | `اإلمارات` | not a word |
|
|
79
|
+
| `السلام` | `السالم` | a **real but different** word |
|
|
80
|
+
|
|
81
|
+
That second row is the whole reason this tool exists rather than a `sed` command.
|
|
82
|
+
The output is still pronounceable Arabic, so it survives a proofread — and the
|
|
83
|
+
definite article followed by alef is one of the most common sequences in the
|
|
84
|
+
language, so this is not a corner case.
|
|
85
|
+
|
|
86
|
+
`arabic-lint` shows you the candidate recovery and tells you when it is unsafe.
|
|
87
|
+
It never rewrites your files.
|
|
88
|
+
|
|
89
|
+
## Verification
|
|
90
|
+
|
|
91
|
+
- **10/10 tests**, no runtime dependency on `arabic_reshaper` or `python-bidi`
|
|
92
|
+
(fixtures are recorded from a real run of both).
|
|
93
|
+
- Block boundaries were **measured, not assumed**: over a wide Arabic sample,
|
|
94
|
+
`arabic_reshaper` 3.0.0 emits 53 distinct codepoints from Presentation Forms-B
|
|
95
|
+
and never emits U+FEFF.
|
|
96
|
+
- **Validated against 3,826 real files** — the false positives that scan found are
|
|
97
|
+
now regression tests:
|
|
98
|
+
- **U+FEFF** sits inside Forms-B but is the byte order mark. Excluded.
|
|
99
|
+
- **Presentation Forms-A is deliberately not a signal.** The reshaper emits
|
|
100
|
+
exactly one codepoint from it (U+FDF2, the Allah ligature), and that character
|
|
101
|
+
— like `ﷺ` U+FDFA, `ﷻ` U+FDFB and `﷽` U+FDFD — is used *intentionally* in
|
|
102
|
+
ordinary Arabic writing. Treating the block as corruption flags correct
|
|
103
|
+
religious and formal text.
|
|
104
|
+
|
|
105
|
+
## Known limits
|
|
106
|
+
|
|
107
|
+
- A document whose only Arabic is a standalone Allah ligature is missed. That is
|
|
108
|
+
the deliberate trade above; any corrupted phrase around it still trips Forms-B.
|
|
109
|
+
- The recovery direction assumes bidi was applied. Text that was reshaped but
|
|
110
|
+
*not* reordered recovers reversed. The tool shows you the candidate so you can
|
|
111
|
+
see which case you have; it does not guess.
|
|
112
|
+
- It detects corruption that is *already stored*. It cannot tell you whether your
|
|
113
|
+
rendering pipeline is about to create some — for that, check
|
|
114
|
+
`PIL.features.check("raqm")` at runtime in the environment doing the rendering.
|
|
115
|
+
|
|
116
|
+
## Related
|
|
117
|
+
|
|
118
|
+
Part of a series measuring where Arabic silently breaks in software.
|
|
119
|
+
See also [`arabic-tts-frontend`](https://pypi.org/project/arabic-tts-frontend/) —
|
|
120
|
+
numerals, dates and currency converted to spoken Arabic before synthesis.
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
# arabic-lint
|
|
2
|
+
|
|
3
|
+
Finds Arabic text that was corrupted **before it was stored** — in your JSON, your
|
|
4
|
+
localisation files, your database exports, your source code.
|
|
5
|
+
|
|
6
|
+
```bash
|
|
7
|
+
pip install arabic-lint
|
|
8
|
+
arabic-lint ./src
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
```
|
|
12
|
+
src/strings.json:3:20: 21 Arabic presentation forms stored [UNSAFE TO AUTO-FIX]
|
|
13
|
+
found : ﺓﺪﺤﺘﻤﻟﺍ ﺔﻴﺑﺮﻌﻟﺍ ﺕﺍﺭﺎﻣﻹﺍ
|
|
14
|
+
would be : اإلمارات العربية المتحدة
|
|
15
|
+
contains a lam-alef ligature; NFKC decomposition reorders the pair, so this
|
|
16
|
+
recovery is wrong even though it looks like Arabic
|
|
17
|
+
|
|
18
|
+
3 corrupted span(s) in 1 file(s); 1 cannot be auto-fixed safely.
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Exit code 1 when anything is found, so it drops into CI unchanged.
|
|
22
|
+
MIT. **Zero dependencies.** Python 3.9+.
|
|
23
|
+
|
|
24
|
+
## What it actually detects
|
|
25
|
+
|
|
26
|
+
The most widely copied recipe for "making Arabic work" in Python is:
|
|
27
|
+
|
|
28
|
+
```python
|
|
29
|
+
text = get_display(arabic_reshaper.reshape(text))
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Those two calls do the job a text engine is supposed to do: substitute each letter
|
|
33
|
+
for its contextual *presentation form*, and reorder the string into visual order.
|
|
34
|
+
If your renderer already does complex text layout — Pillow with Raqm, matplotlib,
|
|
35
|
+
any browser — the work happens twice and the output is wrong.
|
|
36
|
+
|
|
37
|
+
The real damage is when that string gets **written back**: to a config, an export,
|
|
38
|
+
a translation file. Now the corruption is at rest and every downstream reader
|
|
39
|
+
inherits it. It renders as clean-looking Arabic, so nobody who does not read the
|
|
40
|
+
script will ever notice.
|
|
41
|
+
|
|
42
|
+
The signature is unambiguous: Arabic **Presentation Forms-B** codepoints
|
|
43
|
+
(U+FE70–U+FEFF) in stored text. Those exist for legacy-encoding compatibility;
|
|
44
|
+
correctly authored modern Arabic never contains them.
|
|
45
|
+
|
|
46
|
+
## Why it reports instead of fixing
|
|
47
|
+
|
|
48
|
+
You would think you could just undo it: `NFKC` maps every presentation form back
|
|
49
|
+
to its base letter, and reversing undoes the visual reordering. That round-trips
|
|
50
|
+
exactly — **until the span contains a lam-alef ligature**.
|
|
51
|
+
|
|
52
|
+
A lam-alef ligature (`لا`, `لأ`, `لإ`, `لآ`) is *one* codepoint standing for *two*
|
|
53
|
+
letters. NFKC expands it in logical order while the text around it is still in
|
|
54
|
+
visual order, so the pair comes out reversed relative to its neighbours:
|
|
55
|
+
|
|
56
|
+
| original | naive "fix" | |
|
|
57
|
+
|---|---|---|
|
|
58
|
+
| `الإمارات` | `اإلمارات` | not a word |
|
|
59
|
+
| `السلام` | `السالم` | a **real but different** word |
|
|
60
|
+
|
|
61
|
+
That second row is the whole reason this tool exists rather than a `sed` command.
|
|
62
|
+
The output is still pronounceable Arabic, so it survives a proofread — and the
|
|
63
|
+
definite article followed by alef is one of the most common sequences in the
|
|
64
|
+
language, so this is not a corner case.
|
|
65
|
+
|
|
66
|
+
`arabic-lint` shows you the candidate recovery and tells you when it is unsafe.
|
|
67
|
+
It never rewrites your files.
|
|
68
|
+
|
|
69
|
+
## Verification
|
|
70
|
+
|
|
71
|
+
- **10/10 tests**, no runtime dependency on `arabic_reshaper` or `python-bidi`
|
|
72
|
+
(fixtures are recorded from a real run of both).
|
|
73
|
+
- Block boundaries were **measured, not assumed**: over a wide Arabic sample,
|
|
74
|
+
`arabic_reshaper` 3.0.0 emits 53 distinct codepoints from Presentation Forms-B
|
|
75
|
+
and never emits U+FEFF.
|
|
76
|
+
- **Validated against 3,826 real files** — the false positives that scan found are
|
|
77
|
+
now regression tests:
|
|
78
|
+
- **U+FEFF** sits inside Forms-B but is the byte order mark. Excluded.
|
|
79
|
+
- **Presentation Forms-A is deliberately not a signal.** The reshaper emits
|
|
80
|
+
exactly one codepoint from it (U+FDF2, the Allah ligature), and that character
|
|
81
|
+
— like `ﷺ` U+FDFA, `ﷻ` U+FDFB and `﷽` U+FDFD — is used *intentionally* in
|
|
82
|
+
ordinary Arabic writing. Treating the block as corruption flags correct
|
|
83
|
+
religious and formal text.
|
|
84
|
+
|
|
85
|
+
## Known limits
|
|
86
|
+
|
|
87
|
+
- A document whose only Arabic is a standalone Allah ligature is missed. That is
|
|
88
|
+
the deliberate trade above; any corrupted phrase around it still trips Forms-B.
|
|
89
|
+
- The recovery direction assumes bidi was applied. Text that was reshaped but
|
|
90
|
+
*not* reordered recovers reversed. The tool shows you the candidate so you can
|
|
91
|
+
see which case you have; it does not guess.
|
|
92
|
+
- It detects corruption that is *already stored*. It cannot tell you whether your
|
|
93
|
+
rendering pipeline is about to create some — for that, check
|
|
94
|
+
`PIL.features.check("raqm")` at runtime in the environment doing the rendering.
|
|
95
|
+
|
|
96
|
+
## Related
|
|
97
|
+
|
|
98
|
+
Part of a series measuring where Arabic silently breaks in software.
|
|
99
|
+
See also [`arabic-tts-frontend`](https://pypi.org/project/arabic-tts-frontend/) —
|
|
100
|
+
numerals, dates and currency converted to spoken Arabic before synthesis.
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "arabic-lint"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Find Arabic text corrupted by reshape+bidi before it was stored. Zero dependencies."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.9"
|
|
11
|
+
license = "MIT"
|
|
12
|
+
authors = [{ name = "Syamjith NK", email = "hello@syamjithnk.com" }]
|
|
13
|
+
keywords = ["arabic", "unicode", "bidi", "linter", "i18n", "rtl", "text-shaping"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 4 - Beta",
|
|
16
|
+
"Intended Audience :: Developers",
|
|
17
|
+
"Natural Language :: Arabic",
|
|
18
|
+
"Programming Language :: Python :: 3",
|
|
19
|
+
"Topic :: Software Development :: Quality Assurance",
|
|
20
|
+
"Topic :: Text Processing :: Linguistic",
|
|
21
|
+
]
|
|
22
|
+
dependencies = []
|
|
23
|
+
|
|
24
|
+
[project.urls]
|
|
25
|
+
Homepage = "https://github.com/Syamjith-NK/arabic-lint"
|
|
26
|
+
Issues = "https://github.com/Syamjith-NK/arabic-lint/issues"
|
|
27
|
+
|
|
28
|
+
[project.scripts]
|
|
29
|
+
arabic-lint = "arabic_lint.cli:main"
|
|
30
|
+
|
|
31
|
+
[tool.setuptools.packages.find]
|
|
32
|
+
where = ["src"]
|
|
File without changes
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
"""arabic-lint — find Arabic text that was corrupted before it was stored.
|
|
2
|
+
|
|
3
|
+
arabic-lint path/to/repo
|
|
4
|
+
arabic-lint data.json --json
|
|
5
|
+
arabic-lint . --exclude node_modules --exclude .git
|
|
6
|
+
|
|
7
|
+
Exit codes:
|
|
8
|
+
0 clean
|
|
9
|
+
1 corrupted Arabic found
|
|
10
|
+
2 usage / IO error
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import argparse
|
|
16
|
+
import json
|
|
17
|
+
import sys
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
|
|
20
|
+
from .detect import scan_text
|
|
21
|
+
|
|
22
|
+
TEXT_SUFFIXES = {
|
|
23
|
+
".txt", ".json", ".jsonl", ".csv", ".tsv", ".md", ".yml", ".yaml",
|
|
24
|
+
".xml", ".html", ".htm", ".svg", ".po", ".properties", ".strings",
|
|
25
|
+
".py", ".js", ".ts", ".tsx", ".jsx", ".java", ".kt", ".swift",
|
|
26
|
+
".php", ".rb", ".go", ".rs", ".c", ".h", ".cpp", ".cs", ".sql",
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
DEFAULT_EXCLUDES = {".git", "node_modules", "__pycache__", ".venv", "venv", "dist", "build"}
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def iter_files(root: Path, excludes: set[str]):
|
|
33
|
+
if root.is_file():
|
|
34
|
+
yield root
|
|
35
|
+
return
|
|
36
|
+
for p in sorted(root.rglob("*")):
|
|
37
|
+
if not p.is_file():
|
|
38
|
+
continue
|
|
39
|
+
if any(part in excludes for part in p.parts):
|
|
40
|
+
continue
|
|
41
|
+
if p.suffix.lower() in TEXT_SUFFIXES:
|
|
42
|
+
yield p
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def main(argv: list[str] | None = None) -> int:
|
|
46
|
+
ap = argparse.ArgumentParser(
|
|
47
|
+
prog="arabic-lint",
|
|
48
|
+
description="Find Arabic text corrupted by reshape+bidi before storage.",
|
|
49
|
+
)
|
|
50
|
+
ap.add_argument("paths", nargs="+", type=Path)
|
|
51
|
+
ap.add_argument("--json", action="store_true", dest="as_json",
|
|
52
|
+
help="machine-readable output")
|
|
53
|
+
ap.add_argument("--exclude", action="append", default=[],
|
|
54
|
+
help="directory name to skip (repeatable)")
|
|
55
|
+
ap.add_argument("--quiet", "-q", action="store_true",
|
|
56
|
+
help="only print the summary line")
|
|
57
|
+
args = ap.parse_args(argv)
|
|
58
|
+
|
|
59
|
+
excludes = DEFAULT_EXCLUDES | set(args.exclude)
|
|
60
|
+
results: list[dict] = []
|
|
61
|
+
scanned = 0
|
|
62
|
+
|
|
63
|
+
for root in args.paths:
|
|
64
|
+
if not root.exists():
|
|
65
|
+
print(f"arabic-lint: no such path: {root}", file=sys.stderr)
|
|
66
|
+
return 2
|
|
67
|
+
for path in iter_files(root, excludes):
|
|
68
|
+
try:
|
|
69
|
+
text = path.read_text(encoding="utf-8")
|
|
70
|
+
except (UnicodeDecodeError, OSError):
|
|
71
|
+
continue
|
|
72
|
+
scanned += 1
|
|
73
|
+
report = scan_text(text)
|
|
74
|
+
for f in report.findings:
|
|
75
|
+
results.append({
|
|
76
|
+
"file": str(path),
|
|
77
|
+
"line": f.line,
|
|
78
|
+
"col": f.col,
|
|
79
|
+
"presentation_forms": f.n_presentation,
|
|
80
|
+
"text": f.text,
|
|
81
|
+
"recovered": f.recovered,
|
|
82
|
+
"safe_to_autofix": f.recoverable,
|
|
83
|
+
"note": f.note,
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
if args.as_json:
|
|
87
|
+
json.dump({"scanned": scanned, "findings": results}, sys.stdout,
|
|
88
|
+
ensure_ascii=False, indent=2)
|
|
89
|
+
sys.stdout.write("\n")
|
|
90
|
+
return 1 if results else 0
|
|
91
|
+
|
|
92
|
+
if not args.quiet:
|
|
93
|
+
for r in results:
|
|
94
|
+
flag = "" if r["safe_to_autofix"] else " [UNSAFE TO AUTO-FIX]"
|
|
95
|
+
print(f"{r['file']}:{r['line']}:{r['col']}: "
|
|
96
|
+
f"{r['presentation_forms']} Arabic presentation forms stored{flag}")
|
|
97
|
+
print(f" found : {r['text']}")
|
|
98
|
+
print(f" would be : {r['recovered']}")
|
|
99
|
+
print(f" {r['note']}")
|
|
100
|
+
print()
|
|
101
|
+
|
|
102
|
+
unsafe = sum(1 for r in results if not r["safe_to_autofix"])
|
|
103
|
+
if results:
|
|
104
|
+
print(f"{len(results)} corrupted span(s) in {scanned} file(s); "
|
|
105
|
+
f"{unsafe} cannot be auto-fixed safely.")
|
|
106
|
+
return 1
|
|
107
|
+
print(f"clean — {scanned} file(s) scanned, no corrupted Arabic found.")
|
|
108
|
+
return 0
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
if __name__ == "__main__":
|
|
112
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
"""Detect Arabic text that was corrupted before it was stored.
|
|
2
|
+
|
|
3
|
+
The corruption this finds is the result of the most widely copied recipe for
|
|
4
|
+
"making Arabic work" in Python:
|
|
5
|
+
|
|
6
|
+
text = get_display(arabic_reshaper.reshape(text))
|
|
7
|
+
|
|
8
|
+
That pair does two things a rendering engine is supposed to do: it substitutes
|
|
9
|
+
each letter for its contextual *presentation form*, and it reorders the string
|
|
10
|
+
into visual order. When the renderer already does complex text layout (Pillow
|
|
11
|
+
with Raqm, matplotlib, any browser), the work is done twice and the output is
|
|
12
|
+
wrong. Worse, the result is often written back to a file, a database or a JSON
|
|
13
|
+
export -- at which point the corruption is at rest, and every downstream reader
|
|
14
|
+
inherits it.
|
|
15
|
+
|
|
16
|
+
The signature is unambiguous: **Arabic Presentation Forms** codepoints in stored
|
|
17
|
+
text. Those blocks exist for compatibility with legacy encodings; correctly
|
|
18
|
+
authored modern Arabic never contains them. Zero false positives on clean
|
|
19
|
+
Arabic, on Arabic with tashkeel, on mixed Arabic/Latin, or on any other script.
|
|
20
|
+
|
|
21
|
+
Zero dependencies. Python 3.9+.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
from __future__ import annotations
|
|
25
|
+
|
|
26
|
+
import unicodedata
|
|
27
|
+
from dataclasses import dataclass, field
|
|
28
|
+
|
|
29
|
+
# Arabic Presentation Forms-B is the corruption signal. Measured against
|
|
30
|
+
# arabic_reshaper 3.0.0 over a wide Arabic sample (26 Aug 2026): it emits 53
|
|
31
|
+
# distinct codepoints from this block and never emits U+FEFF.
|
|
32
|
+
PRESENTATION_B = (0xFE70, 0xFEFF)
|
|
33
|
+
|
|
34
|
+
# U+FEFF sits inside that range but is the BYTE ORDER MARK / zero-width
|
|
35
|
+
# no-break space. It is not Arabic and appears in perfectly healthy files.
|
|
36
|
+
# Flagging it was a real false positive found by scanning a live codebase.
|
|
37
|
+
BOM = 0xFEFF
|
|
38
|
+
|
|
39
|
+
# Presentation Forms-A (U+FB50-U+FDFF) is deliberately NOT a signal. The
|
|
40
|
+
# reshaper emits exactly one codepoint from it -- U+FDF2, the Allah ligature --
|
|
41
|
+
# and that character, like U+FDFA (ﷺ), U+FDFB (ﷻ) and U+FDFD (﷽), is used
|
|
42
|
+
# intentionally in ordinary Arabic writing. Treating the block as corruption
|
|
43
|
+
# flags correct religious and formal text. The cost of leaving it out is that a
|
|
44
|
+
# document containing only the word Allah as a ligature is missed; any real
|
|
45
|
+
# corrupted phrase around it trips Forms-B anyway.
|
|
46
|
+
PRESENTATION_A_INFORMATIONAL = (0xFB50, 0xFDFF)
|
|
47
|
+
|
|
48
|
+
# Arabic proper (letters, tashkeel, Arabic-Indic digits).
|
|
49
|
+
ARABIC = (0x0600, 0x06FF)
|
|
50
|
+
|
|
51
|
+
# The lam-alef ligatures. These are why recovery is not simply reversible:
|
|
52
|
+
# each is ONE codepoint that decomposes to TWO, and it decomposes in logical
|
|
53
|
+
# order while the text around it is in visual order -- so the pair comes out
|
|
54
|
+
# swapped relative to its neighbours.
|
|
55
|
+
LAM_ALEF = frozenset(range(0xFEF5, 0xFEFD)) # U+FEF5..U+FEFC
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _in(cp: int, rng: tuple[int, int]) -> bool:
|
|
59
|
+
return rng[0] <= cp <= rng[1]
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def is_presentation_form(ch: str) -> bool:
|
|
63
|
+
"""True only for codepoints that indicate baked-in Arabic glyph choices."""
|
|
64
|
+
cp = ord(ch)
|
|
65
|
+
return cp != BOM and _in(cp, PRESENTATION_B)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def is_arabic(ch: str) -> bool:
|
|
69
|
+
return _in(ord(ch), ARABIC)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def has_lam_alef(text: str) -> bool:
|
|
73
|
+
"""True if the text contains a lam-alef ligature codepoint."""
|
|
74
|
+
return any(ord(c) in LAM_ALEF for c in text)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
@dataclass
|
|
78
|
+
class Finding:
|
|
79
|
+
"""One corrupted span."""
|
|
80
|
+
|
|
81
|
+
line: int
|
|
82
|
+
col: int
|
|
83
|
+
text: str
|
|
84
|
+
n_presentation: int
|
|
85
|
+
recoverable: bool
|
|
86
|
+
recovered: str | None = None
|
|
87
|
+
note: str = ""
|
|
88
|
+
|
|
89
|
+
def __str__(self) -> str:
|
|
90
|
+
status = "recoverable" if self.recoverable else "UNSAFE to auto-fix"
|
|
91
|
+
return f"{self.line}:{self.col}: {self.n_presentation} presentation forms ({status})"
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
@dataclass
|
|
95
|
+
class Report:
|
|
96
|
+
findings: list[Finding] = field(default_factory=list)
|
|
97
|
+
|
|
98
|
+
@property
|
|
99
|
+
def ok(self) -> bool:
|
|
100
|
+
return not self.findings
|
|
101
|
+
|
|
102
|
+
@property
|
|
103
|
+
def unsafe(self) -> list[Finding]:
|
|
104
|
+
return [f for f in self.findings if not f.recoverable]
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def recover(text: str) -> tuple[str, bool, str]:
|
|
108
|
+
"""Best-effort undo of reshape+bidi.
|
|
109
|
+
|
|
110
|
+
Returns (recovered_text, is_safe, note).
|
|
111
|
+
|
|
112
|
+
NFKC maps each presentation form back to its base letter, and reversing
|
|
113
|
+
undoes the visual reordering. That round-trips exactly -- *unless* the span
|
|
114
|
+
contains a lam-alef ligature.
|
|
115
|
+
|
|
116
|
+
A lam-alef ligature is a single codepoint standing for two letters. NFKC
|
|
117
|
+
expands it in logical order, but the surrounding text is in visual order, so
|
|
118
|
+
the expanded pair ends up reversed relative to everything around it:
|
|
119
|
+
|
|
120
|
+
الإمارات -> اإلمارات ("the Emirates" -> not a word)
|
|
121
|
+
السلام -> السالم ("the peace" -> "as-saalim", a real but different word)
|
|
122
|
+
|
|
123
|
+
That second case is why this is reported rather than silently fixed: the
|
|
124
|
+
output is still pronounceable Arabic, so it survives a proofread.
|
|
125
|
+
"""
|
|
126
|
+
if not any(is_presentation_form(c) for c in text):
|
|
127
|
+
return text, True, "nothing to recover"
|
|
128
|
+
|
|
129
|
+
unsafe = has_lam_alef(text)
|
|
130
|
+
guess = unicodedata.normalize("NFKC", text)[::-1]
|
|
131
|
+
if unsafe:
|
|
132
|
+
return guess, False, (
|
|
133
|
+
"contains a lam-alef ligature; NFKC decomposition reorders the pair, "
|
|
134
|
+
"so this recovery is wrong even though it looks like Arabic"
|
|
135
|
+
)
|
|
136
|
+
return guess, True, "NFKC + reverse round-trips exactly for this span"
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def scan_text(text: str) -> Report:
|
|
140
|
+
"""Find corrupted spans in a string. Reports one finding per contiguous run."""
|
|
141
|
+
report = Report()
|
|
142
|
+
line = 1
|
|
143
|
+
col = 1
|
|
144
|
+
run_start: tuple[int, int] | None = None
|
|
145
|
+
run: list[str] = []
|
|
146
|
+
|
|
147
|
+
def flush() -> None:
|
|
148
|
+
nonlocal run, run_start
|
|
149
|
+
if run and run_start is not None:
|
|
150
|
+
span = "".join(run)
|
|
151
|
+
rec, safe, note = recover(span)
|
|
152
|
+
report.findings.append(
|
|
153
|
+
Finding(
|
|
154
|
+
line=run_start[0],
|
|
155
|
+
col=run_start[1],
|
|
156
|
+
text=span,
|
|
157
|
+
n_presentation=sum(1 for c in span if is_presentation_form(c)),
|
|
158
|
+
recoverable=safe,
|
|
159
|
+
recovered=rec,
|
|
160
|
+
note=note,
|
|
161
|
+
)
|
|
162
|
+
)
|
|
163
|
+
run = []
|
|
164
|
+
run_start = None
|
|
165
|
+
|
|
166
|
+
for ch in text:
|
|
167
|
+
if ch == "\n":
|
|
168
|
+
flush()
|
|
169
|
+
line += 1
|
|
170
|
+
col = 1
|
|
171
|
+
continue
|
|
172
|
+
if is_presentation_form(ch):
|
|
173
|
+
if run_start is None:
|
|
174
|
+
run_start = (line, col)
|
|
175
|
+
run.append(ch)
|
|
176
|
+
else:
|
|
177
|
+
# a space inside a corrupted phrase should not split the finding
|
|
178
|
+
if run_start is not None and ch.isspace():
|
|
179
|
+
run.append(ch)
|
|
180
|
+
else:
|
|
181
|
+
flush()
|
|
182
|
+
col += 1
|
|
183
|
+
flush()
|
|
184
|
+
return report
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: arabic-lint
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Find Arabic text corrupted by reshape+bidi before it was stored. Zero dependencies.
|
|
5
|
+
Author-email: Syamjith NK <hello@syamjithnk.com>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/Syamjith-NK/arabic-lint
|
|
8
|
+
Project-URL: Issues, https://github.com/Syamjith-NK/arabic-lint/issues
|
|
9
|
+
Keywords: arabic,unicode,bidi,linter,i18n,rtl,text-shaping
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: Natural Language :: Arabic
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Topic :: Software Development :: Quality Assurance
|
|
15
|
+
Classifier: Topic :: Text Processing :: Linguistic
|
|
16
|
+
Requires-Python: >=3.9
|
|
17
|
+
Description-Content-Type: text/markdown
|
|
18
|
+
License-File: LICENSE
|
|
19
|
+
Dynamic: license-file
|
|
20
|
+
|
|
21
|
+
# arabic-lint
|
|
22
|
+
|
|
23
|
+
Finds Arabic text that was corrupted **before it was stored** — in your JSON, your
|
|
24
|
+
localisation files, your database exports, your source code.
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
pip install arabic-lint
|
|
28
|
+
arabic-lint ./src
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
```
|
|
32
|
+
src/strings.json:3:20: 21 Arabic presentation forms stored [UNSAFE TO AUTO-FIX]
|
|
33
|
+
found : ﺓﺪﺤﺘﻤﻟﺍ ﺔﻴﺑﺮﻌﻟﺍ ﺕﺍﺭﺎﻣﻹﺍ
|
|
34
|
+
would be : اإلمارات العربية المتحدة
|
|
35
|
+
contains a lam-alef ligature; NFKC decomposition reorders the pair, so this
|
|
36
|
+
recovery is wrong even though it looks like Arabic
|
|
37
|
+
|
|
38
|
+
3 corrupted span(s) in 1 file(s); 1 cannot be auto-fixed safely.
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Exit code 1 when anything is found, so it drops into CI unchanged.
|
|
42
|
+
MIT. **Zero dependencies.** Python 3.9+.
|
|
43
|
+
|
|
44
|
+
## What it actually detects
|
|
45
|
+
|
|
46
|
+
The most widely copied recipe for "making Arabic work" in Python is:
|
|
47
|
+
|
|
48
|
+
```python
|
|
49
|
+
text = get_display(arabic_reshaper.reshape(text))
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Those two calls do the job a text engine is supposed to do: substitute each letter
|
|
53
|
+
for its contextual *presentation form*, and reorder the string into visual order.
|
|
54
|
+
If your renderer already does complex text layout — Pillow with Raqm, matplotlib,
|
|
55
|
+
any browser — the work happens twice and the output is wrong.
|
|
56
|
+
|
|
57
|
+
The real damage is when that string gets **written back**: to a config, an export,
|
|
58
|
+
a translation file. Now the corruption is at rest and every downstream reader
|
|
59
|
+
inherits it. It renders as clean-looking Arabic, so nobody who does not read the
|
|
60
|
+
script will ever notice.
|
|
61
|
+
|
|
62
|
+
The signature is unambiguous: Arabic **Presentation Forms-B** codepoints
|
|
63
|
+
(U+FE70–U+FEFF) in stored text. Those exist for legacy-encoding compatibility;
|
|
64
|
+
correctly authored modern Arabic never contains them.
|
|
65
|
+
|
|
66
|
+
## Why it reports instead of fixing
|
|
67
|
+
|
|
68
|
+
You would think you could just undo it: `NFKC` maps every presentation form back
|
|
69
|
+
to its base letter, and reversing undoes the visual reordering. That round-trips
|
|
70
|
+
exactly — **until the span contains a lam-alef ligature**.
|
|
71
|
+
|
|
72
|
+
A lam-alef ligature (`لا`, `لأ`, `لإ`, `لآ`) is *one* codepoint standing for *two*
|
|
73
|
+
letters. NFKC expands it in logical order while the text around it is still in
|
|
74
|
+
visual order, so the pair comes out reversed relative to its neighbours:
|
|
75
|
+
|
|
76
|
+
| original | naive "fix" | |
|
|
77
|
+
|---|---|---|
|
|
78
|
+
| `الإمارات` | `اإلمارات` | not a word |
|
|
79
|
+
| `السلام` | `السالم` | a **real but different** word |
|
|
80
|
+
|
|
81
|
+
That second row is the whole reason this tool exists rather than a `sed` command.
|
|
82
|
+
The output is still pronounceable Arabic, so it survives a proofread — and the
|
|
83
|
+
definite article followed by alef is one of the most common sequences in the
|
|
84
|
+
language, so this is not a corner case.
|
|
85
|
+
|
|
86
|
+
`arabic-lint` shows you the candidate recovery and tells you when it is unsafe.
|
|
87
|
+
It never rewrites your files.
|
|
88
|
+
|
|
89
|
+
## Verification
|
|
90
|
+
|
|
91
|
+
- **10/10 tests**, no runtime dependency on `arabic_reshaper` or `python-bidi`
|
|
92
|
+
(fixtures are recorded from a real run of both).
|
|
93
|
+
- Block boundaries were **measured, not assumed**: over a wide Arabic sample,
|
|
94
|
+
`arabic_reshaper` 3.0.0 emits 53 distinct codepoints from Presentation Forms-B
|
|
95
|
+
and never emits U+FEFF.
|
|
96
|
+
- **Validated against 3,826 real files** — the false positives that scan found are
|
|
97
|
+
now regression tests:
|
|
98
|
+
- **U+FEFF** sits inside Forms-B but is the byte order mark. Excluded.
|
|
99
|
+
- **Presentation Forms-A is deliberately not a signal.** The reshaper emits
|
|
100
|
+
exactly one codepoint from it (U+FDF2, the Allah ligature), and that character
|
|
101
|
+
— like `ﷺ` U+FDFA, `ﷻ` U+FDFB and `﷽` U+FDFD — is used *intentionally* in
|
|
102
|
+
ordinary Arabic writing. Treating the block as corruption flags correct
|
|
103
|
+
religious and formal text.
|
|
104
|
+
|
|
105
|
+
## Known limits
|
|
106
|
+
|
|
107
|
+
- A document whose only Arabic is a standalone Allah ligature is missed. That is
|
|
108
|
+
the deliberate trade above; any corrupted phrase around it still trips Forms-B.
|
|
109
|
+
- The recovery direction assumes bidi was applied. Text that was reshaped but
|
|
110
|
+
*not* reordered recovers reversed. The tool shows you the candidate so you can
|
|
111
|
+
see which case you have; it does not guess.
|
|
112
|
+
- It detects corruption that is *already stored*. It cannot tell you whether your
|
|
113
|
+
rendering pipeline is about to create some — for that, check
|
|
114
|
+
`PIL.features.check("raqm")` at runtime in the environment doing the rendering.
|
|
115
|
+
|
|
116
|
+
## Related
|
|
117
|
+
|
|
118
|
+
Part of a series measuring where Arabic silently breaks in software.
|
|
119
|
+
See also [`arabic-tts-frontend`](https://pypi.org/project/arabic-tts-frontend/) —
|
|
120
|
+
numerals, dates and currency converted to spoken Arabic before synthesis.
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
src/arabic_lint/__init__.py
|
|
5
|
+
src/arabic_lint/cli.py
|
|
6
|
+
src/arabic_lint/detect.py
|
|
7
|
+
src/arabic_lint.egg-info/PKG-INFO
|
|
8
|
+
src/arabic_lint.egg-info/SOURCES.txt
|
|
9
|
+
src/arabic_lint.egg-info/dependency_links.txt
|
|
10
|
+
src/arabic_lint.egg-info/entry_points.txt
|
|
11
|
+
src/arabic_lint.egg-info/top_level.txt
|
|
12
|
+
tests/test_detect.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
arabic_lint
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
"""Tests. Run: python3 -m pytest -q (or python3 tests/test_detect.py)
|
|
2
|
+
|
|
3
|
+
The reference corruption is produced with arabic_reshaper + python-bidi when they
|
|
4
|
+
are installed; otherwise the same strings are hard-coded from a recorded run, so
|
|
5
|
+
the suite has no runtime dependency on either package.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import sys
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "src"))
|
|
12
|
+
|
|
13
|
+
from arabic_lint.detect import ( # noqa: E402
|
|
14
|
+
has_lam_alef, is_presentation_form, recover, scan_text,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
# Recorded from arabic_reshaper 3.0.0 + python-bidi, verified 26 Aug 2026.
|
|
18
|
+
EMIRATES_CLEAN = "الإمارات"
|
|
19
|
+
EMIRATES_BAD = "ﺕﺍﺭﺎﻣﻹﺍ" # has lam-alef -> unsafe
|
|
20
|
+
MARHABA_CLEAN = "مرحبا"
|
|
21
|
+
MARHABA_BAD = "ﺎﺒﺣﺮﻣ" # no lam-alef -> safe
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def test_clean_arabic_is_not_flagged():
|
|
25
|
+
for s in [EMIRATES_CLEAN, "مَرْحَبًا بِكُمْ", "Total: 1,250 درهم", "hello world",
|
|
26
|
+
"שלום עולם", "المبيعات ٢٠٢٦"]:
|
|
27
|
+
assert scan_text(s).ok, f"false positive on {s!r}"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def test_corrupted_arabic_is_flagged():
|
|
31
|
+
r = scan_text(EMIRATES_BAD)
|
|
32
|
+
assert not r.ok
|
|
33
|
+
assert len(r.findings) == 1
|
|
34
|
+
assert r.findings[0].n_presentation == 7
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def test_lam_alef_span_is_reported_unsafe():
|
|
38
|
+
r = scan_text(EMIRATES_BAD)
|
|
39
|
+
assert has_lam_alef(EMIRATES_BAD)
|
|
40
|
+
assert r.findings[0].recoverable is False
|
|
41
|
+
assert r.unsafe
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def test_span_without_lam_alef_round_trips_exactly():
|
|
45
|
+
recovered, safe, _ = recover(MARHABA_BAD)
|
|
46
|
+
assert safe is True
|
|
47
|
+
assert recovered == MARHABA_CLEAN
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def test_lam_alef_recovery_is_wrong_and_says_so():
|
|
51
|
+
"""The whole reason the tool exists: this 'fix' looks like Arabic and is not."""
|
|
52
|
+
recovered, safe, note = recover(EMIRATES_BAD)
|
|
53
|
+
assert safe is False
|
|
54
|
+
assert recovered != EMIRATES_CLEAN
|
|
55
|
+
assert "lam-alef" in note
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def test_line_and_column_are_reported():
|
|
59
|
+
text = "line one\nok here\n" + EMIRATES_BAD + "\n"
|
|
60
|
+
r = scan_text(text)
|
|
61
|
+
assert r.findings[0].line == 3
|
|
62
|
+
assert r.findings[0].col == 1
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def test_spaces_inside_a_corrupted_phrase_do_not_split_it():
|
|
66
|
+
r = scan_text(MARHABA_BAD + " " + MARHABA_BAD)
|
|
67
|
+
assert len(r.findings) == 1
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def test_presentation_form_classifier():
|
|
71
|
+
assert is_presentation_form("ﺕ")
|
|
72
|
+
assert not is_presentation_form("ا")
|
|
73
|
+
assert not is_presentation_form("a")
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def test_bom_is_not_flagged():
|
|
78
|
+
"""U+FEFF is the byte order mark, not Arabic. Found as a live false positive."""
|
|
79
|
+
assert not is_presentation_form("\ufeff")
|
|
80
|
+
assert scan_text("\ufeff{\"a\": 1}").ok
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def test_legitimate_arabic_ligatures_are_not_flagged():
|
|
84
|
+
"""These are used deliberately in ordinary Arabic writing."""
|
|
85
|
+
for ch in ["\ufdfa", "\ufdfb", "\ufdf2", "\ufdfd"]: # PBUH, jalla jalaaluhu, Allah, bismillah
|
|
86
|
+
assert not is_presentation_form(ch), f"false positive on U+{ord(ch):04X}"
|
|
87
|
+
assert scan_text("قال النبي \ufdfa كلاما طيبا").ok
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _run():
|
|
91
|
+
fns = [v for k, v in sorted(globals().items()) if k.startswith("test_")]
|
|
92
|
+
failed = 0
|
|
93
|
+
for fn in fns:
|
|
94
|
+
try:
|
|
95
|
+
fn()
|
|
96
|
+
print(f" ok {fn.__name__}")
|
|
97
|
+
except AssertionError as e:
|
|
98
|
+
failed += 1
|
|
99
|
+
print(f" FAIL {fn.__name__}: {e}")
|
|
100
|
+
print(f"\n{len(fns) - failed}/{len(fns)} passed")
|
|
101
|
+
return 1 if failed else 0
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
if __name__ == "__main__":
|
|
105
|
+
raise SystemExit(_run())
|