├── tests ├── __init__.py ├── test_rust_bidi.py └── test_python_bidi.py ├── docs ├── authors.rst ├── contributing.rst ├── history.rst ├── modules.rst ├── index.rst ├── bidi.rst ├── Makefile ├── make.bat └── conf.py ├── .github ├── dependabot.yml └── workflows │ └── CI.yml ├── noxfile.py ├── Cargo.toml ├── .editorconfig ├── AUTHORS.rst ├── MANIFEST.in ├── .gitignore ├── .readthedocs.yaml ├── bidi ├── wrapper.py ├── __init__.py ├── mirror.py └── algorithm.py ├── pyproject.toml ├── Makefile ├── src └── lib.rs ├── CHANGELOG.rst ├── LICENSE-THIRD-PARTY.yml ├── CONTRIBUTING.rst ├── README.rst ├── Cargo.lock ├── COPYING.LESSER └── COPYING /tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/authors.rst: -------------------------------------------------------------------------------- 1 | .. include:: ../AUTHORS.rst -------------------------------------------------------------------------------- /docs/contributing.rst: -------------------------------------------------------------------------------- 1 | .. include:: ../CONTRIBUTING.rst -------------------------------------------------------------------------------- /docs/history.rst: -------------------------------------------------------------------------------- 1 | .. include:: ../CHANGELOG.rst 2 | -------------------------------------------------------------------------------- /docs/modules.rst: -------------------------------------------------------------------------------- 1 | bidi 2 | ==== 3 | 4 | .. toctree:: 5 | :maxdepth: 4 6 | 7 | bidi 8 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "cargo" 4 | directory: "/" 5 | schedule: 6 | interval: "daily" 7 | -------------------------------------------------------------------------------- /noxfile.py: -------------------------------------------------------------------------------- 1 | import nox 2 | 3 | 4 | @nox.session 5 | def python(session): 6 | session.env["MATURIN_PEP517_ARGS"] = "--profile=dev" 7 | session.install(".[dev]") 8 | session.run("pytest") 9 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | authors = ["Meir Kriheli "] 3 | name = "python-bidi" 4 | version = "0.6.7" 5 | edition = "2021" 6 | 7 | [lib] 8 | name = "bidi" 9 | crate-type = ["cdylib"] 10 | 11 | [dependencies] 12 | pyo3 = { version = "0.27.1", features = [ 13 | "extension-module", 14 | "generate-import-lib", 15 | ] } 16 | unicode-bidi = "0.3.18" 17 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | 3 | root = true 4 | 5 | [*] 6 | indent_style = space 7 | indent_size = 4 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | charset = utf-8 11 | end_of_line = lf 12 | 13 | [*.bat] 14 | indent_style = tab 15 | end_of_line = crlf 16 | 17 | [LICENSE] 18 | insert_final_newline = false 19 | 20 | [Makefile] 21 | indent_style = tab -------------------------------------------------------------------------------- /AUTHORS.rst: -------------------------------------------------------------------------------- 1 | ======= 2 | Credits 3 | ======= 4 | 5 | Development Lead 6 | ---------------- 7 | 8 | * Meir Kriheli 9 | 10 | Tests 11 | ------ 12 | 13 | Tests based on fribidi_. 14 | 15 | .. _fribidi: http://fribidi.org/ 16 | 17 | Contributors 18 | ------------ 19 | 20 | * Just van Rossum - https://github.com/justvanrossum 21 | * Jakub Wilk 22 | * Christian Clauss 23 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include AUTHORS.rst 2 | include CONTRIBUTING.rst 3 | include CHANGELOG.rst 4 | include COPYING 5 | include COPYING.LESSER 6 | include README.rst 7 | include LICENSE-THIRD-PARTY.yml 8 | include pyproject.toml Cargo.toml 9 | 10 | recursive-include src * 11 | recursive-include tests * 12 | recursive-exclude * __pycache__ 13 | recursive-exclude * *.py[co] 14 | 15 | recursive-include docs *.rst conf.py Makefile make.bat 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[cod] 2 | 3 | # C extensions 4 | *.so 5 | 6 | # Packages 7 | *.egg 8 | *.egg-info 9 | dist 10 | build 11 | eggs 12 | parts 13 | bin 14 | var 15 | sdist 16 | develop-eggs 17 | .installed.cfg 18 | lib 19 | lib64 20 | 21 | # Installer logs 22 | pip-log.txt 23 | 24 | # Unit test / coverage reports 25 | .coverage 26 | .tox 27 | .nox 28 | nosetests.xml 29 | htmlcov 30 | 31 | # Translations 32 | *.mo 33 | 34 | # Mr Developer 35 | .mr.developer.cfg 36 | .project 37 | .pydevproject 38 | 39 | # Complexity 40 | output/*.html 41 | output/*/index.html 42 | 43 | # Sphinx 44 | docs/_build 45 | 46 | # Rust 47 | target 48 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. complexity documentation master file, created by 2 | sphinx-quickstart on Tue Jul 9 22:26:36 2013. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | Welcome to Python BiDi's documentation! 7 | ======================================= 8 | 9 | .. include:: ../README.rst 10 | 11 | Contents: 12 | 13 | .. toctree:: 14 | :maxdepth: 2 15 | 16 | contributing 17 | authors 18 | bidi 19 | history 20 | 21 | Indices and tables 22 | ================== 23 | 24 | * :ref:`genindex` 25 | * :ref:`modindex` 26 | * :ref:`search` 27 | -------------------------------------------------------------------------------- /docs/bidi.rst: -------------------------------------------------------------------------------- 1 | bidi package 2 | ============ 3 | 4 | Submodules 5 | ---------- 6 | 7 | bidi.algorithm module 8 | --------------------- 9 | 10 | .. automodule:: bidi.algorithm 11 | :members: 12 | :undoc-members: 13 | :show-inheritance: 14 | 15 | bidi.bidi module 16 | ---------------- 17 | 18 | .. automodule:: bidi.bidi 19 | :members: 20 | :undoc-members: 21 | :show-inheritance: 22 | 23 | bidi.mirror module 24 | ------------------ 25 | 26 | .. automodule:: bidi.mirror 27 | :members: 28 | :undoc-members: 29 | :show-inheritance: 30 | 31 | bidi.wrapper module 32 | ------------------- 33 | 34 | .. automodule:: bidi.wrapper 35 | :members: 36 | :undoc-members: 37 | :show-inheritance: 38 | 39 | Module contents 40 | --------------- 41 | 42 | .. automodule:: bidi 43 | :members: 44 | :undoc-members: 45 | :show-inheritance: 46 | -------------------------------------------------------------------------------- /.readthedocs.yaml: -------------------------------------------------------------------------------- 1 | # Read the Docs configuration file for Sphinx projects 2 | # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details 3 | 4 | # Required 5 | version: 2 6 | 7 | # Set the OS, Python version and other tools you might need 8 | build: 9 | os: ubuntu-22.04 10 | tools: 11 | python: "3.12" 12 | rust: "latest" 13 | # You can also specify other tool versions: 14 | # nodejs: "20" 15 | # golang: "1.20" 16 | 17 | # Build documentation in the "docs/" directory with Sphinx 18 | sphinx: 19 | configuration: docs/conf.py 20 | # You can configure Sphinx to use a different builder, for instance use the dirhtml builder for simpler URLs 21 | # builder: "dirhtml" 22 | # Fail on all warnings to avoid broken references 23 | # fail_on_warning: true 24 | # Optionally build your docs in additional formats such as PDF and ePub 25 | # formats: 26 | # - pdf 27 | # - epub 28 | 29 | # Optional but recommended, declare the Python requirements required 30 | # to build your documentation 31 | # See https://docs.readthedocs.io/en/stable/guides/reproducible-builds.html 32 | python: 33 | install: 34 | - method: pip 35 | path: . 36 | -------------------------------------------------------------------------------- /bidi/wrapper.py: -------------------------------------------------------------------------------- 1 | """Provides a wrpper for the Rust based implementation.""" 2 | 3 | from typing import Optional, Union 4 | 5 | from .bidi import get_base_level_inner, get_display_inner 6 | 7 | StrOrBytes = Union[str, bytes] 8 | 9 | def get_display( 10 | str_or_bytes: StrOrBytes, 11 | encoding: str = "utf-8", 12 | base_dir: Optional[str] = None, 13 | debug: bool = False, 14 | ) -> StrOrBytes: 15 | """Accepts string or bytes. In case of bytes, `encoding` 16 | is needed as the inner function expects a valid string (default:"utf-8"). 17 | 18 | Set `base_dir` to 'L' or 'R' to override the calculated base_level. 19 | 20 | Set `debug` to True to return the calculated levels. 21 | 22 | Returns the display layout, either as unicode or `encoding` encoded 23 | string. 24 | 25 | """ 26 | if isinstance(str_or_bytes, bytes): 27 | text = str_or_bytes.decode(encoding) 28 | was_decoded = True 29 | else: 30 | text = str_or_bytes 31 | was_decoded = False 32 | 33 | display = get_display_inner(text, base_dir, debug) 34 | 35 | if was_decoded: 36 | display = display.encode(encoding) 37 | 38 | return display 39 | 40 | 41 | def get_base_level(text: str) -> int: 42 | """Returns the base unicode level of the 1st paragraph in `text`. 43 | 44 | Return value of 0 means LTR, while 1 means RTL. 45 | """ 46 | return get_base_level_inner(text) 47 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["maturin>=1,<2"] 3 | build-backend = "maturin" 4 | 5 | [project] 6 | name = "python-bidi" 7 | version = "0.6.7" 8 | readme = "README.rst" 9 | keywords = ["bidi", "unicode", "layout"] 10 | description = "Python Bidi layout wrapping the Rust crate unicode-bidi" 11 | authors = [{ name = "Meir Kriheli", email = "mkriheli@gmail.com" }] 12 | classifiers = [ 13 | "Development Status :: 4 - Beta", 14 | "Intended Audience :: Developers", 15 | "Operating System :: OS Independent", 16 | "License :: OSI Approved :: GNU Library or Lesser General Public License (LGPL)", 17 | "Topic :: Text Processing", 18 | "Programming Language :: Python :: 3", 19 | "Programming Language :: Python :: 3.9", 20 | "Programming Language :: Python :: 3.10", 21 | "Programming Language :: Python :: 3.11", 22 | "Programming Language :: Python :: 3.12", 23 | "Programming Language :: Python :: 3.13", 24 | "Programming Language :: Python :: 3.14", 25 | ] 26 | 27 | [project.urls] 28 | Homepage = "https://github.com/MeirKriheli/python-bidi" 29 | Repository = "https://github.com/MeirKriheli/python-bidi" 30 | Documentation = "https://python-bidi.readthedocs.io/en/latest/" 31 | Issues = "https://github.com/MeirKriheli/python-bidi/issues" 32 | Changelog = "https://github.com/MeirKriheli/python-bidi/blob/master/CHANGELOG.rst" 33 | 34 | [project.scripts] 35 | pybidi = "bidi:main" 36 | 37 | [project.optional-dependencies] 38 | dev = ["pytest"] 39 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: clean-pyc clean-build docs clean 2 | 3 | help: 4 | @echo "clean-build - remove build artifacts" 5 | @echo "clean-pyc - remove Python file artifacts" 6 | @echo "lint - check style with flake8" 7 | @echo "test - run tests quickly with the default Python" 8 | @echo "test-all - run tests on every Python version with tox" 9 | @echo "coverage - check code coverage quickly with the default Python" 10 | @echo "docs - generate Sphinx HTML documentation, including API docs" 11 | @echo "release - package and upload a release" 12 | @echo "dist - package" 13 | 14 | clean: clean-build clean-pyc 15 | rm -fr htmlcov/ 16 | 17 | clean-build: 18 | rm -fr build/ 19 | rm -fr dist/ 20 | rm -fr *.egg-info 21 | 22 | clean-pyc: 23 | find . -name '*.pyc' -exec rm -f {} + 24 | find . -name '*.pyo' -exec rm -f {} + 25 | find . -name '*~' -exec rm -f {} + 26 | 27 | lint: 28 | flake8 bidi tests 29 | 30 | test: 31 | python setup.py test 32 | 33 | test-all: 34 | tox 35 | 36 | coverage: 37 | coverage run --source python-bidi setup.py test 38 | coverage report -m 39 | coverage html 40 | open htmlcov/index.html 41 | 42 | docs: 43 | rm -f docs/bidi.rst 44 | rm -f docs/modules.rst 45 | sphinx-apidoc -o docs/ bidi 46 | $(MAKE) -C docs clean 47 | $(MAKE) -C docs html 48 | open docs/_build/html/index.html 49 | 50 | release: clean 51 | python setup.py sdist upload 52 | python setup.py bdist_wheel upload 53 | 54 | dist: clean 55 | python setup.py sdist 56 | python setup.py bdist_wheel 57 | ls -l dist 58 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | use pyo3::exceptions::PyValueError; 2 | use pyo3::prelude::*; 3 | use unicode_bidi::{BidiInfo, Level}; 4 | 5 | #[pyfunction] 6 | #[pyo3(signature = (text, base_dir=None, debug=false))] 7 | pub fn get_display_inner(text: &str, base_dir: Option, debug: bool) -> PyResult { 8 | let level: Option = match base_dir { 9 | Some('L') => Some(Level::ltr()), 10 | Some('R') => Some(Level::rtl()), 11 | None => None, 12 | _ => return Err(PyValueError::new_err("base_dir can be 'L', 'R' or None")), 13 | }; 14 | let bidi_info = BidiInfo::new(text, level); 15 | 16 | if debug { 17 | return Ok(format!("{bidi_info:#?}")); 18 | } 19 | 20 | let display: String = bidi_info 21 | .paragraphs 22 | .iter() 23 | .map(|para| { 24 | let line = para.range.clone(); 25 | bidi_info.reorder_line(para, line) 26 | }) 27 | .collect(); 28 | Ok(display) 29 | } 30 | 31 | #[pyfunction] 32 | pub fn get_base_level_inner(text: &str) -> PyResult { 33 | let bidi_info = BidiInfo::new(text, None); 34 | if bidi_info.paragraphs.is_empty() { 35 | return Err(PyValueError::new_err("Text contains no paragraphs")); 36 | } 37 | 38 | Ok(bidi_info.paragraphs[0].level.number()) 39 | } 40 | 41 | #[pymodule] 42 | fn bidi(m: &Bound<'_, PyModule>) -> PyResult<()> { 43 | m.add_function(wrap_pyfunction!(get_display_inner, m)?)?; 44 | m.add_function(wrap_pyfunction!(get_base_level_inner, m)?)?; 45 | Ok(()) 46 | } 47 | -------------------------------------------------------------------------------- /CHANGELOG.rst: -------------------------------------------------------------------------------- 1 | Changelog 2 | ========== 3 | 4 | .. :changelog: 5 | 6 | 0.6.7 7 | ----- 8 | 9 | * Added generate-import-lib for windows [Meir Kriheli] 10 | * Removed pypy3.9 and pypy3.10 from build, removed by pyo3 [Meir Kriheli] 11 | * Added 3.14 and pypy3.11 to targets [Meir Kriheli] 12 | * Bumped pyo3 to 0.27.1 [Meir Kriheli] 13 | 14 | 15 | 0.6.6 16 | ----- 17 | 18 | * Upgraded to macos-13 runner (as 12 is not available anymore). 19 | 20 | 21 | 0.6.5 22 | ----- 23 | 24 | * Revert "Added pypy3.11 to build" 25 | 26 | 27 | 0.6.4 28 | ----- 29 | 30 | * Added pypy3.11 to build 31 | * Removed pypy3.8 from build, not suppurted 32 | * Bumped pyo3 to 0.23.3, drops pypy3.7 and pypy3.8 33 | * Bumped unicode-bidi to 0.3.18 closes #28 34 | 35 | 36 | 0.6.3 37 | ----- 38 | 39 | * Updated pyo3 to 0.22.4 40 | * Python 3.13 wheels are finally working 41 | 42 | 0.6.2 43 | ----- 44 | 45 | * Added check-latest to the build 46 | 47 | 0.6.1 48 | ----- 49 | 50 | * Bumped to build Python 3.13 wheels 51 | 52 | 0.6.0 53 | ----- 54 | 55 | * Added implemention selection (Python or Rust) to pybidi cli, 56 | respecting backward comapt 57 | * Restored older algorithm, supports both implementations closes #25 58 | * Modernize and simplify Python code (Thanks Christian Clauss) 59 | 60 | 0.5.2 61 | ----- 62 | 63 | * Added get_base_level backward compat 64 | * docstring cleanup 65 | 66 | 0.5.1 67 | ------- 68 | 69 | * Added compat for older import, closes #23 70 | * Updated copyrights 71 | 72 | 73 | 0.5.0 74 | ----- 75 | 76 | Backwards incompatible changes! 77 | 78 | * Switched to using Rust based unicode-bidi using PyO3 79 | * Dropped Python < 3.9 support 80 | * Removed "upper_is_rtl" 81 | * Import of ``get_display`` changed to ``from bidi import get_display`` 82 | 83 | 84 | 0.4.2 85 | ----- 86 | 87 | * Type Fixes, thanks jwilk 88 | 89 | 90 | 0.4.1 91 | ----- 92 | 93 | * Merged Fix for mixed RTL and numbers, Thanks Just van Rossum 94 | 95 | 0.4.0 96 | ----- 97 | 98 | * Move to cookiecutter template 99 | * Python 3 support (py2.6, 2.7, 3.3, 3.4 and pypy) 100 | * Better docs 101 | * Travis integration 102 | * Tox tests 103 | * PEP8 cleanup 104 | 105 | 0.3.4 106 | ------ 107 | 108 | * Remove extra newline in console script output 109 | 110 | 0.3.3 111 | ------ 112 | 113 | * Implement overriding base paragraph direction 114 | * Allow overriding base direction in pybidi console script 115 | * Fix returning display in same encoding 116 | 117 | 0.3.2 118 | ------ 119 | 120 | * Test for surrogate pairs 121 | * Fix indentation in documentations 122 | * Specify license in setup.py 123 | 124 | 0.3.1 125 | ----- 126 | 127 | * Added missing description 128 | * docs/INSTALL.rst 129 | 130 | 0.3 131 | --- 132 | 133 | * Apply bidi mirroring 134 | * Move to back function based implementation 135 | 136 | 0.2 137 | --- 138 | 139 | * Move the algorithm to a class based implementation 140 | 141 | 0.1 142 | --- 143 | 144 | * Initial release 145 | -------------------------------------------------------------------------------- /LICENSE-THIRD-PARTY.yml: -------------------------------------------------------------------------------- 1 | root_name: python-bidi 2 | third_party_libraries: 3 | - package_name: cfg-if 4 | package_version: 1.0.0 5 | repository: https://github.com/alexcrichton/cfg-if 6 | license: MIT/Apache-2.0 7 | - package_name: heck 8 | package_version: 0.5.0 9 | repository: https://github.com/withoutboats/heck 10 | license: MIT OR Apache-2.0 11 | - package_name: indoc 12 | package_version: 2.0.5 13 | repository: https://github.com/dtolnay/indoc 14 | license: MIT OR Apache-2.0 15 | - package_name: libc 16 | package_version: 0.2.155 17 | repository: https://github.com/rust-lang/libc 18 | license: MIT OR Apache-2.0 19 | - package_name: memoffset 20 | package_version: 0.9.1 21 | repository: https://github.com/Gilnaa/memoffset 22 | license: MIT 23 | - package_name: once_cell 24 | package_version: 1.19.0 25 | repository: https://github.com/matklad/once_cell 26 | license: MIT OR Apache-2.0 27 | - package_name: portable-atomic 28 | package_version: 1.6.0 29 | repository: https://github.com/taiki-e/portable-atomic 30 | license: Apache-2.0 OR MIT 31 | - package_name: proc-macro2 32 | package_version: 1.0.86 33 | repository: https://github.com/dtolnay/proc-macro2 34 | license: MIT OR Apache-2.0 35 | - package_name: pyo3 36 | package_version: 0.22.1 37 | repository: https://github.com/pyo3/pyo3 38 | license: MIT OR Apache-2.0 39 | - package_name: pyo3-build-config 40 | package_version: 0.22.1 41 | repository: https://github.com/pyo3/pyo3 42 | license: MIT OR Apache-2.0 43 | - package_name: pyo3-ffi 44 | package_version: 0.22.1 45 | repository: https://github.com/pyo3/pyo3 46 | license: MIT OR Apache-2.0 47 | - package_name: pyo3-macros 48 | package_version: 0.22.1 49 | repository: https://github.com/pyo3/pyo3 50 | license: MIT OR Apache-2.0 51 | - package_name: pyo3-macros-backend 52 | package_version: 0.22.1 53 | repository: https://github.com/pyo3/pyo3 54 | license: MIT OR Apache-2.0 55 | - package_name: quote 56 | package_version: 1.0.36 57 | repository: https://github.com/dtolnay/quote 58 | license: MIT OR Apache-2.0 59 | - package_name: syn 60 | package_version: 2.0.70 61 | repository: https://github.com/dtolnay/syn 62 | license: MIT OR Apache-2.0 63 | - package_name: target-lexicon 64 | package_version: 0.12.15 65 | repository: https://github.com/bytecodealliance/target-lexicon 66 | license: Apache-2.0 WITH LLVM-exception 67 | - package_name: unicode-bidi 68 | package_version: 0.3.15 69 | repository: https://github.com/servo/unicode-bidi 70 | license: MIT OR Apache-2.0 71 | - package_name: unicode-ident 72 | package_version: 1.0.12 73 | repository: https://github.com/dtolnay/unicode-ident 74 | license: (MIT OR Apache-2.0) AND Unicode-DFS-2016 75 | - package_name: unindent 76 | package_version: 0.2.3 77 | repository: https://github.com/dtolnay/indoc 78 | license: MIT OR Apache-2.0 79 | -------------------------------------------------------------------------------- /tests/test_rust_bidi.py: -------------------------------------------------------------------------------- 1 | # This file is part of python-bidi 2 | # 3 | # python-bidi is free software: you can redistribute it and/or modify 4 | # it under the terms of the GNU Lesser General Public License as published by 5 | # the Free Software Foundation, either version 3 of the License, or 6 | # (at your option) any later version. 7 | # 8 | # This program is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU Lesser General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU Lesser General Public License 14 | # along with this program. If not, see . 15 | 16 | # Copyright (C) 2008-2010 Yaacov Zamir , 17 | # Meir kriheli 18 | """BiDi algorithm unit tests""" 19 | 20 | import unittest 21 | 22 | from bidi import get_base_level, get_display 23 | 24 | # keep as list with char per line to prevent browsers from changing display order 25 | HELLO_HEB_LOGICAL = "".join(["ש", "ל", "ו", "ם"]) 26 | 27 | HELLO_HEB_DISPLAY = "".join( 28 | [ 29 | "ם", 30 | "ו", 31 | "ל", 32 | "ש", 33 | ] 34 | ) 35 | 36 | 37 | class TestRustBidiAlgorithm(unittest.TestCase): 38 | "Tests the Rust based bidi algorithm" 39 | 40 | def test_surrogate(self): 41 | """Test surrogate pairs""" 42 | 43 | text = HELLO_HEB_LOGICAL + " \U0001d7f612" 44 | 45 | display = get_display(text) 46 | self.assertEqual(display, "\U0001d7f612 " + HELLO_HEB_DISPLAY) 47 | 48 | def test_override_base_dir(self): 49 | """Tests overriding the base paragraph direction""" 50 | 51 | # normally the display should be :MOLAHS be since we're overriding the 52 | # base dir the colon should be at the end of the display 53 | storage = f"{HELLO_HEB_LOGICAL}:" 54 | display = f"{HELLO_HEB_DISPLAY}:" 55 | 56 | self.assertEqual(get_display(storage, base_dir="L"), display) 57 | 58 | def test_output_encoding(self): 59 | """Make sure the display is in the same encoding as the incoming text""" 60 | 61 | storage = b"\xf9\xec\xe5\xed" # Hebrew word shalom in cp1255 62 | display = b"\xed\xe5\xec\xf9" 63 | 64 | self.assertEqual(get_display(storage, encoding="cp1255"), display) 65 | 66 | def test_mixed_hebrew_numbers_issue10(self): 67 | """Test for the case reported in https://github.com/MeirKriheli/python-bidi/issues/10""" 68 | 69 | tests = ( 70 | ( 71 | "1 2 3 \u05E0\u05D9\u05E1\u05D9\u05D5\u05DF", 72 | "\u05DF\u05D5\u05D9\u05E1\u05D9\u05E0 3 2 1", 73 | ), 74 | ( 75 | "1 2 3 123 \u05E0\u05D9\u05E1\u05D9\u05D5\u05DF", 76 | "\u05DF\u05D5\u05D9\u05E1\u05D9\u05E0 123 3 2 1", 77 | ), 78 | ) 79 | for storage, display in tests: 80 | self.assertEqual(get_display(storage), display) 81 | 82 | def test_get_base_level(self): 83 | """Test base dir""" 84 | 85 | self.assertEqual(get_base_level(HELLO_HEB_LOGICAL), 1) 86 | self.assertEqual(get_base_level("Hello"), 0) 87 | 88 | 89 | if __name__ == "__main__": 90 | unittest.main() 91 | -------------------------------------------------------------------------------- /CONTRIBUTING.rst: -------------------------------------------------------------------------------- 1 | ============ 2 | Contributing 3 | ============ 4 | 5 | Contributions are welcome, and they are greatly appreciated! Every 6 | little bit helps, and credit will always be given. 7 | 8 | You can contribute in many ways: 9 | 10 | Types of Contributions 11 | ---------------------- 12 | 13 | Report Bugs 14 | ~~~~~~~~~~~ 15 | 16 | Report bugs at https://github.com/MeirKriheli/python-bidi/issues. 17 | 18 | If you are reporting a bug, please include: 19 | 20 | * Your operating system name and version. 21 | * Any details about your local setup that might be helpful in troubleshooting. 22 | * Detailed steps to reproduce the bug. 23 | 24 | Fix Bugs 25 | ~~~~~~~~ 26 | 27 | Look through the GitHub issues for bugs. Anything tagged with "bug" 28 | is open to whoever wants to implement it. 29 | 30 | Implement Features 31 | ~~~~~~~~~~~~~~~~~~ 32 | 33 | Look through the GitHub issues for features. Anything tagged with "feature" 34 | is open to whoever wants to implement it. 35 | 36 | Write Documentation 37 | ~~~~~~~~~~~~~~~~~~~ 38 | 39 | Python BiDi could always use more documentation, whether as part of the 40 | official Python BiDi docs, in docstrings, or even on the web in blog posts, 41 | articles, and such. 42 | 43 | Submit Feedback 44 | ~~~~~~~~~~~~~~~ 45 | 46 | The best way to send feedback is to file an issue at https://github.com/MeirKriheli/python-bidi/issues. 47 | 48 | If you are proposing a feature: 49 | 50 | * Explain in detail how it would work. 51 | * Keep the scope as narrow as possible, to make it easier to implement. 52 | * Remember that this is a volunteer-driven project, and that contributions 53 | are welcome :) 54 | 55 | Get Started! 56 | ------------ 57 | 58 | Ready to contribute? Here's how to set up `python-bidi` for local development. 59 | 60 | 1. Fork the `python-bidi` repo on GitHub. 61 | 2. Clone your fork locally:: 62 | 63 | $ git clone git@github.com:your_name_here/python-bidi.git 64 | 65 | 3. Install your local copy into a virtualenv. Assuming you have virtualenvwrapper installed, this is how you set up your fork for local development:: 66 | 67 | $ mkvirtualenv python-bidi 68 | $ cd python-bidi/ 69 | $ python setup.py develop 70 | 71 | 4. Create a branch for local development:: 72 | 73 | $ git switch -c name-of-your-bugfix-or-feature 74 | 75 | Now you can make your changes locally. 76 | 77 | 5. When you're done making changes, check that your changes pass flake8 and the tests:: 78 | 79 | $ flake8 python-bidi tests 80 | $ python setup.py test 81 | $ nox 82 | 83 | To get ``flake8`` and ``nox``, just pip install them into your virtualenv. 84 | 85 | 6. Commit your changes and push your branch to GitHub:: 86 | 87 | $ git add . 88 | $ git commit -m "Your detailed description of your changes." 89 | $ git push origin name-of-your-bugfix-or-feature 90 | 91 | 7. Submit a pull request through the GitHub website. 92 | 93 | Pull Request Guidelines 94 | ----------------------- 95 | 96 | Before you submit a pull request, check that it meets these guidelines: 97 | 98 | 1. The pull request should include tests. 99 | 2. If the pull request adds functionality, the docs should be updated. Put 100 | your new functionality into a function with a docstring, and add the 101 | feature to the list in README.rst. 102 | -------------------------------------------------------------------------------- /bidi/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # This file is part of python-bidi 3 | # 4 | # python-bidi is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU Lesser General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU Lesser General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU Lesser General Public License 15 | # along with this program. If not, see . 16 | 17 | # Copyright (C) 2008-2010 Yaacov Zamir , 18 | # Copyright (C) 2010-2024 Meir kriheli . 19 | # 20 | 21 | from .wrapper import get_base_level, get_display 22 | 23 | __all__ = ["get_base_level", "get_display"] 24 | 25 | VERSION_TUPLE = (0, 6, 7) 26 | VERSION = ".".join(str(x) for x in VERSION_TUPLE) 27 | 28 | 29 | def main(): 30 | """Will be used to create the console script""" 31 | 32 | import argparse 33 | import sys 34 | 35 | parser = argparse.ArgumentParser() 36 | 37 | parser.add_argument( 38 | "-e", 39 | "--encoding", 40 | dest="encoding", 41 | default="utf-8", 42 | type=str, 43 | help="Text encoding (default: utf-8)", 44 | ) 45 | 46 | parser.add_argument( 47 | "-u", 48 | "--upper-is-rtl", 49 | dest="upper_is_rtl", 50 | default=False, 51 | action="store_true", 52 | help="Treat upper case chars as strong 'R' " 53 | "for debugging (default: False), Ignored in Rust algo", 54 | ) 55 | 56 | parser.add_argument( 57 | "-d", 58 | "--debug", 59 | dest="debug", 60 | default=False, 61 | action="store_true", 62 | help="Output to stderr steps taken with the algorithm", 63 | ) 64 | 65 | parser.add_argument( 66 | "-b", 67 | "--base-dir", 68 | dest="base_dir", 69 | choices=["L", "R"], 70 | default=None, 71 | type=str, 72 | help="Override base direction [L|R]", 73 | ) 74 | 75 | parser.add_argument( 76 | "-r", 77 | "--rust", 78 | dest="use_rust", 79 | action="store_true", 80 | help="Use the Rust unicode-bidi implemention instead of the Python one", 81 | ) 82 | 83 | parser.add_argument( 84 | "-v", "--version", action="version", version=f"pybidi {VERSION}" 85 | ) 86 | 87 | options, rest = parser.parse_known_args() 88 | 89 | lines = rest or sys.stdin 90 | 91 | params = { 92 | "encoding": options.encoding, 93 | "base_dir": options.base_dir, 94 | "debug": options.debug, 95 | } 96 | 97 | if options.use_rust: 98 | display_func = get_display 99 | else: 100 | from .algorithm import get_display as get_display_python 101 | 102 | display_func = get_display_python 103 | params["upper_is_rtl"] = options.upper_is_rtl 104 | 105 | for line in lines: 106 | display = display_func(line, **params) 107 | # adjust the encoding as unicode, to match the output encoding 108 | if not isinstance(display, str): 109 | display = bytes(display).decode(options.encoding) 110 | 111 | print(display, end="") 112 | 113 | 114 | if __name__ == "__main__": 115 | main() 116 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | =============================== 2 | Python BiDi 3 | =============================== 4 | 5 | `Bi-directional`_ (BiDi) layout for Python providing 2 implementations: 6 | 7 | * V5 of the algorithm implemented with Python. 8 | * Wrapper the `unicode-bidi`_ Rust crate. 9 | 10 | `Package documentation`_ 11 | 12 | .. _Bi-directional: http://en.wikipedia.org/wiki/Bi-directional_text 13 | .. _unicode-bidi: https://crates.io/crates/unicode-bidi 14 | .. _Package documentation: http://python-bidi.readthedocs.org/en/latest/ 15 | 16 | 17 | For the python implementation, and compatible with previous versions, use:: 18 | 19 | from bidi.algorithm import get_display 20 | 21 | 22 | For the newer Rust based one, which seems to implement higher version of 23 | the algorithm (albeit with some missing see #25), use the top level import:: 24 | 25 | from bidi import get_display 26 | 27 | 28 | API 29 | ---- 30 | 31 | The algorithm starts with a single entry point `get_display` (see above for selecting the implementaion). 32 | 33 | **Required arguments:** 34 | 35 | * ``str_or_bytes``: The string or bytes (i.e.: storage). If it's bytes 36 | use the optional argument ``encoding`` to specify it's encoding. 37 | 38 | **Optional arguments:** 39 | 40 | * ``encoding``: If unicode_or_str is a string, specifies the encoding. The 41 | algorithm uses unicodedata_ which requires unicode. This encoding will be 42 | used to decode and encode back to string before returning 43 | (default: "utf-8"). 44 | 45 | * ``base_dir``: ``'L'`` or ``'R'``, override the calculated base_level. 46 | 47 | * ``debug``: ``True`` to display the Unicode levels as seen by the algorithm 48 | (default: ``False``). 49 | 50 | 51 | The Python implementaion adds one more optional argument: 52 | 53 | * ``upper_is_rtl``: True to treat upper case chars as strong 'R' for 54 | debugging (default: False). 55 | 56 | 57 | It returns the display layout, either as ``str`` or ``encoding`` encoded ``bytes`` 58 | (depending on the type of ``str_or_bytes'``). 59 | 60 | .. _unicodedata: http://docs.python.org/library/unicodedata.html 61 | 62 | Example:: 63 | 64 | >>> from bidi import get_display 65 | >>> # keep as list with char per line to prevent browsers from changing display order 66 | >>> HELLO_HEB = "".join([ 67 | ... "ש", 68 | ... "ל", 69 | ... "ו", 70 | ... "ם" 71 | ... ]) 72 | >>> 73 | >>> HELLO_HEB_DISPLAY = "".join([ 74 | ... "ם", 75 | ... "ו", 76 | ... "ל", 77 | ... "ש", 78 | ... ]) 79 | >>> 80 | >>> get_display(HELLO_HEB) == HELLO_HEB_DISPLAY 81 | True 82 | 83 | 84 | CLI 85 | ---- 86 | 87 | ``pybidi`` is a command line utility (calling ``bidi.main``) for running the 88 | display algorithm. The script can get a string as a parameter or read text from 89 | `stdin`. 90 | 91 | Usage:: 92 | 93 | $ pybidi -h 94 | usage: pybidi [-h] [-e ENCODING] [-u] [-d] [-b {L,R}] [-r] [-v] 95 | 96 | options: 97 | -h, --help show this help message and exit 98 | -e ENCODING, --encoding ENCODING 99 | Text encoding (default: utf-8) 100 | -u, --upper-is-rtl Treat upper case chars as strong 'R' for debugging (default: False), Ignored in Rust algo 101 | -d, --debug Output to stderr steps taken with the algorithm 102 | -b {L,R}, --base-dir {L,R} 103 | Override base direction [L|R] 104 | -r, --rust Use the Rust unicode-bidi implemention instead of the Python one 105 | -v, --version show program's version number and exit 106 | 107 | 108 | Examples:: 109 | 110 | $ pybidi -u 'Your string here' 111 | $ cat ~/Documents/example.txt | pybidi 112 | 113 | 114 | Installation 115 | ------------- 116 | 117 | At the command line (assuming you're using some virtualenv):: 118 | 119 | pip install python-bidi 120 | 121 | 122 | Running tests 123 | -------------- 124 | 125 | To run the tests:: 126 | 127 | pip install nox 128 | nox 129 | -------------------------------------------------------------------------------- /.github/workflows/CI.yml: -------------------------------------------------------------------------------- 1 | # This file is autogenerated by maturin v1.7.0 2 | # To update, run 3 | # 4 | # maturin generate-ci github 5 | # 6 | name: CI 7 | 8 | on: 9 | push: 10 | branches: 11 | - main 12 | - master 13 | tags: 14 | - '*' 15 | pull_request: 16 | workflow_dispatch: 17 | 18 | permissions: 19 | contents: read 20 | 21 | jobs: 22 | linux: 23 | runs-on: ${{ matrix.platform.runner }} 24 | strategy: 25 | matrix: 26 | platform: 27 | - runner: ubuntu-latest 28 | target: x86_64 29 | - runner: ubuntu-latest 30 | target: x86 31 | - runner: ubuntu-latest 32 | target: aarch64 33 | - runner: ubuntu-latest 34 | target: armv7 35 | - runner: ubuntu-latest 36 | target: s390x 37 | - runner: ubuntu-latest 38 | target: ppc64le 39 | steps: 40 | - uses: actions/checkout@v4 41 | - uses: actions/setup-python@v5 42 | with: 43 | python-version: 3.x 44 | check-latest: true 45 | - name: Build wheels 46 | uses: PyO3/maturin-action@v1 47 | with: 48 | target: ${{ matrix.platform.target }} 49 | args: --release --out dist -i 3.8 3.9 3.10 3.11 3.12 3.13 pypy3.11 50 | sccache: 'true' 51 | manylinux: auto 52 | - name: Upload wheels 53 | uses: actions/upload-artifact@v4 54 | with: 55 | name: wheels-linux-${{ matrix.platform.target }} 56 | path: dist 57 | 58 | musllinux: 59 | runs-on: ${{ matrix.platform.runner }} 60 | strategy: 61 | matrix: 62 | platform: 63 | - runner: ubuntu-latest 64 | target: x86_64 65 | - runner: ubuntu-latest 66 | target: x86 67 | - runner: ubuntu-latest 68 | target: aarch64 69 | - runner: ubuntu-latest 70 | target: armv7 71 | steps: 72 | - uses: actions/checkout@v4 73 | - uses: actions/setup-python@v5 74 | with: 75 | python-version: 3.x 76 | check-latest: true 77 | - name: Build wheels 78 | uses: PyO3/maturin-action@v1 79 | with: 80 | target: ${{ matrix.platform.target }} 81 | args: --release --out dist -i 3.8 3.9 3.10 3.11 3.12 3.13 3.14 pypy3.11 82 | sccache: 'true' 83 | manylinux: musllinux_1_2 84 | - name: Upload wheels 85 | uses: actions/upload-artifact@v4 86 | with: 87 | name: wheels-musllinux-${{ matrix.platform.target }} 88 | path: dist 89 | 90 | windows: 91 | runs-on: ${{ matrix.platform.runner }} 92 | strategy: 93 | matrix: 94 | platform: 95 | - runner: windows-latest 96 | target: x64 97 | - runner: windows-latest 98 | target: x86 99 | steps: 100 | - uses: actions/checkout@v4 101 | - uses: actions/setup-python@v5 102 | with: 103 | python-version: 3.x 104 | architecture: ${{ matrix.platform.target }} 105 | check-latest: true 106 | - name: Build wheels 107 | uses: PyO3/maturin-action@v1 108 | with: 109 | target: ${{ matrix.platform.target }} 110 | args: --release --out dist -i 3.8 3.9 3.10 3.11 3.12 3.13 3.14 111 | sccache: 'true' 112 | - name: Upload wheels 113 | uses: actions/upload-artifact@v4 114 | with: 115 | name: wheels-windows-${{ matrix.platform.target }} 116 | path: dist 117 | 118 | macos: 119 | runs-on: ${{ matrix.platform.runner }} 120 | strategy: 121 | matrix: 122 | platform: 123 | - runner: macos-13 124 | target: x86_64 125 | - runner: macos-14 126 | target: aarch64 127 | steps: 128 | - uses: actions/checkout@v4 129 | - uses: actions/setup-python@v5 130 | with: 131 | python-version: | 132 | 3.9 133 | 3.10 134 | 3.11 135 | 3.12 136 | 3.13 137 | 3.14 138 | check-latest: true 139 | - name: Build wheels 140 | uses: PyO3/maturin-action@v1 141 | with: 142 | target: ${{ matrix.platform.target }} 143 | args: --release --out dist -i 3.8 3.9 3.10 3.11 3.12 3.13 3.14 pypy3.11 144 | sccache: 'true' 145 | - name: Upload wheels 146 | uses: actions/upload-artifact@v4 147 | with: 148 | name: wheels-macos-${{ matrix.platform.target }} 149 | path: dist 150 | 151 | sdist: 152 | runs-on: ubuntu-latest 153 | steps: 154 | - uses: actions/checkout@v4 155 | - name: Build sdist 156 | uses: PyO3/maturin-action@v1 157 | with: 158 | command: sdist 159 | args: --out dist 160 | - name: Upload sdist 161 | uses: actions/upload-artifact@v4 162 | with: 163 | name: wheels-sdist 164 | path: dist 165 | 166 | release: 167 | name: Release 168 | runs-on: ubuntu-latest 169 | if: "startsWith(github.ref, 'refs/tags/')" 170 | needs: [linux, musllinux, windows, macos, sdist] 171 | steps: 172 | - uses: actions/download-artifact@v4 173 | - name: Publish to PyPI 174 | uses: PyO3/maturin-action@v1 175 | env: 176 | MATURIN_PYPI_TOKEN: ${{ secrets.PYPI_API_TOKEN }} 177 | with: 178 | command: upload 179 | args: --non-interactive --skip-existing wheels-*/* 180 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 4 4 | 5 | [[package]] 6 | name = "autocfg" 7 | version = "1.3.0" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" 10 | 11 | [[package]] 12 | name = "cc" 13 | version = "1.2.39" 14 | source = "registry+https://github.com/rust-lang/crates.io-index" 15 | checksum = "e1354349954c6fc9cb0deab020f27f783cf0b604e8bb754dc4658ecf0d29c35f" 16 | dependencies = [ 17 | "find-msvc-tools", 18 | "shlex", 19 | ] 20 | 21 | [[package]] 22 | name = "find-msvc-tools" 23 | version = "0.1.2" 24 | source = "registry+https://github.com/rust-lang/crates.io-index" 25 | checksum = "1ced73b1dacfc750a6db6c0a0c3a3853c8b41997e2e2c563dc90804ae6867959" 26 | 27 | [[package]] 28 | name = "heck" 29 | version = "0.5.0" 30 | source = "registry+https://github.com/rust-lang/crates.io-index" 31 | checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" 32 | 33 | [[package]] 34 | name = "indoc" 35 | version = "2.0.5" 36 | source = "registry+https://github.com/rust-lang/crates.io-index" 37 | checksum = "b248f5224d1d606005e02c97f5aa4e88eeb230488bcc03bc9ca4d7991399f2b5" 38 | 39 | [[package]] 40 | name = "libc" 41 | version = "0.2.155" 42 | source = "registry+https://github.com/rust-lang/crates.io-index" 43 | checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c" 44 | 45 | [[package]] 46 | name = "memoffset" 47 | version = "0.9.1" 48 | source = "registry+https://github.com/rust-lang/crates.io-index" 49 | checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" 50 | dependencies = [ 51 | "autocfg", 52 | ] 53 | 54 | [[package]] 55 | name = "once_cell" 56 | version = "1.21.3" 57 | source = "registry+https://github.com/rust-lang/crates.io-index" 58 | checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" 59 | 60 | [[package]] 61 | name = "portable-atomic" 62 | version = "1.6.0" 63 | source = "registry+https://github.com/rust-lang/crates.io-index" 64 | checksum = "7170ef9988bc169ba16dd36a7fa041e5c4cbeb6a35b76d4c03daded371eae7c0" 65 | 66 | [[package]] 67 | name = "proc-macro2" 68 | version = "1.0.86" 69 | source = "registry+https://github.com/rust-lang/crates.io-index" 70 | checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77" 71 | dependencies = [ 72 | "unicode-ident", 73 | ] 74 | 75 | [[package]] 76 | name = "pyo3" 77 | version = "0.27.1" 78 | source = "registry+https://github.com/rust-lang/crates.io-index" 79 | checksum = "37a6df7eab65fc7bee654a421404947e10a0f7085b6951bf2ea395f4659fb0cf" 80 | dependencies = [ 81 | "indoc", 82 | "libc", 83 | "memoffset", 84 | "once_cell", 85 | "portable-atomic", 86 | "pyo3-build-config", 87 | "pyo3-ffi", 88 | "pyo3-macros", 89 | "unindent", 90 | ] 91 | 92 | [[package]] 93 | name = "pyo3-build-config" 94 | version = "0.27.1" 95 | source = "registry+https://github.com/rust-lang/crates.io-index" 96 | checksum = "f77d387774f6f6eec64a004eac0ed525aab7fa1966d94b42f743797b3e395afb" 97 | dependencies = [ 98 | "python3-dll-a", 99 | "target-lexicon", 100 | ] 101 | 102 | [[package]] 103 | name = "pyo3-ffi" 104 | version = "0.27.1" 105 | source = "registry+https://github.com/rust-lang/crates.io-index" 106 | checksum = "2dd13844a4242793e02df3e2ec093f540d948299a6a77ea9ce7afd8623f542be" 107 | dependencies = [ 108 | "libc", 109 | "pyo3-build-config", 110 | ] 111 | 112 | [[package]] 113 | name = "pyo3-macros" 114 | version = "0.27.1" 115 | source = "registry+https://github.com/rust-lang/crates.io-index" 116 | checksum = "eaf8f9f1108270b90d3676b8679586385430e5c0bb78bb5f043f95499c821a71" 117 | dependencies = [ 118 | "proc-macro2", 119 | "pyo3-macros-backend", 120 | "quote", 121 | "syn", 122 | ] 123 | 124 | [[package]] 125 | name = "pyo3-macros-backend" 126 | version = "0.27.1" 127 | source = "registry+https://github.com/rust-lang/crates.io-index" 128 | checksum = "70a3b2274450ba5288bc9b8c1b69ff569d1d61189d4bff38f8d22e03d17f932b" 129 | dependencies = [ 130 | "heck", 131 | "proc-macro2", 132 | "pyo3-build-config", 133 | "quote", 134 | "syn", 135 | ] 136 | 137 | [[package]] 138 | name = "python-bidi" 139 | version = "0.6.7" 140 | dependencies = [ 141 | "pyo3", 142 | "unicode-bidi", 143 | ] 144 | 145 | [[package]] 146 | name = "python3-dll-a" 147 | version = "0.2.14" 148 | source = "registry+https://github.com/rust-lang/crates.io-index" 149 | checksum = "d381ef313ae70b4da5f95f8a4de773c6aa5cd28f73adec4b4a31df70b66780d8" 150 | dependencies = [ 151 | "cc", 152 | ] 153 | 154 | [[package]] 155 | name = "quote" 156 | version = "1.0.36" 157 | source = "registry+https://github.com/rust-lang/crates.io-index" 158 | checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" 159 | dependencies = [ 160 | "proc-macro2", 161 | ] 162 | 163 | [[package]] 164 | name = "shlex" 165 | version = "1.3.0" 166 | source = "registry+https://github.com/rust-lang/crates.io-index" 167 | checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" 168 | 169 | [[package]] 170 | name = "syn" 171 | version = "2.0.70" 172 | source = "registry+https://github.com/rust-lang/crates.io-index" 173 | checksum = "2f0209b68b3613b093e0ec905354eccaedcfe83b8cb37cbdeae64026c3064c16" 174 | dependencies = [ 175 | "proc-macro2", 176 | "quote", 177 | "unicode-ident", 178 | ] 179 | 180 | [[package]] 181 | name = "target-lexicon" 182 | version = "0.13.3" 183 | source = "registry+https://github.com/rust-lang/crates.io-index" 184 | checksum = "df7f62577c25e07834649fc3b39fafdc597c0a3527dc1c60129201ccfcbaa50c" 185 | 186 | [[package]] 187 | name = "unicode-bidi" 188 | version = "0.3.18" 189 | source = "registry+https://github.com/rust-lang/crates.io-index" 190 | checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5" 191 | 192 | [[package]] 193 | name = "unicode-ident" 194 | version = "1.0.12" 195 | source = "registry+https://github.com/rust-lang/crates.io-index" 196 | checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" 197 | 198 | [[package]] 199 | name = "unindent" 200 | version = "0.2.3" 201 | source = "registry+https://github.com/rust-lang/crates.io-index" 202 | checksum = "c7de7d73e1754487cb58364ee906a499937a0dfabd86bcb980fa99ec8c8fa2ce" 203 | -------------------------------------------------------------------------------- /tests/test_python_bidi.py: -------------------------------------------------------------------------------- 1 | # This file is part of python-bidi 2 | # 3 | # python-bidi is free software: you can redistribute it and/or modify 4 | # it under the terms of the GNU Lesser General Public License as published by 5 | # the Free Software Foundation, either version 3 of the License, or 6 | # (at your option) any later version. 7 | # 8 | # This program is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU Lesser General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU Lesser General Public License 14 | # along with this program. If not, see . 15 | 16 | # Copyright (C) 2008-2010 Yaacov Zamir , 17 | # Meir kriheli 18 | """BiDi algorithm unit tests""" 19 | 20 | import unittest 21 | 22 | from bidi.algorithm import get_display, get_embedding_levels, get_empty_storage 23 | 24 | 25 | class TestPythonBidiAlgorithm(unittest.TestCase): 26 | "Tests the Python based bidi algorithm (based on GNU fribidi ones)" 27 | 28 | def test_surrogate(self): 29 | """Test for storage and base levels in case of surrogate pairs""" 30 | 31 | storage = get_empty_storage() 32 | 33 | text = 'HELLO \U0001d7f612' 34 | get_embedding_levels(text, storage, upper_is_rtl=True) 35 | 36 | # should return 9, not 10 even in --with-unicode=ucs2 37 | self.assertEqual(len(storage['chars']), 9) 38 | 39 | # Is the expected result ? should be EN 40 | _ch = storage['chars'][6] 41 | self.assertEqual(_ch['ch'], '\U0001d7f6') 42 | self.assertEqual(_ch['type'], 'EN') 43 | 44 | display = get_display(text, upper_is_rtl=True) 45 | self.assertEqual(display, '\U0001d7f612 OLLEH') 46 | 47 | def test_implict_with_upper_is_rtl(self): 48 | '''Implicit tests''' 49 | 50 | tests = ( 51 | ('car is THE CAR in arabic', 'car is RAC EHT in arabic'), 52 | ('CAR IS the car IN ENGLISH', 'HSILGNE NI the car SI RAC'), 53 | ('he said "IT IS 123, 456, OK"', 'he said "KO ,456 ,123 SI TI"'), 54 | ('he said "IT IS (123, 456), OK"', 55 | 'he said "KO ,(456 ,123) SI TI"'), 56 | ('he said "IT IS 123,456, OK"', 'he said "KO ,123,456 SI TI"'), 57 | ('he said "IT IS (123,456), OK"', 58 | 'he said "KO ,(123,456) SI TI"'), 59 | ('HE SAID "it is 123, 456, ok"', '"it is 123, 456, ok" DIAS EH'), 60 | ('shalom', '<123H/>shalom<123H>'), 61 | ('SAALAM', 'MALAAS'), 62 | ('HE SAID "it is a car!" AND RAN', 63 | 'NAR DNA "!it is a car" DIAS EH'), 64 | ('HE SAID "it is a car!x" AND RAN', 65 | 'NAR DNA "it is a car!x" DIAS EH'), 66 | ('SOLVE 1*5 1-5 1/5 1+5', '1+5 1/5 1-5 5*1 EVLOS'), 67 | ('THE RANGE IS 2.5..5', '5..2.5 SI EGNAR EHT'), 68 | ('-2 CELSIUS IS COLD', 'DLOC SI SUISLEC 2-'), 69 | ) 70 | 71 | for storage, display in tests: 72 | self.assertEqual(get_display(storage, upper_is_rtl=True), display) 73 | 74 | def test_override_base_dir(self): 75 | """Tests overriding the base paragraph direction""" 76 | 77 | # normally the display should be :MOLAHS be since we're overriding the 78 | # base dir the colon should be at the end of the display 79 | storage = 'SHALOM:' 80 | display = 'MOLAHS:' 81 | 82 | self.assertEqual(get_display(storage, upper_is_rtl=True, base_dir='L'), 83 | display) 84 | 85 | def test_output_encoding(self): 86 | """Make sure the display is in the same encoding as the incoming text""" 87 | 88 | storage = b'\xf9\xec\xe5\xed' # Hebrew word shalom in cp1255 89 | display = b'\xed\xe5\xec\xf9' 90 | 91 | self.assertEqual(get_display(storage, encoding='cp1255'), display) 92 | 93 | def test_explicit_with_upper_is_rtl(self): 94 | """Explicit tests""" 95 | tests = ( 96 | ('this is _LJUST_o', 'this is JUST'), 97 | ('a _lsimple _RteST_o th_oat', 'a simple TSet that'), 98 | ('HAS A _LPDF missing', 'PDF missing A SAH'), 99 | ('AnD hOw_L AbOuT, 123,987 tHiS_o', 100 | 'w AbOuT, 123,987 tHiSOh DnA'), 101 | ('a GOOD - _L_oTEST.', 'a TSET - DOOG.'), 102 | ('here_L is_o_o_o _R a good one_o', 'here is eno doog a '), 103 | ('THE _rbest _lONE and', 'best ENO and EHT'), 104 | ('A REAL BIG_l_o BUG!', '!GUB GIB LAER A'), 105 | ('a _L_L_L_L_L_L_L_L_L_L_L_L_L_L_L_L_L_L_L_L_L_L_L_L_L_L_L_L_L_L_L_Rbug', 106 | 'a gub'), 107 | # FIXME the following commented explicit test fails 108 | # (u'AN ARABIC _l_o 123-456 NICE ONE!', 109 | # u'!ENO ECIN 456-123 CIBARA NA'), 110 | ('AN ARABIC _l _o 123-456 PAIR', 'RIAP 123-456 CIBARA NA'), 111 | ('this bug 67_r_o89 caught!', 'this bug 6789 caught!'), 112 | ) 113 | 114 | # adopt fribidi's CapRtl encoding 115 | mappings = { 116 | '_>': "\u200E", # LRM 117 | '_<': "\u200F", # RLM 118 | '_l': "\u202A", # LRE 119 | '_r': "\u202B", # RLE 120 | '_o': "\u202C", # PDF 121 | '_L': "\u202D", # LRO 122 | '_R': "\u202E", # RLO 123 | '__': '_', 124 | } 125 | 126 | for storage, display in tests: 127 | for key, val in mappings.items(): 128 | storage = storage.replace(key, val) 129 | self.assertEqual(get_display(storage, upper_is_rtl=True), display) 130 | 131 | def test_mixed_hebrew_numbers_issue10(self): 132 | """Test for the case reported in 133 | https://github.com/MeirKriheli/python-bidi/issues/10 134 | """ 135 | tests = ( 136 | ('1 2 3 \u05E0\u05D9\u05E1\u05D9\u05D5\u05DF', '\u05DF\u05D5\u05D9\u05E1\u05D9\u05E0 3 2 1'), 137 | ('1 2 3 123 \u05E0\u05D9\u05E1\u05D9\u05D5\u05DF', '\u05DF\u05D5\u05D9\u05E1\u05D9\u05E0 123 3 2 1'), 138 | ) 139 | for storage, display in tests: 140 | self.assertEqual(get_display(storage), display) 141 | 142 | 143 | if __name__ == '__main__': 144 | unittest.main() 145 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | BUILDDIR = _build 9 | 10 | # User-friendly check for sphinx-build 11 | ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) 12 | $(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/) 13 | endif 14 | 15 | # Internal variables. 16 | PAPEROPT_a4 = -D latex_paper_size=a4 17 | PAPEROPT_letter = -D latex_paper_size=letter 18 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 19 | # the i18n builder cannot share the environment and doctrees with the others 20 | I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 21 | 22 | .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext 23 | 24 | help: 25 | @echo "Please use \`make ' where is one of" 26 | @echo " html to make standalone HTML files" 27 | @echo " dirhtml to make HTML files named index.html in directories" 28 | @echo " singlehtml to make a single large HTML file" 29 | @echo " pickle to make pickle files" 30 | @echo " json to make JSON files" 31 | @echo " htmlhelp to make HTML files and a HTML help project" 32 | @echo " qthelp to make HTML files and a qthelp project" 33 | @echo " devhelp to make HTML files and a Devhelp project" 34 | @echo " epub to make an epub" 35 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 36 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 37 | @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" 38 | @echo " text to make text files" 39 | @echo " man to make manual pages" 40 | @echo " texinfo to make Texinfo files" 41 | @echo " info to make Texinfo files and run them through makeinfo" 42 | @echo " gettext to make PO message catalogs" 43 | @echo " changes to make an overview of all changed/added/deprecated items" 44 | @echo " xml to make Docutils-native XML files" 45 | @echo " pseudoxml to make pseudoxml-XML files for display purposes" 46 | @echo " linkcheck to check all external links for integrity" 47 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 48 | 49 | clean: 50 | rm -rf $(BUILDDIR)/* 51 | 52 | html: 53 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 54 | @echo 55 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 56 | 57 | dirhtml: 58 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 59 | @echo 60 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 61 | 62 | singlehtml: 63 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 64 | @echo 65 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 66 | 67 | pickle: 68 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 69 | @echo 70 | @echo "Build finished; now you can process the pickle files." 71 | 72 | json: 73 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 74 | @echo 75 | @echo "Build finished; now you can process the JSON files." 76 | 77 | htmlhelp: 78 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 79 | @echo 80 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 81 | ".hhp project file in $(BUILDDIR)/htmlhelp." 82 | 83 | qthelp: 84 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 85 | @echo 86 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 87 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 88 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/complexity.qhcp" 89 | @echo "To view the help file:" 90 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/complexity.qhc" 91 | 92 | devhelp: 93 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 94 | @echo 95 | @echo "Build finished." 96 | @echo "To view the help file:" 97 | @echo "# mkdir -p $$HOME/.local/share/devhelp/complexity" 98 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/complexity" 99 | @echo "# devhelp" 100 | 101 | epub: 102 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 103 | @echo 104 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 105 | 106 | latex: 107 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 108 | @echo 109 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 110 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 111 | "(use \`make latexpdf' here to do that automatically)." 112 | 113 | latexpdf: 114 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 115 | @echo "Running LaTeX files through pdflatex..." 116 | $(MAKE) -C $(BUILDDIR)/latex all-pdf 117 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 118 | 119 | latexpdfja: 120 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 121 | @echo "Running LaTeX files through platex and dvipdfmx..." 122 | $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja 123 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 124 | 125 | text: 126 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 127 | @echo 128 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 129 | 130 | man: 131 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 132 | @echo 133 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 134 | 135 | texinfo: 136 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 137 | @echo 138 | @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." 139 | @echo "Run \`make' in that directory to run these through makeinfo" \ 140 | "(use \`make info' here to do that automatically)." 141 | 142 | info: 143 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 144 | @echo "Running Texinfo files through makeinfo..." 145 | make -C $(BUILDDIR)/texinfo info 146 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." 147 | 148 | gettext: 149 | $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale 150 | @echo 151 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." 152 | 153 | changes: 154 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 155 | @echo 156 | @echo "The overview file is in $(BUILDDIR)/changes." 157 | 158 | linkcheck: 159 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 160 | @echo 161 | @echo "Link check complete; look for any errors in the above output " \ 162 | "or in $(BUILDDIR)/linkcheck/output.txt." 163 | 164 | doctest: 165 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 166 | @echo "Testing of doctests in the sources finished, look at the " \ 167 | "results in $(BUILDDIR)/doctest/output.txt." 168 | 169 | xml: 170 | $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml 171 | @echo 172 | @echo "Build finished. The XML files are in $(BUILDDIR)/xml." 173 | 174 | pseudoxml: 175 | $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml 176 | @echo 177 | @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | REM Command file for Sphinx documentation 4 | 5 | if "%SPHINXBUILD%" == "" ( 6 | set SPHINXBUILD=sphinx-build 7 | ) 8 | set BUILDDIR=_build 9 | set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . 10 | set I18NSPHINXOPTS=%SPHINXOPTS% . 11 | if NOT "%PAPER%" == "" ( 12 | set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% 13 | set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% 14 | ) 15 | 16 | if "%1" == "" goto help 17 | 18 | if "%1" == "help" ( 19 | :help 20 | echo.Please use `make ^` where ^ is one of 21 | echo. html to make standalone HTML files 22 | echo. dirhtml to make HTML files named index.html in directories 23 | echo. singlehtml to make a single large HTML file 24 | echo. pickle to make pickle files 25 | echo. json to make JSON files 26 | echo. htmlhelp to make HTML files and a HTML help project 27 | echo. qthelp to make HTML files and a qthelp project 28 | echo. devhelp to make HTML files and a Devhelp project 29 | echo. epub to make an epub 30 | echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter 31 | echo. text to make text files 32 | echo. man to make manual pages 33 | echo. texinfo to make Texinfo files 34 | echo. gettext to make PO message catalogs 35 | echo. changes to make an overview over all changed/added/deprecated items 36 | echo. xml to make Docutils-native XML files 37 | echo. pseudoxml to make pseudoxml-XML files for display purposes 38 | echo. linkcheck to check all external links for integrity 39 | echo. doctest to run all doctests embedded in the documentation if enabled 40 | goto end 41 | ) 42 | 43 | if "%1" == "clean" ( 44 | for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i 45 | del /q /s %BUILDDIR%\* 46 | goto end 47 | ) 48 | 49 | 50 | %SPHINXBUILD% 2> nul 51 | if errorlevel 9009 ( 52 | echo. 53 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 54 | echo.installed, then set the SPHINXBUILD environment variable to point 55 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 56 | echo.may add the Sphinx directory to PATH. 57 | echo. 58 | echo.If you don't have Sphinx installed, grab it from 59 | echo.http://sphinx-doc.org/ 60 | exit /b 1 61 | ) 62 | 63 | if "%1" == "html" ( 64 | %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html 65 | if errorlevel 1 exit /b 1 66 | echo. 67 | echo.Build finished. The HTML pages are in %BUILDDIR%/html. 68 | goto end 69 | ) 70 | 71 | if "%1" == "dirhtml" ( 72 | %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml 73 | if errorlevel 1 exit /b 1 74 | echo. 75 | echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. 76 | goto end 77 | ) 78 | 79 | if "%1" == "singlehtml" ( 80 | %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml 81 | if errorlevel 1 exit /b 1 82 | echo. 83 | echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. 84 | goto end 85 | ) 86 | 87 | if "%1" == "pickle" ( 88 | %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle 89 | if errorlevel 1 exit /b 1 90 | echo. 91 | echo.Build finished; now you can process the pickle files. 92 | goto end 93 | ) 94 | 95 | if "%1" == "json" ( 96 | %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json 97 | if errorlevel 1 exit /b 1 98 | echo. 99 | echo.Build finished; now you can process the JSON files. 100 | goto end 101 | ) 102 | 103 | if "%1" == "htmlhelp" ( 104 | %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp 105 | if errorlevel 1 exit /b 1 106 | echo. 107 | echo.Build finished; now you can run HTML Help Workshop with the ^ 108 | .hhp project file in %BUILDDIR%/htmlhelp. 109 | goto end 110 | ) 111 | 112 | if "%1" == "qthelp" ( 113 | %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp 114 | if errorlevel 1 exit /b 1 115 | echo. 116 | echo.Build finished; now you can run "qcollectiongenerator" with the ^ 117 | .qhcp project file in %BUILDDIR%/qthelp, like this: 118 | echo.^> qcollectiongenerator %BUILDDIR%\qthelp\complexity.qhcp 119 | echo.To view the help file: 120 | echo.^> assistant -collectionFile %BUILDDIR%\qthelp\complexity.ghc 121 | goto end 122 | ) 123 | 124 | if "%1" == "devhelp" ( 125 | %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp 126 | if errorlevel 1 exit /b 1 127 | echo. 128 | echo.Build finished. 129 | goto end 130 | ) 131 | 132 | if "%1" == "epub" ( 133 | %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub 134 | if errorlevel 1 exit /b 1 135 | echo. 136 | echo.Build finished. The epub file is in %BUILDDIR%/epub. 137 | goto end 138 | ) 139 | 140 | if "%1" == "latex" ( 141 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 142 | if errorlevel 1 exit /b 1 143 | echo. 144 | echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. 145 | goto end 146 | ) 147 | 148 | if "%1" == "latexpdf" ( 149 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 150 | cd %BUILDDIR%/latex 151 | make all-pdf 152 | cd %BUILDDIR%/.. 153 | echo. 154 | echo.Build finished; the PDF files are in %BUILDDIR%/latex. 155 | goto end 156 | ) 157 | 158 | if "%1" == "latexpdfja" ( 159 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 160 | cd %BUILDDIR%/latex 161 | make all-pdf-ja 162 | cd %BUILDDIR%/.. 163 | echo. 164 | echo.Build finished; the PDF files are in %BUILDDIR%/latex. 165 | goto end 166 | ) 167 | 168 | if "%1" == "text" ( 169 | %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text 170 | if errorlevel 1 exit /b 1 171 | echo. 172 | echo.Build finished. The text files are in %BUILDDIR%/text. 173 | goto end 174 | ) 175 | 176 | if "%1" == "man" ( 177 | %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man 178 | if errorlevel 1 exit /b 1 179 | echo. 180 | echo.Build finished. The manual pages are in %BUILDDIR%/man. 181 | goto end 182 | ) 183 | 184 | if "%1" == "texinfo" ( 185 | %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo 186 | if errorlevel 1 exit /b 1 187 | echo. 188 | echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. 189 | goto end 190 | ) 191 | 192 | if "%1" == "gettext" ( 193 | %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale 194 | if errorlevel 1 exit /b 1 195 | echo. 196 | echo.Build finished. The message catalogs are in %BUILDDIR%/locale. 197 | goto end 198 | ) 199 | 200 | if "%1" == "changes" ( 201 | %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes 202 | if errorlevel 1 exit /b 1 203 | echo. 204 | echo.The overview file is in %BUILDDIR%/changes. 205 | goto end 206 | ) 207 | 208 | if "%1" == "linkcheck" ( 209 | %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck 210 | if errorlevel 1 exit /b 1 211 | echo. 212 | echo.Link check complete; look for any errors in the above output ^ 213 | or in %BUILDDIR%/linkcheck/output.txt. 214 | goto end 215 | ) 216 | 217 | if "%1" == "doctest" ( 218 | %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest 219 | if errorlevel 1 exit /b 1 220 | echo. 221 | echo.Testing of doctests in the sources finished, look at the ^ 222 | results in %BUILDDIR%/doctest/output.txt. 223 | goto end 224 | ) 225 | 226 | if "%1" == "xml" ( 227 | %SPHINXBUILD% -b xml %ALLSPHINXOPTS% %BUILDDIR%/xml 228 | if errorlevel 1 exit /b 1 229 | echo. 230 | echo.Build finished. The XML files are in %BUILDDIR%/xml. 231 | goto end 232 | ) 233 | 234 | if "%1" == "pseudoxml" ( 235 | %SPHINXBUILD% -b pseudoxml %ALLSPHINXOPTS% %BUILDDIR%/pseudoxml 236 | if errorlevel 1 exit /b 1 237 | echo. 238 | echo.Build finished. The pseudo-XML files are in %BUILDDIR%/pseudoxml. 239 | goto end 240 | ) 241 | 242 | :end -------------------------------------------------------------------------------- /COPYING.LESSER: -------------------------------------------------------------------------------- 1 | GNU LESSER GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | 9 | This version of the GNU Lesser General Public License incorporates 10 | the terms and conditions of version 3 of the GNU General Public 11 | License, supplemented by the additional permissions listed below. 12 | 13 | 0. Additional Definitions. 14 | 15 | As used herein, "this License" refers to version 3 of the GNU Lesser 16 | General Public License, and the "GNU GPL" refers to version 3 of the GNU 17 | General Public License. 18 | 19 | "The Library" refers to a covered work governed by this License, 20 | other than an Application or a Combined Work as defined below. 21 | 22 | An "Application" is any work that makes use of an interface provided 23 | by the Library, but which is not otherwise based on the Library. 24 | Defining a subclass of a class defined by the Library is deemed a mode 25 | of using an interface provided by the Library. 26 | 27 | A "Combined Work" is a work produced by combining or linking an 28 | Application with the Library. The particular version of the Library 29 | with which the Combined Work was made is also called the "Linked 30 | Version". 31 | 32 | The "Minimal Corresponding Source" for a Combined Work means the 33 | Corresponding Source for the Combined Work, excluding any source code 34 | for portions of the Combined Work that, considered in isolation, are 35 | based on the Application, and not on the Linked Version. 36 | 37 | The "Corresponding Application Code" for a Combined Work means the 38 | object code and/or source code for the Application, including any data 39 | and utility programs needed for reproducing the Combined Work from the 40 | Application, but excluding the System Libraries of the Combined Work. 41 | 42 | 1. Exception to Section 3 of the GNU GPL. 43 | 44 | You may convey a covered work under sections 3 and 4 of this License 45 | without being bound by section 3 of the GNU GPL. 46 | 47 | 2. Conveying Modified Versions. 48 | 49 | If you modify a copy of the Library, and, in your modifications, a 50 | facility refers to a function or data to be supplied by an Application 51 | that uses the facility (other than as an argument passed when the 52 | facility is invoked), then you may convey a copy of the modified 53 | version: 54 | 55 | a) under this License, provided that you make a good faith effort to 56 | ensure that, in the event an Application does not supply the 57 | function or data, the facility still operates, and performs 58 | whatever part of its purpose remains meaningful, or 59 | 60 | b) under the GNU GPL, with none of the additional permissions of 61 | this License applicable to that copy. 62 | 63 | 3. Object Code Incorporating Material from Library Header Files. 64 | 65 | The object code form of an Application may incorporate material from 66 | a header file that is part of the Library. You may convey such object 67 | code under terms of your choice, provided that, if the incorporated 68 | material is not limited to numerical parameters, data structure 69 | layouts and accessors, or small macros, inline functions and templates 70 | (ten or fewer lines in length), you do both of the following: 71 | 72 | a) Give prominent notice with each copy of the object code that the 73 | Library is used in it and that the Library and its use are 74 | covered by this License. 75 | 76 | b) Accompany the object code with a copy of the GNU GPL and this license 77 | document. 78 | 79 | 4. Combined Works. 80 | 81 | You may convey a Combined Work under terms of your choice that, 82 | taken together, effectively do not restrict modification of the 83 | portions of the Library contained in the Combined Work and reverse 84 | engineering for debugging such modifications, if you also do each of 85 | the following: 86 | 87 | a) Give prominent notice with each copy of the Combined Work that 88 | the Library is used in it and that the Library and its use are 89 | covered by this License. 90 | 91 | b) Accompany the Combined Work with a copy of the GNU GPL and this license 92 | document. 93 | 94 | c) For a Combined Work that displays copyright notices during 95 | execution, include the copyright notice for the Library among 96 | these notices, as well as a reference directing the user to the 97 | copies of the GNU GPL and this license document. 98 | 99 | d) Do one of the following: 100 | 101 | 0) Convey the Minimal Corresponding Source under the terms of this 102 | License, and the Corresponding Application Code in a form 103 | suitable for, and under terms that permit, the user to 104 | recombine or relink the Application with a modified version of 105 | the Linked Version to produce a modified Combined Work, in the 106 | manner specified by section 6 of the GNU GPL for conveying 107 | Corresponding Source. 108 | 109 | 1) Use a suitable shared library mechanism for linking with the 110 | Library. A suitable mechanism is one that (a) uses at run time 111 | a copy of the Library already present on the user's computer 112 | system, and (b) will operate properly with a modified version 113 | of the Library that is interface-compatible with the Linked 114 | Version. 115 | 116 | e) Provide Installation Information, but only if you would otherwise 117 | be required to provide such information under section 6 of the 118 | GNU GPL, and only to the extent that such information is 119 | necessary to install and execute a modified version of the 120 | Combined Work produced by recombining or relinking the 121 | Application with a modified version of the Linked Version. (If 122 | you use option 4d0, the Installation Information must accompany 123 | the Minimal Corresponding Source and Corresponding Application 124 | Code. If you use option 4d1, you must provide the Installation 125 | Information in the manner specified by section 6 of the GNU GPL 126 | for conveying Corresponding Source.) 127 | 128 | 5. Combined Libraries. 129 | 130 | You may place library facilities that are a work based on the 131 | Library side by side in a single library together with other library 132 | facilities that are not Applications and are not covered by this 133 | License, and convey such a combined library under terms of your 134 | choice, if you do both of the following: 135 | 136 | a) Accompany the combined library with a copy of the same work based 137 | on the Library, uncombined with any other library facilities, 138 | conveyed under the terms of this License. 139 | 140 | b) Give prominent notice with the combined library that part of it 141 | is a work based on the Library, and explaining where to find the 142 | accompanying uncombined form of the same work. 143 | 144 | 6. Revised Versions of the GNU Lesser General Public License. 145 | 146 | The Free Software Foundation may publish revised and/or new versions 147 | of the GNU Lesser General Public License from time to time. Such new 148 | versions will be similar in spirit to the present version, but may 149 | differ in detail to address new problems or concerns. 150 | 151 | Each version is given a distinguishing version number. If the 152 | Library as you received it specifies that a certain numbered version 153 | of the GNU Lesser General Public License "or any later version" 154 | applies to it, you have the option of following the terms and 155 | conditions either of that published version or of any later version 156 | published by the Free Software Foundation. If the Library as you 157 | received it does not specify a version number of the GNU Lesser 158 | General Public License, you may choose any version of the GNU Lesser 159 | General Public License ever published by the Free Software Foundation. 160 | 161 | If the Library as you received it specifies that a proxy can decide 162 | whether future versions of the GNU Lesser General Public License shall 163 | apply, that proxy's public statement of acceptance of any version is 164 | permanent authorization for you to choose that version for the 165 | Library. 166 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # complexity documentation build configuration file, created by 4 | # sphinx-quickstart on Tue Jul 9 22:26:36 2013. 5 | # 6 | # This file is execfile()d with the current directory set to its 7 | # containing dir. 8 | # 9 | # Note that not all possible configuration values are present in this 10 | # autogenerated file. 11 | # 12 | # All configuration values have a default; values that are commented out 13 | # serve to show the default. 14 | 15 | import os 16 | import sys 17 | 18 | # If extensions (or modules to document with autodoc) are in another 19 | # directory, add these directories to sys.path here. If the directory is 20 | # relative to the documentation root, use os.path.abspath to make it 21 | # absolute, like shown here. 22 | #sys.path.insert(0, os.path.abspath('.')) 23 | 24 | # Get the project root dir, which is the parent dir of this 25 | cwd = os.getcwd() 26 | project_root = os.path.dirname(cwd) 27 | 28 | # Insert the project root dir as the first element in the PYTHONPATH. 29 | # This lets us ensure that the source package is imported, and that its 30 | # version is used. 31 | #sys.path.insert(0, project_root) 32 | 33 | import bidi 34 | 35 | # -- General configuration --------------------------------------------- 36 | 37 | # If your documentation needs a minimal Sphinx version, state it here. 38 | #needs_sphinx = '1.0' 39 | 40 | # Add any Sphinx extension module names here, as strings. They can be 41 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom ones. 42 | extensions = ['sphinx.ext.autodoc', 'sphinx.ext.viewcode'] 43 | 44 | # Add any paths that contain templates here, relative to this directory. 45 | templates_path = ['_templates'] 46 | 47 | # The suffix of source filenames. 48 | source_suffix = '.rst' 49 | 50 | # The encoding of source files. 51 | #source_encoding = 'utf-8-sig' 52 | 53 | # The master toctree document. 54 | master_doc = 'index' 55 | 56 | # General information about the project. 57 | project = 'Python BiDi' 58 | copyright = '2014-2024, Meir Kriheli' 59 | 60 | # The version info for the project you're documenting, acts as replacement 61 | # for |version| and |release|, also used in various other places throughout 62 | # the built documents. 63 | # 64 | # The short X.Y version. 65 | version = bidi.VERSION 66 | # The full version, including alpha/beta/rc tags. 67 | release = bidi.VERSION 68 | 69 | # The language for content autogenerated by Sphinx. Refer to documentation 70 | # for a list of supported languages. 71 | #language = None 72 | 73 | # There are two options for replacing |today|: either, you set today to 74 | # some non-false value, then it is used: 75 | #today = '' 76 | # Else, today_fmt is used as the format for a strftime call. 77 | #today_fmt = '%B %d, %Y' 78 | 79 | # List of patterns, relative to source directory, that match files and 80 | # directories to ignore when looking for source files. 81 | exclude_patterns = ['_build'] 82 | 83 | # The reST default role (used for this markup: `text`) to use for all 84 | # documents. 85 | #default_role = None 86 | 87 | # If true, '()' will be appended to :func: etc. cross-reference text. 88 | #add_function_parentheses = True 89 | 90 | # If true, the current module name will be prepended to all description 91 | # unit titles (such as .. function::). 92 | #add_module_names = True 93 | 94 | # If true, sectionauthor and moduleauthor directives will be shown in the 95 | # output. They are ignored by default. 96 | #show_authors = False 97 | 98 | # The name of the Pygments (syntax highlighting) style to use. 99 | pygments_style = 'sphinx' 100 | 101 | # A list of ignored prefixes for module index sorting. 102 | #modindex_common_prefix = [] 103 | 104 | # If true, keep warnings as "system message" paragraphs in the built 105 | # documents. 106 | #keep_warnings = False 107 | 108 | 109 | # -- Options for HTML output ------------------------------------------- 110 | 111 | # The theme to use for HTML and HTML Help pages. See the documentation for 112 | # a list of builtin themes. 113 | html_theme = 'default' 114 | 115 | # Theme options are theme-specific and customize the look and feel of a 116 | # theme further. For a list of options available for each theme, see the 117 | # documentation. 118 | #html_theme_options = {} 119 | 120 | # Add any paths that contain custom themes here, relative to this directory. 121 | #html_theme_path = [] 122 | 123 | # The name for this set of Sphinx documents. If None, it defaults to 124 | # " v documentation". 125 | #html_title = None 126 | 127 | # A shorter title for the navigation bar. Default is the same as 128 | # html_title. 129 | #html_short_title = None 130 | 131 | # The name of an image file (relative to this directory) to place at the 132 | # top of the sidebar. 133 | #html_logo = None 134 | 135 | # The name of an image file (within the static path) to use as favicon 136 | # of the docs. This file should be a Windows icon file (.ico) being 137 | # 16x16 or 32x32 pixels large. 138 | #html_favicon = None 139 | 140 | # Add any paths that contain custom static files (such as style sheets) 141 | # here, relative to this directory. They are copied after the builtin 142 | # static files, so a file named "default.css" will overwrite the builtin 143 | # "default.css". 144 | html_static_path = ['_static'] 145 | 146 | # If not '', a 'Last updated on:' timestamp is inserted at every page 147 | # bottom, using the given strftime format. 148 | #html_last_updated_fmt = '%b %d, %Y' 149 | 150 | # If true, SmartyPants will be used to convert quotes and dashes to 151 | # typographically correct entities. 152 | #html_use_smartypants = True 153 | 154 | # Custom sidebar templates, maps document names to template names. 155 | #html_sidebars = {} 156 | 157 | # Additional templates that should be rendered to pages, maps page names 158 | # to template names. 159 | #html_additional_pages = {} 160 | 161 | # If false, no module index is generated. 162 | #html_domain_indices = True 163 | 164 | # If false, no index is generated. 165 | #html_use_index = True 166 | 167 | # If true, the index is split into individual pages for each letter. 168 | #html_split_index = False 169 | 170 | # If true, links to the reST sources are added to the pages. 171 | #html_show_sourcelink = True 172 | 173 | # If true, "Created using Sphinx" is shown in the HTML footer. 174 | # Default is True. 175 | #html_show_sphinx = True 176 | 177 | # If true, "(C) Copyright ..." is shown in the HTML footer. 178 | # Default is True. 179 | #html_show_copyright = True 180 | 181 | # If true, an OpenSearch description file will be output, and all pages 182 | # will contain a tag referring to it. The value of this option 183 | # must be the base URL from which the finished HTML is served. 184 | #html_use_opensearch = '' 185 | 186 | # This is the file name suffix for HTML files (e.g. ".xhtml"). 187 | #html_file_suffix = None 188 | 189 | # Output file base name for HTML help builder. 190 | htmlhelp_basename = 'python-bididoc' 191 | 192 | 193 | # -- Options for LaTeX output ------------------------------------------ 194 | 195 | latex_elements = { 196 | # The paper size ('letterpaper' or 'a4paper'). 197 | #'papersize': 'letterpaper', 198 | 199 | # The font size ('10pt', '11pt' or '12pt'). 200 | #'pointsize': '10pt', 201 | 202 | # Additional stuff for the LaTeX preamble. 203 | #'preamble': '', 204 | } 205 | 206 | # Grouping the document tree into LaTeX files. List of tuples 207 | # (source start file, target name, title, author, documentclass 208 | # [howto/manual]). 209 | latex_documents = [ 210 | ('index', 'python-bidi.tex', 211 | 'Python BiDi Documentation', 212 | 'Meir Kriheli', 'manual'), 213 | ] 214 | 215 | # The name of an image file (relative to this directory) to place at 216 | # the top of the title page. 217 | #latex_logo = None 218 | 219 | # For "manual" documents, if this is true, then toplevel headings 220 | # are parts, not chapters. 221 | #latex_use_parts = False 222 | 223 | # If true, show page references after internal links. 224 | #latex_show_pagerefs = False 225 | 226 | # If true, show URL addresses after external links. 227 | #latex_show_urls = False 228 | 229 | # Documents to append as an appendix to all manuals. 230 | #latex_appendices = [] 231 | 232 | # If false, no module index is generated. 233 | #latex_domain_indices = True 234 | 235 | 236 | # -- Options for manual page output ------------------------------------ 237 | 238 | # One entry per manual page. List of tuples 239 | # (source start file, name, description, authors, manual section). 240 | man_pages = [ 241 | ('index', 'python-bidi', 242 | 'Python BiDi Documentation', 243 | ['Meir Kriheli'], 1) 244 | ] 245 | 246 | # If true, show URL addresses after external links. 247 | #man_show_urls = False 248 | 249 | 250 | # -- Options for Texinfo output ---------------------------------------- 251 | 252 | # Grouping the document tree into Texinfo files. List of tuples 253 | # (source start file, target name, title, author, 254 | # dir menu entry, description, category) 255 | texinfo_documents = [ 256 | ('index', 'python-bidi', 257 | 'Python BiDi Documentation', 258 | 'Meir Kriheli', 259 | 'python-bidi', 260 | 'One line description of project.', 261 | 'Miscellaneous'), 262 | ] 263 | 264 | # Documents to append as an appendix to all manuals. 265 | #texinfo_appendices = [] 266 | 267 | # If false, no module index is generated. 268 | #texinfo_domain_indices = True 269 | 270 | # How to display URL addresses: 'footnote', 'no', or 'inline'. 271 | #texinfo_show_urls = 'footnote' 272 | 273 | # If true, do not generate a @detailmenu in the "Top" node's menu. 274 | #texinfo_no_detailmenu = False 275 | -------------------------------------------------------------------------------- /bidi/mirror.py: -------------------------------------------------------------------------------- 1 | # This file is part of python-bidi 2 | # 3 | # python-bidi is free software: you can redistribute it and/or modify 4 | # it under the terms of the GNU Lesser General Public License as published by 5 | # the Free Software Foundation, either version 3 of the License, or 6 | # (at your option) any later version. 7 | # 8 | # This program is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU Lesser General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU Lesser General Public License 14 | # along with this program. If not, see . 15 | 16 | # Copyright (C) 2008-2010 Yaacov Zamir , 17 | # Copyright (C) 2010-2015 Meir kriheli . 18 | """Mirrored chars""" 19 | 20 | # Can't seem to get this data from python's unicode data, so this is imported 21 | # from http://www.unicode.org/Public/UNIDATA/BidiMirroring.txt 22 | MIRRORED = { 23 | "\u0028": "\u0029", # LEFT PARENTHESIS 24 | "\u0029": "\u0028", # RIGHT PARENTHESIS 25 | "\u003C": "\u003E", # LESS-THAN SIGN 26 | "\u003E": "\u003C", # GREATER-THAN SIGN 27 | "\u005B": "\u005D", # LEFT SQUARE BRACKET 28 | "\u005D": "\u005B", # RIGHT SQUARE BRACKET 29 | "\u007B": "\u007D", # LEFT CURLY BRACKET 30 | "\u007D": "\u007B", # RIGHT CURLY BRACKET 31 | "\u00AB": "\u00BB", # LEFT-POINTING DOUBLE ANGLE QUOTATION MARK 32 | "\u00BB": "\u00AB", # RIGHT-POINTING DOUBLE ANGLE QUOTATION MARK 33 | "\u0F3A": "\u0F3B", # TIBETAN MARK GUG RTAGS GYON 34 | "\u0F3B": "\u0F3A", # TIBETAN MARK GUG RTAGS GYAS 35 | "\u0F3C": "\u0F3D", # TIBETAN MARK ANG KHANG GYON 36 | "\u0F3D": "\u0F3C", # TIBETAN MARK ANG KHANG GYAS 37 | "\u169B": "\u169C", # OGHAM FEATHER MARK 38 | "\u169C": "\u169B", # OGHAM REVERSED FEATHER MARK 39 | "\u2039": "\u203A", # SINGLE LEFT-POINTING ANGLE QUOTATION MARK 40 | "\u203A": "\u2039", # SINGLE RIGHT-POINTING ANGLE QUOTATION MARK 41 | "\u2045": "\u2046", # LEFT SQUARE BRACKET WITH QUILL 42 | "\u2046": "\u2045", # RIGHT SQUARE BRACKET WITH QUILL 43 | "\u207D": "\u207E", # SUPERSCRIPT LEFT PARENTHESIS 44 | "\u207E": "\u207D", # SUPERSCRIPT RIGHT PARENTHESIS 45 | "\u208D": "\u208E", # SUBSCRIPT LEFT PARENTHESIS 46 | "\u208E": "\u208D", # SUBSCRIPT RIGHT PARENTHESIS 47 | "\u2208": "\u220B", # ELEMENT OF 48 | "\u2209": "\u220C", # NOT AN ELEMENT OF 49 | "\u220A": "\u220D", # SMALL ELEMENT OF 50 | "\u220B": "\u2208", # CONTAINS AS MEMBER 51 | "\u220C": "\u2209", # DOES NOT CONTAIN AS MEMBER 52 | "\u220D": "\u220A", # SMALL CONTAINS AS MEMBER 53 | "\u2215": "\u29F5", # DIVISION SLASH 54 | "\u223C": "\u223D", # TILDE OPERATOR 55 | "\u223D": "\u223C", # REVERSED TILDE 56 | "\u2243": "\u22CD", # ASYMPTOTICALLY EQUAL TO 57 | "\u2252": "\u2253", # APPROXIMATELY EQUAL TO OR THE IMAGE OF 58 | "\u2253": "\u2252", # IMAGE OF OR APPROXIMATELY EQUAL TO 59 | "\u2254": "\u2255", # COLON EQUALS 60 | "\u2255": "\u2254", # EQUALS COLON 61 | "\u2264": "\u2265", # LESS-THAN OR EQUAL TO 62 | "\u2265": "\u2264", # GREATER-THAN OR EQUAL TO 63 | "\u2266": "\u2267", # LESS-THAN OVER EQUAL TO 64 | "\u2267": "\u2266", # GREATER-THAN OVER EQUAL TO 65 | "\u2268": "\u2269", # [BEST FIT] LESS-THAN BUT NOT EQUAL TO 66 | "\u2269": "\u2268", # [BEST FIT] GREATER-THAN BUT NOT EQUAL TO 67 | "\u226A": "\u226B", # MUCH LESS-THAN 68 | "\u226B": "\u226A", # MUCH GREATER-THAN 69 | "\u226E": "\u226F", # [BEST FIT] NOT LESS-THAN 70 | "\u226F": "\u226E", # [BEST FIT] NOT GREATER-THAN 71 | "\u2270": "\u2271", # [BEST FIT] NEITHER LESS-THAN NOR EQUAL TO 72 | "\u2271": "\u2270", # [BEST FIT] NEITHER GREATER-THAN NOR EQUAL TO 73 | "\u2272": "\u2273", # [BEST FIT] LESS-THAN OR EQUIVALENT TO 74 | "\u2273": "\u2272", # [BEST FIT] GREATER-THAN OR EQUIVALENT TO 75 | "\u2274": "\u2275", # [BEST FIT] NEITHER LESS-THAN NOR EQUIVALENT TO 76 | "\u2275": "\u2274", # [BEST FIT] NEITHER GREATER-THAN NOR EQUIVALENT TO 77 | "\u2276": "\u2277", # LESS-THAN OR GREATER-THAN 78 | "\u2277": "\u2276", # GREATER-THAN OR LESS-THAN 79 | "\u2278": "\u2279", # [BEST FIT] NEITHER LESS-THAN NOR GREATER-THAN 80 | "\u2279": "\u2278", # [BEST FIT] NEITHER GREATER-THAN NOR LESS-THAN 81 | "\u227A": "\u227B", # PRECEDES 82 | "\u227B": "\u227A", # SUCCEEDS 83 | "\u227C": "\u227D", # PRECEDES OR EQUAL TO 84 | "\u227D": "\u227C", # SUCCEEDS OR EQUAL TO 85 | "\u227E": "\u227F", # [BEST FIT] PRECEDES OR EQUIVALENT TO 86 | "\u227F": "\u227E", # [BEST FIT] SUCCEEDS OR EQUIVALENT TO 87 | "\u2280": "\u2281", # [BEST FIT] DOES NOT PRECEDE 88 | "\u2281": "\u2280", # [BEST FIT] DOES NOT SUCCEED 89 | "\u2282": "\u2283", # SUBSET OF 90 | "\u2283": "\u2282", # SUPERSET OF 91 | "\u2284": "\u2285", # [BEST FIT] NOT A SUBSET OF 92 | "\u2285": "\u2284", # [BEST FIT] NOT A SUPERSET OF 93 | "\u2286": "\u2287", # SUBSET OF OR EQUAL TO 94 | "\u2287": "\u2286", # SUPERSET OF OR EQUAL TO 95 | "\u2288": "\u2289", # [BEST FIT] NEITHER A SUBSET OF NOR EQUAL TO 96 | "\u2289": "\u2288", # [BEST FIT] NEITHER A SUPERSET OF NOR EQUAL TO 97 | "\u228A": "\u228B", # [BEST FIT] SUBSET OF WITH NOT EQUAL TO 98 | "\u228B": "\u228A", # [BEST FIT] SUPERSET OF WITH NOT EQUAL TO 99 | "\u228F": "\u2290", # SQUARE IMAGE OF 100 | "\u2290": "\u228F", # SQUARE ORIGINAL OF 101 | "\u2291": "\u2292", # SQUARE IMAGE OF OR EQUAL TO 102 | "\u2292": "\u2291", # SQUARE ORIGINAL OF OR EQUAL TO 103 | "\u2298": "\u29B8", # CIRCLED DIVISION SLASH 104 | "\u22A2": "\u22A3", # RIGHT TACK 105 | "\u22A3": "\u22A2", # LEFT TACK 106 | "\u22A6": "\u2ADE", # ASSERTION 107 | "\u22A8": "\u2AE4", # TRUE 108 | "\u22A9": "\u2AE3", # FORCES 109 | "\u22AB": "\u2AE5", # DOUBLE VERTICAL BAR DOUBLE RIGHT TURNSTILE 110 | "\u22B0": "\u22B1", # PRECEDES UNDER RELATION 111 | "\u22B1": "\u22B0", # SUCCEEDS UNDER RELATION 112 | "\u22B2": "\u22B3", # NORMAL SUBGROUP OF 113 | "\u22B3": "\u22B2", # CONTAINS AS NORMAL SUBGROUP 114 | "\u22B4": "\u22B5", # NORMAL SUBGROUP OF OR EQUAL TO 115 | "\u22B5": "\u22B4", # CONTAINS AS NORMAL SUBGROUP OR EQUAL TO 116 | "\u22B6": "\u22B7", # ORIGINAL OF 117 | "\u22B7": "\u22B6", # IMAGE OF 118 | "\u22C9": "\u22CA", # LEFT NORMAL FACTOR SEMIDIRECT PRODUCT 119 | "\u22CA": "\u22C9", # RIGHT NORMAL FACTOR SEMIDIRECT PRODUCT 120 | "\u22CB": "\u22CC", # LEFT SEMIDIRECT PRODUCT 121 | "\u22CC": "\u22CB", # RIGHT SEMIDIRECT PRODUCT 122 | "\u22CD": "\u2243", # REVERSED TILDE EQUALS 123 | "\u22D0": "\u22D1", # DOUBLE SUBSET 124 | "\u22D1": "\u22D0", # DOUBLE SUPERSET 125 | "\u22D6": "\u22D7", # LESS-THAN WITH DOT 126 | "\u22D7": "\u22D6", # GREATER-THAN WITH DOT 127 | "\u22D8": "\u22D9", # VERY MUCH LESS-THAN 128 | "\u22D9": "\u22D8", # VERY MUCH GREATER-THAN 129 | "\u22DA": "\u22DB", # LESS-THAN EQUAL TO OR GREATER-THAN 130 | "\u22DB": "\u22DA", # GREATER-THAN EQUAL TO OR LESS-THAN 131 | "\u22DC": "\u22DD", # EQUAL TO OR LESS-THAN 132 | "\u22DD": "\u22DC", # EQUAL TO OR GREATER-THAN 133 | "\u22DE": "\u22DF", # EQUAL TO OR PRECEDES 134 | "\u22DF": "\u22DE", # EQUAL TO OR SUCCEEDS 135 | "\u22E0": "\u22E1", # [BEST FIT] DOES NOT PRECEDE OR EQUAL 136 | "\u22E1": "\u22E0", # [BEST FIT] DOES NOT SUCCEED OR EQUAL 137 | "\u22E2": "\u22E3", # [BEST FIT] NOT SQUARE IMAGE OF OR EQUAL TO 138 | "\u22E3": "\u22E2", # [BEST FIT] NOT SQUARE ORIGINAL OF OR EQUAL TO 139 | "\u22E4": "\u22E5", # [BEST FIT] SQUARE IMAGE OF OR NOT EQUAL TO 140 | "\u22E5": "\u22E4", # [BEST FIT] SQUARE ORIGINAL OF OR NOT EQUAL TO 141 | "\u22E6": "\u22E7", # [BEST FIT] LESS-THAN BUT NOT EQUIVALENT TO 142 | "\u22E7": "\u22E6", # [BEST FIT] GREATER-THAN BUT NOT EQUIVALENT TO 143 | "\u22E8": "\u22E9", # [BEST FIT] PRECEDES BUT NOT EQUIVALENT TO 144 | "\u22E9": "\u22E8", # [BEST FIT] SUCCEEDS BUT NOT EQUIVALENT TO 145 | "\u22EA": "\u22EB", # [BEST FIT] NOT NORMAL SUBGROUP OF 146 | "\u22EB": "\u22EA", # [BEST FIT] DOES NOT CONTAIN AS NORMAL SUBGROUP 147 | "\u22EC": "\u22ED", # [BEST FIT] NOT NORMAL SUBGROUP OF OR EQUAL TO 148 | # [BEST FIT] DOES NOT CONTAIN AS NORMAL SUBGROUP OR EQUAL 149 | "\u22ED": "\u22EC", 150 | "\u22F0": "\u22F1", # UP RIGHT DIAGONAL ELLIPSIS 151 | "\u22F1": "\u22F0", # DOWN RIGHT DIAGONAL ELLIPSIS 152 | "\u22F2": "\u22FA", # ELEMENT OF WITH LONG HORIZONTAL STROKE 153 | "\u22F3": "\u22FB", # ELEMENT OF WITH VERTICAL BAR AT END OF HORIZONTAL STROKE 154 | "\u22F4": "\u22FC", # SMALL ELEMENT OF WITH VERTICAL BAR AT END OF HORIZONTAL STROKE 155 | "\u22F6": "\u22FD", # ELEMENT OF WITH OVERBAR 156 | "\u22F7": "\u22FE", # SMALL ELEMENT OF WITH OVERBAR 157 | "\u22FA": "\u22F2", # CONTAINS WITH LONG HORIZONTAL STROKE 158 | "\u22FB": "\u22F3", # CONTAINS WITH VERTICAL BAR AT END OF HORIZONTAL STROKE 159 | "\u22FC": "\u22F4", # SMALL CONTAINS WITH VERTICAL BAR AT END OF HORIZONTAL STROKE 160 | "\u22FD": "\u22F6", # CONTAINS WITH OVERBAR 161 | "\u22FE": "\u22F7", # SMALL CONTAINS WITH OVERBAR 162 | "\u2308": "\u2309", # LEFT CEILING 163 | "\u2309": "\u2308", # RIGHT CEILING 164 | "\u230A": "\u230B", # LEFT FLOOR 165 | "\u230B": "\u230A", # RIGHT FLOOR 166 | "\u2329": "\u232A", # LEFT-POINTING ANGLE BRACKET 167 | "\u232A": "\u2329", # RIGHT-POINTING ANGLE BRACKET 168 | "\u2768": "\u2769", # MEDIUM LEFT PARENTHESIS ORNAMENT 169 | "\u2769": "\u2768", # MEDIUM RIGHT PARENTHESIS ORNAMENT 170 | "\u276A": "\u276B", # MEDIUM FLATTENED LEFT PARENTHESIS ORNAMENT 171 | "\u276B": "\u276A", # MEDIUM FLATTENED RIGHT PARENTHESIS ORNAMENT 172 | "\u276C": "\u276D", # MEDIUM LEFT-POINTING ANGLE BRACKET ORNAMENT 173 | "\u276D": "\u276C", # MEDIUM RIGHT-POINTING ANGLE BRACKET ORNAMENT 174 | "\u276E": "\u276F", # HEAVY LEFT-POINTING ANGLE QUOTATION MARK ORNAMENT 175 | "\u276F": "\u276E", # HEAVY RIGHT-POINTING ANGLE QUOTATION MARK ORNAMENT 176 | "\u2770": "\u2771", # HEAVY LEFT-POINTING ANGLE BRACKET ORNAMENT 177 | "\u2771": "\u2770", # HEAVY RIGHT-POINTING ANGLE BRACKET ORNAMENT 178 | "\u2772": "\u2773", # LIGHT LEFT TORTOISE SHELL BRACKET 179 | "\u2773": "\u2772", # LIGHT RIGHT TORTOISE SHELL BRACKET 180 | "\u2774": "\u2775", # MEDIUM LEFT CURLY BRACKET ORNAMENT 181 | "\u2775": "\u2774", # MEDIUM RIGHT CURLY BRACKET ORNAMENT 182 | "\u27C3": "\u27C4", # OPEN SUBSET 183 | "\u27C4": "\u27C3", # OPEN SUPERSET 184 | "\u27C5": "\u27C6", # LEFT S-SHAPED BAG DELIMITER 185 | "\u27C6": "\u27C5", # RIGHT S-SHAPED BAG DELIMITER 186 | "\u27C8": "\u27C9", # REVERSE SOLIDUS PRECEDING SUBSET 187 | "\u27C9": "\u27C8", # SUPERSET PRECEDING SOLIDUS 188 | "\u27D5": "\u27D6", # LEFT OUTER JOIN 189 | "\u27D6": "\u27D5", # RIGHT OUTER JOIN 190 | "\u27DD": "\u27DE", # LONG RIGHT TACK 191 | "\u27DE": "\u27DD", # LONG LEFT TACK 192 | "\u27E2": "\u27E3", # WHITE CONCAVE-SIDED DIAMOND WITH LEFTWARDS TICK 193 | "\u27E3": "\u27E2", # WHITE CONCAVE-SIDED DIAMOND WITH RIGHTWARDS TICK 194 | "\u27E4": "\u27E5", # WHITE SQUARE WITH LEFTWARDS TICK 195 | "\u27E5": "\u27E4", # WHITE SQUARE WITH RIGHTWARDS TICK 196 | "\u27E6": "\u27E7", # MATHEMATICAL LEFT WHITE SQUARE BRACKET 197 | "\u27E7": "\u27E6", # MATHEMATICAL RIGHT WHITE SQUARE BRACKET 198 | "\u27E8": "\u27E9", # MATHEMATICAL LEFT ANGLE BRACKET 199 | "\u27E9": "\u27E8", # MATHEMATICAL RIGHT ANGLE BRACKET 200 | "\u27EA": "\u27EB", # MATHEMATICAL LEFT DOUBLE ANGLE BRACKET 201 | "\u27EB": "\u27EA", # MATHEMATICAL RIGHT DOUBLE ANGLE BRACKET 202 | "\u27EC": "\u27ED", # MATHEMATICAL LEFT WHITE TORTOISE SHELL BRACKET 203 | "\u27ED": "\u27EC", # MATHEMATICAL RIGHT WHITE TORTOISE SHELL BRACKET 204 | "\u27EE": "\u27EF", # MATHEMATICAL LEFT FLATTENED PARENTHESIS 205 | "\u27EF": "\u27EE", # MATHEMATICAL RIGHT FLATTENED PARENTHESIS 206 | "\u2983": "\u2984", # LEFT WHITE CURLY BRACKET 207 | "\u2984": "\u2983", # RIGHT WHITE CURLY BRACKET 208 | "\u2985": "\u2986", # LEFT WHITE PARENTHESIS 209 | "\u2986": "\u2985", # RIGHT WHITE PARENTHESIS 210 | "\u2987": "\u2988", # Z NOTATION LEFT IMAGE BRACKET 211 | "\u2988": "\u2987", # Z NOTATION RIGHT IMAGE BRACKET 212 | "\u2989": "\u298A", # Z NOTATION LEFT BINDING BRACKET 213 | "\u298A": "\u2989", # Z NOTATION RIGHT BINDING BRACKET 214 | "\u298B": "\u298C", # LEFT SQUARE BRACKET WITH UNDERBAR 215 | "\u298C": "\u298B", # RIGHT SQUARE BRACKET WITH UNDERBAR 216 | "\u298D": "\u2990", # LEFT SQUARE BRACKET WITH TICK IN TOP CORNER 217 | "\u298E": "\u298F", # RIGHT SQUARE BRACKET WITH TICK IN BOTTOM CORNER 218 | "\u298F": "\u298E", # LEFT SQUARE BRACKET WITH TICK IN BOTTOM CORNER 219 | "\u2990": "\u298D", # RIGHT SQUARE BRACKET WITH TICK IN TOP CORNER 220 | "\u2991": "\u2992", # LEFT ANGLE BRACKET WITH DOT 221 | "\u2992": "\u2991", # RIGHT ANGLE BRACKET WITH DOT 222 | "\u2993": "\u2994", # LEFT ARC LESS-THAN BRACKET 223 | "\u2994": "\u2993", # RIGHT ARC GREATER-THAN BRACKET 224 | "\u2995": "\u2996", # DOUBLE LEFT ARC GREATER-THAN BRACKET 225 | "\u2996": "\u2995", # DOUBLE RIGHT ARC LESS-THAN BRACKET 226 | "\u2997": "\u2998", # LEFT BLACK TORTOISE SHELL BRACKET 227 | "\u2998": "\u2997", # RIGHT BLACK TORTOISE SHELL BRACKET 228 | "\u29B8": "\u2298", # CIRCLED REVERSE SOLIDUS 229 | "\u29C0": "\u29C1", # CIRCLED LESS-THAN 230 | "\u29C1": "\u29C0", # CIRCLED GREATER-THAN 231 | "\u29C4": "\u29C5", # SQUARED RISING DIAGONAL SLASH 232 | "\u29C5": "\u29C4", # SQUARED FALLING DIAGONAL SLASH 233 | "\u29CF": "\u29D0", # LEFT TRIANGLE BESIDE VERTICAL BAR 234 | "\u29D0": "\u29CF", # VERTICAL BAR BESIDE RIGHT TRIANGLE 235 | "\u29D1": "\u29D2", # BOWTIE WITH LEFT HALF BLACK 236 | "\u29D2": "\u29D1", # BOWTIE WITH RIGHT HALF BLACK 237 | "\u29D4": "\u29D5", # TIMES WITH LEFT HALF BLACK 238 | "\u29D5": "\u29D4", # TIMES WITH RIGHT HALF BLACK 239 | "\u29D8": "\u29D9", # LEFT WIGGLY FENCE 240 | "\u29D9": "\u29D8", # RIGHT WIGGLY FENCE 241 | "\u29DA": "\u29DB", # LEFT DOUBLE WIGGLY FENCE 242 | "\u29DB": "\u29DA", # RIGHT DOUBLE WIGGLY FENCE 243 | "\u29F5": "\u2215", # REVERSE SOLIDUS OPERATOR 244 | "\u29F8": "\u29F9", # BIG SOLIDUS 245 | "\u29F9": "\u29F8", # BIG REVERSE SOLIDUS 246 | "\u29FC": "\u29FD", # LEFT-POINTING CURVED ANGLE BRACKET 247 | "\u29FD": "\u29FC", # RIGHT-POINTING CURVED ANGLE BRACKET 248 | "\u2A2B": "\u2A2C", # MINUS SIGN WITH FALLING DOTS 249 | "\u2A2C": "\u2A2B", # MINUS SIGN WITH RISING DOTS 250 | "\u2A2D": "\u2A2E", # PLUS SIGN IN LEFT HALF CIRCLE 251 | "\u2A2E": "\u2A2D", # PLUS SIGN IN RIGHT HALF CIRCLE 252 | "\u2A34": "\u2A35", # MULTIPLICATION SIGN IN LEFT HALF CIRCLE 253 | "\u2A35": "\u2A34", # MULTIPLICATION SIGN IN RIGHT HALF CIRCLE 254 | "\u2A3C": "\u2A3D", # INTERIOR PRODUCT 255 | "\u2A3D": "\u2A3C", # RIGHTHAND INTERIOR PRODUCT 256 | "\u2A64": "\u2A65", # Z NOTATION DOMAIN ANTIRESTRICTION 257 | "\u2A65": "\u2A64", # Z NOTATION RANGE ANTIRESTRICTION 258 | "\u2A79": "\u2A7A", # LESS-THAN WITH CIRCLE INSIDE 259 | "\u2A7A": "\u2A79", # GREATER-THAN WITH CIRCLE INSIDE 260 | "\u2A7D": "\u2A7E", # LESS-THAN OR SLANTED EQUAL TO 261 | "\u2A7E": "\u2A7D", # GREATER-THAN OR SLANTED EQUAL TO 262 | "\u2A7F": "\u2A80", # LESS-THAN OR SLANTED EQUAL TO WITH DOT INSIDE 263 | "\u2A80": "\u2A7F", # GREATER-THAN OR SLANTED EQUAL TO WITH DOT INSIDE 264 | "\u2A81": "\u2A82", # LESS-THAN OR SLANTED EQUAL TO WITH DOT ABOVE 265 | "\u2A82": "\u2A81", # GREATER-THAN OR SLANTED EQUAL TO WITH DOT ABOVE 266 | "\u2A83": "\u2A84", # LESS-THAN OR SLANTED EQUAL TO WITH DOT ABOVE RIGHT 267 | "\u2A84": "\u2A83", # GREATER-THAN OR SLANTED EQUAL TO WITH DOT ABOVE LEFT 268 | "\u2A8B": "\u2A8C", # LESS-THAN ABOVE DOUBLE-LINE EQUAL ABOVE GREATER-THAN 269 | "\u2A8C": "\u2A8B", # GREATER-THAN ABOVE DOUBLE-LINE EQUAL ABOVE LESS-THAN 270 | "\u2A91": "\u2A92", # LESS-THAN ABOVE GREATER-THAN ABOVE DOUBLE-LINE EQUAL 271 | "\u2A92": "\u2A91", # GREATER-THAN ABOVE LESS-THAN ABOVE DOUBLE-LINE EQUAL 272 | # LESS-THAN ABOVE SLANTED EQUAL ABOVE GREATER-THAN ABOVE SLANTED EQUAL 273 | "\u2A93": "\u2A94", 274 | # GREATER-THAN ABOVE SLANTED EQUAL ABOVE LESS-THAN ABOVE SLANTED EQUAL 275 | "\u2A94": "\u2A93", 276 | "\u2A95": "\u2A96", # SLANTED EQUAL TO OR LESS-THAN 277 | "\u2A96": "\u2A95", # SLANTED EQUAL TO OR GREATER-THAN 278 | "\u2A97": "\u2A98", # SLANTED EQUAL TO OR LESS-THAN WITH DOT INSIDE 279 | "\u2A98": "\u2A97", # SLANTED EQUAL TO OR GREATER-THAN WITH DOT INSIDE 280 | "\u2A99": "\u2A9A", # DOUBLE-LINE EQUAL TO OR LESS-THAN 281 | "\u2A9A": "\u2A99", # DOUBLE-LINE EQUAL TO OR GREATER-THAN 282 | "\u2A9B": "\u2A9C", # DOUBLE-LINE SLANTED EQUAL TO OR LESS-THAN 283 | "\u2A9C": "\u2A9B", # DOUBLE-LINE SLANTED EQUAL TO OR GREATER-THAN 284 | "\u2AA1": "\u2AA2", # DOUBLE NESTED LESS-THAN 285 | "\u2AA2": "\u2AA1", # DOUBLE NESTED GREATER-THAN 286 | "\u2AA6": "\u2AA7", # LESS-THAN CLOSED BY CURVE 287 | "\u2AA7": "\u2AA6", # GREATER-THAN CLOSED BY CURVE 288 | "\u2AA8": "\u2AA9", # LESS-THAN CLOSED BY CURVE ABOVE SLANTED EQUAL 289 | "\u2AA9": "\u2AA8", # GREATER-THAN CLOSED BY CURVE ABOVE SLANTED EQUAL 290 | "\u2AAA": "\u2AAB", # SMALLER THAN 291 | "\u2AAB": "\u2AAA", # LARGER THAN 292 | "\u2AAC": "\u2AAD", # SMALLER THAN OR EQUAL TO 293 | "\u2AAD": "\u2AAC", # LARGER THAN OR EQUAL TO 294 | "\u2AAF": "\u2AB0", # PRECEDES ABOVE SINGLE-LINE EQUALS SIGN 295 | "\u2AB0": "\u2AAF", # SUCCEEDS ABOVE SINGLE-LINE EQUALS SIGN 296 | "\u2AB3": "\u2AB4", # PRECEDES ABOVE EQUALS SIGN 297 | "\u2AB4": "\u2AB3", # SUCCEEDS ABOVE EQUALS SIGN 298 | "\u2ABB": "\u2ABC", # DOUBLE PRECEDES 299 | "\u2ABC": "\u2ABB", # DOUBLE SUCCEEDS 300 | "\u2ABD": "\u2ABE", # SUBSET WITH DOT 301 | "\u2ABE": "\u2ABD", # SUPERSET WITH DOT 302 | "\u2ABF": "\u2AC0", # SUBSET WITH PLUS SIGN BELOW 303 | "\u2AC0": "\u2ABF", # SUPERSET WITH PLUS SIGN BELOW 304 | "\u2AC1": "\u2AC2", # SUBSET WITH MULTIPLICATION SIGN BELOW 305 | "\u2AC2": "\u2AC1", # SUPERSET WITH MULTIPLICATION SIGN BELOW 306 | "\u2AC3": "\u2AC4", # SUBSET OF OR EQUAL TO WITH DOT ABOVE 307 | "\u2AC4": "\u2AC3", # SUPERSET OF OR EQUAL TO WITH DOT ABOVE 308 | "\u2AC5": "\u2AC6", # SUBSET OF ABOVE EQUALS SIGN 309 | "\u2AC6": "\u2AC5", # SUPERSET OF ABOVE EQUALS SIGN 310 | "\u2ACD": "\u2ACE", # SQUARE LEFT OPEN BOX OPERATOR 311 | "\u2ACE": "\u2ACD", # SQUARE RIGHT OPEN BOX OPERATOR 312 | "\u2ACF": "\u2AD0", # CLOSED SUBSET 313 | "\u2AD0": "\u2ACF", # CLOSED SUPERSET 314 | "\u2AD1": "\u2AD2", # CLOSED SUBSET OR EQUAL TO 315 | "\u2AD2": "\u2AD1", # CLOSED SUPERSET OR EQUAL TO 316 | "\u2AD3": "\u2AD4", # SUBSET ABOVE SUPERSET 317 | "\u2AD4": "\u2AD3", # SUPERSET ABOVE SUBSET 318 | "\u2AD5": "\u2AD6", # SUBSET ABOVE SUBSET 319 | "\u2AD6": "\u2AD5", # SUPERSET ABOVE SUPERSET 320 | "\u2ADE": "\u22A6", # SHORT LEFT TACK 321 | "\u2AE3": "\u22A9", # DOUBLE VERTICAL BAR LEFT TURNSTILE 322 | "\u2AE4": "\u22A8", # VERTICAL BAR DOUBLE LEFT TURNSTILE 323 | "\u2AE5": "\u22AB", # DOUBLE VERTICAL BAR DOUBLE LEFT TURNSTILE 324 | "\u2AEC": "\u2AED", # DOUBLE STROKE NOT SIGN 325 | "\u2AED": "\u2AEC", # REVERSED DOUBLE STROKE NOT SIGN 326 | "\u2AF7": "\u2AF8", # TRIPLE NESTED LESS-THAN 327 | "\u2AF8": "\u2AF7", # TRIPLE NESTED GREATER-THAN 328 | "\u2AF9": "\u2AFA", # DOUBLE-LINE SLANTED LESS-THAN OR EQUAL TO 329 | "\u2AFA": "\u2AF9", # DOUBLE-LINE SLANTED GREATER-THAN OR EQUAL TO 330 | "\u2E02": "\u2E03", # LEFT SUBSTITUTION BRACKET 331 | "\u2E03": "\u2E02", # RIGHT SUBSTITUTION BRACKET 332 | "\u2E04": "\u2E05", # LEFT DOTTED SUBSTITUTION BRACKET 333 | "\u2E05": "\u2E04", # RIGHT DOTTED SUBSTITUTION BRACKET 334 | "\u2E09": "\u2E0A", # LEFT TRANSPOSITION BRACKET 335 | "\u2E0A": "\u2E09", # RIGHT TRANSPOSITION BRACKET 336 | "\u2E0C": "\u2E0D", # LEFT RAISED OMISSION BRACKET 337 | "\u2E0D": "\u2E0C", # RIGHT RAISED OMISSION BRACKET 338 | "\u2E1C": "\u2E1D", # LEFT LOW PARAPHRASE BRACKET 339 | "\u2E1D": "\u2E1C", # RIGHT LOW PARAPHRASE BRACKET 340 | "\u2E20": "\u2E21", # LEFT VERTICAL BAR WITH QUILL 341 | "\u2E21": "\u2E20", # RIGHT VERTICAL BAR WITH QUILL 342 | "\u2E22": "\u2E23", # TOP LEFT HALF BRACKET 343 | "\u2E23": "\u2E22", # TOP RIGHT HALF BRACKET 344 | "\u2E24": "\u2E25", # BOTTOM LEFT HALF BRACKET 345 | "\u2E25": "\u2E24", # BOTTOM RIGHT HALF BRACKET 346 | "\u2E26": "\u2E27", # LEFT SIDEWAYS U BRACKET 347 | "\u2E27": "\u2E26", # RIGHT SIDEWAYS U BRACKET 348 | "\u2E28": "\u2E29", # LEFT DOUBLE PARENTHESIS 349 | "\u2E29": "\u2E28", # RIGHT DOUBLE PARENTHESIS 350 | "\u3008": "\u3009", # LEFT ANGLE BRACKET 351 | "\u3009": "\u3008", # RIGHT ANGLE BRACKET 352 | "\u300A": "\u300B", # LEFT DOUBLE ANGLE BRACKET 353 | "\u300B": "\u300A", # RIGHT DOUBLE ANGLE BRACKET 354 | "\u300C": "\u300D", # [BEST FIT] LEFT CORNER BRACKET 355 | "\u300D": "\u300C", # [BEST FIT] RIGHT CORNER BRACKET 356 | "\u300E": "\u300F", # [BEST FIT] LEFT WHITE CORNER BRACKET 357 | "\u300F": "\u300E", # [BEST FIT] RIGHT WHITE CORNER BRACKET 358 | "\u3010": "\u3011", # LEFT BLACK LENTICULAR BRACKET 359 | "\u3011": "\u3010", # RIGHT BLACK LENTICULAR BRACKET 360 | "\u3014": "\u3015", # LEFT TORTOISE SHELL BRACKET 361 | "\u3015": "\u3014", # RIGHT TORTOISE SHELL BRACKET 362 | "\u3016": "\u3017", # LEFT WHITE LENTICULAR BRACKET 363 | "\u3017": "\u3016", # RIGHT WHITE LENTICULAR BRACKET 364 | "\u3018": "\u3019", # LEFT WHITE TORTOISE SHELL BRACKET 365 | "\u3019": "\u3018", # RIGHT WHITE TORTOISE SHELL BRACKET 366 | "\u301A": "\u301B", # LEFT WHITE SQUARE BRACKET 367 | "\u301B": "\u301A", # RIGHT WHITE SQUARE BRACKET 368 | "\uFE59": "\uFE5A", # SMALL LEFT PARENTHESIS 369 | "\uFE5A": "\uFE59", # SMALL RIGHT PARENTHESIS 370 | "\uFE5B": "\uFE5C", # SMALL LEFT CURLY BRACKET 371 | "\uFE5C": "\uFE5B", # SMALL RIGHT CURLY BRACKET 372 | "\uFE5D": "\uFE5E", # SMALL LEFT TORTOISE SHELL BRACKET 373 | "\uFE5E": "\uFE5D", # SMALL RIGHT TORTOISE SHELL BRACKET 374 | "\uFE64": "\uFE65", # SMALL LESS-THAN SIGN 375 | "\uFE65": "\uFE64", # SMALL GREATER-THAN SIGN 376 | "\uFF08": "\uFF09", # FULLWIDTH LEFT PARENTHESIS 377 | "\uFF09": "\uFF08", # FULLWIDTH RIGHT PARENTHESIS 378 | "\uFF1C": "\uFF1E", # FULLWIDTH LESS-THAN SIGN 379 | "\uFF1E": "\uFF1C", # FULLWIDTH GREATER-THAN SIGN 380 | "\uFF3B": "\uFF3D", # FULLWIDTH LEFT SQUARE BRACKET 381 | "\uFF3D": "\uFF3B", # FULLWIDTH RIGHT SQUARE BRACKET 382 | "\uFF5B": "\uFF5D", # FULLWIDTH LEFT CURLY BRACKET 383 | "\uFF5D": "\uFF5B", # FULLWIDTH RIGHT CURLY BRACKET 384 | "\uFF5F": "\uFF60", # FULLWIDTH LEFT WHITE PARENTHESIS 385 | "\uFF60": "\uFF5F", # FULLWIDTH RIGHT WHITE PARENTHESIS 386 | "\uFF62": "\uFF63", # [BEST FIT] HALFWIDTH LEFT CORNER BRACKET 387 | "\uFF63": "\uFF62", # [BEST FIT] HALFWIDTH RIGHT CORNER BRACKET 388 | } 389 | -------------------------------------------------------------------------------- /bidi/algorithm.py: -------------------------------------------------------------------------------- 1 | # This file is part of python-bidi 2 | # 3 | # python-bidi is free software: you can redistribute it and/or modify 4 | # it under the terms of the GNU Lesser General Public License as published by 5 | # the Free Software Foundation, either version 3 of the License, or 6 | # (at your option) any later version. 7 | # 8 | # This program is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU Lesser General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU Lesser General Public License 14 | # along with this program. If not, see . 15 | 16 | # Copyright (C) 2008-2010 Yaacov Zamir , 17 | # Copyright (C) 2010-2015 Meir kriheli . 18 | "bidirectional algorithm implementation" 19 | import inspect 20 | import sys 21 | from collections import deque 22 | from typing import Optional, Union 23 | from unicodedata import bidirectional, mirrored 24 | 25 | from .mirror import MIRRORED 26 | 27 | StrOrBytes = Union[str, bytes] 28 | 29 | # Some definitions 30 | PARAGRAPH_LEVELS = {"L": 0, "AL": 1, "R": 1} 31 | EXPLICIT_LEVEL_LIMIT = 62 32 | 33 | 34 | def _LEAST_GREATER_ODD(x): 35 | return (x + 1) | 1 36 | 37 | 38 | def _LEAST_GREATER_EVEN(x): 39 | return (x + 2) & ~1 40 | 41 | 42 | X2_X5_MAPPINGS = { 43 | "RLE": (_LEAST_GREATER_ODD, "N"), 44 | "LRE": (_LEAST_GREATER_EVEN, "N"), 45 | "RLO": (_LEAST_GREATER_ODD, "R"), 46 | "LRO": (_LEAST_GREATER_EVEN, "L"), 47 | } 48 | 49 | # Added 'B' so X6 won't execute in that case and X8 will run it's course 50 | X6_IGNORED = list(X2_X5_MAPPINGS.keys()) + ["BN", "PDF", "B"] 51 | X9_REMOVED = list(X2_X5_MAPPINGS.keys()) + ["BN", "PDF"] 52 | 53 | 54 | def _embedding_direction(x): 55 | return ("L", "R")[x % 2] 56 | 57 | 58 | _IS_UCS2 = sys.maxunicode == 65535 59 | _SURROGATE_MIN, _SURROGATE_MAX = 55296, 56319 # D800, DBFF 60 | 61 | 62 | def debug_storage(storage, base_info=False, chars=True, runs=False): 63 | "Display debug information for the storage" 64 | 65 | stderr = sys.stderr 66 | 67 | caller = inspect.stack()[1][3] 68 | stderr.write(f"in {caller}\n") 69 | 70 | if base_info: 71 | stderr.write(" base level : %d\n" % storage["base_level"]) 72 | stderr.write(" base dir : {}\n".format(storage["base_dir"])) 73 | 74 | if runs: 75 | stderr.write(" runs : {}\n".format(list(storage["runs"]))) 76 | 77 | if chars: 78 | output = " Chars : " 79 | for _ch in storage["chars"]: 80 | if _ch != "\n": 81 | output += _ch["ch"] 82 | else: 83 | output += "C" 84 | stderr.write(output + "\n") 85 | 86 | output = " Res. levels : {}\n".format("".join( 87 | [str(_ch["level"]) for _ch in storage["chars"]] 88 | )) 89 | stderr.write(output) 90 | 91 | _types = [_ch["type"].ljust(3) for _ch in storage["chars"]] 92 | 93 | for i in range(3): 94 | output = " %s\n" if i else " Res. types : %s\n" 95 | stderr.write(output % "".join([_t[i] for _t in _types])) 96 | 97 | 98 | def get_base_level(text, upper_is_rtl=False) -> int: 99 | """Get the paragraph base embedding level. Returns 0 for LTR, 100 | 1 for RTL. 101 | 102 | `text` a unicode object. 103 | 104 | Set `upper_is_rtl` to True to treat upper case chars as strong 'R' 105 | for debugging (default: False). 106 | 107 | """ 108 | base_level = None 109 | 110 | prev_surrogate = False 111 | # P2 112 | for _ch in text: 113 | # surrogate in case of ucs2 114 | if _IS_UCS2 and (_SURROGATE_MIN <= ord(_ch) <= _SURROGATE_MAX): 115 | prev_surrogate = _ch 116 | continue 117 | elif prev_surrogate: 118 | _ch = prev_surrogate + _ch 119 | prev_surrogate = False 120 | 121 | # treat upper as RTL ? 122 | if upper_is_rtl and _ch.isupper(): 123 | base_level = 1 124 | break 125 | 126 | bidi_type = bidirectional(_ch) 127 | 128 | if bidi_type in ("AL", "R"): 129 | base_level = 1 130 | break 131 | 132 | elif bidi_type == "L": 133 | base_level = 0 134 | break 135 | 136 | # P3 137 | if base_level is None: 138 | base_level = 0 139 | 140 | return base_level 141 | 142 | 143 | def get_embedding_levels(text, storage, upper_is_rtl=False, debug=False): 144 | """Get the paragraph base embedding level and direction, 145 | set the storage to the array of chars""" 146 | 147 | prev_surrogate = False 148 | base_level = storage["base_level"] 149 | 150 | # preset the storage's chars 151 | for _ch in text: 152 | if _IS_UCS2 and (_SURROGATE_MIN <= ord(_ch) <= _SURROGATE_MAX): 153 | prev_surrogate = _ch 154 | continue 155 | elif prev_surrogate: 156 | _ch = prev_surrogate + _ch 157 | prev_surrogate = False 158 | 159 | bidi_type = "R" if upper_is_rtl and _ch.isupper() else bidirectional(_ch) 160 | 161 | storage["chars"].append( 162 | {"ch": _ch, "level": base_level, "type": bidi_type, "orig": bidi_type} 163 | ) 164 | if debug: 165 | debug_storage(storage, base_info=True) 166 | 167 | 168 | def explicit_embed_and_overrides(storage, debug=False): 169 | """Apply X1 to X9 rules of the unicode algorithm. 170 | 171 | See http://unicode.org/reports/tr9/#Explicit_Levels_and_Directions 172 | 173 | """ 174 | overflow_counter = almost_overflow_counter = 0 175 | directional_override = "N" 176 | levels = deque() 177 | 178 | # X1 179 | embedding_level = storage["base_level"] 180 | 181 | for _ch in storage["chars"]: 182 | bidi_type = _ch["type"] 183 | 184 | level_func, override = X2_X5_MAPPINGS.get(bidi_type, (None, None)) 185 | 186 | if level_func: 187 | # So this is X2 to X5 188 | # if we've past EXPLICIT_LEVEL_LIMIT, note it and do nothing 189 | 190 | if overflow_counter != 0: 191 | overflow_counter += 1 192 | continue 193 | 194 | new_level = level_func(embedding_level) 195 | if new_level < EXPLICIT_LEVEL_LIMIT: 196 | levels.append((embedding_level, directional_override)) 197 | embedding_level, directional_override = new_level, override 198 | 199 | elif embedding_level == EXPLICIT_LEVEL_LIMIT - 2: 200 | # The new level is invalid, but a valid level can still be 201 | # achieved if this level is 60 and we encounter an RLE or 202 | # RLO further on. So record that we 'almost' overflowed. 203 | almost_overflow_counter += 1 204 | 205 | else: 206 | overflow_counter += 1 207 | else: 208 | # X6 209 | if bidi_type not in X6_IGNORED: 210 | _ch["level"] = embedding_level 211 | if directional_override != "N": 212 | _ch["type"] = directional_override 213 | 214 | # X7 215 | elif bidi_type == "PDF": 216 | if overflow_counter: 217 | overflow_counter -= 1 218 | elif ( 219 | almost_overflow_counter 220 | and embedding_level != EXPLICIT_LEVEL_LIMIT - 1 221 | ): 222 | almost_overflow_counter -= 1 223 | elif levels: 224 | embedding_level, directional_override = levels.pop() 225 | 226 | # X8 227 | elif bidi_type == "B": 228 | levels.clear() 229 | overflow_counter = almost_overflow_counter = 0 230 | embedding_level = _ch["level"] = storage["base_level"] 231 | directional_override = "N" 232 | 233 | # Removes the explicit embeds and overrides of types 234 | # RLE, LRE, RLO, LRO, PDF, and BN. Adjusts extended chars 235 | # next and prev as well 236 | 237 | # Applies X9. See http://unicode.org/reports/tr9/#X9 238 | storage["chars"] = [ 239 | _ch for _ch in storage["chars"] if _ch["type"] not in X9_REMOVED 240 | ] 241 | 242 | calc_level_runs(storage) 243 | 244 | if debug: 245 | debug_storage(storage, runs=True) 246 | 247 | 248 | def calc_level_runs(storage): 249 | """Split the storage to run of char types at the same level. 250 | 251 | Applies X10. See http://unicode.org/reports/tr9/#X10 252 | """ 253 | # run level depends on the higher of the two levels on either side of 254 | # the boundary If the higher level is odd, the type is R; otherwise, 255 | # it is L 256 | 257 | storage["runs"].clear() 258 | chars = storage["chars"] 259 | 260 | # empty string ? 261 | if not chars: 262 | return 263 | 264 | def calc_level_run(b_l, b_r): 265 | return ["L", "R"][max(b_l, b_r) % 2] 266 | 267 | first_char = chars[0] 268 | 269 | sor = calc_level_run(storage["base_level"], first_char["level"]) 270 | eor = None 271 | 272 | run_start = run_length = 0 273 | 274 | curr_level, curr_type = 0, "" 275 | prev_level, prev_type = first_char["level"], first_char["type"] 276 | 277 | for _ch in chars: 278 | curr_level, curr_type = _ch["level"], _ch["type"] 279 | 280 | if curr_level == prev_level: 281 | run_length += 1 282 | else: 283 | eor = calc_level_run(prev_level, curr_level) 284 | storage["runs"].append( 285 | { 286 | "sor": sor, 287 | "eor": eor, 288 | "start": run_start, 289 | "type": prev_type, 290 | "length": run_length, 291 | } 292 | ) 293 | sor = eor 294 | run_start += run_length 295 | run_length = 1 296 | 297 | prev_level, prev_type = curr_level, curr_type 298 | 299 | # for the last char/runlevel 300 | eor = calc_level_run(curr_level, storage["base_level"]) 301 | storage["runs"].append( 302 | { 303 | "sor": sor, 304 | "eor": eor, 305 | "start": run_start, 306 | "type": curr_type, 307 | "length": run_length, 308 | } 309 | ) 310 | 311 | 312 | def resolve_weak_types(storage, debug=False): 313 | """Resolve weak type rules W1 - W3. 314 | 315 | See: http://unicode.org/reports/tr9/#Resolving_Weak_Types 316 | 317 | """ 318 | 319 | for run in storage["runs"]: 320 | prev_strong = prev_type = run["sor"] 321 | start, length = run["start"], run["length"] 322 | chars = storage["chars"][start : start + length] 323 | for _ch in chars: 324 | # W1. Examine each nonspacing mark (NSM) in the level run, and 325 | # change the type of the NSM to the type of the previous character. 326 | # If the NSM is at the start of the level run, it will get the type 327 | # of sor. 328 | bidi_type = _ch["type"] 329 | 330 | if bidi_type == "NSM": 331 | _ch["type"] = bidi_type = prev_type 332 | 333 | # W2. Search backward from each instance of a European number until 334 | # the first strong type (R, L, AL, or sor) is found. If an AL is 335 | # found, change the type of the European number to Arabic number. 336 | if bidi_type == "EN" and prev_strong == "AL": 337 | _ch["type"] = "AN" 338 | 339 | # update prev_strong if needed 340 | if bidi_type in ("R", "L", "AL"): 341 | prev_strong = bidi_type 342 | 343 | prev_type = _ch["type"] 344 | 345 | # W3. Change all ALs to R 346 | for _ch in chars: 347 | if _ch["type"] == "AL": 348 | _ch["type"] = "R" 349 | 350 | # W4. A single European separator between two European numbers changes 351 | # to a European number. A single common separator between two numbers of 352 | # the same type changes to that type. 353 | for idx in range(1, len(chars) - 1): 354 | bidi_type = chars[idx]["type"] 355 | prev_type = chars[idx - 1]["type"] 356 | next_type = chars[idx + 1]["type"] 357 | 358 | if bidi_type == "ES" and (prev_type == next_type == "EN"): 359 | chars[idx]["type"] = "EN" 360 | 361 | if ( 362 | bidi_type == "CS" 363 | and prev_type == next_type 364 | and prev_type in ("AN", "EN") 365 | ): 366 | chars[idx]["type"] = prev_type 367 | 368 | # W5. A sequence of European terminators adjacent to European numbers 369 | # changes to all European numbers. 370 | for idx in range(len(chars)): 371 | if chars[idx]["type"] == "EN": 372 | for et_idx in range(idx - 1, -1, -1): 373 | if chars[et_idx]["type"] == "ET": 374 | chars[et_idx]["type"] = "EN" 375 | else: 376 | break 377 | for et_idx in range(idx + 1, len(chars)): 378 | if chars[et_idx]["type"] == "ET": 379 | chars[et_idx]["type"] = "EN" 380 | else: 381 | break 382 | 383 | # W6. Otherwise, separators and terminators change to Other Neutral. 384 | for _ch in chars: 385 | if _ch["type"] in ("ET", "ES", "CS"): 386 | _ch["type"] = "ON" 387 | 388 | # W7. Search backward from each instance of a European number until the 389 | # first strong type (R, L, or sor) is found. If an L is found, then 390 | # change the type of the European number to L. 391 | prev_strong = run["sor"] 392 | for _ch in chars: 393 | if _ch["type"] == "EN" and prev_strong == "L": 394 | _ch["type"] = "L" 395 | 396 | if _ch["type"] in ("L", "R"): 397 | prev_strong = _ch["type"] 398 | 399 | if debug: 400 | debug_storage(storage, runs=True) 401 | 402 | 403 | def resolve_neutral_types(storage, debug): 404 | """Resolving neutral types. Implements N1 and N2 405 | 406 | See: http://unicode.org/reports/tr9/#Resolving_Neutral_Types 407 | 408 | """ 409 | 410 | prev_bidi_type = "" 411 | for run in storage["runs"]: 412 | start, length = run["start"], run["length"] 413 | # use sor and eor 414 | chars = ( 415 | [{"type": run["sor"]}] 416 | + storage["chars"][start : start + length] 417 | + [{"type": run["eor"]}] 418 | ) 419 | total_chars = len(chars) 420 | 421 | seq_start = None 422 | for idx in range(total_chars): 423 | _ch = chars[idx] 424 | if _ch["type"] in ("B", "S", "WS", "ON"): 425 | # N1. A sequence of neutrals takes the direction of the 426 | # surrounding strong text if the text on both sides has the same 427 | # direction. European and Arabic numbers act as if they were R 428 | # in terms of their influence on neutrals. Start-of-level-run 429 | # (sor) and end-of-level-run (eor) are used at level run 430 | # boundaries. 431 | if seq_start is None: 432 | seq_start = idx 433 | prev_bidi_type = chars[idx - 1]["type"] 434 | else: 435 | if seq_start is not None: 436 | next_bidi_type = chars[idx]["type"] 437 | 438 | if prev_bidi_type in ("AN", "EN"): 439 | prev_bidi_type = "R" 440 | 441 | if next_bidi_type in ("AN", "EN"): 442 | next_bidi_type = "R" 443 | 444 | for seq_idx in range(seq_start, idx): 445 | if prev_bidi_type == next_bidi_type: 446 | chars[seq_idx]["type"] = prev_bidi_type 447 | else: 448 | # N2. Any remaining neutrals take the embedding 449 | # direction. The embedding direction for the given 450 | # neutral character is derived from its embedding 451 | # level: L if the character is set to an even level, 452 | # and R if the level is odd. 453 | chars[seq_idx]["type"] = _embedding_direction( 454 | chars[seq_idx]["level"] 455 | ) 456 | 457 | seq_start = None 458 | 459 | if debug: 460 | debug_storage(storage) 461 | 462 | 463 | def resolve_implicit_levels(storage, debug): 464 | """Resolving implicit levels (I1, I2) 465 | 466 | See: http://unicode.org/reports/tr9/#Resolving_Implicit_Levels 467 | 468 | """ 469 | for run in storage["runs"]: 470 | start, length = run["start"], run["length"] 471 | chars = storage["chars"][start : start + length] 472 | 473 | for _ch in chars: 474 | # only those types are allowed at this stage 475 | assert _ch["type"] in ("L", "R", "EN", "AN"), ( 476 | "{} not allowed here".format(_ch["type"]) 477 | ) 478 | 479 | if _embedding_direction(_ch["level"]) == "L": 480 | # I1. For all characters with an even (left-to-right) embedding 481 | # direction, those of type R go up one level and those of type 482 | # AN or EN go up two levels. 483 | if _ch["type"] == "R": 484 | _ch["level"] += 1 485 | elif _ch["type"] != "L": 486 | _ch["level"] += 2 487 | else: 488 | # I2. For all characters with an odd (right-to-left) embedding 489 | # direction, those of type L, EN or AN go up one level. 490 | if _ch["type"] != "R": 491 | _ch["level"] += 1 492 | 493 | if debug: 494 | debug_storage(storage, runs=True) 495 | 496 | 497 | def reverse_contiguous_sequence( 498 | chars, line_start, line_end, highest_level, lowest_odd_level 499 | ): 500 | """L2. From the highest level found in the text to the lowest odd 501 | level on each line, including intermediate levels not actually 502 | present in the text, reverse any contiguous sequence of characters 503 | that are at that level or higher. 504 | 505 | """ 506 | for level in range(highest_level, lowest_odd_level - 1, -1): 507 | _start = _end = None 508 | 509 | for run_idx in range(line_start, line_end + 1): 510 | run_ch = chars[run_idx] 511 | 512 | if run_ch["level"] >= level: 513 | if _start is None: 514 | _start = _end = run_idx 515 | else: 516 | _end = run_idx 517 | else: 518 | if _end is not None: 519 | chars[_start : +_end + 1] = reversed(chars[_start : +_end + 1]) 520 | _start = _end = None 521 | 522 | # anything remaining ? 523 | if _start is not None and _end is not None: 524 | chars[_start : +_end + 1] = reversed(chars[_start : +_end + 1]) 525 | 526 | 527 | def reorder_resolved_levels(storage, debug): 528 | """L1 and L2 rules""" 529 | 530 | # Applies L1. 531 | 532 | should_reset = True 533 | chars = storage["chars"] 534 | 535 | for _ch in chars[::-1]: 536 | # L1. On each line, reset the embedding level of the following 537 | # characters to the paragraph embedding level: 538 | if _ch["orig"] in ("B", "S"): 539 | # 1. Segment separators, 540 | # 2. Paragraph separators, 541 | _ch["level"] = storage["base_level"] 542 | should_reset = True 543 | elif should_reset and _ch["orig"] in ("BN", "WS"): 544 | # 3. Any sequence of whitespace characters preceding a segment 545 | # separator or paragraph separator 546 | # 4. Any sequence of white space characters at the end of the 547 | # line. 548 | _ch["level"] = storage["base_level"] 549 | else: 550 | should_reset = False 551 | 552 | max_len = len(chars) 553 | 554 | # L2 should be per line 555 | # Calculates highest level and lowest odd level on the fly. 556 | 557 | line_start = line_end = 0 558 | highest_level = 0 559 | lowest_odd_level = EXPLICIT_LEVEL_LIMIT 560 | 561 | for idx in range(max_len): 562 | _ch = chars[idx] 563 | 564 | # calc the levels 565 | char_level = _ch["level"] 566 | if char_level > highest_level: 567 | highest_level = char_level 568 | 569 | if char_level % 2 and char_level < lowest_odd_level: 570 | lowest_odd_level = char_level 571 | 572 | if _ch["orig"] == "B" or idx == max_len - 1: 573 | line_end = idx 574 | # omit line breaks 575 | if _ch["orig"] == "B": 576 | line_end -= 1 577 | 578 | reverse_contiguous_sequence( 579 | chars, line_start, line_end, highest_level, lowest_odd_level 580 | ) 581 | 582 | # reset for next line run 583 | line_start = idx + 1 584 | highest_level = 0 585 | lowest_odd_level = EXPLICIT_LEVEL_LIMIT 586 | 587 | if debug: 588 | debug_storage(storage) 589 | 590 | 591 | def apply_mirroring(storage, debug): 592 | """Applies L4: mirroring 593 | 594 | See: http://unicode.org/reports/tr9/#L4 595 | 596 | """ 597 | # L4. A character is depicted by a mirrored glyph if and only if (a) the 598 | # resolved directionality of that character is R, and (b) the 599 | # Bidi_Mirrored property value of that character is true. 600 | for _ch in storage["chars"]: 601 | unichar = _ch["ch"] 602 | if mirrored(unichar) and _embedding_direction(_ch["level"]) == "R": 603 | _ch["ch"] = MIRRORED.get(unichar, unichar) 604 | 605 | if debug: 606 | debug_storage(storage) 607 | 608 | 609 | def get_empty_storage(): 610 | """Return an empty storage skeleton, usable for testing""" 611 | return { 612 | "base_level": None, 613 | "base_dir": None, 614 | "chars": [], 615 | "runs": deque(), 616 | } 617 | 618 | 619 | def get_display( 620 | str_or_bytes: StrOrBytes, 621 | encoding: str = "utf-8", 622 | upper_is_rtl: bool = False, 623 | base_dir: Optional[str] = None, 624 | debug: bool = False, 625 | ) -> StrOrBytes: 626 | """Accepts `str` or `bytes`. In case it's `bytes`, `encoding` 627 | is needed as the algorithm works on `str` (default:"utf-8"). 628 | 629 | Set `upper_is_rtl` to True to treat upper case chars as strong 'R' 630 | for debugging (default: False). 631 | 632 | Set `base_dir` to 'L' or 'R' to override the calculated base_level. 633 | 634 | Set `debug` to True to display (using sys.stderr) the steps taken with the 635 | algorithm. 636 | 637 | Returns the display layout, either as unicode or `encoding` encoded 638 | string. 639 | 640 | """ 641 | storage = get_empty_storage() 642 | 643 | # utf-8 ? we need unicode 644 | if isinstance(str_or_bytes, bytes): 645 | text = str_or_bytes.decode(encoding) 646 | was_decoded = True 647 | else: 648 | text = str_or_bytes 649 | was_decoded = False 650 | 651 | if base_dir is None: 652 | base_level = get_base_level(text, upper_is_rtl) 653 | else: 654 | base_level = PARAGRAPH_LEVELS[base_dir] 655 | 656 | storage["base_level"] = base_level 657 | storage["base_dir"] = ("L", "R")[base_level] 658 | 659 | get_embedding_levels(text, storage, upper_is_rtl, debug) 660 | explicit_embed_and_overrides(storage, debug) 661 | resolve_weak_types(storage, debug) 662 | resolve_neutral_types(storage, debug) 663 | resolve_implicit_levels(storage, debug) 664 | reorder_resolved_levels(storage, debug) 665 | apply_mirroring(storage, debug) 666 | 667 | chars = storage["chars"] 668 | display = "".join([_ch["ch"] for _ch in chars]) 669 | 670 | if was_decoded: 671 | display = display.encode(encoding) 672 | 673 | return display 674 | -------------------------------------------------------------------------------- /COPYING: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | Preamble 9 | 10 | The GNU General Public License is a free, copyleft license for 11 | software and other kinds of works. 12 | 13 | The licenses for most software and other practical works are designed 14 | to take away your freedom to share and change the works. By contrast, 15 | the GNU General Public License is intended to guarantee your freedom to 16 | share and change all versions of a program--to make sure it remains free 17 | software for all its users. We, the Free Software Foundation, use the 18 | GNU General Public License for most of our software; it applies also to 19 | any other work released this way by its authors. You can apply it to 20 | your programs, too. 21 | 22 | When we speak of free software, we are referring to freedom, not 23 | price. Our General Public Licenses are designed to make sure that you 24 | have the freedom to distribute copies of free software (and charge for 25 | them if you wish), that you receive source code or can get it if you 26 | want it, that you can change the software or use pieces of it in new 27 | free programs, and that you know you can do these things. 28 | 29 | To protect your rights, we need to prevent others from denying you 30 | these rights or asking you to surrender the rights. Therefore, you have 31 | certain responsibilities if you distribute copies of the software, or if 32 | you modify it: responsibilities to respect the freedom of others. 33 | 34 | For example, if you distribute copies of such a program, whether 35 | gratis or for a fee, you must pass on to the recipients the same 36 | freedoms that you received. You must make sure that they, too, receive 37 | or can get the source code. And you must show them these terms so they 38 | know their rights. 39 | 40 | Developers that use the GNU GPL protect your rights with two steps: 41 | (1) assert copyright on the software, and (2) offer you this License 42 | giving you legal permission to copy, distribute and/or modify it. 43 | 44 | For the developers' and authors' protection, the GPL clearly explains 45 | that there is no warranty for this free software. For both users' and 46 | authors' sake, the GPL requires that modified versions be marked as 47 | changed, so that their problems will not be attributed erroneously to 48 | authors of previous versions. 49 | 50 | Some devices are designed to deny users access to install or run 51 | modified versions of the software inside them, although the manufacturer 52 | can do so. This is fundamentally incompatible with the aim of 53 | protecting users' freedom to change the software. The systematic 54 | pattern of such abuse occurs in the area of products for individuals to 55 | use, which is precisely where it is most unacceptable. Therefore, we 56 | have designed this version of the GPL to prohibit the practice for those 57 | products. If such problems arise substantially in other domains, we 58 | stand ready to extend this provision to those domains in future versions 59 | of the GPL, as needed to protect the freedom of users. 60 | 61 | Finally, every program is threatened constantly by software patents. 62 | States should not allow patents to restrict development and use of 63 | software on general-purpose computers, but in those that do, we wish to 64 | avoid the special danger that patents applied to a free program could 65 | make it effectively proprietary. To prevent this, the GPL assures that 66 | patents cannot be used to render the program non-free. 67 | 68 | The precise terms and conditions for copying, distribution and 69 | modification follow. 70 | 71 | TERMS AND CONDITIONS 72 | 73 | 0. Definitions. 74 | 75 | "This License" refers to version 3 of the GNU General Public License. 76 | 77 | "Copyright" also means copyright-like laws that apply to other kinds of 78 | works, such as semiconductor masks. 79 | 80 | "The Program" refers to any copyrightable work licensed under this 81 | License. Each licensee is addressed as "you". "Licensees" and 82 | "recipients" may be individuals or organizations. 83 | 84 | To "modify" a work means to copy from or adapt all or part of the work 85 | in a fashion requiring copyright permission, other than the making of an 86 | exact copy. The resulting work is called a "modified version" of the 87 | earlier work or a work "based on" the earlier work. 88 | 89 | A "covered work" means either the unmodified Program or a work based 90 | on the Program. 91 | 92 | To "propagate" a work means to do anything with it that, without 93 | permission, would make you directly or secondarily liable for 94 | infringement under applicable copyright law, except executing it on a 95 | computer or modifying a private copy. Propagation includes copying, 96 | distribution (with or without modification), making available to the 97 | public, and in some countries other activities as well. 98 | 99 | To "convey" a work means any kind of propagation that enables other 100 | parties to make or receive copies. Mere interaction with a user through 101 | a computer network, with no transfer of a copy, is not conveying. 102 | 103 | An interactive user interface displays "Appropriate Legal Notices" 104 | to the extent that it includes a convenient and prominently visible 105 | feature that (1) displays an appropriate copyright notice, and (2) 106 | tells the user that there is no warranty for the work (except to the 107 | extent that warranties are provided), that licensees may convey the 108 | work under this License, and how to view a copy of this License. If 109 | the interface presents a list of user commands or options, such as a 110 | menu, a prominent item in the list meets this criterion. 111 | 112 | 1. Source Code. 113 | 114 | The "source code" for a work means the preferred form of the work 115 | for making modifications to it. "Object code" means any non-source 116 | form of a work. 117 | 118 | A "Standard Interface" means an interface that either is an official 119 | standard defined by a recognized standards body, or, in the case of 120 | interfaces specified for a particular programming language, one that 121 | is widely used among developers working in that language. 122 | 123 | The "System Libraries" of an executable work include anything, other 124 | than the work as a whole, that (a) is included in the normal form of 125 | packaging a Major Component, but which is not part of that Major 126 | Component, and (b) serves only to enable use of the work with that 127 | Major Component, or to implement a Standard Interface for which an 128 | implementation is available to the public in source code form. A 129 | "Major Component", in this context, means a major essential component 130 | (kernel, window system, and so on) of the specific operating system 131 | (if any) on which the executable work runs, or a compiler used to 132 | produce the work, or an object code interpreter used to run it. 133 | 134 | The "Corresponding Source" for a work in object code form means all 135 | the source code needed to generate, install, and (for an executable 136 | work) run the object code and to modify the work, including scripts to 137 | control those activities. However, it does not include the work's 138 | System Libraries, or general-purpose tools or generally available free 139 | programs which are used unmodified in performing those activities but 140 | which are not part of the work. For example, Corresponding Source 141 | includes interface definition files associated with source files for 142 | the work, and the source code for shared libraries and dynamically 143 | linked subprograms that the work is specifically designed to require, 144 | such as by intimate data communication or control flow between those 145 | subprograms and other parts of the work. 146 | 147 | The Corresponding Source need not include anything that users 148 | can regenerate automatically from other parts of the Corresponding 149 | Source. 150 | 151 | The Corresponding Source for a work in source code form is that 152 | same work. 153 | 154 | 2. Basic Permissions. 155 | 156 | All rights granted under this License are granted for the term of 157 | copyright on the Program, and are irrevocable provided the stated 158 | conditions are met. This License explicitly affirms your unlimited 159 | permission to run the unmodified Program. The output from running a 160 | covered work is covered by this License only if the output, given its 161 | content, constitutes a covered work. This License acknowledges your 162 | rights of fair use or other equivalent, as provided by copyright law. 163 | 164 | You may make, run and propagate covered works that you do not 165 | convey, without conditions so long as your license otherwise remains 166 | in force. You may convey covered works to others for the sole purpose 167 | of having them make modifications exclusively for you, or provide you 168 | with facilities for running those works, provided that you comply with 169 | the terms of this License in conveying all material for which you do 170 | not control copyright. Those thus making or running the covered works 171 | for you must do so exclusively on your behalf, under your direction 172 | and control, on terms that prohibit them from making any copies of 173 | your copyrighted material outside their relationship with you. 174 | 175 | Conveying under any other circumstances is permitted solely under 176 | the conditions stated below. Sublicensing is not allowed; section 10 177 | makes it unnecessary. 178 | 179 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law. 180 | 181 | No covered work shall be deemed part of an effective technological 182 | measure under any applicable law fulfilling obligations under article 183 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or 184 | similar laws prohibiting or restricting circumvention of such 185 | measures. 186 | 187 | When you convey a covered work, you waive any legal power to forbid 188 | circumvention of technological measures to the extent such circumvention 189 | is effected by exercising rights under this License with respect to 190 | the covered work, and you disclaim any intention to limit operation or 191 | modification of the work as a means of enforcing, against the work's 192 | users, your or third parties' legal rights to forbid circumvention of 193 | technological measures. 194 | 195 | 4. Conveying Verbatim Copies. 196 | 197 | You may convey verbatim copies of the Program's source code as you 198 | receive it, in any medium, provided that you conspicuously and 199 | appropriately publish on each copy an appropriate copyright notice; 200 | keep intact all notices stating that this License and any 201 | non-permissive terms added in accord with section 7 apply to the code; 202 | keep intact all notices of the absence of any warranty; and give all 203 | recipients a copy of this License along with the Program. 204 | 205 | You may charge any price or no price for each copy that you convey, 206 | and you may offer support or warranty protection for a fee. 207 | 208 | 5. Conveying Modified Source Versions. 209 | 210 | You may convey a work based on the Program, or the modifications to 211 | produce it from the Program, in the form of source code under the 212 | terms of section 4, provided that you also meet all of these conditions: 213 | 214 | a) The work must carry prominent notices stating that you modified 215 | it, and giving a relevant date. 216 | 217 | b) The work must carry prominent notices stating that it is 218 | released under this License and any conditions added under section 219 | 7. This requirement modifies the requirement in section 4 to 220 | "keep intact all notices". 221 | 222 | c) You must license the entire work, as a whole, under this 223 | License to anyone who comes into possession of a copy. This 224 | License will therefore apply, along with any applicable section 7 225 | additional terms, to the whole of the work, and all its parts, 226 | regardless of how they are packaged. This License gives no 227 | permission to license the work in any other way, but it does not 228 | invalidate such permission if you have separately received it. 229 | 230 | d) If the work has interactive user interfaces, each must display 231 | Appropriate Legal Notices; however, if the Program has interactive 232 | interfaces that do not display Appropriate Legal Notices, your 233 | work need not make them do so. 234 | 235 | A compilation of a covered work with other separate and independent 236 | works, which are not by their nature extensions of the covered work, 237 | and which are not combined with it such as to form a larger program, 238 | in or on a volume of a storage or distribution medium, is called an 239 | "aggregate" if the compilation and its resulting copyright are not 240 | used to limit the access or legal rights of the compilation's users 241 | beyond what the individual works permit. Inclusion of a covered work 242 | in an aggregate does not cause this License to apply to the other 243 | parts of the aggregate. 244 | 245 | 6. Conveying Non-Source Forms. 246 | 247 | You may convey a covered work in object code form under the terms 248 | of sections 4 and 5, provided that you also convey the 249 | machine-readable Corresponding Source under the terms of this License, 250 | in one of these ways: 251 | 252 | a) Convey the object code in, or embodied in, a physical product 253 | (including a physical distribution medium), accompanied by the 254 | Corresponding Source fixed on a durable physical medium 255 | customarily used for software interchange. 256 | 257 | b) Convey the object code in, or embodied in, a physical product 258 | (including a physical distribution medium), accompanied by a 259 | written offer, valid for at least three years and valid for as 260 | long as you offer spare parts or customer support for that product 261 | model, to give anyone who possesses the object code either (1) a 262 | copy of the Corresponding Source for all the software in the 263 | product that is covered by this License, on a durable physical 264 | medium customarily used for software interchange, for a price no 265 | more than your reasonable cost of physically performing this 266 | conveying of source, or (2) access to copy the 267 | Corresponding Source from a network server at no charge. 268 | 269 | c) Convey individual copies of the object code with a copy of the 270 | written offer to provide the Corresponding Source. This 271 | alternative is allowed only occasionally and noncommercially, and 272 | only if you received the object code with such an offer, in accord 273 | with subsection 6b. 274 | 275 | d) Convey the object code by offering access from a designated 276 | place (gratis or for a charge), and offer equivalent access to the 277 | Corresponding Source in the same way through the same place at no 278 | further charge. You need not require recipients to copy the 279 | Corresponding Source along with the object code. If the place to 280 | copy the object code is a network server, the Corresponding Source 281 | may be on a different server (operated by you or a third party) 282 | that supports equivalent copying facilities, provided you maintain 283 | clear directions next to the object code saying where to find the 284 | Corresponding Source. Regardless of what server hosts the 285 | Corresponding Source, you remain obligated to ensure that it is 286 | available for as long as needed to satisfy these requirements. 287 | 288 | e) Convey the object code using peer-to-peer transmission, provided 289 | you inform other peers where the object code and Corresponding 290 | Source of the work are being offered to the general public at no 291 | charge under subsection 6d. 292 | 293 | A separable portion of the object code, whose source code is excluded 294 | from the Corresponding Source as a System Library, need not be 295 | included in conveying the object code work. 296 | 297 | A "User Product" is either (1) a "consumer product", which means any 298 | tangible personal property which is normally used for personal, family, 299 | or household purposes, or (2) anything designed or sold for incorporation 300 | into a dwelling. In determining whether a product is a consumer product, 301 | doubtful cases shall be resolved in favor of coverage. For a particular 302 | product received by a particular user, "normally used" refers to a 303 | typical or common use of that class of product, regardless of the status 304 | of the particular user or of the way in which the particular user 305 | actually uses, or expects or is expected to use, the product. A product 306 | is a consumer product regardless of whether the product has substantial 307 | commercial, industrial or non-consumer uses, unless such uses represent 308 | the only significant mode of use of the product. 309 | 310 | "Installation Information" for a User Product means any methods, 311 | procedures, authorization keys, or other information required to install 312 | and execute modified versions of a covered work in that User Product from 313 | a modified version of its Corresponding Source. The information must 314 | suffice to ensure that the continued functioning of the modified object 315 | code is in no case prevented or interfered with solely because 316 | modification has been made. 317 | 318 | If you convey an object code work under this section in, or with, or 319 | specifically for use in, a User Product, and the conveying occurs as 320 | part of a transaction in which the right of possession and use of the 321 | User Product is transferred to the recipient in perpetuity or for a 322 | fixed term (regardless of how the transaction is characterized), the 323 | Corresponding Source conveyed under this section must be accompanied 324 | by the Installation Information. But this requirement does not apply 325 | if neither you nor any third party retains the ability to install 326 | modified object code on the User Product (for example, the work has 327 | been installed in ROM). 328 | 329 | The requirement to provide Installation Information does not include a 330 | requirement to continue to provide support service, warranty, or updates 331 | for a work that has been modified or installed by the recipient, or for 332 | the User Product in which it has been modified or installed. Access to a 333 | network may be denied when the modification itself materially and 334 | adversely affects the operation of the network or violates the rules and 335 | protocols for communication across the network. 336 | 337 | Corresponding Source conveyed, and Installation Information provided, 338 | in accord with this section must be in a format that is publicly 339 | documented (and with an implementation available to the public in 340 | source code form), and must require no special password or key for 341 | unpacking, reading or copying. 342 | 343 | 7. Additional Terms. 344 | 345 | "Additional permissions" are terms that supplement the terms of this 346 | License by making exceptions from one or more of its conditions. 347 | Additional permissions that are applicable to the entire Program shall 348 | be treated as though they were included in this License, to the extent 349 | that they are valid under applicable law. If additional permissions 350 | apply only to part of the Program, that part may be used separately 351 | under those permissions, but the entire Program remains governed by 352 | this License without regard to the additional permissions. 353 | 354 | When you convey a copy of a covered work, you may at your option 355 | remove any additional permissions from that copy, or from any part of 356 | it. (Additional permissions may be written to require their own 357 | removal in certain cases when you modify the work.) You may place 358 | additional permissions on material, added by you to a covered work, 359 | for which you have or can give appropriate copyright permission. 360 | 361 | Notwithstanding any other provision of this License, for material you 362 | add to a covered work, you may (if authorized by the copyright holders of 363 | that material) supplement the terms of this License with terms: 364 | 365 | a) Disclaiming warranty or limiting liability differently from the 366 | terms of sections 15 and 16 of this License; or 367 | 368 | b) Requiring preservation of specified reasonable legal notices or 369 | author attributions in that material or in the Appropriate Legal 370 | Notices displayed by works containing it; or 371 | 372 | c) Prohibiting misrepresentation of the origin of that material, or 373 | requiring that modified versions of such material be marked in 374 | reasonable ways as different from the original version; or 375 | 376 | d) Limiting the use for publicity purposes of names of licensors or 377 | authors of the material; or 378 | 379 | e) Declining to grant rights under trademark law for use of some 380 | trade names, trademarks, or service marks; or 381 | 382 | f) Requiring indemnification of licensors and authors of that 383 | material by anyone who conveys the material (or modified versions of 384 | it) with contractual assumptions of liability to the recipient, for 385 | any liability that these contractual assumptions directly impose on 386 | those licensors and authors. 387 | 388 | All other non-permissive additional terms are considered "further 389 | restrictions" within the meaning of section 10. If the Program as you 390 | received it, or any part of it, contains a notice stating that it is 391 | governed by this License along with a term that is a further 392 | restriction, you may remove that term. If a license document contains 393 | a further restriction but permits relicensing or conveying under this 394 | License, you may add to a covered work material governed by the terms 395 | of that license document, provided that the further restriction does 396 | not survive such relicensing or conveying. 397 | 398 | If you add terms to a covered work in accord with this section, you 399 | must place, in the relevant source files, a statement of the 400 | additional terms that apply to those files, or a notice indicating 401 | where to find the applicable terms. 402 | 403 | Additional terms, permissive or non-permissive, may be stated in the 404 | form of a separately written license, or stated as exceptions; 405 | the above requirements apply either way. 406 | 407 | 8. Termination. 408 | 409 | You may not propagate or modify a covered work except as expressly 410 | provided under this License. Any attempt otherwise to propagate or 411 | modify it is void, and will automatically terminate your rights under 412 | this License (including any patent licenses granted under the third 413 | paragraph of section 11). 414 | 415 | However, if you cease all violation of this License, then your 416 | license from a particular copyright holder is reinstated (a) 417 | provisionally, unless and until the copyright holder explicitly and 418 | finally terminates your license, and (b) permanently, if the copyright 419 | holder fails to notify you of the violation by some reasonable means 420 | prior to 60 days after the cessation. 421 | 422 | Moreover, your license from a particular copyright holder is 423 | reinstated permanently if the copyright holder notifies you of the 424 | violation by some reasonable means, this is the first time you have 425 | received notice of violation of this License (for any work) from that 426 | copyright holder, and you cure the violation prior to 30 days after 427 | your receipt of the notice. 428 | 429 | Termination of your rights under this section does not terminate the 430 | licenses of parties who have received copies or rights from you under 431 | this License. If your rights have been terminated and not permanently 432 | reinstated, you do not qualify to receive new licenses for the same 433 | material under section 10. 434 | 435 | 9. Acceptance Not Required for Having Copies. 436 | 437 | You are not required to accept this License in order to receive or 438 | run a copy of the Program. Ancillary propagation of a covered work 439 | occurring solely as a consequence of using peer-to-peer transmission 440 | to receive a copy likewise does not require acceptance. However, 441 | nothing other than this License grants you permission to propagate or 442 | modify any covered work. These actions infringe copyright if you do 443 | not accept this License. Therefore, by modifying or propagating a 444 | covered work, you indicate your acceptance of this License to do so. 445 | 446 | 10. Automatic Licensing of Downstream Recipients. 447 | 448 | Each time you convey a covered work, the recipient automatically 449 | receives a license from the original licensors, to run, modify and 450 | propagate that work, subject to this License. You are not responsible 451 | for enforcing compliance by third parties with this License. 452 | 453 | An "entity transaction" is a transaction transferring control of an 454 | organization, or substantially all assets of one, or subdividing an 455 | organization, or merging organizations. If propagation of a covered 456 | work results from an entity transaction, each party to that 457 | transaction who receives a copy of the work also receives whatever 458 | licenses to the work the party's predecessor in interest had or could 459 | give under the previous paragraph, plus a right to possession of the 460 | Corresponding Source of the work from the predecessor in interest, if 461 | the predecessor has it or can get it with reasonable efforts. 462 | 463 | You may not impose any further restrictions on the exercise of the 464 | rights granted or affirmed under this License. For example, you may 465 | not impose a license fee, royalty, or other charge for exercise of 466 | rights granted under this License, and you may not initiate litigation 467 | (including a cross-claim or counterclaim in a lawsuit) alleging that 468 | any patent claim is infringed by making, using, selling, offering for 469 | sale, or importing the Program or any portion of it. 470 | 471 | 11. Patents. 472 | 473 | A "contributor" is a copyright holder who authorizes use under this 474 | License of the Program or a work on which the Program is based. The 475 | work thus licensed is called the contributor's "contributor version". 476 | 477 | A contributor's "essential patent claims" are all patent claims 478 | owned or controlled by the contributor, whether already acquired or 479 | hereafter acquired, that would be infringed by some manner, permitted 480 | by this License, of making, using, or selling its contributor version, 481 | but do not include claims that would be infringed only as a 482 | consequence of further modification of the contributor version. For 483 | purposes of this definition, "control" includes the right to grant 484 | patent sublicenses in a manner consistent with the requirements of 485 | this License. 486 | 487 | Each contributor grants you a non-exclusive, worldwide, royalty-free 488 | patent license under the contributor's essential patent claims, to 489 | make, use, sell, offer for sale, import and otherwise run, modify and 490 | propagate the contents of its contributor version. 491 | 492 | In the following three paragraphs, a "patent license" is any express 493 | agreement or commitment, however denominated, not to enforce a patent 494 | (such as an express permission to practice a patent or covenant not to 495 | sue for patent infringement). To "grant" such a patent license to a 496 | party means to make such an agreement or commitment not to enforce a 497 | patent against the party. 498 | 499 | If you convey a covered work, knowingly relying on a patent license, 500 | and the Corresponding Source of the work is not available for anyone 501 | to copy, free of charge and under the terms of this License, through a 502 | publicly available network server or other readily accessible means, 503 | then you must either (1) cause the Corresponding Source to be so 504 | available, or (2) arrange to deprive yourself of the benefit of the 505 | patent license for this particular work, or (3) arrange, in a manner 506 | consistent with the requirements of this License, to extend the patent 507 | license to downstream recipients. "Knowingly relying" means you have 508 | actual knowledge that, but for the patent license, your conveying the 509 | covered work in a country, or your recipient's use of the covered work 510 | in a country, would infringe one or more identifiable patents in that 511 | country that you have reason to believe are valid. 512 | 513 | If, pursuant to or in connection with a single transaction or 514 | arrangement, you convey, or propagate by procuring conveyance of, a 515 | covered work, and grant a patent license to some of the parties 516 | receiving the covered work authorizing them to use, propagate, modify 517 | or convey a specific copy of the covered work, then the patent license 518 | you grant is automatically extended to all recipients of the covered 519 | work and works based on it. 520 | 521 | A patent license is "discriminatory" if it does not include within 522 | the scope of its coverage, prohibits the exercise of, or is 523 | conditioned on the non-exercise of one or more of the rights that are 524 | specifically granted under this License. You may not convey a covered 525 | work if you are a party to an arrangement with a third party that is 526 | in the business of distributing software, under which you make payment 527 | to the third party based on the extent of your activity of conveying 528 | the work, and under which the third party grants, to any of the 529 | parties who would receive the covered work from you, a discriminatory 530 | patent license (a) in connection with copies of the covered work 531 | conveyed by you (or copies made from those copies), or (b) primarily 532 | for and in connection with specific products or compilations that 533 | contain the covered work, unless you entered into that arrangement, 534 | or that patent license was granted, prior to 28 March 2007. 535 | 536 | Nothing in this License shall be construed as excluding or limiting 537 | any implied license or other defenses to infringement that may 538 | otherwise be available to you under applicable patent law. 539 | 540 | 12. No Surrender of Others' Freedom. 541 | 542 | If conditions are imposed on you (whether by court order, agreement or 543 | otherwise) that contradict the conditions of this License, they do not 544 | excuse you from the conditions of this License. If you cannot convey a 545 | covered work so as to satisfy simultaneously your obligations under this 546 | License and any other pertinent obligations, then as a consequence you may 547 | not convey it at all. For example, if you agree to terms that obligate you 548 | to collect a royalty for further conveying from those to whom you convey 549 | the Program, the only way you could satisfy both those terms and this 550 | License would be to refrain entirely from conveying the Program. 551 | 552 | 13. Use with the GNU Affero General Public License. 553 | 554 | Notwithstanding any other provision of this License, you have 555 | permission to link or combine any covered work with a work licensed 556 | under version 3 of the GNU Affero General Public License into a single 557 | combined work, and to convey the resulting work. The terms of this 558 | License will continue to apply to the part which is the covered work, 559 | but the special requirements of the GNU Affero General Public License, 560 | section 13, concerning interaction through a network will apply to the 561 | combination as such. 562 | 563 | 14. Revised Versions of this License. 564 | 565 | The Free Software Foundation may publish revised and/or new versions of 566 | the GNU General Public License from time to time. Such new versions will 567 | be similar in spirit to the present version, but may differ in detail to 568 | address new problems or concerns. 569 | 570 | Each version is given a distinguishing version number. If the 571 | Program specifies that a certain numbered version of the GNU General 572 | Public License "or any later version" applies to it, you have the 573 | option of following the terms and conditions either of that numbered 574 | version or of any later version published by the Free Software 575 | Foundation. If the Program does not specify a version number of the 576 | GNU General Public License, you may choose any version ever published 577 | by the Free Software Foundation. 578 | 579 | If the Program specifies that a proxy can decide which future 580 | versions of the GNU General Public License can be used, that proxy's 581 | public statement of acceptance of a version permanently authorizes you 582 | to choose that version for the Program. 583 | 584 | Later license versions may give you additional or different 585 | permissions. However, no additional obligations are imposed on any 586 | author or copyright holder as a result of your choosing to follow a 587 | later version. 588 | 589 | 15. Disclaimer of Warranty. 590 | 591 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY 592 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT 593 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY 594 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, 595 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 596 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM 597 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF 598 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 599 | 600 | 16. Limitation of Liability. 601 | 602 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 603 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS 604 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY 605 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE 606 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF 607 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD 608 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), 609 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF 610 | SUCH DAMAGES. 611 | 612 | 17. Interpretation of Sections 15 and 16. 613 | 614 | If the disclaimer of warranty and limitation of liability provided 615 | above cannot be given local legal effect according to their terms, 616 | reviewing courts shall apply local law that most closely approximates 617 | an absolute waiver of all civil liability in connection with the 618 | Program, unless a warranty or assumption of liability accompanies a 619 | copy of the Program in return for a fee. 620 | 621 | END OF TERMS AND CONDITIONS 622 | 623 | How to Apply These Terms to Your New Programs 624 | 625 | If you develop a new program, and you want it to be of the greatest 626 | possible use to the public, the best way to achieve this is to make it 627 | free software which everyone can redistribute and change under these terms. 628 | 629 | To do so, attach the following notices to the program. It is safest 630 | to attach them to the start of each source file to most effectively 631 | state the exclusion of warranty; and each file should have at least 632 | the "copyright" line and a pointer to where the full notice is found. 633 | 634 | 635 | Copyright (C) 636 | 637 | This program is free software: you can redistribute it and/or modify 638 | it under the terms of the GNU General Public License as published by 639 | the Free Software Foundation, either version 3 of the License, or 640 | (at your option) any later version. 641 | 642 | This program is distributed in the hope that it will be useful, 643 | but WITHOUT ANY WARRANTY; without even the implied warranty of 644 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 645 | GNU General Public License for more details. 646 | 647 | You should have received a copy of the GNU General Public License 648 | along with this program. If not, see . 649 | 650 | Also add information on how to contact you by electronic and paper mail. 651 | 652 | If the program does terminal interaction, make it output a short 653 | notice like this when it starts in an interactive mode: 654 | 655 | Copyright (C) 656 | This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 657 | This is free software, and you are welcome to redistribute it 658 | under certain conditions; type `show c' for details. 659 | 660 | The hypothetical commands `show w' and `show c' should show the appropriate 661 | parts of the General Public License. Of course, your program's commands 662 | might be different; for a GUI interface, you would use an "about box". 663 | 664 | You should also get your employer (if you work as a programmer) or school, 665 | if any, to sign a "copyright disclaimer" for the program, if necessary. 666 | For more information on this, and how to apply and follow the GNU GPL, see 667 | . 668 | 669 | The GNU General Public License does not permit incorporating your program 670 | into proprietary programs. If your program is a subroutine library, you 671 | may consider it more useful to permit linking proprietary applications with 672 | the library. If this is what you want to do, use the GNU Lesser General 673 | Public License instead of this License. But first, please read 674 | . 675 | --------------------------------------------------------------------------------