├── .github └── workflows │ ├── codeql.yml │ ├── deploy-pypi.yml │ ├── tests-ci-js.yml │ └── tests-ci.yml ├── .gitignore ├── .readthedocs.yaml ├── LICENSE.txt ├── MANIFEST.in ├── README.rst ├── doc ├── Makefile ├── _static │ └── custom.css ├── changes.rst ├── conf.py ├── example_latex2text_custom_quotes.py ├── index.rst ├── latex2text.rst ├── latexencode.rst ├── latexnodes.nodes.rst ├── latexnodes.parsers.rst ├── latexnodes.rst ├── latexwalker.rst ├── macrospec.rst ├── new-in-pylatexenc-2.rst ├── new-in-pylatexenc-3.rst └── requirements.txt ├── js-transcrypt ├── .gitignore ├── .yarnrc.yml ├── README.md ├── generate_pylatexenc_js.py ├── import_pylatexenc_modules.py ├── libpatches │ ├── bisect.py │ ├── collections.py │ ├── customjspatches.js │ ├── functools.py │ ├── json.py │ ├── logging.js │ ├── unique_object_id.js │ └── unittest.py ├── my_test_script.py ├── mytestjscode │ ├── my_test_js_code.js │ ├── node_modules │ │ └── pylatexenc-js │ └── package.json ├── package.json ├── preprocesslib-pylatexenc.config.yaml ├── preprocesslib-tests.config.yaml ├── transcrypt_runtime_patches.js └── yarn.lock ├── poetry.lock ├── pylatexenc ├── __init__.py ├── _util.py ├── _util_support.py ├── latex2text │ ├── __init__.py │ ├── __main__.py │ ├── _defaultspecs.py │ └── _inputlatexfile.py ├── latexencode │ ├── __init__.py │ ├── __main__.py │ ├── _partial_latex_encoder.py │ ├── _rule.py │ ├── _uni2latexmap.py │ ├── _uni2latexmap_xml.py │ ├── _unicode_to_latex_encoder.py │ └── get_builtin_rules.py ├── latexnodes │ ├── __init__.py │ ├── _callablespecbase.py │ ├── _exctypes.py │ ├── _latex_recomposer.py │ ├── _latexcontextdbbase.py │ ├── _nodescollector.py │ ├── _parsedargs.py │ ├── _parsedargsinfo.py │ ├── _parsingstate.py │ ├── _parsingstatedelta.py │ ├── _token.py │ ├── _tokenreader.py │ ├── _tokenreaderbase.py │ ├── _walkerbase.py │ ├── nodes.py │ └── parsers │ │ ├── __init__.py │ │ ├── _base.py │ │ ├── _delimited.py │ │ ├── _expression.py │ │ ├── _generalnodes.py │ │ ├── _math.py │ │ ├── _optionals.py │ │ ├── _stdarg.py │ │ └── _verbatim.py ├── latexwalker │ ├── __init__.py │ ├── __main__.py │ ├── _defaultspecs.py │ ├── _get_defaultspecs.py │ ├── _helpers.py │ ├── _legacy_py1x.py │ └── _walker.py ├── macrospec │ ├── __init__.py │ ├── _argumentsparser.py │ ├── _environmentbodyparser.py │ ├── _latexcontextdb.py │ ├── _macrocallparser.py │ ├── _pyltxenc2_argparsers │ │ ├── __init__.py │ │ ├── _base.py │ │ └── _verbatimargsparser.py │ ├── _specclasses.py │ └── _spechelpers.py └── version.py ├── pyproject.toml ├── setup.py ├── test ├── .gitignore ├── __init__.py ├── _helpers_tests.py ├── dummy │ └── readme.txt ├── test_2_latex2text.py ├── test_2_latexwalker.py ├── test_2_macrospec.py ├── test_input_1.tex ├── test_latexencode.py ├── test_latexencode_all.py ├── test_latexnodes_latex_recomposer.py ├── test_latexnodes_nodes.py ├── test_latexnodes_nodescollector.py ├── test_latexnodes_parsedargsinfo.py ├── test_latexnodes_parsers_delimited.py ├── test_latexnodes_parsers_expression.py ├── test_latexnodes_parsers_generalnodes.py ├── test_latexnodes_parsers_math.py ├── test_latexnodes_parsers_optionals.py ├── test_latexnodes_parsers_stdarg.py ├── test_latexnodes_parsers_verbatim.py ├── test_latexnodes_tokenreader.py ├── test_latexnodes_tokenreaderbase.py ├── test_macrospec_argumentsparser.py ├── test_macrospec_environmentbodyparser.py ├── test_macrospec_latexcontextdb.py ├── test_macrospec_macrocallparser.py ├── test_pylatexenc.py ├── test_util.py └── uni_chars_test_previous.txt └── tools ├── gen_l2t_from_lenc.py ├── gen_xml_dic.py ├── preprocess_lib.py ├── unicode.xml ├── unicode.xml.LICENSE └── utils_transcrypt_generate_js.py /.github/workflows/codeql.yml: -------------------------------------------------------------------------------- 1 | name: "CodeQL" 2 | 3 | on: 4 | push: 5 | branches: [ "main" ] 6 | pull_request: 7 | branches: [ "main" ] 8 | schedule: 9 | - cron: "13 20 * * 3" 10 | 11 | jobs: 12 | analyze: 13 | name: Analyze 14 | runs-on: ubuntu-latest 15 | permissions: 16 | actions: read 17 | contents: read 18 | security-events: write 19 | 20 | strategy: 21 | fail-fast: false 22 | matrix: 23 | language: [ python ] 24 | 25 | steps: 26 | - name: Checkout 27 | uses: actions/checkout@v3 28 | 29 | - name: Initialize CodeQL 30 | uses: github/codeql-action/init@v2 31 | with: 32 | languages: ${{ matrix.language }} 33 | queries: +security-and-quality 34 | 35 | - name: Autobuild 36 | uses: github/codeql-action/autobuild@v2 37 | 38 | - name: Perform CodeQL Analysis 39 | uses: github/codeql-action/analyze@v2 40 | with: 41 | category: "/language:${{ matrix.language }}" 42 | -------------------------------------------------------------------------------- /.github/workflows/deploy-pypi.yml: -------------------------------------------------------------------------------- 1 | 2 | name: 'deploy-pypi' 3 | 4 | 5 | on: 6 | workflow_dispatch: {} 7 | 8 | jobs: 9 | deploy-pypi: 10 | 11 | runs-on: ubuntu-latest 12 | 13 | strategy: 14 | matrix: 15 | python-version: ["3.11"] 16 | poetry-version: ["1.4"] 17 | 18 | environment: env-deploy-pypi 19 | 20 | steps: 21 | - uses: actions/checkout@v3 22 | 23 | - uses: actions/setup-python@v4 24 | with: 25 | python-version: '${{ matrix.python-version }}' 26 | 27 | - name: 'Set up poetry - Run image' 28 | uses: abatilo/actions-poetry@v2 29 | with: 30 | poetry-version: '${{ matrix.poetry-version }}' 31 | 32 | - name: 'Poetry Build Package' 33 | run: poetry build 34 | 35 | - name: 'Poetry Publish package' 36 | run: 'poetry publish -p ${{ secrets.PYPI_API_TOKEN}} -u __token__' 37 | 38 | 39 | -------------------------------------------------------------------------------- /.github/workflows/tests-ci-js.yml: -------------------------------------------------------------------------------- 1 | 2 | name: 'tests-ci-js' 3 | 4 | 5 | on: 6 | push: 7 | branches: [ "main", "devel" ] 8 | pull_request: 9 | branches: [ "main", "devel" ] 10 | schedule: 11 | - cron: "13 20 * * 3" 12 | 13 | jobs: 14 | tests-ci-js: 15 | 16 | strategy: 17 | #fail-fast: true 18 | matrix: 19 | python-version: 20 | - "3.11" 21 | poetry-version: 22 | - "1.4" 23 | node-version: 24 | - "14" 25 | - "19" 26 | os: 27 | - 'ubuntu-latest' 28 | 29 | runs-on: '${{ matrix.os }}' 30 | 31 | steps: 32 | - uses: actions/checkout@v3 33 | 34 | # Install NodeJS 35 | - uses: actions/setup-node@v3 36 | with: 37 | node-version: '${{ matrix.node-version }}' 38 | 39 | - name: 'Setting up some node packages' 40 | run: 'npm i debug' 41 | 42 | # Poetry & Python are needed to transpile Python library 43 | - uses: actions/setup-python@v4 44 | with: 45 | python-version: '${{ matrix.python-version }}' 46 | 47 | - name: 'Set up poetry - Run image' 48 | uses: abatilo/actions-poetry@v2 49 | 50 | - name: 'Poetry Install (with buildjslib)' 51 | run: 'poetry install --with buildjslib' 52 | 53 | - name: 'Generate pylatexenc JS library' 54 | working-directory: 'js-transcrypt' 55 | run: 'poetry run python ./generate_pylatexenc_js.py --compile-tests' 56 | 57 | - name: 'Run JS tests' 58 | working-directory: 'js-transcrypt' 59 | run: 'node test-pylatexenc-js/runtests.js' 60 | -------------------------------------------------------------------------------- /.github/workflows/tests-ci.yml: -------------------------------------------------------------------------------- 1 | name: 'tests-ci' 2 | 3 | on: 4 | push: 5 | branches: [ "main", "devel" ] 6 | pull_request: 7 | branches: [ "main", "devel" ] 8 | schedule: 9 | - cron: "13 20 * * 3" 10 | 11 | jobs: 12 | tests-ci: 13 | 14 | strategy: 15 | #fail-fast: true 16 | matrix: 17 | python-version: 18 | - "3.8" 19 | - "3.9" 20 | - "3.10" 21 | - "3.11" 22 | - "3.12" 23 | - "3.13" 24 | os: 25 | - 'ubuntu-latest' 26 | include: 27 | # Deprecated system for github actions :/ 28 | # - python-version: "3.4" 29 | # os: "ubuntu-18.04" 30 | # - python-version: "2.7" 31 | # os: 'ubuntu-20.04' 32 | - python-version: "3.6" 33 | os: 'ubuntu-20.04' 34 | - python-version: "3.7" 35 | os: 'ubuntu-20.04' 36 | 37 | runs-on: '${{ matrix.os }}' 38 | 39 | steps: 40 | - uses: actions/checkout@v3 41 | - uses: actions/setup-python@v4 42 | with: 43 | python-version: '${{ matrix.python-version }}' 44 | 45 | - name: 'Install dependencies' 46 | run: |- 47 | pip install pytest toml; if [ "${{ matrix.python-version }}" == "2.7" ]; then pip install chainmap; fi 48 | 49 | - name: 'Run tests' 50 | run: pytest 51 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | *~ 3 | 4 | .cache 5 | 6 | *.py[cdo] 7 | pylatexenc.egg-info 8 | 9 | doc/_build 10 | 11 | dist 12 | build 13 | 14 | tools/error.log 15 | tools/output.log 16 | tools/transcryptable_output 17 | -------------------------------------------------------------------------------- /.readthedocs.yaml: -------------------------------------------------------------------------------- 1 | # 2 | # Configuration for readthedocs.org 3 | # 4 | 5 | # See poetry builds on RTD: 6 | # https://docs.readthedocs.io/en/stable/build-customization.html#install-dependencies-with-poetry 7 | 8 | version: 2 9 | 10 | build: 11 | 12 | os: "ubuntu-22.04" 13 | 14 | tools: 15 | python: "3.10" 16 | 17 | jobs: 18 | 19 | post_create_environment: 20 | # Install poetry 21 | # https://python-poetry.org/docs/#installing-manually 22 | - 'pip install "poetry>=1.4"' 23 | 24 | post_install: 25 | # Install dependencies with 'docs' dependency group 26 | # https://python-poetry.org/docs/managing-dependencies/#dependency-groups 27 | # VIRTUAL_ENV needs to be set manually for now. 28 | # See https://github.com/readthedocs/readthedocs.org/pull/11152/ 29 | - 'VIRTUAL_ENV=$READTHEDOCS_VIRTUALENV_PATH poetry install --with builddoc' 30 | 31 | 32 | sphinx: 33 | configuration: doc/conf.py 34 | builder: 'dirhtml' 35 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015-2023 Philippe Faist 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include LICENSE.txt 2 | include test/uni_chars_test_previous.txt test/test_input_1.tex test/dummy/readme.txt 3 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | pylatexenc 2 | ========== 3 | 4 | Simple LaTeX parser providing latex-to-unicode and unicode-to-latex conversion 5 | 6 | .. image:: https://img.shields.io/github/license/phfaist/pylatexenc.svg?style=flat 7 | :target: https://github.com/phfaist/pylatexenc/blob/master/LICENSE.txt 8 | 9 | .. image:: https://img.shields.io/pypi/v/pylatexenc.svg?style=flat 10 | :target: https://pypi.org/project/pylatexenc/ 11 | 12 | Python: ≥ 3.4 or ≥ 2.7. The library is designed to be as backwards-compatible as 13 | reasonably possible and is able to run on old python verisons should it be 14 | necessary. (Use the setup.py script directly if you have python<3.7, poetry 15 | doesn't seem to work with old python versions.) 16 | 17 | **NEW (4/2023)**: *PYLATEXENC 3.0alpha* is in pre-release on PyPI. See `new features 18 | and major changes `_. 19 | The `documentation `_ is still 20 | incomplete, and the new APIs are still subject to changes. The code is meant 21 | to be as backwards compatible as is reasonably possible. Feel free to try it 22 | out & submit feedback! 23 | 24 | 25 | Unicode Text to LaTeX code 26 | -------------------------- 27 | 28 | The ``pylatexenc.latexencode`` module provides a function ``unicode_to_latex()`` 29 | which converts a unicode string into LaTeX text and escape sequences. It should 30 | recognize accented characters and most math symbols. A couple of switches allow 31 | you to alter how this function behaves. 32 | 33 | You can also run ``latexencode`` in command-line to convert plain unicode text 34 | (from the standard input or from files given on the command line) into LaTeX 35 | code, written on to the standard output. 36 | 37 | A third party plug-in for Vim 38 | `vim-latexencode `_ 39 | by `@Konfekt `_ 40 | provides a corresponding command to operate on a given range. 41 | 42 | 43 | Parsing LaTeX code & converting to plain text (unicode) 44 | ------------------------------------------------------- 45 | 46 | The ``pylatexenc.latexwalker`` module provides a series of routines that parse 47 | the LaTeX structure of given LaTeX code and returns a logical structure of 48 | objects, which can then be used to produce output in another format such as 49 | plain text. This is not a replacement for a full (La)TeX engine, rather, this 50 | module provides a way to parse a chunk of LaTeX code as mark-up code. 51 | 52 | The ``pylatexenc.latex2text`` module builds up on top of 53 | ``pylatexenc.latexwalker`` and provides functions to convert given LaTeX code to 54 | plain text with unicode characters. 55 | 56 | You can also run ``latex2text`` in command-line to convert LaTeX input (either 57 | from the standard input, or from files given on the command line) into plain 58 | text written on the standard output. 59 | 60 | 61 | Documentation 62 | ------------- 63 | 64 | Full documentation is available at https://pylatexenc.readthedocs.io/. 65 | 66 | To build the documentation manually, run:: 67 | 68 | > poetry install --with=builddoc 69 | > cd doc/ 70 | doc> poetry run make html 71 | 72 | 73 | License 74 | ------- 75 | 76 | See LICENSE.txt (MIT License). 77 | 78 | NOTE: See copyright notice and license information for file 79 | ``tools/unicode.xml`` provided in ``tools/unicode.xml.LICENSE``. (The file 80 | ``tools/unicode.xml`` was downloaded from 81 | https://www.w3.org/2003/entities/2007xml/unicode.xml as linked from 82 | https://www.w3.org/TR/xml-entity-names/#source.) 83 | 84 | 85 | Javascript Library 86 | ------------------ 87 | 88 | Some core parts of this library can be transcribed to JavaScript. This feature 89 | is used (and was developed for) my `Flexible Latex-like Markup 90 | project `_. See the *js-transcrypt/* folder and 91 | its `README file `_. 92 | -------------------------------------------------------------------------------- /doc/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | SPHINXPROJ = pylatexenc 8 | SOURCEDIR = . 9 | BUILDDIR = _build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) -------------------------------------------------------------------------------- /doc/_static/custom.css: -------------------------------------------------------------------------------- 1 | /*@import url('https://fonts.googleapis.com/css?family=Open+Sans:400,400i,600,600i');*/ 2 | @import url('https://fonts.googleapis.com/css2?family=Fira+Mono:wght@400;500&family=Fira+Sans:ital,wght@0,400;0,600;1,400;1,600&display=swap'); 3 | 4 | 5 | div.body p, div.body dd, div.body li, div.body blockquote { 6 | -moz-hyphens: none; 7 | hyphens: none; 8 | } 9 | 10 | div.document { 11 | margin-top: 10px; 12 | } 13 | 14 | 15 | /* SIDEBAR */ 16 | 17 | div.sphinxsidebar { 18 | font-size: 0.9rem; 19 | line-height: inherit; 20 | 21 | /*background-color: rgba(120,80,50,0.1);*/ 22 | box-shadow: 3px 5px 10px rgba(0,0,0,0.1); 23 | border-right: 2px solid rgba(120,80,50,0.1); 24 | padding-right: 10px; 25 | box-sizing: border-box; 26 | 27 | border-radius: 12px; 28 | 29 | background-color: rgba(255,255,255,0.8); 30 | } 31 | 32 | @media screen and (max-width: 875px) { 33 | div.sphinxsidebar { 34 | width: 100vw; 35 | background-color: #333; 36 | } 37 | } 38 | 39 | div.sphinxsidebar ul { 40 | list-style: square; 41 | margin-left: 10px; 42 | } 43 | div.sphinxsidebar ul > li { 44 | margin-bottom: 0.2em; 45 | } 46 | div.sphinxsidebar ul ul { 47 | list-style: url('data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0nMS4wJyBlbmNvZGluZz0nVVRGLTgnPz4KPCEtLSBUaGlzIGZpbGUgd2FzIGdlbmVyYXRlZCBieSBkdmlzdmdtIDIuMS4zIC0tPgo8c3ZnIGhlaWdodD0nNC42MTQ2OXB0JyB2ZXJzaW9uPScxLjEnIHZpZXdCb3g9JzIzMC41MjQgNzcuOTk1NSA1LjUzMTI2IDQuNjE0NjknIHdpZHRoPSc1LjUzMTI2cHQnIHhtbG5zPSdodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZycgeG1sbnM6eGxpbms9J2h0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsnPgo8ZGVmcz4KPHBhdGggZD0nTTUuMzcxODYgLTEuODA5MjJDNS40OTkzOCAtMS44NjUwMSA1LjUzMTI2IC0xLjkyMDggNS41MzEyNiAtMS45ODQ1NlM1LjUwNzM1IC0yLjEwNDExIDUuMzcxODYgLTIuMTY3ODdMMC45NjQzODQgLTQuMjQwMUMwLjg2MDc3MiAtNC4yOTU4OSAwLjg0NDgzMiAtNC4yOTU4OSAwLjgyODg5MiAtNC4yOTU4OUMwLjY2MTUxOSAtNC4yOTU4OSAwLjY2MTUxOSAtNC4xNTI0MyAwLjY2MTUxOSAtNC4wMDg5N1YwLjAzOTg1MDZDMC42NjE1MTkgMC4xNzUzNDIgMC42NjE1MTkgMC4zMTg4MDQgMC44MjA5MjIgMC4zMTg4MDRDMC44NDQ4MzIgMC4zMTg4MDQgMC44NjA3NzIgMC4zMTg4MDQgMC45NjQzODQgMC4yNjMwMTRMNS4zNzE4NiAtMS44MDkyMlonIGlkPSdnMC03MycvPgo8L2RlZnM+CjxnIGlkPSdwYWdlMSc+Cjx1c2UgeD0nMjMwLjUyNCcgeGxpbms6aHJlZj0nI2cwLTczJyB5PSc4Mi4yOTE0Jy8+CjwvZz4KPC9zdmc+'); 48 | } 49 | 50 | 51 | /* BODY */ 52 | 53 | div.body { 54 | padding-left: 25px; 55 | padding-right: 0px; 56 | padding-top: 1em; /*0px;*/ 57 | padding-bottom: 0px; 58 | } 59 | 60 | div.body dl { 61 | margin-bottom: 1em; 62 | } 63 | 64 | div.body li { 65 | margin-bottom: 0.2em; 66 | } 67 | div.body li > ul { 68 | margin-top: 0.2em; 69 | margin-bottom: 0.2em; 70 | } 71 | 72 | div.body section > dl, 73 | div.body .section > dl { 74 | background: rgba(120,80,50,0.05); 75 | padding: 1.5rem 2rem; 76 | border-radius: 1rem; 77 | margin-bottom: 2rem; 78 | box-shadow: 3px 3px 6px rgba(0,0,0,0.1); 79 | } 80 | div.body section > dl > dt, 81 | div.body .section > dl > dt { 82 | margin-bottom: 1.5em; 83 | } 84 | div.body section > dl > dd, 85 | div.body .section > dl > dd { 86 | margin-left: 0px; 87 | } 88 | 89 | div.body li > blockquote { 90 | margin-left: 0px; 91 | } 92 | 93 | 94 | 95 | .sig { 96 | /*font-family: 'Roboto', sans-serif;*/ 97 | font-family: inherit; 98 | font-weight: 350; 99 | color: rgb(100,100,100); 100 | letter-spacing: 0.4px; 101 | } 102 | .sig pre { 103 | font-family: inherit; 104 | } 105 | .sig > em.property:first-child { 106 | font-style: normal; 107 | font-variant: small-caps; 108 | display: block; 109 | } 110 | .sig-prename { 111 | font-size: 0.9em; 112 | } 113 | .sig-name { 114 | color: #000; 115 | font-weight: 550; 116 | font-size: inherit; 117 | } 118 | .sig-param { 119 | color: #000; 120 | font-size: 0.95em; 121 | } 122 | 123 | 124 | pre, tt, code { 125 | letter-spacing: -.2pt; /* condense mono/code font a bit */ 126 | } 127 | 128 | tt, code { 129 | background-color: #b7a48629; 130 | } 131 | 132 | code { 133 | color: #000; 134 | font-weight: 550; 135 | padding: 2px 3px; 136 | border-radius: 4px; 137 | } 138 | code.xref.py { 139 | font-family: inherit; 140 | letter-spacing: 0.2pt; 141 | padding: 0px 0px; 142 | border-radius: 0px; 143 | } 144 | code.xref, a code { 145 | font-weight: 550; 146 | } 147 | 148 | /* link colors */ 149 | a, a code.xref { 150 | color: rgb(117, 63, 10); 151 | } 152 | 153 | 154 | /* when we have a local TOC, don't make headers into prominent links */ 155 | a.toc-backref { 156 | color: inherit; 157 | text-decoration: inherit; 158 | } 159 | 160 | 161 | .versionmodified { 162 | color: rgb(125, 42, 109); 163 | } 164 | 165 | 166 | 167 | /* --- */ 168 | 169 | nav.contents, aside.topic, div.topic { 170 | border: 0px none; 171 | background-color: rgba(120,80,50,0.05); 172 | border-radius: 10px; 173 | padding: 20px; 174 | box-shadow: 3px 3px 6px rgba(0,0,0,0.1); 175 | } 176 | 177 | 178 | div.admonition { 179 | border-radius: 15px; 180 | } 181 | -------------------------------------------------------------------------------- /doc/changes.rst: -------------------------------------------------------------------------------- 1 | ============ 2 | Changes 3 | ============ 4 | 5 | 6 | pylatexenc 3.0alpha 7 | =================== 8 | 9 | (Still in development.) 10 | 11 | .. toctree:: 12 | :maxdepth: 1 13 | 14 | new-in-pylatexenc-3 15 | 16 | - see in particular the :ref:`list of changes that might affect existing code 17 | ` if you're using some more 18 | advanced features of `pylatexenc`. 19 | 20 | 21 | 22 | 23 | pylatexenc 2.10 24 | =============== 25 | 26 | - Added :py:class:`pylatexenc.latexencode.PartialLatexToLatexEncoder` which can 27 | help to avoid double-escaping some existing LaTeX expressions in the string 28 | that we want to LaTeX-encode. 29 | 30 | - The :py:class:`pylatexenc.latexencode.UnicodeToTextConversionRule` now has a 31 | :py:attr:`~pylatexenc.latexencode.UnicodeToTextConversionRule.replacement_text_protection` 32 | field which can be used to override the default `replacement_text_protection` 33 | set on a :py:class:`pylatexenc.latexencode.UnicodeToTextEncoder` object. Also 34 | the `replacement_text_protection` fields accept an arbitrary callable object. 35 | 36 | - added some known macro definitions for :py:mod:`~pylatexenc.latexwalker` and 37 | :py:mod:`~pylatexenc.latex2text`, such as ``\definecolor`` and ``\textcolor``. 38 | 39 | - Bug fixes (including :issue:`57`) 40 | 41 | pylatexenc 2.9 42 | ============== 43 | 44 | - Bug fixes (including issues :issue:`49`, :issue:`51`, :pr:`52`, :pr:`53`, 45 | :pr:`54`) 46 | 47 | pylatexenc 2.8 48 | ============== 49 | 50 | - `latex2text` module: Basic support for array and matrix environments. 51 | Matrices are represented inline, in the form ``[ a b; c d ]``. 52 | 53 | - `latexencode` bugfix (issue :issue:`44`) 54 | 55 | pylatexenc 2.7 56 | ============== 57 | 58 | - Bug fix: the parser now disambiguates ``$$`` as either a display math 59 | delimiter or two inline math delimiters as in ``$a$$b$`` (issue :issue:`43`) 60 | 61 | pylatexenc 2.6 62 | ============== 63 | 64 | - In `latex2text`: 65 | 66 | + Bug fix: default behavior of the `strict_latex_spaces` option in the 67 | :py:class:`pylatexenc.latex2text.LatexNodes2Text()` constructor 68 | 69 | + fix ``\le``, ``\ge``, ``\leqslant``, ``\geqslant`` (issue :issue:`41`) 70 | 71 | + reorganized the default latex symbol categories 72 | 73 | 74 | pylatexenc 2.5 75 | ============== 76 | 77 | - `latex2text`: Add support for ``\mathbb{}``, ``\mathbf{}`` and some friends 78 | (issue :issue:`40`) 79 | 80 | pylatexenc 2.4 81 | ============== 82 | 83 | - Bug fixes in how `latex2text` attempts to recover from parse errors in 84 | tolerant mode 85 | 86 | pylatexenc 2.3 87 | ============== 88 | 89 | - Minor bug fixes in `latex2text` 90 | 91 | 92 | pylatexenc 2.2 93 | ============== 94 | 95 | Version 2.2 brings a few minor bug fixes and improvements over version 2.1: 96 | 97 | - `pylatexenc.latex2text` supports more LaTeX symbols 98 | 99 | - `latex2text` and `latexwalker` command-line utilities accept a new `-c` option 100 | where you can directly specify LaTeX code 101 | 102 | - minor bug fixes 103 | 104 | 105 | pylatexenc 2.1 106 | ============== 107 | 108 | Version 2.1 brings a few minor bug fixes to version 2.0. 109 | 110 | 111 | pylatexenc 2.0 112 | ============== 113 | 114 | .. toctree:: 115 | :maxdepth: 1 116 | 117 | new-in-pylatexenc-2 118 | 119 | - see in particular the :ref:`list of changes that might affect existing code 120 | ` if you're using some advanced features of 121 | `pylatexenc`. 122 | 123 | 124 | pylatexenc 1.x 125 | ============== 126 | 127 | See description of updates and changes on the `github releases page 128 | `_. 129 | -------------------------------------------------------------------------------- /doc/conf.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | # 4 | # pylatexenc documentation build configuration file, created by 5 | # sphinx-quickstart on Mon Apr 24 16:32:21 2017. 6 | # 7 | # This file is execfile()d with the current directory set to its 8 | # containing dir. 9 | # 10 | # Note that not all possible configuration values are present in this 11 | # autogenerated file. 12 | # 13 | # All configuration values have a default; values that are commented out 14 | # serve to show the default. 15 | 16 | # If extensions (or modules to document with autodoc) are in another directory, 17 | # add these directories to sys.path here. If the directory is relative to the 18 | # documentation root, use os.path.abspath to make it absolute, like shown here. 19 | # 20 | import os.path 21 | import sys 22 | sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) 23 | 24 | import pylatexenc.version 25 | 26 | # -- General configuration ------------------------------------------------ 27 | 28 | # If your documentation needs a minimal Sphinx version, state it here. 29 | # 30 | # needs_sphinx = '1.0' 31 | 32 | # Add any Sphinx extension module names here, as strings. They can be 33 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 34 | # ones. 35 | extensions = [ 36 | 'sphinx.ext.autodoc', 37 | 'sphinx.ext.intersphinx', 38 | 'sphinx.ext.mathjax', 39 | 40 | 'sphinx_issues', 41 | ] 42 | 43 | # Add any paths that contain templates here, relative to this directory. 44 | templates_path = ['_templates'] 45 | 46 | # The suffix(es) of source filenames. 47 | # You can specify multiple suffix as a list of string: 48 | # 49 | # source_suffix = ['.rst', '.md'] 50 | source_suffix = '.rst' 51 | 52 | # The master toctree document. 53 | master_doc = 'index' 54 | 55 | # General information about the project. 56 | project = 'pylatexenc' 57 | copyright = '2023, Philippe Faist' 58 | author = 'Philippe Faist' 59 | 60 | # The version info for the project you're documenting, acts as replacement for 61 | # |version| and |release|, also used in various other places throughout the 62 | # built documents. 63 | # 64 | # The short X.Y version. 65 | version = pylatexenc.version.version_str 66 | # The full version, including alpha/beta/rc tags. 67 | release = version 68 | 69 | # The language for content autogenerated by Sphinx. Refer to documentation 70 | # for a list of supported languages. 71 | # 72 | # This is also used if you do content translation via gettext catalogs. 73 | # Usually you set "language" from the command line for these cases. 74 | #language = None 75 | 76 | # List of patterns, relative to source directory, that match files and 77 | # directories to ignore when looking for source files. 78 | # This patterns also effect to html_static_path and html_extra_path 79 | exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] 80 | 81 | # The name of the Pygments (syntax highlighting) style to use. 82 | pygments_style = 'sphinx' 83 | 84 | # If true, `todo` and `todoList` produce output, else they produce nothing. 85 | todo_include_todos = False 86 | 87 | 88 | 89 | #autodoc_docstring_signature = True 90 | autodoc_member_order = 'bysource' 91 | autodoc_inherit_docstrings = False 92 | 93 | # autodoc_default_options = { 94 | # 'members': True, # 'var1, var2', 95 | # #'member-order': 'bysource', 96 | # 'special-members': '__init__', 97 | # #'undoc-members': True, 98 | # #'exclude-members': '__weakref__' 99 | # } 100 | 101 | 102 | # -- Options for sphinx_issues -------------------------------------------- 103 | 104 | # GitHub repo 105 | issues_github_path = "phfaist/pylatexenc" 106 | 107 | 108 | # -- Options for HTML output ---------------------------------------------- 109 | 110 | # The theme to use for HTML and HTML Help pages. See the documentation for 111 | # a list of builtin themes. 112 | # 113 | 114 | html_theme = 'alabaster' 115 | 116 | 117 | # import guzzle_sphinx_theme 118 | # html_theme_path = guzzle_sphinx_theme.html_theme_path() 119 | # html_theme = 'guzzle_sphinx_theme' 120 | # html_style = 'custom.css' /* don't forget to add ''' @import url("guzzle.css"); ''' */ 121 | 122 | #import sphinx_bootstrap_theme 123 | #html_theme_path = sphinx_bootstrap_theme.get_html_theme_path() 124 | #html_theme = 'bootstrap' 125 | 126 | 127 | 128 | # Theme options are theme-specific and customize the look and feel of a theme 129 | # further. For a list of options available for each theme, see the 130 | # documentation. 131 | # 132 | html_theme_options = { 133 | 'font_family': 'Fira Sans', 134 | 'font_size': '15px', 135 | 'head_font_family': 'Fira Sans', 136 | 'code_font_family': 'Fira Mono', 137 | 'github_user': 'phfaist', 138 | 'github_repo': 'pylatexenc', 139 | 'github_button': True, 140 | 'github_type': 'star', 141 | 'github_count': 'true', 142 | 143 | 'fixed_sidebar': True, 144 | 'page_width': '950px', 145 | 'sidebar_width': '220px', 146 | } 147 | html_sidebars = { 148 | '**': [ 149 | 'about.html', 150 | 'navigation.html', 151 | 'relations.html', 152 | 'searchbox.html', 153 | # 'donate.html', 154 | ] 155 | } 156 | 157 | # Add any paths that contain custom static files (such as style sheets) here, 158 | # relative to this directory. They are copied after the builtin static files, 159 | # so a file named "default.css" will overwrite the builtin "default.css". 160 | html_static_path = [ '_static' ] 161 | 162 | 163 | # -- Options for HTMLHelp output ------------------------------------------ 164 | 165 | # Output file base name for HTML help builder. 166 | htmlhelp_basename = 'pylatexencdoc' 167 | 168 | 169 | # -- Options for LaTeX output --------------------------------------------- 170 | 171 | latex_elements = { 172 | # The paper size ('letterpaper' or 'a4paper'). 173 | # 174 | # 'papersize': 'letterpaper', 175 | 176 | # The font size ('10pt', '11pt' or '12pt'). 177 | # 178 | # 'pointsize': '10pt', 179 | 180 | # Additional stuff for the LaTeX preamble. 181 | # 182 | # 'preamble': '', 183 | 184 | # Latex figure (float) alignment 185 | # 186 | # 'figure_align': 'htbp', 187 | } 188 | 189 | # Grouping the document tree into LaTeX files. List of tuples 190 | # (source start file, target name, title, 191 | # author, documentclass [howto, manual, or own class]). 192 | latex_documents = [ 193 | (master_doc, 'pylatexenc.tex', 'pylatexenc Documentation', 194 | 'Philippe Faist', 'manual'), 195 | ] 196 | 197 | 198 | # -- Options for manual page output --------------------------------------- 199 | 200 | # One entry per manual page. List of tuples 201 | # (source start file, name, description, authors, manual section). 202 | man_pages = [ 203 | (master_doc, 'pylatexenc', 'pylatexenc Documentation', 204 | [author], 1) 205 | ] 206 | 207 | 208 | # -- Options for Texinfo output ------------------------------------------- 209 | 210 | # Grouping the document tree into Texinfo files. List of tuples 211 | # (source start file, target name, title, author, 212 | # dir menu entry, description, category) 213 | texinfo_documents = [ 214 | (master_doc, 'pylatexenc', 'pylatexenc Documentation', 215 | author, 'pylatexenc', 'One line description of project.', 216 | 'Miscellaneous'), 217 | ] 218 | 219 | 220 | 221 | 222 | # Example configuration for intersphinx: refer to the Python standard library. 223 | intersphinx_mapping = { 224 | 'python': ('https://docs.python.org/3', None) 225 | } 226 | -------------------------------------------------------------------------------- /doc/example_latex2text_custom_quotes.py: -------------------------------------------------------------------------------- 1 | from pylatexenc import latexwalker, latex2text, macrospec 2 | 3 | # 4 | # Define macros, environments, specials for the *parser* 5 | # 6 | lw_context_db = latexwalker.get_default_latex_context_db() 7 | lw_context_db.add_context_category( 8 | 'my-quotes', 9 | prepend=True, 10 | macros=[ 11 | macrospec.MacroSpec("putindblquotes", "{"), 12 | macrospec.MacroSpec("putinquotes", "[[{"), 13 | ], 14 | environments=[ 15 | macrospec.EnvironmentSpec("indblquotes", ""), 16 | macrospec.EnvironmentSpec("inquotes", "[["), 17 | ], 18 | specials=[ 19 | macrospec.SpecialsSpec("`"), 20 | macrospec.SpecialsSpec("'"), 21 | macrospec.SpecialsSpec("``"), 22 | macrospec.SpecialsSpec("''"), 23 | ], 24 | ) 25 | 26 | # 27 | # Implement macros, environments, specials for the *conversion to text* 28 | # 29 | 30 | def _get_optional_arg(node, default, l2tobj): 31 | """Helper that returns the `node` converted to text, or `default` 32 | if the node is `None` (e.g. an optional argument that was not 33 | specified)""" 34 | if node is None: 35 | return default 36 | return l2tobj.nodelist_to_text([node]) 37 | 38 | def put_in_quotes_macro_repl(n, l2tobj): 39 | """Get the text replacement for the macro 40 | \putinquotes[open-quote][close-quote]{text}""" 41 | if not n.nodeargd: 42 | # n.nodeargd can be empty if e.g. \putinquotes was a single 43 | # token passed as an argument to a macro, 44 | # e.g. \newcommand\putinquotes... 45 | return '' 46 | open_q_s = _get_optional_arg(n.nodeargd.argnlist[0], '“', l2tobj) 47 | close_q_s = _get_optional_arg(n.nodeargd.argnlist[1], '”', l2tobj) 48 | return (open_q_s + l2tobj.nodelist_to_text([n.nodeargd.argnlist[2]]) 49 | + close_q_s) 50 | 51 | def in_quotes_env_repl(n, l2tobj): 52 | """Get the text replacement for the {inquotes} environment""" 53 | open_q_s = _get_optional_arg(n.nodeargd.argnlist[0], '“', l2tobj) 54 | close_q_s = _get_optional_arg(n.nodeargd.argnlist[1], '”', l2tobj) 55 | return open_q_s + l2tobj.nodelist_to_text(n.nodelist) + close_q_s 56 | 57 | l2t_context_db = latex2text.get_default_latex_context_db() 58 | l2t_context_db.add_context_category( 59 | 'my-quotes', 60 | prepend=True, 61 | macros=[ 62 | latex2text.MacroTextSpec("putindblquotes", 63 | simplify_repl=r'“%(1)s”'), 64 | latex2text.MacroTextSpec("putinquotes", 65 | simplify_repl=put_in_quotes_macro_repl), 66 | ], 67 | environments=[ 68 | latex2text.EnvironmentTextSpec("indblquotes", 69 | simplify_repl=r'“%(body)s”'), 70 | latex2text.EnvironmentTextSpec("inquotes", 71 | simplify_repl=in_quotes_env_repl), 72 | ], 73 | specials=[ 74 | latex2text.SpecialsTextSpec('`', "‘"), 75 | latex2text.SpecialsTextSpec("'", "’"), 76 | latex2text.SpecialsTextSpec('``', "“"), 77 | latex2text.SpecialsTextSpec("''", "”"), 78 | ], 79 | ) 80 | 81 | 82 | # 83 | # Here is an example usage: 84 | # 85 | 86 | def custom_latex_to_text( input_latex ): 87 | # the latex parser instance with custom latex_context 88 | lw_obj = latexwalker.LatexWalker(input_latex, 89 | latex_context=lw_context_db) 90 | # parse to node list 91 | nodelist, pos, length = lw_obj.get_latex_nodes() 92 | # initialize the converter to text with custom latex_context 93 | l2t_obj = latex2text.LatexNodes2Text(latex_context=l2t_context_db) 94 | # convert to text 95 | return l2t_obj.nodelist_to_text( nodelist ) 96 | 97 | 98 | print(custom_latex_to_text( 99 | r"""\begin{inquotes}[`][']Hello, world\end{inquotes}""")) 100 | # ‘Hello, world’ 101 | 102 | print(custom_latex_to_text(r"""\putinquotes[``]['']{Hello, world}""")) 103 | # “Hello, world” 104 | 105 | print(custom_latex_to_text(r"""\putinquotes{Hello, world}""")) 106 | # “Hello, world” 107 | 108 | print(custom_latex_to_text(r"""\putinquotes[`][']{Hello, world}""")) 109 | # ‘Hello, world’ 110 | -------------------------------------------------------------------------------- /doc/index.rst: -------------------------------------------------------------------------------- 1 | .. pylatexenc documentation master file, created by 2 | sphinx-quickstart on Mon Apr 24 16:32:21 2017. 3 | You can adapt this file completely to your liking, 4 | but it should at least contain the root `toctree` directive. 5 | 6 | Welcome to pylatexenc's documentation! 7 | ====================================== 8 | 9 | [pylatexenc version: |version|] 10 | 11 | A simple LaTeX parser providing latex-to-unicode and unicode-to-latex conversion. 12 | 13 | Quick example:: 14 | 15 | >>> from pylatexenc.latex2text import LatexNodes2Text 16 | >>> latex = r"""\textbf{Hi there!} Here is \emph{an equation}: 17 | ... \begin{equation} 18 | ... \zeta = x + i y 19 | ... \end{equation} 20 | ... where $i$ is the imaginary unit. 21 | ... """ 22 | >>> print(LatexNodes2Text().latex_to_text(latex)) 23 | Hi there! Here is an equation: 24 | 25 | ζ = x + i y 26 | 27 | where i is the imaginary unit. 28 | 29 | And the other way around:: 30 | 31 | >>> from pylatexenc.latexencode import unicode_to_latex 32 | >>> text = "À votre santé!" 33 | >>> print(unicode_to_latex(text)) 34 | \`A votre sant\'e! 35 | 36 | 37 | You can also use these utilities directly in command line, e.g.:: 38 | 39 | $ echo 'À votre santé!' | latexencode 40 | \`A votre sant\'e! 41 | 42 | 43 | Documentation 44 | ============= 45 | 46 | 47 | .. toctree:: 48 | :maxdepth: 2 49 | :caption: Contents: 50 | 51 | latexnodes 52 | macrospec 53 | latexwalker 54 | latex2text 55 | latexencode 56 | changes 57 | 58 | 59 | Indices and tables 60 | ================== 61 | 62 | * :ref:`genindex` 63 | * :ref:`modindex` 64 | * :ref:`search` 65 | -------------------------------------------------------------------------------- /doc/latex2text.rst: -------------------------------------------------------------------------------- 1 | `latex2text` — Simple Latex to Text Converter 2 | --------------------------------------------- 3 | 4 | .. automodule:: pylatexenc.latex2text 5 | :no-undoc-members: 6 | 7 | .. contents:: Contents: 8 | :local: 9 | 10 | 11 | Custom latex conversion rules: A simple template 12 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 13 | 14 | Here is a short introduction on how to customize the way that 15 | :py:class:`~pylatexenc.latex2text.LatexNodes2Text` converts LaTeX constructs 16 | (macros, environments, and specials) to unicode text. You can start off with 17 | the example template below and adapt it to your needs. 18 | 19 | Macros, environments and specials are parsed as corresponding node objects by 20 | the parser (see :py:class:`pylatexenc.latexwalker.LatexMacroNode`, 21 | :py:class:`pylatexenc.latexwalker.LatexEnvironmentNode`, and 22 | :py:class:`pylatexenc.latexwalker.LatexSpecialsNode`). These node objects are 23 | then converted to unicode text by the 24 | :py:class:`~pylatexenc.latex2text.LatexNodes2Text` object. 25 | 26 | You can define new macros, environments, or specials, or override existing 27 | definitions. The definitions need to be provided twice. First, at the level of 28 | the parser using the :py:mod:`~pylatexenc.macrospec` module; the parser needs to 29 | know the argument structure of your macros, environments, and specials, along 30 | with which characters to recognize as "specials". Second, at the level of 31 | `latex2text`, you need to specify what the replacement strings are for the 32 | different LaTeX constructs after they have been parsed into the latex node tree 33 | by the parser. 34 | 35 | The following template is a simple illustrative example that implements the 36 | following definitions: 37 | 38 | - A new macro ``\putinquotes[`][']{text}`` that puts its mandatory argument 39 | into quotes defined by the two optional arguments. Let's say that the 40 | default quotes that are used are `````` and ``''``. Another simpler macro 41 | ``\putindblquotes{text}`` is also provided for the sake of the example. 42 | 43 | - A new environment ``\begin{inquotes}[`]['] ... \end{inquotes}`` that does 44 | the same thing as its macro equivalent. Another simpler environment 45 | ``\begin{indblquotes}...\end{indblquotes}`` is also provided for the sake of 46 | the example. 47 | 48 | - The usual LaTeX quote symbols `````, ``````, ``'``, and ``''`` for unicode 49 | quotes. (See also issue :issue:`39`) 50 | 51 | Here is the code (see also docs for :py:class:`pylatexenc.macrospec.MacroSpec`, 52 | :py:class:`pylatexenc.macrospec.EnvironmentSpec`, 53 | :py:class:`pylatexenc.macrospec.SpecialsSpec`, as well as 54 | :py:class:`pylatexenc.latex2text.MacroTextSpec`, 55 | :py:class:`pylatexenc.latex2text.EnvironmentTextSpec`, 56 | :py:class:`pylatexenc.latex2text.SpecialsTextSpec`): 57 | 58 | .. literalinclude:: example_latex2text_custom_quotes.py 59 | :language: python 60 | 61 | 62 | Latex to Text Converter Class 63 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 64 | 65 | .. autoclass:: pylatexenc.latex2text.LatexNodes2Text 66 | :members: 67 | 68 | 69 | .. autofunction:: pylatexenc.latex2text.get_default_latex_context_db 70 | 71 | 72 | 73 | Define replacement texts 74 | ~~~~~~~~~~~~~~~~~~~~~~~~ 75 | 76 | .. autoclass:: pylatexenc.latex2text.MacroTextSpec 77 | :members: 78 | 79 | .. autoclass:: pylatexenc.latex2text.EnvironmentTextSpec 80 | :members: 81 | 82 | .. autoclass:: pylatexenc.latex2text.SpecialsTextSpec 83 | :members: 84 | 85 | 86 | 87 | Obsolete members 88 | ~~~~~~~~~~~~~~~~ 89 | 90 | .. autofunction:: pylatexenc.latex2text.EnvDef 91 | 92 | .. autofunction:: pylatexenc.latex2text.MacroDef 93 | 94 | 95 | .. autodata:: pylatexenc.latex2text.default_env_dict 96 | :annotation: 97 | 98 | .. autodata:: pylatexenc.latex2text.default_macro_dict 99 | :annotation: 100 | 101 | .. autodata:: pylatexenc.latex2text.default_text_replacements 102 | :annotation: 103 | -------------------------------------------------------------------------------- /doc/latexencode.rst: -------------------------------------------------------------------------------- 1 | `latexencode` — Encode Unicode to LaTeX 2 | --------------------------------------- 3 | 4 | .. automodule:: pylatexenc.latexencode 5 | :no-undoc-members: 6 | 7 | .. contents:: Contents: 8 | :local: 9 | 10 | 11 | Unicode to Latex Conversion Class and Helper Function 12 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 13 | 14 | .. autoclass:: pylatexenc.latexencode.UnicodeToLatexEncoder 15 | :members: 16 | 17 | .. autofunction:: pylatexenc.latexencode.unicode_to_latex 18 | 19 | 20 | Specifying conversion rules 21 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~ 22 | 23 | .. autodata:: pylatexenc.latexencode.RULE_DICT 24 | 25 | .. autodata:: pylatexenc.latexencode.RULE_REGEX 26 | 27 | .. autodata:: pylatexenc.latexencode.RULE_CALLABLE 28 | 29 | 30 | 31 | .. autoclass:: pylatexenc.latexencode.UnicodeToLatexConversionRule 32 | :members: 33 | 34 | 35 | 36 | .. autofunction:: pylatexenc.latexencode.get_builtin_conversion_rules 37 | 38 | .. autofunction:: pylatexenc.latexencode.get_builtin_uni2latex_dict 39 | 40 | 41 | Compatibility with `pylatexenc 1.x` 42 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 43 | 44 | .. autofunction:: pylatexenc.latexencode.utf8tolatex 45 | 46 | .. autodata:: pylatexenc.latexencode.utf82latex 47 | -------------------------------------------------------------------------------- /doc/latexnodes.nodes.rst: -------------------------------------------------------------------------------- 1 | `latexnodes.nodes` — LaTeX Nodes Classes 2 | ======================================== 3 | 4 | .. automodule:: pylatexenc.latexnodes.nodes 5 | :no-undoc-members: 6 | :show-inheritance: 7 | 8 | .. contents:: Contents: 9 | :local: 10 | 11 | 12 | 13 | Nodes, Node Lists, and Visitors 14 | ------------------------------- 15 | 16 | 17 | .. autoclass:: pylatexenc.latexnodes.nodes.LatexNode 18 | :members: 19 | 20 | .. autoclass:: pylatexenc.latexnodes.nodes.LatexNodeList 21 | :members: 22 | 23 | .. autoclass:: pylatexenc.latexnodes.nodes.LatexNodesVisitor 24 | :members: 25 | 26 | 27 | LaTeX Node Types 28 | ---------------- 29 | 30 | .. autoclass:: pylatexenc.latexnodes.nodes.LatexCharsNode 31 | :members: 32 | :show-inheritance: 33 | 34 | .. autoclass:: pylatexenc.latexnodes.nodes.LatexGroupNode 35 | :members: 36 | :show-inheritance: 37 | 38 | .. autoclass:: pylatexenc.latexnodes.nodes.LatexCommentNode 39 | :members: 40 | :show-inheritance: 41 | 42 | .. autoclass:: pylatexenc.latexnodes.nodes.LatexMacroNode 43 | :members: 44 | :show-inheritance: 45 | 46 | .. autoclass:: pylatexenc.latexnodes.nodes.LatexEnvironmentNode 47 | :members: 48 | :show-inheritance: 49 | 50 | .. autoclass:: pylatexenc.latexnodes.nodes.LatexSpecialsNode 51 | :members: 52 | :show-inheritance: 53 | 54 | .. autoclass:: pylatexenc.latexnodes.nodes.LatexMathNode 55 | :members: 56 | :show-inheritance: 57 | 58 | 59 | -------------------------------------------------------------------------------- /doc/latexnodes.parsers.rst: -------------------------------------------------------------------------------- 1 | `latexnodes.parsers` — Latex Construct Parsers 2 | ============================================== 3 | 4 | .. automodule:: pylatexenc.latexnodes.parsers 5 | :members: 6 | :no-undoc-members: 7 | :show-inheritance: 8 | 9 | .. contents:: Contents: 10 | :local: 11 | 12 | 13 | Parser base class 14 | ~~~~~~~~~~~~~~~~~ 15 | 16 | .. autoclass:: LatexParserBase 17 | :members: 18 | 19 | 20 | General nodes 21 | ~~~~~~~~~~~~~ 22 | 23 | .. autoclass:: LatexGeneralNodesParser 24 | :members: 25 | :show-inheritance: 26 | 27 | .. autoclass:: LatexSingleNodeParser 28 | :members: 29 | :show-inheritance: 30 | 31 | 32 | Delimited expressions 33 | ~~~~~~~~~~~~~~~~~~~~~ 34 | 35 | .. autoclass:: LatexDelimitedExpressionParserInfo 36 | :members: 37 | 38 | .. autoclass:: LatexDelimitedExpressionParser 39 | :members: 40 | :show-inheritance: 41 | 42 | .. autoclass:: LatexDelimitedGroupParserInfo 43 | :members: 44 | :show-inheritance: 45 | 46 | .. autoclass:: LatexDelimitedGroupParser 47 | :members: 48 | :show-inheritance: 49 | 50 | .. autoclass:: LatexDelimitedMultiDelimGroupParserInfo 51 | :members: 52 | :show-inheritance: 53 | 54 | .. autoclass:: LatexDelimitedMultiDelimGroupParser 55 | :members: 56 | :show-inheritance: 57 | 58 | .. autoclass:: LatexDelimitedExpressionParserOpeningDelimiterNotFound 59 | :members: 60 | :show-inheritance: 61 | 62 | .. autoclass:: LatexMathParser 63 | :members: 64 | :show-inheritance: 65 | 66 | 67 | 68 | Single expression parser 69 | ~~~~~~~~~~~~~~~~~~~~~~~~ 70 | 71 | .. autoclass:: LatexExpressionParser 72 | :members: 73 | :show-inheritance: 74 | 75 | 76 | Optional expression parser 77 | ~~~~~~~~~~~~~~~~~~~~~~~~~~ 78 | 79 | .. autoclass:: LatexOptionalSquareBracketsParser 80 | :members: 81 | :show-inheritance: 82 | 83 | .. autoclass:: LatexOptionalCharsMarkerParser 84 | :members: 85 | :show-inheritance: 86 | 87 | .. autoclass:: LatexOptionalEmbellishmentArgsParser 88 | :members: 89 | :show-inheritance: 90 | 91 | 92 | Verbatim/literal expressions 93 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 94 | 95 | .. autoclass:: LatexVerbatimBaseParser 96 | :members: 97 | :show-inheritance: 98 | 99 | .. autoclass:: LatexDelimitedVerbatimParser 100 | :members: 101 | :show-inheritance: 102 | 103 | .. autoclass:: LatexVerbatimEnvironmentContentsParser 104 | :members: 105 | :show-inheritance: 106 | 107 | 108 | 109 | Typical macro arguments 110 | ~~~~~~~~~~~~~~~~~~~~~~~ 111 | 112 | .. autofunction:: get_standard_argument_parser 113 | 114 | .. autoclass:: LatexStandardArgumentParser 115 | :members: 116 | :show-inheritance: 117 | 118 | .. autoclass:: LatexCharsCommaSeparatedListParser 119 | :members: 120 | :show-inheritance: 121 | 122 | .. autoclass:: LatexCharsGroupParser 123 | :members: 124 | :show-inheritance: 125 | 126 | .. autoclass:: LatexTackOnInformationFieldMacrosParser 127 | :members: 128 | :show-inheritance: 129 | 130 | -------------------------------------------------------------------------------- /doc/latexnodes.rst: -------------------------------------------------------------------------------- 1 | `latexnodes` — LaTeX Nodes Tree and Parsers 2 | =========================================== 3 | 4 | .. automodule:: pylatexenc.latexnodes 5 | :no-undoc-members: 6 | :show-inheritance: 7 | 8 | .. contents:: Contents: 9 | :local: 10 | 11 | 12 | 13 | Parsing State 14 | ------------- 15 | 16 | .. autoclass:: ParsingState 17 | :members: 18 | 19 | .. autoclass:: ParsingStateDelta 20 | :members: 21 | 22 | .. autoclass:: ParsingStateDeltaReplaceParsingState 23 | :members: 24 | 25 | .. autoclass:: ParsingStateDeltaChained 26 | :members: 27 | 28 | .. autoclass:: ParsingStateDeltaWalkerEvent 29 | :members: 30 | 31 | .. autoclass:: ParsingStateDeltaEnterMathMode 32 | :members: 33 | 34 | .. autoclass:: ParsingStateDeltaLeaveMathMode 35 | :members: 36 | 37 | 38 | 39 | Latex Token 40 | ----------- 41 | 42 | .. autoclass:: LatexToken 43 | :members: 44 | 45 | 46 | Token Readers 47 | ------------- 48 | 49 | .. autoclass:: LatexTokenReaderBase 50 | :members: 51 | 52 | .. autoclass:: LatexTokenReader 53 | :members: 54 | 55 | .. autoclass:: LatexTokenListTokenReader 56 | :members: 57 | 58 | 59 | Arguments and Parsed Arguments 60 | ------------------------------ 61 | 62 | .. autoclass:: LatexArgumentSpec 63 | :members: 64 | 65 | .. autoclass:: ParsedArguments 66 | :members: 67 | 68 | .. autoclass:: ParsedArgumentsInfo 69 | :members: 70 | 71 | .. autoclass:: SingleParsedArgumentInfo 72 | :members: 73 | 74 | 75 | Nodes Collector 76 | --------------- 77 | 78 | .. autoclass:: LatexNodesCollector 79 | :members: 80 | 81 | 82 | Exception classes 83 | ----------------- 84 | 85 | .. autoclass:: LatexWalkerError 86 | :members: 87 | 88 | .. autoclass:: LatexWalkerLocatedError 89 | :members: 90 | 91 | .. autoclass:: LatexWalkerLocatedErrorFormatter 92 | :members: 93 | 94 | .. autoclass:: LatexWalkerParseError 95 | :members: 96 | 97 | .. autoclass:: LatexWalkerNodesParseError 98 | :members: 99 | 100 | .. autoclass:: LatexWalkerTokenParseError 101 | :members: 102 | 103 | .. autoclass:: LatexWalkerEndOfStream 104 | :members: 105 | 106 | 107 | Base classes 108 | ------------ 109 | 110 | .. autoclass:: CallableSpecBase 111 | :members: 112 | 113 | .. autoclass:: LatexWalkerParsingStateEventHandler 114 | :members: 115 | 116 | .. autoclass:: LatexWalkerBase 117 | :members: 118 | 119 | .. autoclass:: LatexContextDbBase 120 | :members: 121 | 122 | 123 | 124 | Node Classes 125 | ------------ 126 | 127 | .. toctree:: 128 | :maxdepth: 2 129 | 130 | latexnodes.nodes 131 | 132 | 133 | Parser Classes 134 | -------------- 135 | 136 | .. toctree:: 137 | :maxdepth: 2 138 | 139 | latexnodes.parsers 140 | -------------------------------------------------------------------------------- /doc/latexwalker.rst: -------------------------------------------------------------------------------- 1 | `latexwalker` — Calling Parsers for LaTeX Code 2 | ---------------------------------------------- 3 | 4 | .. automodule:: pylatexenc.latexwalker 5 | :no-undoc-members: 6 | :show-inheritance: 7 | 8 | .. contents:: Contents: 9 | :local: 10 | 11 | 12 | The main `LatexWalker` class 13 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 14 | 15 | .. autoclass:: pylatexenc.latexwalker.LatexWalker 16 | :members: 17 | 18 | 19 | .. autofunction:: pylatexenc.latexwalker.get_default_latex_context_db 20 | 21 | 22 | Exception Classes 23 | ~~~~~~~~~~~~~~~~~ 24 | 25 | .. py:class:: pylatexenc.latexwalker.LatexWalkerError 26 | 27 | Moved to :py:class:`pylatexenc.latexnodes.LatexWalkerError`. 28 | 29 | .. deprecated:: 3.0 30 | 31 | Since Pylatexenc 3.0, this class now resides in the new module 32 | :py:mod:`pylatexenc.latexnodes` as 33 | :py:class:`pylatexenc.latexnodes.LatexWalkerError`. It is aliased in 34 | `pylatexenc.latexwalker` for backwards compatibility. 35 | 36 | .. py:class:: pylatexenc.latexwalker.LatexWalkerParseError 37 | 38 | Moved to :py:class:`pylatexenc.latexnodes.LatexWalkerParseError`. 39 | 40 | .. deprecated:: 3.0 41 | 42 | Since Pylatexenc 3.0, this class now resides in the new module 43 | :py:mod:`pylatexenc.latexnodes` as 44 | :py:class:`pylatexenc.latexnodes.LatexWalkerParseError`. It is aliased in 45 | `pylatexenc.latexwalker` for backwards compatibility. 46 | 47 | .. py:class:: pylatexenc.latexwalker.LatexWalkerEndOfStream 48 | 49 | Moved to :py:class:`pylatexenc.latexnodes.LatexWalkerEndOfStream`. 50 | 51 | .. deprecated:: 3.0 52 | 53 | Since Pylatexenc 3.0, this class now resides in the new module 54 | :py:mod:`pylatexenc.latexnodes` as 55 | :py:class:`pylatexenc.latexnodes.LatexWalkerEndOfStream`. It is aliased 56 | in `pylatexenc.latexwalker` for backwards compatibility. 57 | 58 | 59 | Data Node Classes 60 | ~~~~~~~~~~~~~~~~~ 61 | 62 | .. py:class:: pylatexenc.latexwalker.LatexNode 63 | 64 | Moved to :py:class:`pylatexenc.latexnodes.nodes.LatexNode`. 65 | 66 | .. deprecated:: 3.0 67 | 68 | Since Pylatexenc 3.0, this class now resides in the new module 69 | :py:mod:`pylatexenc.latexnodes.nodes` as 70 | :py:class:`pylatexenc.latexnodes.nodes.LatexNode`. It is aliased in 71 | `pylatexenc.latexwalker` for backwards compatibility. 72 | 73 | .. py:class:: pylatexenc.latexwalker.LatexCharsNode 74 | 75 | Moved to :py:class:`pylatexenc.latexnodes.nodes.LatexCharsNode`. 76 | 77 | .. deprecated:: 3.0 78 | 79 | Since Pylatexenc 3.0, this class now resides in the new module 80 | :py:mod:`pylatexenc.latexnodes.nodes` as 81 | :py:class:`pylatexenc.latexnodes.nodes.LatexCharsNode`. It is aliased in 82 | `pylatexenc.latexwalker` for backwards compatibility. 83 | 84 | .. py:class:: pylatexenc.latexwalker.LatexGroupNode 85 | 86 | Moved to :py:class:`pylatexenc.latexnodes.nodes.LatexGroupNode`. 87 | 88 | .. deprecated:: 3.0 89 | 90 | Since Pylatexenc 3.0, this class now resides in the new module 91 | :py:mod:`pylatexenc.latexnodes.nodes` as 92 | :py:class:`pylatexenc.latexnodes.nodes.LatexGroupNode`. It is aliased in 93 | `pylatexenc.latexwalker` for backwards compatibility. 94 | 95 | .. py:class:: pylatexenc.latexwalker.LatexCommentNode 96 | 97 | Moved to :py:class:`pylatexenc.latexnodes.nodes.LatexCommentNode`. 98 | 99 | .. deprecated:: 3.0 100 | 101 | Since Pylatexenc 3.0, this class now resides in the new module 102 | :py:mod:`pylatexenc.latexnodes.nodes` as 103 | :py:class:`pylatexenc.latexnodes.nodes.LatexCommentNode`. It is aliased 104 | in `pylatexenc.latexwalker` for backwards compatibility. 105 | 106 | .. py:class:: pylatexenc.latexwalker.LatexMacroNode 107 | 108 | Moved to :py:class:`pylatexenc.latexnodes.nodes.LatexMacroNode`. 109 | 110 | .. deprecated:: 3.0 111 | 112 | Since Pylatexenc 3.0, this class now resides in the new module 113 | :py:mod:`pylatexenc.latexnodes.nodes` as 114 | :py:class:`pylatexenc.latexnodes.nodes.LatexMacroNode`. It is aliased in 115 | `pylatexenc.latexwalker` for backwards compatibility. 116 | 117 | .. py:class:: pylatexenc.latexwalker.LatexEnvironmentNode 118 | 119 | Moved to :py:class:`pylatexenc.latexnodes.nodes.LatexEnvironmentNode`. 120 | 121 | .. deprecated:: 3.0 122 | 123 | Since Pylatexenc 3.0, this class now resides in the new module 124 | :py:mod:`pylatexenc.latexnodes.nodes` as 125 | :py:class:`pylatexenc.latexnodes.nodes.LatexEnvironmentNode`. It is 126 | aliased in `pylatexenc.latexwalker` for backwards compatibility. 127 | 128 | .. py:class:: pylatexenc.latexwalker.LatexSpecialsNode 129 | 130 | Moved to :py:class:`pylatexenc.latexnodes.nodes.LatexSpecialsNode`. 131 | 132 | .. deprecated:: 3.0 133 | 134 | Since Pylatexenc 3.0, this class now resides in the new module 135 | :py:mod:`pylatexenc.latexnodes.nodes` as 136 | :py:class:`pylatexenc.latexnodes.nodes.LatexSpecialsNode`. It is aliased 137 | in `pylatexenc.latexwalker` for backwards compatibility. 138 | 139 | .. py:class:: pylatexenc.latexwalker.LatexMathNode 140 | 141 | Moved to :py:class:`pylatexenc.latexnodes.nodes.LatexMathNode`. 142 | 143 | .. deprecated:: 3.0 144 | 145 | Since Pylatexenc 3.0, this class now resides in the new module 146 | :py:mod:`pylatexenc.latexnodes.nodes` as 147 | :py:class:`pylatexenc.latexnodes.nodes.LatexMathNode`. It is aliased in 148 | `pylatexenc.latexwalker` for backwards compatibility. 149 | 150 | 151 | Parsing helpers 152 | ~~~~~~~~~~~~~~~ 153 | 154 | .. py:class:: pylatexenc.latexwalker.ParsingState 155 | 156 | .. deprecated:: 3.0 157 | 158 | Since Pylatexenc 3.0, this class now resides in the new module 159 | :py:mod:`pylatexenc.latexnodes`. It is aliased in 160 | `pylatexenc.latexwalker` for backwards compatibility. 161 | 162 | .. py:class:: pylatexenc.latexwalker.LatexToken 163 | 164 | .. deprecated:: 3.0 165 | 166 | Since Pylatexenc 3.0, this class now resides in the new module 167 | :py:mod:`pylatexenc.latexnodes`. It is aliased in 168 | `pylatexenc.latexwalker` for backwards compatibility. 169 | 170 | 171 | 172 | Legacy Macro Definitions (for `pylatexenc 1.x`) 173 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 174 | 175 | .. autodata:: pylatexenc.latexwalker.MacrosDef 176 | 177 | .. autodata:: pylatexenc.latexwalker.default_macro_dict 178 | :annotation: 179 | 180 | 181 | -------------------------------------------------------------------------------- /doc/macrospec.rst: -------------------------------------------------------------------------------- 1 | `macrospec` — Specifying definitions for the parser 2 | --------------------------------------------------- 3 | 4 | .. automodule:: pylatexenc.macrospec 5 | :no-undoc-members: 6 | :show-inheritance: 7 | 8 | .. contents:: Contents: 9 | :local: 10 | 11 | 12 | Macro and environment definitions 13 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 14 | 15 | .. autoclass:: pylatexenc.macrospec.MacroSpec 16 | :members: 17 | :inherited-members: 18 | 19 | .. autoclass:: pylatexenc.macrospec.EnvironmentSpec 20 | :members: 21 | :inherited-members: 22 | 23 | .. autoclass:: pylatexenc.macrospec.SpecialsSpec 24 | :members: 25 | :inherited-members: 26 | 27 | 28 | .. autofunction:: pylatexenc.macrospec.std_macro 29 | 30 | .. autofunction:: pylatexenc.macrospec.std_environment 31 | 32 | .. autofunction:: pylatexenc.macrospec.std_specials 33 | 34 | 35 | Latex Context "Database" 36 | ~~~~~~~~~~~~~~~~~~~~~~~~ 37 | 38 | .. autoclass:: pylatexenc.macrospec.LatexContextDb 39 | :members: 40 | 41 | .. autoclass:: pylatexenc.macrospec.ParsingStateDeltaExtendLatexContextDb 42 | :show-inheritance: 43 | :members: 44 | 45 | 46 | Lower-level parsers for macro, environments, and specials 47 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 48 | 49 | You shouldn't have to use these directly. 50 | 51 | .. autoclass:: pylatexenc.macrospec.LatexNoArgumentsParser 52 | :show-inheritance: 53 | :members: 54 | 55 | .. autoclass:: pylatexenc.macrospec.LatexArgumentsParser 56 | :show-inheritance: 57 | :members: 58 | 59 | .. autoclass:: pylatexenc.macrospec.LatexEnvironmentBodyContentsParserInfo 60 | :show-inheritance: 61 | :members: 62 | 63 | .. autoclass:: pylatexenc.macrospec.LatexEnvironmentBodyContentsParser 64 | :show-inheritance: 65 | :members: 66 | 67 | .. autoclass:: pylatexenc.macrospec.LatexMacroCallParser 68 | :show-inheritance: 69 | :members: 70 | 71 | .. autoclass:: pylatexenc.macrospec.LatexEnvironmentCallParser 72 | :show-inheritance: 73 | :members: 74 | 75 | .. autoclass:: pylatexenc.macrospec.LatexSpecialsCallParser 76 | :show-inheritance: 77 | :members: 78 | 79 | 80 | 81 | 82 | Legacy (2.x) Macro arguments parsers 83 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 84 | 85 | .. autoclass:: pylatexenc.macrospec.MacroStandardArgsParser 86 | :members: 87 | 88 | .. autoclass:: pylatexenc.macrospec.ParsedMacroArgs 89 | :members: 90 | 91 | .. autoclass:: pylatexenc.macrospec.VerbatimArgsParser 92 | :show-inheritance: 93 | :members: 94 | 95 | .. autoclass:: pylatexenc.macrospec.ParsedVerbatimArgs 96 | :show-inheritance: 97 | :members: 98 | 99 | -------------------------------------------------------------------------------- /doc/new-in-pylatexenc-3.rst: -------------------------------------------------------------------------------- 1 | What's new in `pylatexenc 3` 2 | ============================ 3 | 4 | Wow, a *lot* of stuff has changed in the `latexwalker` and `macrospec` modules. 5 | There's even a new `latexnodes` module. I don't know where to start! 6 | 7 | The good news is, if you're simply using the latex-to-unicode and 8 | unicode-to-latex conversion tools, your code depending on `pylatexenc 2` should 9 | run without any chagnes. You might get some deprecation warnings which you can 10 | silence using python's warnings filter management (e.g., ``python -W 11 | 'ignore::DeprecationWarnings'`` or using :py:func:`warnings.simplefilter`) 12 | 13 | The `latex2text` and `latexencode` modules have barely changed. 14 | 15 | - New parsing mechanism in a new `latexnodes` module — everything gets delegated 16 | to "parser objects" that are specialized in parsing a specific construct. See 17 | :py:class:`pylatexenc.latexnodes.parsers.LatexParserBase`. 18 | 19 | - The parser has new enhanced handling of macro, environment, and specials 20 | arguments. Arguments can be named for easier lookup when traversing the node 21 | tree. 22 | 23 | - **WARNING**: While in *alpha* stage, I'm expecting that the new APIs might 24 | still change. I'll try to remain as backwards-compatible as possible with 25 | `pylatexenc 2.x` but new APIs introduced in the `3.0alphaX` versions might 26 | still change a bit until they are finalized. 27 | 28 | - Lists of latex node objects 29 | (:py:class:`~pylatexenc.latexnodes.nodes.LatexNode`) are now wrapped in a 30 | special object for node lists → 31 | :py:class:`pylatexenc.latexnodes.nodes.LatexNodeList`. 32 | 33 | - so much more ... ... 34 | 35 | - The `len` attribute in node objects is replaced by a `pos_end` attribute. The 36 | `len` attribute can still be accessed as a read-only computed attribute for 37 | compatibility with existing code using pylatexenc 2. 38 | 39 | 40 | 41 | .. _new-in-pylatexenc-3-possible-pitfall-changes: 42 | 43 | A couple things to look out for 44 | ------------------------------- 45 | 46 | - If you created a :py:class:`~pylatexenc.macrospec.LatexContextDb` database 47 | from scratch, you might suddenly get errors about unknown macros. The default 48 | initialization for unknown macro, environment and specials specification 49 | objects for :py:class:`~pylatexenc.macrospec.LatexContextDb` was, and remains, 50 | `None`. What has changed is the interpretation of this `None`: Now, the latex 51 | walker (more precisely, 52 | :py:class:`~pylatexenc.latexnodes.LatexNodesCollector`) reports an error, 53 | whereas previously, the parser would simply assume the macro doesn't accept 54 | any arguments. To restore the earlier behavior, simply set the spec objects 55 | for unknown macro/environment/specials in your latex context db object:: 56 | 57 | latex_context_db = macrospec.LatexContextDb() 58 | # ... 59 | latex_context_db.add_context_category( ... ) 60 | # ... 61 | latex_context_db.set_unknown_macro_spec(macrospec.MacroSpec('')) 62 | latex_context_db.set_unknown_environment_spec(macrospec.EnvironmentSpec('')) 63 | # 64 | # unknown macros and environemnts are now accepted and are assumed 65 | # not to take any arguments 66 | # 67 | 68 | - Node lists are now encapsulated in a 69 | :py:class:`~pylatexenc.latexnodes.nodes.LatexNodeList`. It behaves very much 70 | like a list in all respects (indexing, slicing, etc.), except that it does not 71 | satisfy ``isinstance(nodelist, list)``. If you relied on such tests, you'll 72 | need to update them to the liking of ``isinstance(nodelist, (LatexNodeList, 73 | list))``. 74 | -------------------------------------------------------------------------------- /doc/requirements.txt: -------------------------------------------------------------------------------- 1 | sphinx_issues 2 | -------------------------------------------------------------------------------- /js-transcrypt/.gitignore: -------------------------------------------------------------------------------- 1 | # 2 | # additional git ignores 3 | # 4 | 5 | 6 | # 7 | # preprocessed python source code output 8 | # 9 | pp-tmp 10 | 11 | # 12 | # output folders 13 | # 14 | *_js_output 15 | pylatexenc-js 16 | test-pylatexenc-js 17 | 18 | 19 | node_modules 20 | .yarn 21 | -------------------------------------------------------------------------------- /js-transcrypt/.yarnrc.yml: -------------------------------------------------------------------------------- 1 | nodeLinker: 'node-modules' 2 | -------------------------------------------------------------------------------- /js-transcrypt/README.md: -------------------------------------------------------------------------------- 1 | # Building a Javascript version of pylatexenc.latexnodes library via *transcrypt* 2 | 3 | You can use the fantastic [Transcrypt](http://www.transcrypt.org/) tool ([also 4 | on github](https://github.com/QQuick/Transcrypt)) for converting parts of the 5 | pylatexenc code base into JavaScript to make a JavaScript-based parser for 6 | simple LaTeX code. 7 | 8 | This procedure is very much still in alpha stage. Don't rely too much on it! 9 | 10 | To use commands listed here, make sure you installed the optional poetry 11 | dependency group "buildjslib": 12 | 13 | > poetry install --with=buildjslib 14 | 15 | 16 | ## The build script 17 | 18 | To generate the JS python sources simply run in this folder: 19 | 20 | # generates pylatexenc-js/ 21 | > poetry run ./generate_pylatexenc_js.py 22 | 23 | (Make sure you've removed the `pylatexenc-js` folder from any previous run, or 24 | pass the `--delete-target-dir` option to the generator script.) 25 | 26 | To compile the tests along with the library, in its own folder: 27 | 28 | # generates both pylatexenc-js/ and test-pylatexenc-js/ 29 | > poetry run ./generate_pylatexenc_js.py --compile-tests 30 | 31 | To run the tests using `node`, do: 32 | 33 | > node test-pylatexenc-js/runtests.js 34 | 35 | 36 | ## Steps handled by the build script 37 | 38 | These are broadly the steps that the build script will apply. 39 | 40 | ### Preprocessing the pylatexenc library in preparation for transcrypt: 41 | 42 | The script will first preprocess the pylatexenc source code to make it suitable 43 | for use with transcrypt. You can also do this manually with 44 | 45 | > export PYLATEXENC_SRC_DIR=/path/to/root/folder/of/pylatexenc/ 46 | > export PREPROCESS_LIB_OUTPUT_DIR=pp-tmp/ # or some other temporary folder 47 | > poetry run python ../tools/preprocess_lib.py preprocesslib-pylatexenc.config.yaml 48 | 49 | ### Run Transcrypt to generate the Javascript sources 50 | 51 | We need to enable a lot of features in transcrypt, some of which are disabled by 52 | default. The build script basically follows the following commands. 53 | 54 | Transcrypt is called with the `import_pylatexenc_modules.py` module as entry 55 | point. This python module simply imports the subset of the `pylatexenc` library 56 | that we'll be compiling to JavaScript. The command to run is essentially: 57 | 58 | > poetry run transcrypt import_pylatexenc_modules.py --dassert --dext --ecom --gen --tconv --sform --kwargs --keycheck --opov --xreex --nomin --build --anno --parent .none -u .auto -xp 'pp-tmp$libpatches' -od pylatexenc-js 59 | 60 | The JavaScript files are output in the `pylatexenc-js` folder. 61 | 62 | ### Final touches 63 | 64 | The build script will then apply some additional steps and patches: 65 | 66 | - Create a `package.json` file that defines a module, so that you can import the 67 | sources using for instance: 68 | 69 | // js code 70 | import { Symbol1 [, ...] } from './pylatexenc-js/pylatexenc.latexnodes.js' 71 | 72 | - Create a `py.js` module that exports the functions `$$kw` and `repr`, exposing 73 | the keyword-argument functionality as well as python's `repr()` function. 74 | You can pass keywords to transcrypted functions as follows: 75 | 76 | // js code 77 | call_function_from_transcrypt(arg1, arg2, $$kw({ keywordarg1: value1, 78 | keywordarg2: value2 })) 79 | 80 | - Patch Transcrypt's internal runtime methods to add some missing support for 81 | additional functionality (see `transcrypt_runtime_patches.js`) 82 | 83 | -------------------------------------------------------------------------------- /js-transcrypt/generate_pylatexenc_js.py: -------------------------------------------------------------------------------- 1 | import os 2 | import os.path 3 | import re 4 | import sys 5 | import argparse 6 | import json 7 | 8 | import shutil 9 | import subprocess 10 | 11 | import logging 12 | logger = logging.getLogger('generate_pylatexenc_js') 13 | 14 | pylatexenc_src_dir = os.path.normpath(os.path.join(os.path.dirname(__file__), '..')) 15 | 16 | def run_main(): 17 | parser = argparse.ArgumentParser() 18 | 19 | parser.add_argument('--pylatexenc-js-output-dir', action='store', 20 | default='pylatexenc-js', 21 | help="Folder where to output generated JavaScript pylatexenc sources") 22 | 23 | parser.add_argument('--delete-target-dir', action='store_true', default=False, 24 | help="With this option, the target directory is removed if it exists " 25 | "at the beginning of the script instead of throwing an error. Will " 26 | "also remove the tests target directory if --compile-tests is given.") 27 | 28 | parser.add_argument('--preprocess-lib-output-dir', action='store', default='pp-tmp', 29 | help="Temporary folder in which to write intermediate, " 30 | "preprocessed sources to be fed into Transcrypt") 31 | 32 | parser.add_argument('--compile-tests', action='store_true', default=False, 33 | help="Also compile the pylatexenc tests into a separate " 34 | "folder (by default ./test-pylatexenc-js)") 35 | 36 | parser.add_argument('--test-pylatexenc-js-output-dir', action='store', 37 | default='test-pylatexenc-js', 38 | help="Folder where to output generated JavaScript pylatexenc " 39 | "test sources. " 40 | "The main entry point for the tests will be the script 'runtests.js'") 41 | 42 | args = parser.parse_args() 43 | 44 | logging.basicConfig(level=logging.DEBUG) 45 | 46 | if args.delete_target_dir: 47 | if os.path.exists(args.pylatexenc_js_output_dir): 48 | shutil.rmtree(args.pylatexenc_js_output_dir) 49 | if args.compile_tests: 50 | if os.path.exists(args.test_pylatexenc_js_output_dir): 51 | shutil.rmtree(args.test_pylatexenc_js_output_dir) 52 | 53 | os.makedirs(args.preprocess_lib_output_dir, exist_ok=True) 54 | 55 | if os.path.exists(args.pylatexenc_js_output_dir): 56 | raise RuntimeError( 57 | f"Target destination ‘{args.pylatexenc_js_output_dir}’ already exists. " 58 | f"Please remove it first." 59 | ) 60 | 61 | # pick up pylatexenc's generation script tool 62 | 63 | pylatexenc_tools_dir = os.path.join(pylatexenc_src_dir, 'tools') 64 | logger.info(f"Using pylatexenc_tools_dir = {pylatexenc_tools_dir!r}") 65 | sys.path.insert(0, pylatexenc_tools_dir) 66 | 67 | import utils_transcrypt_generate_js 68 | 69 | genutils = utils_transcrypt_generate_js.GenUtils( 70 | pylatexenc_src_dir=pylatexenc_src_dir, 71 | preprocess_lib_output_dir=args.preprocess_lib_output_dir, 72 | ) 73 | 74 | # preprocess both pylatexenc & pylatexenc libraries to prepare them for Transcrypt --> 75 | genutils.preprocess_pylatexenc_lib() 76 | if args.compile_tests: 77 | genutils.preprocess_lib('preprocesslib-tests.config.yaml') 78 | 79 | # run Transcrypt pylatexenc lib now --> 80 | genutils.run_transcrypt( 81 | 'import_pylatexenc_modules.py', 82 | output_dir=args.pylatexenc_js_output_dir, 83 | ) 84 | # final tweaks to finalize the JS package 85 | genutils.finalize_transcrypt_package( 86 | args.pylatexenc_js_output_dir, 87 | package_name='pylatexenc-js', 88 | package_version='0.0.1', 89 | package_description=\ 90 | 'Automatically transliterated Javascript version of the pylatexenc sources' 91 | ) 92 | 93 | 94 | if args.compile_tests: 95 | 96 | # Generate the test runner script 97 | runtests_py = genutils.generate_runtests_script( 98 | os.path.join(pylatexenc_src_dir, 'test'), 99 | test_file_patterns=[ 100 | # these are regexes that are matched as ^( <...> )[.]py$ 101 | 'test_latexnodes_.*', 102 | 'test_macrospec_.*', 103 | 'test_latexwalker_.*', 104 | 'test_latexencode', 105 | 'test_util', 106 | ] 107 | ) 108 | 109 | # Transcrypt it 110 | genutils.run_transcrypt( 111 | runtests_py, 112 | add_import_paths=[ 113 | os.path.join(args.preprocess_lib_output_dir, 'test') 114 | ], 115 | output_dir=args.test_pylatexenc_js_output_dir, 116 | ) 117 | genutils.finalize_transcrypt_package( 118 | args.test_pylatexenc_js_output_dir, 119 | package_name='test-pylatexenc-js', 120 | ) 121 | 122 | logger.info("Compiled the tests. To run them, try ‘node {}/runtests.js’" 123 | .format(args.test_pylatexenc_js_output_dir)) 124 | 125 | logger.info(f"Done!") 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | if __name__ == '__main__': 134 | run_main() 135 | -------------------------------------------------------------------------------- /js-transcrypt/import_pylatexenc_modules.py: -------------------------------------------------------------------------------- 1 | 2 | import pylatexenc 3 | # 4 | import pylatexenc.latexnodes 5 | import pylatexenc.macrospec 6 | import pylatexenc.latexwalker 7 | 8 | import pylatexenc.latexencode 9 | import pylatexenc.latexencode.get_builtin_rules 10 | 11 | 12 | # additional modules that we might need: 13 | import logging 14 | import collections 15 | 16 | 17 | # customjspatches is no longer needed, we're now directly patching the 18 | # Transcrypt runtime at JS sources generation time (see 19 | # generate_pylatexenc_js.py) 20 | # 21 | #import customjspatches #lgtm [py/unused-import] 22 | 23 | -------------------------------------------------------------------------------- /js-transcrypt/libpatches/bisect.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | # 5 | # THIS METHOD CAN ASSUME THAT THERE ARE NO DUPLICATES IN THE LIST. 6 | # 7 | def bisect_right(a, x): 8 | # find the first index of a that is > pos 9 | 10 | lo = 0 11 | hi = len(a) 12 | mid = None 13 | 14 | while True: 15 | 16 | #print(f"{a=} {x=} :: {lo=} {hi=} (mid was {mid=})") 17 | 18 | if a[lo] > x: 19 | return lo 20 | if a[hi-1] <= x: 21 | return hi 22 | 23 | # we know that a[lo] <= x and a[hi-1] > x 24 | 25 | if hi - lo <= 2: 26 | if a[lo+1] > x: # a[lo] <= x and a[lo+1] > x --> return lo+1 27 | return lo+1 28 | else: #if a[lo+2] > x: 29 | return lo+2 30 | 31 | mid = (hi + lo) // 2 32 | if a[mid] > x: 33 | hi = mid+1 # we still have a[hi-1] > x 34 | else: # i.e., if a[mid] <= x: 35 | lo = mid # we still have a[lo] <= x 36 | -------------------------------------------------------------------------------- /js-transcrypt/libpatches/collections.py: -------------------------------------------------------------------------------- 1 | 2 | ### ChainMap 3 | # -- straight from the python sources. 4 | 5 | 6 | import logging 7 | logger = logging.getLogger(__name__) 8 | 9 | 10 | 11 | class ChainMap: 12 | ''' A ChainMap groups multiple dicts (or other mappings) together 13 | to create a single, updateable view. 14 | 15 | The underlying mappings are stored in a list. That list is public and can 16 | be accessed or updated using the *maps* attribute. There is no other 17 | state. 18 | 19 | Lookups search the underlying mappings successively until a key is found. 20 | In contrast, writes, updates, and deletions only operate on the first 21 | mapping. 22 | 23 | ''' 24 | 25 | def __init__(self, *maps): 26 | '''Initialize a ChainMap by setting *maps* to the given mappings. 27 | If no mappings are provided, a single empty dictionary is used. 28 | 29 | ''' 30 | self.maps = list(maps) or [{}] # always at least one map 31 | 32 | def __missing__(self, key): 33 | raise KeyError(key) 34 | #return None 35 | 36 | def __getitem__(self, key): 37 | #logger.debug("Getting item %r", key) 38 | for mapping in self.maps: 39 | #logger.debug("\ttrying mapping: %r", mapping) 40 | if key not in mapping: 41 | continue 42 | #logger.debug("\tfound - %r", mapping[key]) 43 | return mapping[key] # can't use 'key in mapping' with defaultdict 44 | #logger.debug("\tnot found :(") 45 | return self.__missing__(key) # support subclasses that define __missing__ 46 | 47 | def get(self, key, default=None): 48 | return self[key] if key in self else default 49 | 50 | def __len__(self): 51 | return len(set().union(*self.maps)) # reuses stored hash values if possible 52 | 53 | def __iter__(self): 54 | d = {} 55 | for mapping in reversed(self.maps): 56 | d.update(dict.fromkeys(mapping)) # reuses stored hash values if possible 57 | return iter(d) 58 | 59 | def __contains__(self, key): 60 | return any(key in m for m in self.maps) 61 | 62 | def __bool__(self): 63 | return any(self.maps) 64 | 65 | def __repr__(self): 66 | return "{!r}({!r})".format(self.__class__.__name__, self.maps) 67 | # return f'{self.__class__.__name__}({", ".join(map(repr, self.maps))})' 68 | 69 | @classmethod 70 | def fromkeys(cls, iterable, *args): 71 | 'Create a ChainMap with a single dict created from the iterable.' 72 | return cls(dict.fromkeys(iterable, *args)) 73 | 74 | def copy(self): 75 | 'New ChainMap or subclass with a new copy of maps[0] and refs to maps[1:]' 76 | return self.__class__(self.maps[0].copy(), *self.maps[1:]) 77 | 78 | def __copy__(self): 79 | 'New ChainMap or subclass with a new copy of maps[0] and refs to maps[1:]' 80 | return self.__class__(self.maps[0].copy(), *self.maps[1:]) 81 | 82 | def new_child(self, m=None): # like Django's Context.push() 83 | '''New ChainMap with a new map followed by all previous maps. 84 | If no map is provided, an empty dict is used. 85 | ''' 86 | if m is None: 87 | m = {} 88 | return self.__class__(m, *self.maps) 89 | 90 | @property 91 | def parents(self): # like Django's Context.pop() 92 | 'New ChainMap from maps[1:].' 93 | return self.__class__(*self.maps[1:]) 94 | 95 | def __setitem__(self, key, value): 96 | self.maps[0][key] = value 97 | 98 | def __delitem__(self, key): 99 | try: 100 | del self.maps[0][key] 101 | except KeyError: 102 | raise KeyError(f'Key not found in the first mapping: {key!r}') 103 | 104 | def popitem(self): 105 | 'Remove and return an item pair from maps[0]. Raise KeyError is maps[0] is empty.' 106 | try: 107 | return self.maps[0].popitem() 108 | except KeyError: 109 | raise KeyError('No keys found in the first mapping.') 110 | 111 | def pop(self, key, *args): 112 | 'Remove *key* from maps[0] and return its value. Raise KeyError if *key* not in maps[0].' 113 | try: 114 | return self.maps[0].pop(key, *args) 115 | except KeyError: 116 | raise KeyError(f'Key not found in the first mapping: {key!r}') 117 | 118 | def clear(self): 119 | 'Clear maps[0], leaving maps[1:] intact.' 120 | self.maps[0].clear() 121 | 122 | def __ior__(self, other): 123 | self.maps[0].update(other) 124 | return self 125 | 126 | def __or__(self, other): 127 | if not isinstance(other, _collections_abc.Mapping): 128 | return NotImplemented 129 | m = self.copy() 130 | m.maps[0].update(other) 131 | return m 132 | 133 | def __ror__(self, other): 134 | if not isinstance(other, _collections_abc.Mapping): 135 | return NotImplemented 136 | m = dict(other) 137 | for child in reversed(self.maps): 138 | m.update(child) 139 | return self.__class__(m) 140 | 141 | 142 | 143 | -------------------------------------------------------------------------------- /js-transcrypt/libpatches/customjspatches.js: -------------------------------------------------------------------------------- 1 | export function custom_apply_patches() { 2 | 3 | console.log("Applying custom JS patches ..."); 4 | 5 | String.prototype.startswith = function (prefix, start) { 6 | //console.log("Custom startswith()! prefix = ", prefix, ", start = ", start); 7 | var pos_start = (typeof start === 'undefined' ? 0 : start); 8 | if (prefix instanceof Array) { 9 | for (var i=0;i= width) { 31 | return this; 32 | } 33 | return fill_char.repeat(width - this.length) + this; 34 | }; 35 | 36 | 37 | }; 38 | -------------------------------------------------------------------------------- /js-transcrypt/libpatches/functools.py: -------------------------------------------------------------------------------- 1 | 2 | def partial(fn, *args, **kwargs): 3 | return lambda *newargs, **newkwargs: fn(*args, *newargs, **dict(kwargs, **newkwargs)) 4 | -------------------------------------------------------------------------------- /js-transcrypt/libpatches/json.py: -------------------------------------------------------------------------------- 1 | 2 | def loads(s, object_hook=None): 3 | if not object_hook: 4 | return JSON.parse(s) 5 | __pragma__('js', "{}", """ 6 | var wrap_object_hook = (value) => { 7 | if (value instanceof Array || value instanceof Number || value instanceof String) { 8 | return value; 9 | } 10 | return object_hook(value); 11 | };""") 12 | return JSON.parse(s, wrap_object_hook) 13 | 14 | 15 | def dumps(d, indent=0): 16 | return JSON.stringify(d, None, indent) 17 | 18 | 19 | def dump(d, f, indent=0): 20 | f.write(dumps(d, indent)) 21 | 22 | 23 | -------------------------------------------------------------------------------- /js-transcrypt/libpatches/logging.js: -------------------------------------------------------------------------------- 1 | // 2 | // mini-patch logger 3 | // 4 | 5 | import { repr } from './org.transcrypt.__runtime__.js'; 6 | 7 | import debug_module from 'debug'; 8 | 9 | debug_module.formatters.r = (v) => repr(v); 10 | 11 | 12 | class DebugLogger 13 | { 14 | constructor(scope) 15 | { 16 | this.scope = scope; 17 | 18 | this._debug_fn = debug_module(this.scope); 19 | this._debug_fn_star = debug_module(this.scope+'*'); // always output 20 | 21 | //console.debug(`setting up logger ‘${this.scope}’ via debug()`); 22 | 23 | this.error = (msg, ...args) => { 24 | const lastarg = args[args.length-1]; 25 | if (lastarg && lastarg.__kwargtrans__ === null) { this._process_kwargs(lastarg); } 26 | this._debug_fn_star('[[logging.ERROR]] !! ' + msg, ...args); 27 | }; 28 | 29 | this.critical = (msg, ...args) => { 30 | const lastarg = args[args.length-1]; 31 | if (lastarg && lastarg.__kwargtrans__ === null) { this._process_kwargs(lastarg); } 32 | this._debug_fn_star('[[logging.CRITICAL]] !! ' + msg, ...args); 33 | }; 34 | 35 | this.warning = (msg, ...args) => { 36 | const lastarg = args[args.length-1]; 37 | if (lastarg && lastarg.__kwargtrans__ === null) { this._process_kwargs(lastarg); } 38 | this._debug_fn_star('[[logging.WARNING]] !! ' + msg, ...args); 39 | }; 40 | 41 | this.info = (msg, ...args) => { 42 | const lastarg = args[args.length-1]; 43 | if (lastarg && lastarg.__kwargtrans__ === null) { this._process_kwargs(lastarg); } 44 | this._debug_fn_star(msg, ...args); 45 | }; 46 | 47 | this.debug = (msg, ...args) => { 48 | const lastarg = args[args.length-1]; 49 | if (lastarg && lastarg.__kwargtrans__ === null) { this._process_kwargs(lastarg); } 50 | this._debug_fn('logging.debug ~~ ' + msg, ...args); 51 | }; 52 | } 53 | 54 | _process_kwargs(kwargs) 55 | { 56 | if (kwargs.exc_info) { 57 | console.trace(); 58 | } 59 | } 60 | 61 | // _emit(label, sep, msg, args, log_fn) 62 | // { 63 | // let s = label + sep + _assemble_msg(msg, args); 64 | // if (log_fn !== undefined) { 65 | // log_fn(s); 66 | // } else { 67 | // console.log(s); 68 | // } 69 | // } 70 | }; 71 | 72 | function _assemble_msg(msg, args) 73 | { 74 | if (args.length) { 75 | return msg + " // " + args.map( (a) => repr(a) ).join(' ; '); 76 | } 77 | return msg; 78 | } 79 | 80 | 81 | 82 | let _logger_instances = {}; 83 | 84 | export function getLogger(scope) 85 | { 86 | let logger = _logger_instances[scope]; 87 | if (logger == null) { // null or undefined 88 | logger = new DebugLogger(scope); 89 | _logger_instances[scope] = logger; 90 | } 91 | return logger; 92 | } 93 | 94 | export function basicConfig() 95 | { 96 | } 97 | 98 | -------------------------------------------------------------------------------- /js-transcrypt/libpatches/unique_object_id.js: -------------------------------------------------------------------------------- 1 | 2 | // thanks https://stackoverflow.com/a/43963612/1694896 3 | 4 | export var fn_unique_object_id = (() => { 5 | let currentId = 0; 6 | const map = new WeakMap(); 7 | 8 | return (object) => { 9 | if (!map.has(object)) { 10 | map.set(object, ++currentId); 11 | } 12 | 13 | return map.get(object); 14 | }; 15 | })(); 16 | -------------------------------------------------------------------------------- /js-transcrypt/my_test_script.py: -------------------------------------------------------------------------------- 1 | # some custom JS patches are necessary ... comment out these lines to run with python 2 | #import customjspatches 3 | #customjspatches.custom_apply_patches() 4 | 5 | 6 | #import pylatexenc.latexnodes as latexnodes 7 | import pylatexenc.latexnodes.parsers as parsers 8 | from pylatexenc.macrospec import LatexContextDb, MacroSpec, EnvironmentSpec, SpecialsSpec 9 | from pylatexenc.latexwalker import LatexWalker 10 | 11 | 12 | # # --- minitest --- 13 | # from pylatexenc.latexnodes import ParsingState 14 | # ps = ParsingState(s='', enable_comments=False) 15 | # from unique_object_id import fn_unique_object_id 16 | # print("Parsing state's id is = ", fn_unique_object_id(ps), "and its repr is = ", repr(ps)) 17 | # raise StopHereThatllBeAllThanks 18 | # # --- 19 | 20 | 21 | latextext = r""" 22 | Here is some text that can contain some simple LaTeX macros, to produce 23 | for instance~\textbf{bold text} and \emph{italic text}. 24 | 25 | Two line breaks start a new paragraph. You can use inline math like 26 | \(\alpha=\sum_j\beta_j\) and display equations like 27 | \begin{align} 28 | S_1 &= I\,X\,Z\,Z\,X\ ; \nonumber\\ 29 | S_2, \ldots, S_4 &= \text{cyclical permutations of \(S_1\)}\ . 30 | \label{eq:stabilizers} 31 | \end{align} 32 | 33 | Refer to equations with~\eqref{eq:stabilizers}, etc. ... 34 | 35 | Can we also parse citation commands like~\cite{Key1,Key2}. 36 | """ 37 | 38 | lw_context = LatexContextDb() 39 | lw_context.add_context_category( 40 | 'my-base-latex-category', 41 | macros=[ 42 | MacroSpec('textbf', '{',), 43 | MacroSpec('textit', '{',), 44 | MacroSpec('emph', '{',), 45 | MacroSpec('cite', '{',), 46 | MacroSpec('text', '{',), 47 | MacroSpec('label', '{',), 48 | MacroSpec('eqref', '{',), 49 | ], 50 | specials=[ 51 | SpecialsSpec('~'), 52 | # new paragraph 53 | SpecialsSpec('\n\n'), 54 | ], 55 | environments=[ 56 | EnvironmentSpec('align') 57 | ] 58 | ) 59 | 60 | # for \alpha, \, etc. 61 | lw_context.set_unknown_macro_spec( MacroSpec('','') ) 62 | 63 | 64 | 65 | lw = LatexWalker( 66 | latextext, 67 | latex_context=lw_context, 68 | tolerant_parsing=False 69 | ) 70 | 71 | nodes, carryover_info = lw.parse_content( parsers.LatexGeneralNodesParser() ) 72 | 73 | print("Got node list ->") 74 | print(nodes) 75 | -------------------------------------------------------------------------------- /js-transcrypt/mytestjscode/my_test_js_code.js: -------------------------------------------------------------------------------- 1 | // some custom JS patches are necessary ... comment out these lines to run with python 2 | import * as latexnodes from 'pylatexenc-js/pylatexenc.latexnodes.js'; 3 | import * as macrospec from 'pylatexenc-js/pylatexenc.macrospec.js'; 4 | import * as latexwalker from 'pylatexenc-js/pylatexenc.latexwalker.js'; 5 | import * as parsers from 'pylatexenc-js/pylatexenc.latexnodes.parsers.js'; 6 | 7 | // some setup code 8 | 9 | import * as customjspatches from 'pylatexenc-js/customjspatches.js'; 10 | customjspatches.custom_apply_patches(); 11 | 12 | import {__kwargtrans__, repr} from 'pylatexenc-js/org.transcrypt.__runtime__.js'; 13 | const $$kw = __kwargtrans__; 14 | 15 | 16 | 17 | const {LatexContextDb, MacroSpec, EnvironmentSpec, SpecialsSpec} = macrospec; 18 | const {LatexWalker} = latexwalker; 19 | 20 | 21 | const latextext = ` 22 | Here is some text that can contain some simple LaTeX macros, to produce 23 | for instance~\\textbf{bold text} and \\emph{italic text}. 24 | 25 | Two line breaks start a new paragraph. You can use inline math like 26 | \\(\\alpha=\\sum_j\\beta_j\\) and display equations like 27 | \\begin{align} 28 | S_1 &= I\\,X\\,Z\\,Z\\,X\\ ; \\nonumber\\\\ 29 | S_2, \\ldots, S_4 &= \\text{cyclical permutations of \\(S_1\\)}\\ . 30 | \\label{eq:stabilizers} 31 | \\end{align} 32 | 33 | Refer to equations with~\\eqref{eq:stabilizers}, etc. ... 34 | 35 | Can we also parse citation commands like~\\cite{Key1,Key2}. 36 | `; 37 | 38 | console.log('latextext = ', latextext); 39 | 40 | 41 | const lw_context = new LatexContextDb() 42 | lw_context.add_context_category( 43 | 'my-base-latex-category', 44 | $$kw({ 45 | macros: [ 46 | new MacroSpec('textbf', '{',), 47 | new MacroSpec('textit', '{',), 48 | new MacroSpec('emph', '{',), 49 | new MacroSpec('cite', '{',), 50 | new MacroSpec('text', '{',), 51 | new MacroSpec('label', '{',), 52 | new MacroSpec('eqref', '{',), 53 | ], 54 | environments: [ 55 | new EnvironmentSpec('align') 56 | ], 57 | specials: [ 58 | new SpecialsSpec('~'), 59 | // new paragraph 60 | new SpecialsSpec('\n\n'), 61 | ], 62 | }) 63 | ) 64 | 65 | // for \alpha, \, etc. 66 | lw_context.set_unknown_macro_spec( new MacroSpec('','') ) 67 | 68 | const lw = new LatexWalker( 69 | latextext, 70 | $$kw({ 71 | latex_context: lw_context, 72 | tolerant_parsing: false 73 | }) 74 | ) 75 | 76 | const [nodes, carryover_info] = lw.parse_content( new parsers.LatexGeneralNodesParser() ) 77 | 78 | console.log("Got node list ->") 79 | console.log(repr(nodes)) 80 | console.log(nodes) 81 | 82 | -------------------------------------------------------------------------------- /js-transcrypt/mytestjscode/node_modules/pylatexenc-js: -------------------------------------------------------------------------------- 1 | ../../pylatexenc-js -------------------------------------------------------------------------------- /js-transcrypt/mytestjscode/package.json: -------------------------------------------------------------------------------- 1 | {"type": "module"} 2 | -------------------------------------------------------------------------------- /js-transcrypt/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "debug": "^4.3.4" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /js-transcrypt/preprocesslib-pylatexenc.config.yaml: -------------------------------------------------------------------------------- 1 | 2 | # source directory -- will be provided by the js sources generation script 3 | source_dir: $PYLATEXENC_SRC_DIR 4 | 5 | # output directory -- will create a pylatexenc/ subfolder 6 | target_dir: $PREPROCESS_LIB_OUTPUT_DIR 7 | 8 | # which modules to preprocess 9 | module_list: 10 | - 'pylatexenc.latexnodes' 11 | - 'pylatexenc.macrospec' 12 | - 'pylatexenc.latexwalker' 13 | - 'pylatexenc.latexencode' 14 | - 'pylatexenc.latexencode.get_builtin_rules' 15 | 16 | # features 17 | enabled_features: 18 | keep_future_statements: False 19 | keep_relative_imports: False 20 | keep_super_arguments: False 21 | keep_dict_with_generator: False 22 | keep_frozenset: False 23 | keep_logger_debug: False #True # speed things up by removing logger.debug() calls 24 | guards: 25 | PYTHON2_SUPPORT_CODE: False 26 | PYLATEXENC1_LEGACY_SUPPORT_CODE: False 27 | PYLATEXENC2_LEGACY_SUPPORT_CODE: False 28 | PYLATEXENC_GET_DEFAULT_SPECS_FN: False 29 | LATEXWALKER_HELPERS: False 30 | DEBUG_SET_EQ_ATTRIBUTE: False 31 | patches: 32 | UNIQUE_OBJECT_ID: | 33 | import unique_object_id 34 | fn_unique_object_id = unique_object_id.fn_unique_object_id 35 | 36 | # Always specify u2lobj= to callables (JS will silently ignore extra args) 37 | LATEXENCODE_CALLABLE_ACCEPTS_U2LOBJ_ARG: | 38 | def _callable_accepts_u2lobj_arg(fn): 39 | return True 40 | -------------------------------------------------------------------------------- /js-transcrypt/preprocesslib-tests.config.yaml: -------------------------------------------------------------------------------- 1 | # output directory -- will create a pylatexenc/ subfolder 2 | target_dir: $PREPROCESS_LIB_OUTPUT_DIR 3 | 4 | source_dir: $PYLATEXENC_SRC_DIR 5 | 6 | # which modules to preprocess 7 | module_list: 8 | - 'pylatexenc.latexnodes' 9 | - 'pylatexenc.macrospec' 10 | - 'pylatexenc.latexwalker' 11 | - 'pylatexenc.latexencode' 12 | - 'test.*' 13 | 14 | # features 15 | enabled_features: 16 | keep_future_statements: False 17 | keep_relative_imports: False 18 | keep_super_arguments: False 19 | keep_dict_with_generator: False 20 | keep_frozenset: False 21 | keep_logger_debug: True 22 | guards: 23 | PYTHON2_SUPPORT_CODE: False 24 | PYLATEXENC1_LEGACY_SUPPORT_CODE: False 25 | PYLATEXENC2_LEGACY_SUPPORT_CODE: False 26 | PYLATEXENC_GET_DEFAULT_SPECS_FN: False 27 | LATEXWALKER_HELPERS: False 28 | DEBUG_SET_EQ_ATTRIBUTE: False 29 | TEST_PYLATEXENC_SKIP: False 30 | patches: 31 | UNIQUE_OBJECT_ID: | 32 | import unique_object_id 33 | fn_unique_object_id = unique_object_id.fn_unique_object_id 34 | 35 | # Always specify u2lobj= to callables (JS will silently ignore extra args) 36 | LATEXENCODE_CALLABLE_ACCEPTS_U2LOBJ_ARG: | 37 | def _callable_accepts_u2lobj_arg(fn): 38 | return True 39 | -------------------------------------------------------------------------------- /js-transcrypt/transcrypt_runtime_patches.js: -------------------------------------------------------------------------------- 1 | /*** PhF/PYLATEXENC - BEGIN CUSTOM PATCHES ***/ 2 | 3 | // 4 | // Patch Transcrypt's implemnetations of some builtin object methods. 5 | // 6 | String.prototype.startswith = function (prefix, start) { 7 | //console.log("Custom startswith()! prefix = ", prefix, ", start = ", start); 8 | var pos_start = (typeof start === 'undefined' ? 0 : start); 9 | if (prefix instanceof Array) { 10 | for (var i=0;i= width) { 32 | return this; 33 | } 34 | return fill_char.repeat(width - this.length) + this; 35 | }; 36 | String.prototype.rstrip = function(chars) { 37 | if (chars === undefined) { 38 | return this.replace (/\s*$/g, ''); 39 | } 40 | var s = this; 41 | while (s.length && chars.indexOf(s.slice(-1)) !== -1) { 42 | s = s.slice(0, -1); 43 | } 44 | return s; 45 | } 46 | // 47 | // Patch Transcrypt's __pop__() method which has a bug 48 | // (https://github.com/QQuick/Transcrypt/issues/827) 49 | // 50 | __pop__ = function (aKey, aDefault) { 51 | var result = this [aKey]; 52 | if (result !== undefined) { 53 | delete this [aKey]; 54 | return result; 55 | } else { 56 | if ( aDefault === undefined ) { 57 | throw KeyError (aKey, new Error()); 58 | } 59 | } 60 | return aDefault; 61 | } 62 | 63 | // 64 | // Check that a is not null, too, otherwise we get errors with "'__eq__' in a". 65 | // Also check for __eq__ in b object! 66 | // 67 | __eq__ = function (a, b) { 68 | if (typeof a == 'object' && a != null && '__eq__' in a) { 69 | return a.__eq__ (b); 70 | } else if (typeof b == 'object' && b != null && '__eq__' in b) { 71 | return b.__eq__ (a); 72 | } else { 73 | return a == b; 74 | } 75 | }; 76 | 77 | 78 | 79 | 80 | 81 | /*** PhF/PYLATEXENC - END CUSTOM PATCHES ***/ 82 | -------------------------------------------------------------------------------- /js-transcrypt/yarn.lock: -------------------------------------------------------------------------------- 1 | # This file is generated by running "yarn install" inside your project. 2 | # Manual changes might be lost - proceed with caution! 3 | 4 | __metadata: 5 | version: 6 6 | cacheKey: 8 7 | 8 | "debug@npm:^4.3.4": 9 | version: 4.3.4 10 | resolution: "debug@npm:4.3.4" 11 | dependencies: 12 | ms: 2.1.2 13 | peerDependenciesMeta: 14 | supports-color: 15 | optional: true 16 | checksum: 3dbad3f94ea64f34431a9cbf0bafb61853eda57bff2880036153438f50fb5a84f27683ba0d8e5426bf41a8c6ff03879488120cf5b3a761e77953169c0600a708 17 | languageName: node 18 | linkType: hard 19 | 20 | "ms@npm:2.1.2": 21 | version: 2.1.2 22 | resolution: "ms@npm:2.1.2" 23 | checksum: 673cdb2c3133eb050c745908d8ce632ed2c02d85640e2edb3ace856a2266a813b30c613569bf3354fdf4ea7d1a1494add3bfa95e2713baa27d0c2c71fc44f58f 24 | languageName: node 25 | linkType: hard 26 | 27 | "root-workspace-0b6124@workspace:.": 28 | version: 0.0.0-use.local 29 | resolution: "root-workspace-0b6124@workspace:." 30 | dependencies: 31 | debug: ^4.3.4 32 | languageName: unknown 33 | linkType: soft 34 | -------------------------------------------------------------------------------- /pylatexenc/__init__.py: -------------------------------------------------------------------------------- 1 | # 2 | # The MIT License (MIT) 3 | # 4 | # Copyright (c) 2015 Philippe Faist 5 | # 6 | # Permission is hereby granted, free of charge, to any person obtaining a copy 7 | # of this software and associated documentation files (the "Software"), to deal 8 | # in the Software without restriction, including without limitation the rights 9 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | # copies of the Software, and to permit persons to whom the Software is 11 | # furnished to do so, subject to the following conditions: 12 | # 13 | # The above copyright notice and this permission notice shall be included in 14 | # all copies or substantial portions of the Software. 15 | # 16 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | # THE SOFTWARE. 23 | # 24 | 25 | 26 | """ 27 | Utilities for LaTeX to/from Unicode Text conversion. 28 | 29 | Main Site: 30 | 31 | https://github.com/phfaist/pylatexenc/ 32 | 33 | """ 34 | 35 | from .version import version_str as _version_str 36 | 37 | __version__ = _version_str 38 | 39 | -------------------------------------------------------------------------------- /pylatexenc/_util.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # The MIT License (MIT) 4 | # 5 | # Copyright (c) 2019 Philippe Faist 6 | # 7 | # Permission is hereby granted, free of charge, to any person obtaining a copy 8 | # of this software and associated documentation files (the "Software"), to deal 9 | # in the Software without restriction, including without limitation the rights 10 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | # copies of the Software, and to permit persons to whom the Software is 12 | # furnished to do so, subject to the following conditions: 13 | # 14 | # The above copyright notice and this permission notice shall be included in 15 | # all copies or substantial portions of the Software. 16 | # 17 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 | # THE SOFTWARE. 24 | # 25 | 26 | 27 | # Internal module. Internal API may move, disappear or otherwise change at any 28 | # time and without notice. 29 | 30 | 31 | import bisect 32 | bisect_right = bisect.bisect_right 33 | 34 | 35 | 36 | # ------------------------------------------------------------------------------ 37 | 38 | 39 | 40 | class LineNumbersCalculator(object): 41 | r""" 42 | Utility to calculate line numbers. 43 | """ 44 | def __init__(self, s, 45 | line_number_offset=1, first_line_column_offset=0, column_offset=0): 46 | super(LineNumbersCalculator, self).__init__() 47 | 48 | self.line_number_offset = line_number_offset 49 | self.first_line_column_offset = first_line_column_offset 50 | self.column_offset = column_offset 51 | 52 | def find_all_new_lines(x): 53 | # first line starts at the beginning of the string 54 | yield 0 55 | k = 0 56 | while k < len(x): 57 | k = x.find('\n', k) 58 | if k == -1: 59 | return 60 | k += 1 61 | # s[k] is the character after the newline, i.e., the 0-th column 62 | # of the new line 63 | yield k 64 | 65 | self._pos_new_lines = list(find_all_new_lines(s)) 66 | 67 | 68 | def pos_to_lineno_colno(self, pos, as_dict=False): 69 | r""" 70 | Return the line and column number corresponding to the given `pos`. 71 | 72 | Return a tuple `(lineno, colno)` giving line number and column number. 73 | Line numbers start at 1 and column number start at zero, i.e., the 74 | beginning of the document (`pos=0`) has line and column number `(1,0)`. 75 | If `as_dict=True`, then a dictionary with keys 'lineno', 'colno' is 76 | returned instead of a tuple. 77 | """ 78 | 79 | if pos is None: 80 | if as_dict: 81 | return {'lineno': None, 'colno': None} 82 | return (None, None) 83 | 84 | # find line number in list 85 | 86 | # line_no is the index of the last item in self._pos_new_lines that is <= pos. 87 | line_no = bisect_right(self._pos_new_lines, pos)-1 88 | assert line_no >= 0 and line_no < len(self._pos_new_lines) 89 | 90 | col_no = pos - self._pos_new_lines[line_no] 91 | 92 | if line_no == 0: 93 | col_no += self.first_line_column_offset 94 | else: 95 | col_no += self.column_offset 96 | line_no += self.line_number_offset 97 | 98 | if as_dict: 99 | return {'lineno': line_no, 'colno': col_no} 100 | return (line_no, col_no) 101 | 102 | 103 | 104 | # ------------------------------------------------------------------------------ 105 | 106 | 107 | class PushPropOverride(object): 108 | def __init__(self, obj, propname, new_value): 109 | super(PushPropOverride, self).__init__() 110 | self.obj = obj 111 | self.propname = propname 112 | self.new_value = new_value 113 | 114 | def __enter__(self): 115 | if self.new_value is not None: 116 | self.initval = getattr(self.obj, self.propname) 117 | setattr(self.obj, self.propname, self.new_value) 118 | return self 119 | 120 | def __exit__(self, type, value, traceback): 121 | # clean-up 122 | if self.new_value is not None: 123 | setattr(self.obj, self.propname, self.initval) 124 | 125 | 126 | # ------------------------------------------------------------------------------ 127 | 128 | 129 | try: 130 | from collections import ChainMap 131 | except ImportError: 132 | pass 133 | ### BEGIN_PYTHON2_SUPPORT_CODE 134 | from chainmap import ChainMap 135 | ### END_PYTHON2_SUPPORT_CODE 136 | 137 | 138 | 139 | # ------------------------------------------------------------------------------ 140 | 141 | 142 | 143 | pylatexenc_deprecated_ver = lambda *args: None #lgtm [py/multiple-definition] 144 | pylatexenc_deprecated_2 = lambda *args: None #lgtm [py/multiple-definition] 145 | pylatexenc_deprecated_3 = lambda *args: None #lgtm [py/multiple-definition] 146 | LazyDict = None #lgtm [py/multiple-definition] 147 | 148 | ### BEGIN_PYLATEXENC2_LEGACY_SUPPORT_CODE 149 | 150 | from ._util_support import ( # lgtm [py/unused-import] 151 | pylatexenc_deprecated_ver, 152 | pylatexenc_deprecated_2, 153 | pylatexenc_deprecated_3, 154 | # 155 | LazyDict 156 | ) 157 | 158 | ### END_PYLATEXENC2_LEGACY_SUPPORT_CODE 159 | -------------------------------------------------------------------------------- /pylatexenc/_util_support.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # The MIT License (MIT) 4 | # 5 | # Copyright (c) 2019 Philippe Faist 6 | # 7 | # Permission is hereby granted, free of charge, to any person obtaining a copy 8 | # of this software and associated documentation files (the "Software"), to deal 9 | # in the Software without restriction, including without limitation the rights 10 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | # copies of the Software, and to permit persons to whom the Software is 12 | # furnished to do so, subject to the following conditions: 13 | # 14 | # The above copyright notice and this permission notice shall be included in 15 | # all copies or substantial portions of the Software. 16 | # 17 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 | # THE SOFTWARE. 24 | # 25 | 26 | 27 | # Internal module. Internal API may move, disappear or otherwise change at any 28 | # time and without notice. 29 | 30 | from __future__ import print_function, unicode_literals 31 | 32 | 33 | try: 34 | # Python >= 3.3 35 | from collections.abc import MutableMapping 36 | except ImportError: 37 | from collections import MutableMapping 38 | 39 | import warnings 40 | 41 | 42 | 43 | 44 | def pylatexenc_deprecated_ver(ver, msg, stacklevel=2): 45 | warnings.warn( 46 | "Deprecated (pylatexenc {}): {} ".format(ver, msg.strip()), 47 | DeprecationWarning, 48 | stacklevel=stacklevel+1 49 | ) 50 | 51 | 52 | def pylatexenc_deprecated_2(msg, stacklevel=2): 53 | warnings.warn( 54 | ( "Deprecated (pylatexenc 2.0): {} " 55 | "[see https://pylatexenc.readthedocs.io/en/latest/new-in-pylatexenc-2/]" ) 56 | .format(msg.strip()), 57 | DeprecationWarning, 58 | stacklevel=stacklevel+1 59 | ) 60 | 61 | def pylatexenc_deprecated_3(msg, stacklevel=2): 62 | warnings.warn( 63 | ( "Deprecated (pylatexenc 3.0): {} " 64 | "[see https://pylatexenc.readthedocs.io/en/latest/new-in-pylatexenc-3/]" ) 65 | .format(msg.strip()), 66 | DeprecationWarning, 67 | stacklevel=stacklevel+1 68 | ) 69 | 70 | 71 | 72 | # ------------------------------------------------------------------------------ 73 | 74 | 75 | class LazyDict(MutableMapping): 76 | r""" 77 | A lazy dictionary that loads its data when it is first queried. 78 | 79 | This is used to store the legacy 80 | :py:data:`pylatexenc.latexwalker.default_macro_dict` as well as 81 | :py:data:`pylatexenc.latex2text.default_macro_dict` etc. Such that these 82 | "dictionaries" are still exposed at the module-level, but the data is loaded 83 | only if they are actually queried. 84 | """ 85 | def __init__(self, generate_dict_fn): 86 | self._full_dict = None 87 | self._generate_dict_fn = generate_dict_fn 88 | 89 | def _ensure_instance(self): 90 | if self._full_dict is not None: 91 | return 92 | self._full_dict = self._generate_dict_fn() 93 | 94 | def __getitem__(self, key): 95 | self._ensure_instance() 96 | return self._full_dict.__getitem__(key) 97 | 98 | def __setitem__(self, key, val): 99 | self._ensure_instance() 100 | return self._full_dict.__setitem__(key, val) 101 | 102 | def __delitem__(self, key): 103 | self._ensure_instance() 104 | return self._full_dict.__delitem__(key) 105 | 106 | def __iter__(self): 107 | self._ensure_instance() 108 | return iter(self._full_dict) 109 | 110 | def __len__(self): 111 | self._ensure_instance() 112 | return len(self._full_dict) 113 | 114 | def copy(self): 115 | self._ensure_instance() 116 | return self._full_dict.copy() 117 | 118 | def clear(self): 119 | self._ensure_instance() 120 | return self._full_dict.clear() 121 | 122 | 123 | 124 | -------------------------------------------------------------------------------- /pylatexenc/latex2text/_inputlatexfile.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # The MIT License (MIT) 4 | # 5 | # Copyright (c) 2021 Philippe Faist 6 | # 7 | # Permission is hereby granted, free of charge, to any person obtaining a copy 8 | # of this software and associated documentation files (the "Software"), to deal 9 | # in the Software without restriction, including without limitation the rights 10 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | # copies of the Software, and to permit persons to whom the Software is 12 | # furnished to do so, subject to the following conditions: 13 | # 14 | # The above copyright notice and this permission notice shall be included in 15 | # all copies or substantial portions of the Software. 16 | # 17 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 | # THE SOFTWARE. 24 | # 25 | 26 | 27 | # Internal module. Internal API may move, disappear or otherwise change at any 28 | # time and without notice. 29 | 30 | from __future__ import print_function, unicode_literals 31 | 32 | import os.path 33 | 34 | import logging 35 | logger = logging.getLogger(__name__) 36 | 37 | 38 | def read_latex_file(tex_input_directory, strict_input, fn): 39 | 40 | fnfull = os.path.realpath(os.path.join(tex_input_directory, fn)) 41 | if strict_input: 42 | # make sure that the input file is strictly within dirfull, and 43 | # didn't escape with '../..' tricks or via symlinks. 44 | dirfull = os.path.realpath(tex_input_directory) 45 | if not fnfull.startswith(dirfull): 46 | logger.warning( 47 | "Can't access path '%s' leading outside of mandated directory " 48 | "[strict input mode]", 49 | fn 50 | ) 51 | return '' 52 | 53 | if not os.path.exists(fnfull) and os.path.exists(fnfull + '.tex'): 54 | fnfull = fnfull + '.tex' 55 | if not os.path.exists(fnfull) and os.path.exists(fnfull + '.latex'): 56 | fnfull = fnfull + '.latex' 57 | if not os.path.isfile(fnfull): 58 | logger.warning("Error, file doesn't exist: '%s'", fn) 59 | return '' 60 | 61 | logger.debug("Reading input file %r", fnfull) 62 | 63 | try: 64 | with open(fnfull) as f: 65 | return f.read() 66 | except IOError as e: 67 | logger.warning("Error, can't access '%s': %s", fn, e) 68 | return '' 69 | -------------------------------------------------------------------------------- /pylatexenc/latexencode/__main__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # The MIT License (MIT) 4 | # 5 | # Copyright (c) 2019 Philippe Faist 6 | # 7 | # Permission is hereby granted, free of charge, to any person obtaining a copy 8 | # of this software and associated documentation files (the "Software"), to deal 9 | # in the Software without restriction, including without limitation the rights 10 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | # copies of the Software, and to permit persons to whom the Software is 12 | # furnished to do so, subject to the following conditions: 13 | # 14 | # The above copyright notice and this permission notice shall be included in 15 | # all copies or substantial portions of the Software. 16 | # 17 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 | # THE SOFTWARE. 24 | # 25 | 26 | 27 | import sys 28 | import fileinput 29 | import argparse 30 | import logging 31 | 32 | 33 | from ..latexencode import unicode_to_latex 34 | from ..version import version_str 35 | 36 | 37 | 38 | def main(argv=None): 39 | 40 | if argv is None: 41 | argv = sys.argv[1:] 42 | 43 | parser = argparse.ArgumentParser(prog='latexencode', add_help=False) 44 | parser.add_argument('files', metavar="FILE", nargs='*', 45 | help='Input files (if none specified, read from stdandard input)') 46 | 47 | parser.add_argument('--non-ascii-only', action='store_const', const=True, 48 | dest='non_ascii_only', default=False) 49 | parser.add_argument('--no-non-ascii-only', action='store_const', const=False, 50 | dest='non_ascii_only', 51 | help="The option --non-ascii-only specifies that only non-ascii characters " 52 | "are to be encoded into LaTeX sequences, and not characters like '$' " 53 | "even though they might have a special LaTeX meaning.") 54 | 55 | parser.add_argument('--replacement-latex-protection', 56 | choices=('braces', 'braces-all', 'braces-almost-all', 'braces-after-macro', 57 | 'none'), 58 | dest='replacement_latex_protection', default='braces', 59 | help=r"How to protect replacement latex code from producing invalid latex code " 60 | r"when concatenated in a longer string. One of 'braces', 'braces-all', " 61 | r"'braces-almost-all', 'braces-after-macro', 'none'. Example: using " 62 | r"choice 'braces' we avoid the invalid replacement 'a→b' -> 'a\tob' " 63 | r"with instead 'a{\to}b'.") 64 | 65 | parser.add_argument('--unknown-char-policy', 66 | choices=('keep', 'replace', 'ignore', 'fail'), 67 | dest='unknown_char_policy', default='keep', 68 | help="How to deal with nonascii characters with no known latex code equivalent.") 69 | 70 | parser.add_argument('-q', '--quiet', dest='logging_level', action='store_const', 71 | const=logging.ERROR, default=logging.INFO, 72 | help="Suppress warning messages") 73 | parser.add_argument('--version', action='version', 74 | version='pylatexenc {}'.format(version_str), 75 | help="Show version information and exit") 76 | parser.add_argument('--help', action='help', 77 | help="Show this help information and exit") 78 | 79 | args = parser.parse_args(argv) 80 | 81 | logging.basicConfig() 82 | logging.getLogger().setLevel(args.logging_level) 83 | 84 | latex = '' 85 | for line in fileinput.input(files=args.files): 86 | latex += line 87 | 88 | result = unicode_to_latex( 89 | latex, 90 | non_ascii_only=args.non_ascii_only, 91 | replacement_latex_protection=args.replacement_latex_protection, 92 | unknown_char_policy=args.unknown_char_policy 93 | ) 94 | 95 | sys.stdout.write(result) 96 | 97 | 98 | def run_main(): 99 | try: 100 | 101 | main() 102 | 103 | except SystemExit: 104 | raise 105 | except: # lgtm [py/catch-base-exception] 106 | import pdb 107 | import traceback 108 | traceback.print_exc() 109 | pdb.post_mortem() 110 | 111 | 112 | if __name__ == '__main__': 113 | 114 | # run_main() ## DEBUG 115 | main() 116 | -------------------------------------------------------------------------------- /pylatexenc/latexencode/_partial_latex_encoder.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # The MIT License (MIT) 4 | # 5 | # Copyright (c) 2021 Philippe Faist 6 | # 7 | # Permission is hereby granted, free of charge, to any person obtaining a copy 8 | # of this software and associated documentation files (the "Software"), to deal 9 | # in the Software without restriction, including without limitation the rights 10 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | # copies of the Software, and to permit persons to whom the Software is 12 | # furnished to do so, subject to the following conditions: 13 | # 14 | # The above copyright notice and this permission notice shall be included in 15 | # all copies or substantial portions of the Software. 16 | # 17 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 | # THE SOFTWARE. 24 | # 25 | 26 | from __future__ import print_function, absolute_import, unicode_literals 27 | 28 | #import sys 29 | import logging 30 | 31 | logger = logging.getLogger(__name__) 32 | 33 | 34 | from ._unicode_to_latex_encoder import ( 35 | RULE_CALLABLE, 36 | UnicodeToLatexConversionRule, 37 | UnicodeToLatexEncoder 38 | ) 39 | 40 | 41 | from ..latexwalker import _walker 42 | 43 | 44 | class PartialLatexToLatexEncoder(UnicodeToLatexEncoder): 45 | r""" 46 | Encode a string while preserving some (fuzzily detected) LaTeX constructs 47 | that the input string already has (e.g. accent macros or inline math modes). 48 | 49 | Sometimes you need to fully LaTeX-encode a string that already has some 50 | LaTeX constructs. For instance, titles of bibliographic entries might 51 | include some inline math or accents, but they might also include unicode 52 | characters that need to be encoded. Using a 53 | :py:class:`UnicodeToLatexEncoder` on such strings would result in ugly 54 | doubly-escaped strings such as ``\textbackslash{}'\{e\}``. Instead, 55 | constructs such as ``\'{e}`` should be preserved while other characters 56 | and/or constructs (say '&' or '%') as well as unicode characters should be 57 | encoded. 58 | 59 | This class offers a simple partial solution: Characters are encoded as per 60 | the given `conversion_rules` (or the default conversion rules of 61 | :py:class:`UnicodeToLatexEncoder` objects), except that the characters in 62 | `keep_latex_chars` are to be interpreted as LaTeX and are not to be further 63 | encoded. 64 | 65 | .. versionadded: 2.10 66 | """ 67 | def __init__(self, 68 | # keyword arguments: 69 | keep_latex_chars=r'\${}^_', 70 | conversion_rules=None, 71 | **kwargs): 72 | 73 | base_conversion_rules = conversion_rules 74 | if base_conversion_rules is None: 75 | base_conversion_rules = ['defaults'] 76 | 77 | super(PartialLatexToLatexEncoder, self).__init__( 78 | # only a single rule, our own special method that tries to parse 79 | # partial latex. 80 | conversion_rules=[UnicodeToLatexConversionRule( 81 | rule_type=RULE_CALLABLE, 82 | rule=self._do_partial_latex_encode_step, 83 | replacement_latex_protection='none' 84 | )] + base_conversion_rules, 85 | **kwargs 86 | ) 87 | 88 | self.keep_latex_chars = keep_latex_chars 89 | 90 | 91 | def _do_partial_latex_encode_step(self, s, pos): 92 | r""" 93 | This method is used as a "callable rule" for the 94 | :py:class:`UnicodeToLatexEncoder` object. 95 | 96 | The strategy is to see if we have something that looks like a LaTeX char 97 | we want to keep. If so, keep it as is; if not, return `None` so that 98 | further rules can be considered by the base unicode encoder. 99 | """ 100 | 101 | if s[pos] in self.keep_latex_chars: 102 | # Read a token and if it is a macro, keep the full macro! 103 | lw = _walker.LatexWalker(s, tolerant_parsing=False) 104 | ps = lw.make_parsing_state() 105 | tok = lw.make_token_reader(pos=pos).peek_token(parsing_state=ps) 106 | 107 | tok_as_latex = tok.pre_space + s[tok.pos : tok.pos+tok.len] 108 | 109 | # keep the LaTeX token as-is 110 | return (tok.pos+tok.len - pos, tok_as_latex) 111 | 112 | return None 113 | -------------------------------------------------------------------------------- /pylatexenc/latexencode/get_builtin_rules.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # The MIT License (MIT) 4 | # 5 | # Copyright (c) 2023 Philippe Faist 6 | # 7 | # Permission is hereby granted, free of charge, to any person obtaining a copy 8 | # of this software and associated documentation files (the "Software"), to deal 9 | # in the Software without restriction, including without limitation the rights 10 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | # copies of the Software, and to permit persons to whom the Software is 12 | # furnished to do so, subject to the following conditions: 13 | # 14 | # The above copyright notice and this permission notice shall be included in 15 | # all copies or substantial portions of the Software. 16 | # 17 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 | # THE SOFTWARE. 24 | # 25 | 26 | 27 | 28 | # Internal module. Internal API may move, disappear or otherwise change at any 29 | # time and without notice. 30 | 31 | from __future__ import print_function, unicode_literals 32 | 33 | 34 | _MappingProxyType = dict 35 | #__pragma__('skip') 36 | import sys 37 | if sys.version_info.major > 2: 38 | from types import MappingProxyType as _MappingProxyType 39 | else: 40 | _MappingProxyType = dict 41 | #__pragma__('noskip') 42 | 43 | 44 | from ._rule import ( 45 | RULE_DICT, 46 | RULE_REGEX, 47 | RULE_CALLABLE, 48 | UnicodeToLatexConversionRule, 49 | ) 50 | 51 | 52 | 53 | 54 | def get_builtin_uni2latex_dict(): 55 | r""" 56 | Return a dictionary that contains the default collection of known LaTeX 57 | escape sequences for unicode characters. 58 | 59 | The keys of the dictionary are integers that correspond to unicode code 60 | points (i.e., `ord(char)`). The values are the corresponding LaTeX 61 | replacement strings. 62 | 63 | The returned dictionary may not be modified. To alter the behavior of 64 | :py:func:`unicode_to_latex()`, you should specify custom rules to a new 65 | instance of :py:class:`UnicodeToLatexEncoder`. 66 | 67 | .. versionadded:: 2.0 68 | 69 | This function was introduced in `pylatexenc 2.0`. 70 | """ 71 | 72 | from ._uni2latexmap import uni2latex as _uni2latex 73 | return _MappingProxyType(_uni2latex) 74 | 75 | 76 | def get_builtin_conversion_rules(builtin_name): 77 | r""" 78 | Return a built-in set of conversion rules specified by a given name 79 | `builtin_name`. 80 | 81 | There are two builtin conversion rules, with the following names: 82 | 83 | - `'defaults'`: the default conversion rules, a custom-curated list of 84 | unicode chars to LaTeX escapes. 85 | 86 | - `'unicode-xml'`: the conversion rules derived from the `unicode.xml` file 87 | maintained at https://www.w3.org/TR/xml-entity-names/#source by David 88 | Carlisle. 89 | 90 | The return value is a list of :py:class:`UnicodeToLatexConversionRule` 91 | objects that can be either directly specified to the `conversion_rules=` 92 | argument of :py:class:`UnicodeToLatexEncoder`, or included in a larger list 93 | that can be provided to that argument. 94 | 95 | .. versionadded:: 2.0 96 | 97 | This function was introduced in `pylatexenc 2.0`. 98 | """ 99 | if builtin_name == 'defaults': 100 | return [ UnicodeToLatexConversionRule(rule_type=RULE_DICT, 101 | rule=get_builtin_uni2latex_dict()) ] 102 | 103 | if builtin_name == 'unicode-xml': 104 | from . import _uni2latexmap_xml 105 | return [ UnicodeToLatexConversionRule(rule_type=RULE_DICT, 106 | rule=_uni2latexmap_xml.uni2latex) ] 107 | 108 | raise ValueError("Unknown builtin rule set: {}".format(builtin_name)) 109 | 110 | -------------------------------------------------------------------------------- /pylatexenc/latexnodes/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # The MIT License (MIT) 4 | # 5 | # Copyright (c) 2022 Philippe Faist 6 | # 7 | # Permission is hereby granted, free of charge, to any person obtaining a copy 8 | # of this software and associated documentation files (the "Software"), to deal 9 | # in the Software without restriction, including without limitation the rights 10 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | # copies of the Software, and to permit persons to whom the Software is 12 | # furnished to do so, subject to the following conditions: 13 | # 14 | # The above copyright notice and this permission notice shall be included in 15 | # all copies or substantial portions of the Software. 16 | # 17 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 | # THE SOFTWARE. 24 | # 25 | 26 | r""" 27 | .. versionadded:: 3.0 28 | 29 | The `latexnodes` module was introduced in `pylatexenc` 3. 30 | 31 | """ 32 | 33 | 34 | from ._exctypes import * 35 | 36 | from ._token import LatexToken 37 | 38 | from ._nodescollector import ( 39 | LatexNodesCollector 40 | ) 41 | 42 | from ._parsingstate import ( 43 | ParsingState 44 | ) 45 | 46 | from ._parsingstatedelta import ( 47 | ParsingStateDelta, 48 | ParsingStateDeltaReplaceParsingState, 49 | ParsingStateDeltaChained, 50 | ParsingStateDeltaWalkerEvent, 51 | ParsingStateDeltaEnterMathMode, 52 | ParsingStateDeltaLeaveMathMode, 53 | get_updated_parsing_state_from_delta, 54 | ) 55 | from ._parsedargs import ( 56 | LatexArgumentSpec, 57 | ParsedArguments, 58 | ) 59 | 60 | from ._tokenreaderbase import ( 61 | LatexTokenReaderBase, 62 | LatexTokenListTokenReader, 63 | ) 64 | from ._tokenreader import ( 65 | LatexTokenReader, 66 | ) 67 | 68 | from ._callablespecbase import ( 69 | CallableSpecBase 70 | ) 71 | 72 | from ._walkerbase import ( 73 | LatexWalkerParsingStateEventHandler, 74 | LatexWalkerBase, 75 | ) 76 | 77 | from ._latexcontextdbbase import ( 78 | LatexContextDbBase 79 | ) 80 | 81 | from ._parsedargsinfo import ( 82 | ParsedArgumentsInfo, 83 | SingleParsedArgumentInfo, 84 | ) 85 | 86 | from ._latex_recomposer import ( 87 | LatexNodesLatexRecomposer 88 | ) 89 | -------------------------------------------------------------------------------- /pylatexenc/latexnodes/_callablespecbase.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # The MIT License (MIT) 4 | # 5 | # Copyright (c) 2022 Philippe Faist 6 | # 7 | # Permission is hereby granted, free of charge, to any person obtaining a copy 8 | # of this software and associated documentation files (the "Software"), to deal 9 | # in the Software without restriction, including without limitation the rights 10 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | # copies of the Software, and to permit persons to whom the Software is 12 | # furnished to do so, subject to the following conditions: 13 | # 14 | # The above copyright notice and this permission notice shall be included in 15 | # all copies or substantial portions of the Software. 16 | # 17 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 | # THE SOFTWARE. 24 | # 25 | 26 | 27 | # Internal module. Internal API may move, disappear or otherwise change at any 28 | # time and without notice. 29 | 30 | from __future__ import print_function, unicode_literals 31 | 32 | 33 | 34 | class CallableSpecBase(object): 35 | r""" 36 | The base class for macro, environment, and specials spec classes (see the 37 | :py:mod:`pylatexenc.macrospec` module). 38 | 39 | As far as this :py:mod:`latexnodes` module's classes are concerned, a spec 40 | object is simply something that can provide a parser to parse the given 41 | construct (macro, environment, or specials). 42 | 43 | The spec object should implement :py:meth:`get_node_parser()`, and it should 44 | return a parser instance that can be used to parse the entire construct. 45 | 46 | See :py:class:`macrospec.MacroSpec` for how this is implemented in the 47 | :py:mod:`pylatexenc.macrospec` module. 48 | 49 | .. versionadded:: 3.0 50 | 51 | The :py:class:`CallableSpecBase` class was added in `pylatexenc 3.0`. 52 | """ 53 | 54 | def get_node_parser(self, token): 55 | raise RuntimeError("Subclasses must reimplement get_node_parser()") 56 | -------------------------------------------------------------------------------- /pylatexenc/latexnodes/_latexcontextdbbase.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # The MIT License (MIT) 4 | # 5 | # Copyright (c) 2022 Philippe Faist 6 | # 7 | # Permission is hereby granted, free of charge, to any person obtaining a copy 8 | # of this software and associated documentation files (the "Software"), to deal 9 | # in the Software without restriction, including without limitation the rights 10 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | # copies of the Software, and to permit persons to whom the Software is 12 | # furnished to do so, subject to the following conditions: 13 | # 14 | # The above copyright notice and this permission notice shall be included in 15 | # all copies or substantial portions of the Software. 16 | # 17 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 | # THE SOFTWARE. 24 | # 25 | 26 | 27 | # Internal module. Internal API may move, disappear or otherwise change at any 28 | # time and without notice. 29 | 30 | from __future__ import print_function, unicode_literals 31 | 32 | 33 | class LatexContextDbBase(object): 34 | r""" 35 | Base class for a parsing state's LaTeX context database. 36 | 37 | A full implementation of how to specify macro, environment, and specials 38 | definitions are actually in the :py:mod:`pylatexenc.macrospec` module. As 39 | far as this :py:mod:`latexnodes` is concerned, a latex context database 40 | object is simply an object that provides the :py:meth:`get_***_spec()` 41 | family of methods along with :py:meth:`test_for_specials()`, and they return 42 | relevant spec objects. 43 | 44 | The spec objects returned by :py:meth:`get_***_spec()` and 45 | :py:meth:`test_for_specials()` are subclasses of 46 | :py:class:`CallableSpecBase`. 47 | 48 | 49 | .. versionadded:: 3.0 50 | 51 | The :py:class:`LatexContextDbBase` class was added in `pylatexenc 3.0`. 52 | """ 53 | 54 | def get_macro_spec(self, macroname): 55 | r""" 56 | Return the macro spec to use to parse a macro named `macroname`. The 57 | `macroname` does not contain the escape character (``\``) itself. 58 | 59 | This method should return the relevant spec object, which should be an 60 | instance of a subclass of :py:class:`CallableSpecBase`. 61 | 62 | The latex context database object may choose to provide a default spec 63 | object if `macroname` wasn't formally defined. As far as the parsers 64 | are concerned, if `get_macro_spec()` returns a spec object, then the 65 | parsers know how to parse the given macro and will happily proceed. 66 | 67 | If a macro of name `macroname` should not be considered as defined, and 68 | the parser should not attempt to parse a macro and raise an error 69 | instead (or recover from it in tolerant parsing mode), then this method 70 | should return `None`. 71 | """ 72 | return None 73 | 74 | def get_environment_spec(self, environmentname): 75 | r""" 76 | Like :py:meth:`get_macro_spec()`, but for environments. The 77 | `environmentname` is the name of the environment specified between the 78 | curly braces after the ``\begin`` call. 79 | 80 | This method should return the relevant spec object, which should be an 81 | instance of a subclass of :py:class:`CallableSpecBase`. 82 | 83 | The latex context database object may choose to provide a default spec 84 | object if an environment named `environmentname` wasn't somehow formally 85 | defined. As far as the parsers are concerned, if 86 | `get_environment_spec()` returns a spec object, then the parsers know 87 | how to parse the given environment and will happily proceed. 88 | 89 | If an environment of name `environmentname` should not be considered as 90 | defined, and the parser should not attempt to parse the environment and 91 | raise an error instead (or recover from it in tolerant parsing mode), 92 | then this method should return `None`. 93 | """ 94 | return None 95 | 96 | def get_specials_spec(self, specials_chars): 97 | r""" 98 | Like :py:meth:`get_macro_spec()`, but for specials. The `specials_chars` is 99 | the sequence of characters for which we'd like to find if they are a 100 | specials construct. 101 | 102 | Parsing of specials is different from macros and environments, because 103 | there is no universal syntax that distinguishes them (macros and 104 | environments are always initiated with the escape character ``\``). So 105 | the token reader will call :py:meth:`test_for_specials()` to see if the 106 | string at the given position can be matched for specials. 107 | 108 | The result is that :py:meth:`get_specials_spec()` usually doesn't get 109 | called when parsing tokens. The :py:meth:`get_specials_spec()` method 110 | is only called in certain specific situations, such as to get the spec 111 | object associated with the new paragraph token ``\n\n``. 112 | 113 | This method should return the relevant spec object, which should be an 114 | instance of a subclass of :py:class:`CallableSpecBase`, or `None` if 115 | these characters are not to be considered as specials. 116 | """ 117 | return None 118 | 119 | def test_for_specials(self, s, pos, parsing_state): 120 | r""" 121 | Test the string `s` at position `pos` for the presence of specials. 122 | 123 | For instance, if the parser tests the string ``"Eq.~\eqref{eq:xyz}"`` at 124 | position 3, then the latex context database might want to report the 125 | character ``~`` as a specials construct and return a specials spec for 126 | it. 127 | 128 | If specials characters are recognized, then this method should return a 129 | corresponding spec object. The spec object should be an instance of a 130 | :py:class:`CallableSpecBase` subclass. In addition, the returned spec 131 | object must expose the attribute :py:attr:`specials_chars`. That 132 | attribute should contain the sequence of characters that were recognized 133 | as special. 134 | 135 | If no specials characters are recongized at exactly the position `pos`, 136 | then this method should return `None`. 137 | """ 138 | return None 139 | -------------------------------------------------------------------------------- /pylatexenc/latexnodes/_walkerbase.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # The MIT License (MIT) 4 | # 5 | # Copyright (c) 2021 Philippe Faist 6 | # 7 | # Permission is hereby granted, free of charge, to any person obtaining a copy 8 | # of this software and associated documentation files (the "Software"), to deal 9 | # in the Software without restriction, including without limitation the rights 10 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | # copies of the Software, and to permit persons to whom the Software is 12 | # furnished to do so, subject to the following conditions: 13 | # 14 | # The above copyright notice and this permission notice shall be included in 15 | # all copies or substantial portions of the Software. 16 | # 17 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 | # THE SOFTWARE. 24 | # 25 | 26 | 27 | # Internal module. Internal API may move, disappear or otherwise change at any 28 | # time and without notice. 29 | 30 | from __future__ import print_function, unicode_literals 31 | 32 | from ._parsingstatedelta import ParsingStateDelta 33 | 34 | 35 | 36 | class LatexWalkerParsingStateEventHandler(object): 37 | r""" 38 | A LatexWalker parsing state event handler. 39 | 40 | The LatexWalker instance will call methods on this object to determine how 41 | to update the parsing state upon certain events, such as entering or exiting 42 | math mode. 43 | 44 | Events: 45 | 46 | - enter math mode 47 | 48 | - exit math mode 49 | 50 | .. versionadded:: 3.0 51 | 52 | The :py:class:`LatexWalkerParsingStateEventHandler` class was added in 53 | `pylatexenc 3.0`. 54 | """ 55 | 56 | def enter_math_mode(self, math_mode_delimiter=None, trigger_token=None): 57 | return ParsingStateDelta( 58 | set_attributes=dict( 59 | in_math_mode=True, 60 | math_mode_delimiter=math_mode_delimiter 61 | ) 62 | ) 63 | 64 | def leave_math_mode(self, trigger_token=None): 65 | return ParsingStateDelta( 66 | set_attributes=dict( 67 | in_math_mode=False, 68 | math_mode_delimiter=None 69 | ) 70 | ) 71 | 72 | 73 | _default_parsing_state_event_handler = LatexWalkerParsingStateEventHandler() 74 | 75 | 76 | class LatexWalkerBase(object): 77 | r""" 78 | Base class for a latex-walker. Essentially, this is all that the 79 | classes and methods in the :py:mod:`latexnodes` module need to know about 80 | what a LatexWalker does. 81 | 82 | See also :py:class:`latexwalker.LatexWalker`. 83 | 84 | .. versionadded:: 3.0 85 | 86 | The :py:class:`LatexWalkerBase` class was added in `pylatexenc 3.0`. 87 | """ 88 | 89 | def parsing_state_event_handler(self): 90 | r""" 91 | Doc...... 92 | """ 93 | return _default_parsing_state_event_handler 94 | 95 | def parse_content(self, parser, token_reader=None, parsing_state=None, 96 | open_context=None, **kwargs): 97 | r""" 98 | Doc...... 99 | """ 100 | raise RuntimeError("LatexWalkerBase subclasses must reimplement parse_content()") 101 | 102 | def make_node(self, node_class, **kwargs): 103 | r""" 104 | Doc...... 105 | """ 106 | raise RuntimeError("LatexWalkerBase subclasses must reimplement make_node()") 107 | 108 | def make_nodelist(self, nodelist, **kwargs): 109 | r""" 110 | Doc...... 111 | """ 112 | raise RuntimeError("LatexWalkerBase subclasses must reimplement make_nodelist()") 113 | 114 | def make_nodes_collector(self, 115 | token_reader, 116 | parsing_state, 117 | **kwargs): 118 | r""" 119 | Doc...... 120 | """ 121 | raise RuntimeError( 122 | "LatexWalkerBase subclasses must reimplement make_nodes_collector()") 123 | 124 | def make_latex_group_parser(self, delimiters): 125 | r""" 126 | Doc...... 127 | """ 128 | raise RuntimeError( 129 | "LatexWalkerBase subclasses must reimplement make_latex_group_parser()") 130 | 131 | def make_latex_math_parser(self, math_mode_delimiters): 132 | r""" 133 | Doc...... 134 | """ 135 | raise RuntimeError( 136 | "LatexWalkerBase subclasses must reimplement make_latex_math_parser()") 137 | 138 | 139 | def check_tolerant_parsing_ignore_error(self, exc): 140 | r""" 141 | You can inspect the exception object `exc` and decide whether or not to 142 | attempt to recover from the exception (if you want to be tolerant to 143 | parsing errors). 144 | 145 | Return the exception object if it should be raised, or return None if 146 | recovery should be attempted. 147 | """ 148 | return exc 149 | 150 | def format_node_pos(self, node): 151 | r""" 152 | Doc...... 153 | """ 154 | return 'character position '+repr(node.pos) 155 | -------------------------------------------------------------------------------- /pylatexenc/latexnodes/parsers/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # The MIT License (MIT) 4 | # 5 | # Copyright (c) 2022 Philippe Faist 6 | # 7 | # Permission is hereby granted, free of charge, to any person obtaining a copy 8 | # of this software and associated documentation files (the "Software"), to deal 9 | # in the Software without restriction, including without limitation the rights 10 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | # copies of the Software, and to permit persons to whom the Software is 12 | # furnished to do so, subject to the following conditions: 13 | # 14 | # The above copyright notice and this permission notice shall be included in 15 | # all copies or substantial portions of the Software. 16 | # 17 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 | # THE SOFTWARE. 24 | # 25 | 26 | r""" 27 | Collection of Parser objects that can parse specific types of LaTeX 28 | constructs. 29 | """ 30 | 31 | from ._base import LatexParserBase 32 | 33 | from ._generalnodes import ( 34 | LatexGeneralNodesParser, 35 | LatexSingleNodeParser, 36 | ) 37 | from ._delimited import ( 38 | LatexDelimitedExpressionParserInfo, 39 | LatexDelimitedExpressionParser, 40 | LatexDelimitedGroupParserInfo, 41 | LatexDelimitedGroupParser, 42 | LatexDelimitedMultiDelimGroupParserInfo, 43 | LatexDelimitedMultiDelimGroupParser, 44 | LatexDelimitedExpressionParserOpeningDelimiterNotFound, 45 | ) 46 | from ._math import ( 47 | LatexMathParser, 48 | ) 49 | 50 | from ._expression import ( 51 | LatexExpressionParser, 52 | ) 53 | 54 | from ._optionals import ( 55 | LatexOptionalSquareBracketsParser, 56 | LatexOptionalCharsMarkerParser, 57 | ) 58 | 59 | from ._stdarg import ( 60 | LatexStandardArgumentParser, 61 | get_standard_argument_parser, 62 | LatexCharsCommaSeparatedListParser, 63 | LatexCharsGroupParser, 64 | LatexTackOnInformationFieldMacrosParser, 65 | ) 66 | 67 | 68 | from ._verbatim import ( 69 | LatexVerbatimBaseParser, 70 | LatexDelimitedVerbatimParser, 71 | LatexVerbatimEnvironmentContentsParser, 72 | ) 73 | -------------------------------------------------------------------------------- /pylatexenc/latexnodes/parsers/_base.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # The MIT License (MIT) 4 | # 5 | # Copyright (c) 2022 Philippe Faist 6 | # 7 | # Permission is hereby granted, free of charge, to any person obtaining a copy 8 | # of this software and associated documentation files (the "Software"), to deal 9 | # in the Software without restriction, including without limitation the rights 10 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | # copies of the Software, and to permit persons to whom the Software is 12 | # furnished to do so, subject to the following conditions: 13 | # 14 | # The above copyright notice and this permission notice shall be included in 15 | # all copies or substantial portions of the Software. 16 | # 17 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 | # THE SOFTWARE. 24 | # 25 | 26 | 27 | # Internal module. Internal API may move, disappear or otherwise change at any 28 | # time and without notice. 29 | 30 | from __future__ import print_function, unicode_literals 31 | 32 | 33 | 34 | 35 | 36 | # ------------------------------------------------------------------------------ 37 | 38 | class LatexParserBase(object): 39 | r""" 40 | The base class for :py:mod:`pylatexenc.latexnodes.parsers` parsers. 41 | 42 | Parsers are objects that are designed to parse a specific type of latex 43 | construct, such as content enclosed in curly braces, into a node tree. 44 | 45 | When invoked, parse objects return a tuple `(nodes, parsing_state_delta)`. 46 | The first element, `nodes`, is the result nodes. It is usually a 47 | :py:class:`~pylatexenc.latexnodes.LatexNodeList` instance, but it can also 48 | be a specific node instance, or another related object like a 49 | :py:class:`~pylatexenc.latexnodes.ParsedArguments` instance. The second 50 | element, `parsing_state_delta`, encode any changes in the parsing state that 51 | should be caused by parsing the given construct. The `parsing_state_delta` 52 | should be either `None` (no parsing state changes) or a 53 | :py:class:`~pylatexenc.latexnodes.ParsingStateDelta` instance. For 54 | instance, if the parser encountered a ``\newcommand`` it can relay the 55 | corresponding state change through the `parsing_state_delta` object. 56 | 57 | The main functionality of the parser is implemented in the 58 | :py:meth:`parse()` method. 59 | 60 | Parser objects should be invoked via the latex walker instance, using 61 | `LatexWalker.parse_content()` (see :py:class:`LatexWalkerBase` and 62 | :py:class:`pylatexenc.latexwalker.LatexWalker`): 63 | 64 | .. code:: 65 | 66 | my_latex_walker = LatexWalker(....) 67 | my_parser = .... # some LatexParserBase subclass 68 | 69 | token_reader = my_latex_walker.make_token_reader() 70 | parsing_state = my_latex_walker.make_parsing_state() 71 | 72 | # parse that specific construct: 73 | nodes, parsing_state_delta = my_latex_walker.parse_content( 74 | my_parser, 75 | token_reader, 76 | parsing_state 77 | ) 78 | """ 79 | def __init__(self): 80 | super(LatexParserBase, self).__init__() 81 | 82 | def parse(self, latex_walker, token_reader, parsing_state, **kwargs): 83 | r""" 84 | The main functionality of the parser is implemented in this method. 85 | 86 | Parser objects should not be called directly, but rather be invoked via 87 | the latex walker instance, using `LatexWalker.parse_content()`. (See 88 | class doc above.) 89 | 90 | Subclasses should implement this method to construct the relevant node 91 | tree by reading tokens from the `token_reader` (use 92 | `token_reader.next_token()` and friends, see 93 | :py:class:`~pylatexenc.latexnodes.LatexTokenReaderBase`) 94 | 95 | Subclasses should return a tuple pair `(nodes, parsing_state_delta)`. 96 | 97 | The `nodes` is the node list, node, or object that resulted from the 98 | parsing. 99 | 100 | The `parsing_state_delta` encodes any parsing state changes that 101 | resulted during the parsing of this construct. If there are no parsing 102 | state changes, `parsing_state_delta` can be set to `None`. 103 | """ 104 | raise RuntimeError("LatexParserBase subclasses must reimplement parse()") 105 | 106 | 107 | def contents_can_be_empty(self): 108 | r""" 109 | If absorbing no tokens is a valid option for the thing this object is meant 110 | to parse, then we should return `True` here. This would be the case, 111 | for instance, for group contents, for optional arguments, etc. But a 112 | parser for a mandatory argument would return `False` here. 113 | 114 | This is used in certain special situations, for instance if a closing 115 | brace is immediately encountered after a macro that expected an argument 116 | (say ``\mymacro}`` --- it's an error if ``\mymacro`` requires a 117 | mandatory argument but it's ok if it accepts an optional argument). In 118 | this case, we need to check all the macro arguments' parser to see if it 119 | is okay that they have no contents. 120 | """ 121 | return True 122 | 123 | 124 | def __repr__(self): 125 | return "<{}>".format(self.__class__.__name__) 126 | -------------------------------------------------------------------------------- /pylatexenc/latexnodes/parsers/_math.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # The MIT License (MIT) 4 | # 5 | # Copyright (c) 2022 Philippe Faist 6 | # 7 | # Permission is hereby granted, free of charge, to any person obtaining a copy 8 | # of this software and associated documentation files (the "Software"), to deal 9 | # in the Software without restriction, including without limitation the rights 10 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | # copies of the Software, and to permit persons to whom the Software is 12 | # furnished to do so, subject to the following conditions: 13 | # 14 | # The above copyright notice and this permission notice shall be included in 15 | # all copies or substantial portions of the Software. 16 | # 17 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 | # THE SOFTWARE. 24 | # 25 | 26 | 27 | # Internal module. Internal API may move, disappear or otherwise change at any 28 | # time and without notice. 29 | 30 | from __future__ import print_function, unicode_literals 31 | 32 | import logging 33 | logger = logging.getLogger(__name__) 34 | 35 | from .._exctypes import * 36 | from .. import nodes 37 | from .._parsingstatedelta import ( 38 | ParsingStateDeltaEnterMathMode, 39 | get_updated_parsing_state_from_delta, 40 | ) 41 | 42 | from ._delimited import ( 43 | LatexDelimitedExpressionParserInfo, 44 | LatexDelimitedExpressionParser, 45 | ) 46 | 47 | 48 | 49 | # for Py3 50 | _basestring = str 51 | 52 | ### BEGIN_PYTHON2_SUPPORT_CODE 53 | import sys 54 | if sys.version_info.major == 2: 55 | _basestring = basestring 56 | ### END_PYTHON2_SUPPORT_CODE 57 | 58 | 59 | 60 | 61 | 62 | class LatexMathParserInfo(LatexDelimitedExpressionParserInfo): 63 | r""" 64 | Reimplementation of the :py:class:`LatexDelimitedExpressionParserInfo` class 65 | for math environments, for :py:class:`LatexMathParser`. 66 | """ 67 | 68 | @classmethod 69 | def is_opening_delimiter(cls, delimiters, first_token, group_parsing_state, 70 | delimited_expression_parser, latex_walker, **kwargs): 71 | 72 | if first_token.tok not in ('mathmode_inline', 'mathmode_display'): 73 | return False 74 | 75 | if not cls.check_opening_delimiter( 76 | delimiters=delimiters, 77 | parsed_opening_delimiter=first_token.arg, 78 | latex_walker=latex_walker 79 | ): 80 | return False 81 | 82 | return True 83 | 84 | @classmethod 85 | def get_acceptable_open_delimiter_list(cls, delimiters, group_parsing_state, 86 | delimited_expression_parser, latex_walker, 87 | **kwargs): 88 | if delimiters is not None: 89 | if isinstance(delimiters, _basestring): 90 | return [delimiters] 91 | else: 92 | return [delimiters[0]] 93 | 94 | return [ 95 | od 96 | for (od, cd) in ( 97 | group_parsing_state.latex_inline_math_delimiters 98 | + group_parsing_state.latex_display_math_delimiters 99 | ) 100 | ] 101 | 102 | 103 | # --- 104 | 105 | def initialize(self): 106 | # set up all the relevant fields manually: 107 | 108 | self.math_mode_type = self.first_token.tok 109 | self.math_mode_delimiter = self.first_token.arg 110 | 111 | # enter math mode ! 112 | self.math_parsing_state = get_updated_parsing_state_from_delta( 113 | self.parsing_state, 114 | ParsingStateDeltaEnterMathMode( 115 | math_mode_delimiter=self.math_mode_delimiter, 116 | trigger_token=self.first_token 117 | ), 118 | self.latex_walker, 119 | ) 120 | 121 | self.contents_parsing_state = self.math_parsing_state 122 | self.parsed_delimiters = self.get_parsed_delimiters() 123 | 124 | def stop_token_condition(self, token): 125 | if token.tok == self.math_mode_type and token.arg == self.parsed_delimiters[1]: 126 | return True 127 | return False 128 | 129 | def get_matching_delimiter(self, opening_delimiter): 130 | return self.math_parsing_state._math_expecting_close_delim_info['close_delim'] 131 | 132 | 133 | def make_group_node_and_parsing_state_delta(self, latex_walker, token_reader, 134 | nodelist, parsing_state_delta): 135 | 136 | # As for the delimited group parser, use cur_pos() so that it includes 137 | # the closing math mode delimiter. 138 | pos_end = token_reader.cur_pos() 139 | 140 | # note that nodelist can be None in case of a parse error 141 | 142 | if self.math_mode_type == 'mathmode_inline': 143 | displaytype = 'inline' 144 | elif self.math_mode_type == 'mathmode_display': 145 | displaytype = 'display' 146 | else: 147 | displaytype = '' 148 | 149 | math_node = latex_walker.make_node( 150 | nodes.LatexMathNode, 151 | displaytype=displaytype, 152 | nodelist=nodelist, 153 | parsing_state=self.parsing_state, 154 | delimiters=self.parsed_delimiters, 155 | pos=self.first_token.pos, 156 | pos_end=pos_end, 157 | ) 158 | 159 | return math_node, parsing_state_delta 160 | 161 | 162 | # ------------------------------------------------------------------------------ 163 | 164 | class LatexMathParser(LatexDelimitedExpressionParser): 165 | def __init__(self, 166 | math_mode_delimiters, 167 | **kwargs): 168 | super(LatexMathParser, self).__init__( 169 | delimiters=math_mode_delimiters, 170 | discard_parsing_state_delta=False, 171 | delimited_expression_parser_info_class=LatexMathParserInfo, 172 | **kwargs 173 | ) 174 | -------------------------------------------------------------------------------- /pylatexenc/latexwalker/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # The MIT License (MIT) 4 | # 5 | # Copyright (c) 2018 Philippe Faist 6 | # 7 | # Permission is hereby granted, free of charge, to any person obtaining a copy 8 | # of this software and associated documentation files (the "Software"), to deal 9 | # in the Software without restriction, including without limitation the rights 10 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | # copies of the Software, and to permit persons to whom the Software is 12 | # furnished to do so, subject to the following conditions: 13 | # 14 | # The above copyright notice and this permission notice shall be included in 15 | # all copies or substantial portions of the Software. 16 | # 17 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 | # THE SOFTWARE. 24 | # 25 | 26 | r''' 27 | The ``latexwalker`` module provides a simple API for parsing LaTeX snippets, 28 | and representing the contents using a data structure based on node classes. 29 | 30 | LatexWalker will understand the syntax of most common macros. However, 31 | ``latexwalker`` is NOT a replacement for a full LaTeX engine. (Originally, 32 | ``latexwalker`` was designed to extract useful text for indexing for text 33 | database searches of LaTeX content.) 34 | 35 | Simple example usage:: 36 | 37 | >>> from pylatexenc.latexwalker import LatexWalker, LatexEnvironmentNode 38 | >>> w = LatexWalker(r""" 39 | ... \textbf{Hi there!} Here is \emph{a list}: 40 | ... \begin{enumerate}[label=(i)] 41 | ... \item One 42 | ... \item Two 43 | ... \end{enumerate} 44 | ... and $x$ is a variable. 45 | ... """) 46 | >>> (nodelist, pos, len_) = w.get_latex_nodes(pos=0) 47 | >>> nodelist[0] 48 | LatexCharsNode(pos=0, len=1, chars='\n') 49 | >>> nodelist[1] 50 | LatexMacroNode(pos=1, len=18, macroname='textbf', 51 | nodeargd=ParsedMacroArgs(argnlist=[LatexGroupNode(pos=8, len=11, 52 | nodelist=[LatexCharsNode(pos=9, len=9, chars='Hi there!')], 53 | delimiters=('{', '}'))], argspec='{'), macro_post_space='') 54 | >>> nodelist[5].isNodeType(LatexEnvironmentNode) 55 | True 56 | >>> nodelist[5].environmentname 57 | 'enumerate' 58 | >>> nodelist[5].nodeargd.argspec 59 | '[' 60 | >>> nodelist[5].nodeargd.argnlist 61 | [LatexGroupNode(pos=60, len=11, nodelist=[LatexCharsNode(pos=61, len=9, 62 | chars='label=(i)')], delimiters=('[', ']'))] 63 | >>> nodelist[7].latex_verbatim() 64 | '$x$' 65 | 66 | You can also use `latexwalker` directly in command-line, producing JSON or a 67 | human-readable node tree:: 68 | 69 | $ echo '\textit{italic} text' | latexwalker --output-format=json 70 | { 71 | "nodelist": [ 72 | { 73 | "nodetype": "LatexMacroNode", 74 | "pos": 0, 75 | "len": 15, 76 | "macroname": "textit", 77 | [...] 78 | 79 | $ latexwalker --help 80 | [...] 81 | 82 | The parser can be influenced by specifying a collection of known macros and 83 | environments (the "latex context") that are specified using 84 | :py:class:`pylatexenc.macrospec.MacroSpec` and 85 | :py:class:`pylatexenc.macrospec.EnvironmentSpec` objects in a 86 | :py:class:`pylatexenc.macrospec.LatexContextDb` object. See the doc of the 87 | module :py:mod:`pylatexenc.macrospec` for more information. 88 | ''' 89 | 90 | from __future__ import print_function, unicode_literals 91 | 92 | 93 | import logging 94 | logger = logging.getLogger(__name__) 95 | 96 | 97 | 98 | from .. import macrospec 99 | 100 | 101 | # ------------------------------------------------------------------------------ 102 | 103 | 104 | ### BEGIN_PYLATEXENC2_LEGACY_SUPPORT_CODE 105 | from ..latexnodes._exctypes import * 106 | from ..latexnodes.nodes import * 107 | from ..latexnodes._token import LatexToken 108 | ### END_PYLATEXENC2_LEGACY_SUPPORT_CODE 109 | 110 | 111 | 112 | from ..latexnodes import ParsingState 113 | 114 | from ._walker import LatexWalker 115 | 116 | 117 | ### BEGIN_PYLATEXENC_GET_DEFAULT_SPECS_FN 118 | from ._get_defaultspecs import get_default_latex_context_db 119 | ### END_PYLATEXENC_GET_DEFAULT_SPECS_FN 120 | 121 | 122 | ### BEGIN_PYLATEXENC1_LEGACY_SUPPORT_CODE 123 | from ._legacy_py1x import ( 124 | MacrosDef, 125 | default_macro_dict, 126 | get_token, 127 | get_latex_expression, 128 | get_latex_maybe_optional_arg, 129 | get_latex_braced_group, 130 | get_latex_environment, 131 | get_latex_nodes, 132 | ) 133 | ### END_PYLATEXENC1_LEGACY_SUPPORT_CODE 134 | 135 | 136 | 137 | ### BEGIN_LATEXWALKER_HELPERS 138 | from ._helpers import ( 139 | nodelist_to_latex, 140 | put_in_braces, 141 | disp_node, 142 | make_json_encoder, 143 | ) 144 | ### END_LATEXWALKER_HELPERS 145 | -------------------------------------------------------------------------------- /pylatexenc/latexwalker/__main__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # The MIT License (MIT) 4 | # 5 | # Copyright (c) 2018 Philippe Faist 6 | # 7 | # Permission is hereby granted, free of charge, to any person obtaining a copy 8 | # of this software and associated documentation files (the "Software"), to deal 9 | # in the Software without restriction, including without limitation the rights 10 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | # copies of the Software, and to permit persons to whom the Software is 12 | # furnished to do so, subject to the following conditions: 13 | # 14 | # The above copyright notice and this permission notice shall be included in 15 | # all copies or substantial portions of the Software. 16 | # 17 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 | # THE SOFTWARE. 24 | # 25 | 26 | import sys 27 | import fileinput 28 | import argparse 29 | import json 30 | import logging 31 | 32 | 33 | from ..latexwalker import LatexWalker, disp_node, make_json_encoder 34 | from ..version import version_str 35 | 36 | 37 | 38 | def main(argv=None): 39 | 40 | if argv is None: 41 | argv = sys.argv[1:] 42 | 43 | parser = argparse.ArgumentParser(prog='latexwalker', add_help=False) 44 | 45 | parser.add_argument('--output-format', metavar="FORMAT", dest="output_format", 46 | choices=["human", "json"], default='human', 47 | help='Requested output format for the node tree ("human" or "json")') 48 | parser.add_argument('--json-indent', metavar="NUMSPACES", dest="json_indent", 49 | type=int, default=2, 50 | help='Indentation in JSON output (specify number of spaces ' 51 | 'per indentation level)') 52 | parser.add_argument('--json-compact', dest="json_indent", action='store_const', const=None, 53 | help='Output compact JSON') 54 | 55 | parser.add_argument('--keep-inline-math', action='store_const', const=True, 56 | dest='keep_inline_math', default=True, 57 | help=argparse.SUPPRESS) 58 | parser.add_argument('--no-keep-inline-math', action='store_const', const=False, 59 | dest='keep_inline_math', 60 | help=argparse.SUPPRESS) 61 | 62 | parser.add_argument('--tolerant-parsing', action='store_const', const=True, 63 | dest='tolerant_parsing', default=True) 64 | parser.add_argument('--no-tolerant-parsing', action='store_const', const=False, 65 | dest='tolerant_parsing', 66 | help="Tolerate syntax errors when parsing, and attempt " 67 | "to continue (default yes)") 68 | 69 | # I'm not sure this flag is useful and if it should be exposed at all. 70 | # Accept it, but make it hidden. 71 | parser.add_argument('--strict-braces', action='store_const', const=True, 72 | dest='strict_braces', default=False, 73 | help=argparse.SUPPRESS) 74 | parser.add_argument('--no-strict-braces', action='store_const', const=False, 75 | dest='strict_braces', 76 | #help="Report errors for mismatching LaTeX braces (default no)" 77 | help=argparse.SUPPRESS) 78 | 79 | parser.add_argument('-q', '--quiet', dest='logging_level', action='store_const', 80 | const=logging.ERROR, default=logging.INFO, 81 | help="Suppress warning messages") 82 | parser.add_argument('-v', '--verbose', dest='logging_level', action='store_const', 83 | const=logging.DEBUG, 84 | help="Verbose output") 85 | parser.add_argument('--version', action='version', 86 | version='pylatexenc {}'.format(version_str), 87 | help="Show version information and exit") 88 | parser.add_argument('--help', action='help', 89 | help="Show this help information and exit") 90 | 91 | 92 | parser.add_argument('--code', '-c', action='store', default=None, metavar="LATEX_CODE", 93 | help="Convert the given LATEX_CODE to unicode text instead of reading " 94 | "from FILE or standard input. You cannot specify FILEs if you use this " 95 | "option, and any standard input is ignored.") 96 | 97 | parser.add_argument('files', metavar="FILE", nargs='*', 98 | help='Input files (if none specified, read from stdandard input)') 99 | 100 | args = parser.parse_args(argv) 101 | 102 | logging.basicConfig() 103 | logging.getLogger().setLevel(args.logging_level) 104 | logger = logging.getLogger(__name__) 105 | 106 | latex = '' 107 | if args.code: 108 | if args.files: 109 | logger.error("Cannot specify both FILEs and --code option. " 110 | "Use --help option for more information.") 111 | sys.exit(1) 112 | latex = args.code 113 | else: 114 | for line in fileinput.input(files=args.files): 115 | latex += line 116 | 117 | latexwalker = LatexWalker(latex, 118 | tolerant_parsing=args.tolerant_parsing, 119 | strict_braces=args.strict_braces) 120 | 121 | (nodelist, pos, len_) = latexwalker.get_latex_nodes() 122 | 123 | 124 | if args.output_format == 'human': 125 | print('\n--- NODES ---\n') 126 | for n in nodelist: 127 | disp_node(n) 128 | print('\n-------------\n') 129 | return 130 | 131 | if args.output_format == 'json': 132 | json.dump({ 'nodelist': nodelist, }, 133 | sys.stdout, 134 | cls=make_json_encoder(latexwalker), 135 | indent=args.json_indent) 136 | sys.stdout.write("\n") 137 | return 138 | 139 | raise ValueError("Invalid output format: "+args.output_format) 140 | 141 | 142 | 143 | def run_main(): 144 | 145 | try: 146 | 147 | main() 148 | 149 | except SystemExit: 150 | raise 151 | except: # lgtm [py/catch-base-exception] 152 | import pdb 153 | import traceback 154 | traceback.print_exc() 155 | pdb.post_mortem() 156 | 157 | 158 | 159 | if __name__ == '__main__': 160 | 161 | run_main() # debug 162 | #main() 163 | -------------------------------------------------------------------------------- /pylatexenc/latexwalker/_get_defaultspecs.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # The MIT License (MIT) 4 | # 5 | # Copyright (c) 2019 Philippe Faist 6 | # 7 | # Permission is hereby granted, free of charge, to any person obtaining a copy 8 | # of this software and associated documentation files (the "Software"), to deal 9 | # in the Software without restriction, including without limitation the rights 10 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | # copies of the Software, and to permit persons to whom the Software is 12 | # furnished to do so, subject to the following conditions: 13 | # 14 | # The above copyright notice and this permission notice shall be included in 15 | # all copies or substantial portions of the Software. 16 | # 17 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 | # THE SOFTWARE. 24 | # 25 | 26 | 27 | # Internal module. Internal API may move, disappear or otherwise change at any 28 | # time and without notice. 29 | 30 | from __future__ import print_function, unicode_literals 31 | 32 | 33 | # don't define this function in the `_defaultspecs.py` source file because we 34 | # would like to be able to define this function without having to actually load 35 | # the entire default specs module. 36 | 37 | def get_default_latex_context_db(): 38 | r""" 39 | Return a :py:class:`pylatexenc.macrospec.LatexContextDb` instance 40 | initialized with a collection of known macros and environments. 41 | 42 | TODO: document categories. 43 | 44 | If you want to add your own definitions, you should use the 45 | :py:meth:`pylatexenc.macrospec.LatexContextDb.add_context_category()` 46 | method. If you would like to override some definitions, use that method 47 | with the argument `prepend=True`. See docs for 48 | :py:meth:`pylatexenc.macrospec.LatexContextDb.add_context_category()`. 49 | 50 | If there are too many macro/environment definitions, or if there are some 51 | irrelevant ones, you can always filter the returned database using 52 | :py:meth:`pylatexenc.macrospec.LatexContextDb.filter_context()`. 53 | 54 | .. versionadded:: 2.0 55 | 56 | The :py:class:`pylatexenc.macrospec.LatexContextDb` class as well as this 57 | method, were all introduced in `pylatexenc 2.0`. 58 | """ 59 | 60 | from .. import macrospec 61 | from ._defaultspecs import specs 62 | 63 | db = macrospec.LatexContextDb() 64 | 65 | for cat, catspecs in specs: 66 | db.add_context_category( 67 | cat, 68 | macros=catspecs['macros'], 69 | environments=catspecs['environments'], 70 | specials=catspecs['specials'] 71 | ) 72 | 73 | db.set_unknown_macro_spec(macrospec.MacroSpec('')) 74 | db.set_unknown_environment_spec(macrospec.EnvironmentSpec('')) 75 | 76 | return db 77 | 78 | # 79 | -------------------------------------------------------------------------------- /pylatexenc/macrospec/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # The MIT License (MIT) 4 | # 5 | # Copyright (c) 2022 Philippe Faist 6 | # 7 | # Permission is hereby granted, free of charge, to any person obtaining a copy 8 | # of this software and associated documentation files (the "Software"), to deal 9 | # in the Software without restriction, including without limitation the rights 10 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | # copies of the Software, and to permit persons to whom the Software is 12 | # furnished to do so, subject to the following conditions: 13 | # 14 | # The above copyright notice and this permission notice shall be included in 15 | # all copies or substantial portions of the Software. 16 | # 17 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 | # THE SOFTWARE. 24 | # 25 | 26 | r""" 27 | Provides classes and helper functions to describe a LaTeX context of known 28 | macros and environments, specifying how they should be parsed by 29 | :py:mod:`pylatexenc.latexwalker`. 30 | 31 | .. versionadded:: 2.0 32 | 33 | The entire module :py:mod:`pylatexenc.macrospec` was introduced in 34 | `pylatexenc 2.0`. 35 | """ 36 | 37 | 38 | from ._specclasses import ( 39 | CallableSpec, 40 | MacroSpec, 41 | EnvironmentSpec, 42 | SpecialsSpec, 43 | ) 44 | 45 | ### BEGIN_PYLATEXENC2_LEGACY_SUPPORT_CODE 46 | from ._spechelpers import std_macro, std_environment, std_specials 47 | ### END_PYLATEXENC2_LEGACY_SUPPORT_CODE 48 | 49 | 50 | from ._latexcontextdb import ( 51 | LatexContextDb, 52 | ParsingStateDeltaExtendLatexContextDb, 53 | ) 54 | 55 | from ._argumentsparser import ( 56 | LatexArgumentsParser, 57 | LatexNoArgumentsParser, 58 | ) 59 | 60 | from ._environmentbodyparser import ( 61 | LatexEnvironmentBodyContentsParserInfo, 62 | LatexEnvironmentBodyContentsParser 63 | ) 64 | 65 | from ._macrocallparser import ( 66 | LatexMacroCallParser, 67 | LatexEnvironmentCallParser, 68 | LatexSpecialsCallParser 69 | ) 70 | 71 | 72 | ### BEGIN_PYLATEXENC2_LEGACY_SUPPORT_CODE 73 | from ..latexnodes import ParsedArguments as ParsedMacroArgs 74 | from ._pyltxenc2_argparsers import ( 75 | MacroStandardArgsParser, 76 | ParsedVerbatimArgs, 77 | VerbatimArgsParser, 78 | ParsedLstListingArgs, 79 | LstListingArgsParser, 80 | ) 81 | ### END_PYLATEXENC2_LEGACY_SUPPORT_CODE 82 | 83 | -------------------------------------------------------------------------------- /pylatexenc/macrospec/_pyltxenc2_argparsers/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # The MIT License (MIT) 4 | # 5 | # Copyright (c) 2021 Philippe Faist 6 | # 7 | # Permission is hereby granted, free of charge, to any person obtaining a copy 8 | # of this software and associated documentation files (the "Software"), to deal 9 | # in the Software without restriction, including without limitation the rights 10 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | # copies of the Software, and to permit persons to whom the Software is 12 | # furnished to do so, subject to the following conditions: 13 | # 14 | # The above copyright notice and this permission notice shall be included in 15 | # all copies or substantial portions of the Software. 16 | # 17 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 | # THE SOFTWARE. 24 | # 25 | 26 | 27 | # Internal module. Internal API may move, disappear or otherwise change at any 28 | # time and without notice. 29 | 30 | 31 | 32 | from ._base import ( 33 | MacroStandardArgsParser 34 | ) 35 | 36 | 37 | # ------------------------------------------------------------------------------ 38 | 39 | 40 | from ._verbatimargsparser import ( 41 | ParsedVerbatimArgs, 42 | VerbatimArgsParser, 43 | ParsedLstListingArgs, 44 | LstListingArgsParser, 45 | ) 46 | -------------------------------------------------------------------------------- /pylatexenc/version.py: -------------------------------------------------------------------------------- 1 | # 2 | # The MIT License (MIT) 3 | # 4 | # Copyright (c) 2021 Philippe Faist 5 | # 6 | # Permission is hereby granted, free of charge, to any person obtaining a copy 7 | # of this software and associated documentation files (the "Software"), to deal 8 | # in the Software without restriction, including without limitation the rights 9 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | # copies of the Software, and to permit persons to whom the Software is 11 | # furnished to do so, subject to the following conditions: 12 | # 13 | # The above copyright notice and this permission notice shall be included in 14 | # all copies or substantial portions of the Software. 15 | # 16 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | # THE SOFTWARE. 23 | # 24 | 25 | 26 | # 27 | # Self-note: Checklist --- NOTE THESE ARE OLD INSTRUCTIONS DATING FROM 2.X VERSIONS 28 | # 29 | # 1) First some checks: 30 | # 31 | # - Set below in this file ' version_str = "X.Xb" ' (beta version for next 32 | # release) for the following tests. 33 | # 34 | # - tests pass: https://travis-ci.org/github/phfaist/pylatexenc 35 | # 36 | # - LGTM looks good: https://lgtm.com/projects/g/phfaist/pylatexenc/ 37 | # 38 | # - python package creation works: (python setup.py sdist, pip install 39 | # dist/pylatexenc-xxx.tar.gz) 40 | # 41 | # 2) update change log (doc/changes.rst) 42 | # 43 | # 3) bump version number here 44 | # 45 | # 4) git commit any remaining changes 46 | # 47 | # 5) " git tag vX.X -am '' " 48 | # 49 | # 6) " git push && git push --tags " 50 | # 51 | # 7) on github.com, fill in release details with a summary of changes etc. 52 | # 53 | # 8) create the source package for PyPI (" python3 setup.py sdist ") 54 | # 55 | # 8) upload package to PyPI (twine upload dist/pylatexenc-X.X.tar.gz -r realpypi) 56 | # 57 | 58 | 59 | # ALSO BUMP IN pyproject.toml ! 60 | version_str = "3.0alpha000032" 61 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "pylatexenc" 3 | version = "3.0alpha000032" # ALSO BUMP IN pylatexenc/version.py 4 | description = "Simple LaTeX parser providing latex-to-unicode and unicode-to-latex conversion" 5 | authors = ["Philippe Faist "] 6 | license = "MIT" 7 | readme = "README.rst" 8 | 9 | [tool.poetry.scripts] 10 | latexwalker = 'pylatexenc.latexwalker.__main__:main' 11 | latex2text = 'pylatexenc.latex2text.__main__:main' 12 | latexencode = 'pylatexenc.latexencode.__main__:main' 13 | 14 | 15 | [tool.poetry.dependencies] 16 | #python = "^2.7 || ^3.4" # This simply creates a dependency resolution mess that doesn't work. 17 | #python = "^3.7" # hmm seems like it severly constrains other package versions 18 | python = "^3.8" 19 | 20 | [tool.poetry.dev-dependencies] 21 | pytest = ">=7.0" 22 | toml = "^0.10.2" 23 | 24 | [tool.poetry.group.builddoc] 25 | optional = true 26 | 27 | [tool.poetry.group.builddoc.dependencies] 28 | Sphinx = ">=5.0.0" 29 | sphinx-issues = ">=3.0.0" 30 | 31 | [tool.poetry.group.buildjslib] 32 | optional = true 33 | 34 | [tool.poetry.group.buildjslib.dependencies] 35 | Transcrypt = ">=3.9.0" 36 | PyYAML = ">=5.0" 37 | 38 | 39 | [build-system] 40 | requires = ["poetry-core>=1.0.0"] 41 | build-backend = "poetry.core.masonry.api" 42 | 43 | 44 | 45 | [tool.pytest.ini_options] 46 | testpaths = [ 47 | "test", 48 | ] 49 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # 2 | # The MIT License (MIT) 3 | # 4 | # Copyright (c) 2019 Philippe Faist 5 | # 6 | # Permission is hereby granted, free of charge, to any person obtaining a copy 7 | # of this software and associated documentation files (the "Software"), to deal 8 | # in the Software without restriction, including without limitation the rights 9 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | # copies of the Software, and to permit persons to whom the Software is 11 | # furnished to do so, subject to the following conditions: 12 | # 13 | # The above copyright notice and this permission notice shall be included in 14 | # all copies or substantial portions of the Software. 15 | # 16 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | # THE SOFTWARE. 23 | # 24 | 25 | import os 26 | import os.path 27 | #import sys 28 | 29 | from setuptools import setup, find_packages 30 | 31 | from pylatexenc.version import version_str 32 | 33 | def read(*paths): 34 | """Build a file path from *paths* and return the contents.""" 35 | with open(os.path.join(*paths), 'r') as f: 36 | return f.read() 37 | 38 | setup( 39 | name = "pylatexenc", 40 | version = version_str, 41 | 42 | # metadata for upload to PyPI 43 | author = "Philippe Faist", 44 | author_email = "philippe.faist@bluewin.ch", 45 | description = "Simple LaTeX parser providing latex-to-unicode and unicode-to-latex conversion", 46 | long_description = read("README.rst"), 47 | license = "MIT", 48 | keywords = "latex text unicode encode parse expression", 49 | url = "https://github.com/phfaist/pylatexenc", 50 | classifiers=[ 51 | 'Development Status :: 5 - Production/Stable', 52 | 'License :: OSI Approved :: MIT License', 53 | 'Programming Language :: Python', 54 | 'Programming Language :: Python :: 2', 55 | 'Programming Language :: Python :: 2.7', 56 | 'Programming Language :: Python :: 3', 57 | 'Operating System :: MacOS :: MacOS X', 58 | 'Operating System :: Microsoft :: Windows', 59 | 'Operating System :: POSIX :: Linux', 60 | 'Intended Audience :: Developers', 61 | 'Topic :: Scientific/Engineering', 62 | 'Topic :: Text Processing :: General', 63 | 'Topic :: Text Processing :: Markup :: LaTeX', 64 | ], 65 | 66 | # files 67 | packages = find_packages(), 68 | entry_points = { 69 | 'console_scripts': [ 70 | 'latexwalker=pylatexenc.latexwalker.__main__:main', 71 | 'latex2text=pylatexenc.latex2text.__main__:main', 72 | 'latexencode=pylatexenc.latexencode.__main__:main', 73 | ], 74 | }, 75 | install_requires = [], 76 | package_data = { 77 | }, 78 | ) 79 | -------------------------------------------------------------------------------- /test/.gitignore: -------------------------------------------------------------------------------- 1 | _tmp_uni_chars_test.temp.txt 2 | -------------------------------------------------------------------------------- /test/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phfaist/pylatexenc/6dc2ce7fcd89b7cd1536c79c800f49f09535f5e9/test/__init__.py -------------------------------------------------------------------------------- /test/dummy/readme.txt: -------------------------------------------------------------------------------- 1 | This directory is just used as base '\input{}' directory, to test the strict_input flag of LatexNodes2Text. 2 | -------------------------------------------------------------------------------- /test/test_input_1.tex: -------------------------------------------------------------------------------- 1 | \textit{hi there!} This is {\em an equation}: 2 | \begin{equation} 3 | x + y i = 0 4 | \end{equation} 5 | 6 | where $i$ is the imaginary unit. 7 | -------------------------------------------------------------------------------- /test/test_latexencode_all.py: -------------------------------------------------------------------------------- 1 | 2 | from __future__ import unicode_literals, print_function 3 | 4 | import unittest 5 | 6 | import sys 7 | import codecs 8 | import difflib 9 | import unicodedata 10 | import logging 11 | import os.path 12 | 13 | if sys.version_info.major >= 3: 14 | PY3 = True 15 | else: 16 | PY3 = False 17 | 18 | if PY3: 19 | def unicode(string): return string 20 | basestring = str 21 | unichr = chr 22 | else: 23 | range = xrange 24 | 25 | 26 | from pylatexenc.latexencode import UnicodeToLatexEncoder 27 | 28 | 29 | 30 | 31 | class TestLatexEncodeAll(unittest.TestCase): 32 | 33 | def __init__(self, *args, **kwargs): 34 | super(TestLatexEncodeAll, self).__init__(*args, **kwargs) 35 | 36 | # def test_pythonunicoderange(self): 37 | # self.assertGreater(sys.maxunicode, 0xFFFF+1, 38 | # "Your python build only supports unicode characters up to U+FFFF." 39 | # " Tests of unicode coverage will fail.") 40 | 41 | def test_all(self): 42 | 43 | loglevel = logging.getLogger().level 44 | logging.getLogger().setLevel(logging.CRITICAL) 45 | 46 | u = UnicodeToLatexEncoder(unknown_char_policy='fail', 47 | replacement_latex_protection='braces-almost-all') 48 | 49 | def fn(x, bdir=os.path.realpath(os.path.abspath(os.path.dirname(__file__)))): 50 | return os.path.join(bdir, x) 51 | 52 | with codecs.open(fn('_tmp_uni_chars_test.temp.txt'), 'w', encoding='utf-8') as testf: 53 | 54 | for i in range(0x10FFFF): 55 | # iter over all valid unicode characters 56 | try: 57 | chrname = unicodedata.name(unichr(i)) # test if valid, i.e., it has a UNICODE NAME 58 | except ValueError: 59 | continue 60 | 61 | line = "0x%04X %-50s |%s|\n"%(i, '['+chrname+']', unichr(i)) 62 | 63 | # try to encode it using our unicode_to_latex routines 64 | try: 65 | enc = u.unicode_to_latex(line) 66 | except ValueError: 67 | continue 68 | testf.write(enc) 69 | 70 | with codecs.open(fn('uni_chars_test_previous.txt'), 'r', encoding='utf-8') as reff, \ 71 | codecs.open(fn('_tmp_uni_chars_test.temp.txt'), 'r', encoding='utf-8') as testf: 72 | a = reff.readlines() 73 | b = testf.readlines() 74 | 75 | logging.getLogger().setLevel(loglevel) 76 | logger = logging.getLogger(__name__) 77 | 78 | # only check up to the supported unicode range 79 | if sys.maxunicode < 0x10FFFF: 80 | logger.warning("Only checking up to unicode U+%X, your python build doesn't support higher", 81 | sys.maxunicode) 82 | afiltered = [ aline for aline in a 83 | if int(aline[:aline.find(' ')], 0) < sys.maxunicode ] 84 | a = afiltered 85 | 86 | s = difflib.unified_diff(a, b, 87 | fromfile='uni_chars_test_previous.txt', 88 | tofile='_tmp_uni_chars_test.temp.txt') 89 | diffmsg = "".join(list(s)).strip() 90 | if diffmsg: 91 | print(diffmsg) 92 | raise self.failureException("Unicode coverage tests failed. See full diff above.") 93 | 94 | 95 | if __name__ == '__main__': 96 | logging.basicConfig(level=logging.DEBUG) 97 | unittest.main() 98 | # 99 | 100 | -------------------------------------------------------------------------------- /test/test_latexnodes_parsers_optionals.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import logging 3 | 4 | 5 | from pylatexenc.latexnodes.parsers._optionals import ( 6 | LatexOptionalCharsMarkerParser, 7 | LatexOptionalSquareBracketsParser, 8 | ) 9 | 10 | from pylatexenc.latexnodes import ( 11 | LatexWalkerParseError, 12 | LatexTokenReader, 13 | LatexToken, 14 | ParsingState, 15 | ) 16 | from pylatexenc.latexnodes.nodes import * 17 | 18 | from ._helpers_tests import ( 19 | DummyWalker, 20 | DummyLatexContextDb, 21 | ) 22 | 23 | 24 | 25 | 26 | # -------------------------------------- 27 | 28 | class TestLatexOptionalCharsMarkerParser(unittest.TestCase): 29 | 30 | maxDiff = None 31 | 32 | def test_simple_chars_marker_isthere(self): 33 | 34 | latextext = r'''*more''' 35 | 36 | tr = LatexTokenReader(latextext) 37 | ps = ParsingState(s=latextext, latex_context=DummyLatexContextDb()) 38 | lw = DummyWalker() 39 | 40 | parser = LatexOptionalCharsMarkerParser('*') 41 | 42 | nodes, parsing_state_delta = lw.parse_content(parser, token_reader=tr, parsing_state=ps) 43 | 44 | self.assertEqual( 45 | nodes, 46 | LatexNodeList( 47 | [ 48 | LatexCharsNode( 49 | parsing_state=ps, 50 | chars='*', 51 | pos=0, 52 | pos_end=1, 53 | ) 54 | ], 55 | parsing_state=ps 56 | ) 57 | ) 58 | 59 | 60 | def test_simple_chars_marker_notthere(self): 61 | 62 | latextext = r'''more''' 63 | 64 | tr = LatexTokenReader(latextext) 65 | ps = ParsingState(s=latextext, latex_context=DummyLatexContextDb()) 66 | lw = DummyWalker() 67 | 68 | parser = LatexOptionalCharsMarkerParser('*') 69 | 70 | nodes, parsing_state_delta = lw.parse_content(parser, token_reader=tr, parsing_state=ps) 71 | 72 | self.assertEqual( 73 | nodes, 74 | None, 75 | ) 76 | 77 | def test_simple_chars_marker_notthere_reqempty(self): 78 | 79 | latextext = r'''more''' 80 | 81 | tr = LatexTokenReader(latextext) 82 | ps = ParsingState(s=latextext, latex_context=DummyLatexContextDb()) 83 | lw = DummyWalker() 84 | 85 | parser = LatexOptionalCharsMarkerParser('*', return_none_instead_of_empty=False) 86 | 87 | nodes, parsing_state_delta = lw.parse_content(parser, token_reader=tr, parsing_state=ps) 88 | 89 | self.assertEqual( 90 | nodes, 91 | LatexNodeList([], parsing_state=ps, pos=0, pos_end=0), 92 | ) 93 | 94 | 95 | def test_simple_chars_marker_isthere_notlist(self): 96 | 97 | latextext = r'''*more''' 98 | 99 | tr = LatexTokenReader(latextext) 100 | ps = ParsingState(s=latextext, latex_context=DummyLatexContextDb()) 101 | lw = DummyWalker() 102 | 103 | parser = LatexOptionalCharsMarkerParser('*', return_full_node_list=False) 104 | 105 | nodes, parsing_state_delta = lw.parse_content(parser, token_reader=tr, parsing_state=ps) 106 | 107 | self.assertEqual( 108 | nodes, 109 | LatexCharsNode( 110 | parsing_state=ps, 111 | chars='*', 112 | pos=0, 113 | pos_end=1, 114 | ) 115 | ) 116 | 117 | 118 | def test_simple_chars_marker_notthere_notlist(self): 119 | 120 | latextext = r'''more''' 121 | 122 | tr = LatexTokenReader(latextext) 123 | ps = ParsingState(s=latextext, latex_context=DummyLatexContextDb()) 124 | lw = DummyWalker() 125 | 126 | parser = LatexOptionalCharsMarkerParser('*', return_full_node_list=False) 127 | 128 | nodes, parsing_state_delta = lw.parse_content(parser, token_reader=tr, parsing_state=ps) 129 | 130 | self.assertEqual( 131 | nodes, 132 | None, 133 | ) 134 | 135 | 136 | 137 | 138 | # --- 139 | 140 | if __name__ == '__main__': 141 | logging.basicConfig(level=logging.DEBUG) 142 | unittest.main() 143 | # 144 | -------------------------------------------------------------------------------- /test/test_latexnodes_tokenreaderbase.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import logging 3 | logger = logging.getLogger(__name__) 4 | 5 | 6 | from pylatexenc.latexnodes._tokenreaderbase import ( 7 | LatexTokenReaderBase, 8 | LatexTokenListTokenReader, 9 | ) 10 | 11 | from pylatexenc.latexnodes import ( 12 | LatexWalkerEndOfStream, 13 | LatexToken, 14 | ParsingState, 15 | ) 16 | 17 | 18 | 19 | 20 | class TestTokenReaderBase(unittest.TestCase): 21 | 22 | def test_make_token(self): 23 | tb = LatexTokenReaderBase() 24 | 25 | self.assertEqual( 26 | tb.make_token(tok='char', arg='*', pos=3), 27 | LatexToken(tok='char', arg='*', pos=3) 28 | ) 29 | 30 | def test_peek_token_or_none(self): 31 | 32 | class MyTokenReader(LatexTokenReaderBase): 33 | def __init__(self, at_end=False): 34 | super(MyTokenReader, self).__init__() 35 | self.at_end = at_end 36 | 37 | def peek_token(self, parsing_state): 38 | if not self.at_end: 39 | return self.make_token(tok='char', arg='-', pos=5) 40 | raise LatexWalkerEndOfStream() 41 | 42 | ps = ParsingState() 43 | 44 | tb = MyTokenReader(False) 45 | self.assertEqual( tb.peek_token_or_none(ps), 46 | LatexToken(tok='char', arg='-', pos=5) ) 47 | tb = MyTokenReader(True) 48 | self.assertIsNone( tb.peek_token_or_none(ps) ) 49 | 50 | 51 | 52 | class TestTokenReaderTokenList(unittest.TestCase): 53 | 54 | def test_reads_tokens(self): 55 | 56 | tlist = [ 57 | LatexToken(tok='char', arg='a', pos=0, pos_end=1, pre_space=''), 58 | LatexToken(tok='char', arg='b', pos=1, pos_end=1+2, pre_space=''), 59 | LatexToken(tok='macro', arg='relax', pos=2, pos_end=2+2+len(r'\relax'), 60 | pre_space='', post_space='\t '), 61 | ] 62 | 63 | tr = LatexTokenListTokenReader(tlist) 64 | 65 | ps = ParsingState() 66 | 67 | self.assertEqual(tr.peek_token(ps), tlist[0]) 68 | self.assertEqual(tr.peek_token(ps), tlist[0]) 69 | 70 | self.assertEqual(tr.cur_pos(), tlist[0].pos) 71 | 72 | self.assertEqual(tr.next_token(ps), tlist[0]) 73 | 74 | self.assertEqual(tr.peek_token(ps), tlist[1]) 75 | self.assertEqual(tr.peek_token(ps), tlist[1]) 76 | 77 | self.assertEqual(tr.next_token(ps), tlist[1]) 78 | 79 | self.assertEqual(tr.next_token(ps), tlist[2]) 80 | 81 | tr.move_to_token(tlist[1]) 82 | 83 | self.assertEqual(tr.next_token(ps), tlist[1]) 84 | 85 | tr.move_past_token(tlist[0]) 86 | 87 | self.assertEqual(tr.next_token(ps), tlist[1]) 88 | 89 | self.assertEqual(tr.cur_pos(), tlist[2].pos) 90 | 91 | 92 | 93 | 94 | if __name__ == '__main__': 95 | logging.basicConfig(level=logging.DEBUG) 96 | unittest.main() 97 | # 98 | -------------------------------------------------------------------------------- /test/test_macrospec_argumentsparser.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import sys 3 | import logging 4 | 5 | 6 | 7 | # from pylatexenc.macrospec._argumentsparser import ( 8 | # LatexArgumentSpec, 9 | # LatexNoArgumentsParser, 10 | # LatexArgumentsParser, 11 | # ) 12 | 13 | # from pylatexenc.latexnodes import ( 14 | # LatexWalkerTokenParseError, 15 | # LatexToken, 16 | # ParsingState 17 | # ) 18 | 19 | 20 | 21 | class TestLatexArgumentsParser(unittest.TestCase): 22 | 23 | # ............. TODO, need to write good tests ................. 24 | 25 | 26 | pass 27 | 28 | 29 | 30 | # class Test__LegacyPyltxenc2MacroArgsParserWrapper(unittest.TestCase): 31 | # def 32 | 33 | 34 | 35 | # --- 36 | 37 | if __name__ == '__main__': 38 | logging.basicConfig(level=logging.DEBUG) 39 | unittest.main() 40 | # 41 | -------------------------------------------------------------------------------- /test/test_macrospec_environmentbodyparser.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | 4 | from pylatexenc.macrospec._environmentbodyparser import ( 5 | LatexEnvironmentBodyContentsParser, 6 | ) 7 | 8 | from pylatexenc.latexnodes import ( 9 | LatexTokenReader, 10 | LatexArgumentSpec, 11 | ParsedArguments, 12 | ParsingState, 13 | ) 14 | from pylatexenc.latexnodes.nodes import * 15 | from pylatexenc.macrospec import ( 16 | LatexContextDb, 17 | MacroSpec, 18 | ParsingStateDeltaExtendLatexContextDb, 19 | ) 20 | from pylatexenc.latexwalker import LatexWalker 21 | 22 | from ._helpers_tests import ( 23 | add_not_equal_warning_to_object 24 | ) 25 | 26 | 27 | add_not_equal_warning_to_object(LatexNode) 28 | add_not_equal_warning_to_object(ParsingState) 29 | add_not_equal_warning_to_object(ParsedArguments) 30 | add_not_equal_warning_to_object(LatexArgumentSpec) 31 | 32 | 33 | class TestEnvironmentBodyContentsParser(unittest.TestCase): 34 | 35 | maxDiff = None 36 | 37 | def test_simple_1(self): 38 | 39 | # \begin{environment} 40 | latextext = r'''a+b=c\end{environment}''' 41 | 42 | tr = LatexTokenReader(latextext) 43 | lw = LatexWalker(latextext, latex_context=LatexContextDb()) 44 | ps = lw.make_parsing_state() 45 | 46 | print("ps = ", ps) 47 | 48 | parser = LatexEnvironmentBodyContentsParser('environment') 49 | nodes, parsing_state_delta = \ 50 | lw.parse_content(parser, token_reader=tr, parsing_state=ps) 51 | 52 | self.assertEqual( 53 | nodes, 54 | LatexNodeList( 55 | [ 56 | LatexCharsNode( 57 | parsing_state=ps, 58 | latex_walker=lw, 59 | chars='a+b=c', 60 | pos=0, 61 | pos_end=5, 62 | ), 63 | ], 64 | pos=0, 65 | pos_end=5, 66 | ) 67 | ) 68 | 69 | def test_contents_and_child_parsing_state_delta(self): 70 | 71 | # \begin{enumerate} 72 | latextext = r''' 73 | \item A \textbf{\localcommand}\end{enumerate}'''.lstrip() 74 | 75 | latex_context = LatexContextDb() 76 | latex_context.add_context_category( 77 | 'main-context-category', 78 | macros=[ 79 | MacroSpec("textbf", '{') 80 | ] 81 | ) 82 | 83 | tr = LatexTokenReader(latextext) 84 | lw = LatexWalker(latextext, latex_context=latex_context, tolerant_parsing=False) 85 | ps = lw.make_parsing_state() 86 | 87 | print("ps = ", ps) 88 | 89 | ps_content_delta = ParsingStateDeltaExtendLatexContextDb( 90 | extend_latex_context=dict( 91 | macros=[ 92 | MacroSpec("item", ''), 93 | ] 94 | ) 95 | ) 96 | ps_child_delta = ParsingStateDeltaExtendLatexContextDb( 97 | extend_latex_context=dict( 98 | macros=[ 99 | MacroSpec("localcommand", ''), 100 | ] 101 | ) 102 | ) 103 | 104 | parser = LatexEnvironmentBodyContentsParser( 105 | 'enumerate', 106 | contents_parsing_state_delta=ps_content_delta, 107 | child_parsing_state_delta=ps_child_delta, 108 | ) 109 | nodes, parsing_state_delta = \ 110 | lw.parse_content(parser, token_reader=tr, parsing_state=ps) 111 | 112 | ps_content = nodes[1].parsing_state 113 | ps_child = nodes[0].parsing_state 114 | ps_child2 = nodes[2].parsing_state 115 | 116 | print("ps_content =", ps_content) 117 | print("ps_child =", ps_child) 118 | 119 | nodes_expected = LatexNodeList( 120 | [ 121 | LatexMacroNode( 122 | parsing_state=ps_child, 123 | latex_walker=lw, 124 | macroname='item', 125 | spec=ps_content.latex_context.get_macro_spec('item'), 126 | nodeargd=ParsedArguments(argnlist=LatexNodeList([]),), 127 | pos=0, 128 | pos_end=6, 129 | macro_post_space=' ', 130 | ), 131 | LatexCharsNode( 132 | parsing_state=ps_content, 133 | latex_walker=lw, 134 | chars='A ', 135 | pos=6, 136 | pos_end=8, 137 | ), 138 | LatexMacroNode( 139 | parsing_state=ps_child2, 140 | latex_walker=lw, 141 | macroname='textbf', 142 | spec=ps.latex_context.get_macro_spec('textbf'), 143 | nodeargd=ParsedArguments( 144 | argnlist=[ 145 | LatexGroupNode( 146 | parsing_state=ps_child2, 147 | latex_walker=lw, 148 | delimiters=('{','}'), 149 | nodelist=LatexNodeList( 150 | [ 151 | LatexMacroNode( 152 | parsing_state=ps_child2, 153 | latex_walker=lw, 154 | spec=ps_child2.latex_context \ 155 | .get_macro_spec('localcommand'), 156 | macroname='localcommand', 157 | nodeargd=ParsedArguments( 158 | argnlist=LatexNodeList([]), 159 | ), 160 | pos=16, 161 | pos_end=29, 162 | macro_post_space='', 163 | ), 164 | ], 165 | pos=16, 166 | pos_end=29, 167 | ), 168 | pos=15, 169 | pos_end=30, 170 | ) 171 | ], 172 | arguments_spec_list=[ 173 | LatexArgumentSpec(argname=None, parser='{'), 174 | ], 175 | ), 176 | pos=8, 177 | pos_end=30, 178 | macro_post_space='', 179 | ), 180 | ], 181 | pos=0, 182 | pos_end=30, 183 | ) 184 | 185 | print(nodes) 186 | print(nodes_expected) 187 | 188 | # check that ps_content is the parsing state by inspecting the context db 189 | self.assertIsNotNone( ps_content.latex_context.get_macro_spec('item') ) 190 | self.assertIsNone( ps_content.latex_context.get_macro_spec('localcommand') ) 191 | 192 | # check that ps_child is the parsing state by inspecting the context db 193 | self.assertIsNone( ps_child.latex_context.get_macro_spec('item') ) 194 | self.assertIsNotNone( ps_child.latex_context.get_macro_spec('localcommand') ) 195 | self.assertIsNone( ps_child2.latex_context.get_macro_spec('item') ) 196 | self.assertIsNotNone( ps_child2.latex_context.get_macro_spec('localcommand') ) 197 | 198 | 199 | self.assertEqual( 200 | nodes, 201 | nodes_expected 202 | ) 203 | 204 | if __name__ == '__main__': 205 | unittest.main() 206 | -------------------------------------------------------------------------------- /test/test_macrospec_latexcontextdb.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import sys 3 | import logging 4 | logger = logging.getLogger(__name__) 5 | 6 | from pylatexenc.macrospec._latexcontextdb import ( 7 | LatexContextDb, 8 | _autogen_category_prefix 9 | ) 10 | 11 | from pylatexenc.macrospec import ( 12 | MacroSpec, 13 | EnvironmentSpec, 14 | SpecialsSpec, 15 | ) 16 | 17 | 18 | 19 | class TestLatexContextDb(unittest.TestCase): 20 | 21 | 22 | # TODO........ need more tests here 23 | 24 | 25 | 26 | def test_extended_with(self): 27 | 28 | context = LatexContextDb() 29 | context.add_context_category( 30 | 'base-category', 31 | macros=[ MacroSpec('base', '{'), ], 32 | environments=[ EnvironmentSpec('baseenv', '{'), ], 33 | specials=[ SpecialsSpec('~'), ], 34 | ) 35 | context.freeze() 36 | 37 | logger.debug("context's category list = %r", context.category_list) 38 | logger.debug("context's d = %r", context.d) 39 | logger.debug("context's lookup maps are = %r", context.lookup_chain_maps) 40 | 41 | extd1 = dict( 42 | macros=[ MacroSpec('more', '{'), ], 43 | environments=[ EnvironmentSpec('moreenv', '{'), ], 44 | specials=[ SpecialsSpec('!'), ], 45 | ) 46 | context2 = context.extended_with(**extd1) 47 | 48 | logger.debug("context2's category list = %r", context2.category_list) 49 | logger.debug("context2's d = %r", context2.d) 50 | logger.debug("context2's lookup maps are = %r", context2.lookup_chain_maps) 51 | 52 | self.assertEqual(len(context2.category_list), 2) 53 | self.assertTrue(context2.category_list[0].startswith(_autogen_category_prefix)) 54 | 55 | self.assertEqual(context2.get_macro_spec('base'), 56 | context.get_macro_spec('base')) 57 | self.assertEqual(context2.get_environment_spec('baseenv'), 58 | context.get_environment_spec('baseenv')) 59 | self.assertEqual(context2.get_specials_spec('~'), 60 | context.get_specials_spec('~')) 61 | self.assertEqual(context2.test_for_specials('~~~~~~~', pos=0), 62 | context.get_specials_spec('~')) 63 | self.assertEqual(context2.get_macro_spec('more'), extd1['macros'][0]) 64 | self.assertEqual(context2.get_environment_spec('moreenv'), extd1['environments'][0]) 65 | self.assertEqual(context2.get_specials_spec('!'), extd1['specials'][0]) 66 | self.assertEqual(context2.test_for_specials('!!!!!', pos=0), extd1['specials'][0]) 67 | 68 | extd2 = dict( 69 | macros=[ MacroSpec('evenmore', '{'), ], 70 | environments=[ EnvironmentSpec('baseenv', '{'), ], # override baseenv 71 | specials=[ SpecialsSpec('!!'), ], 72 | ) 73 | context3 = context2.extended_with(**extd2) 74 | 75 | # extended_with() a second time shouldn't add a new category as the 76 | # first category is already an autogenerated one 77 | self.assertEqual(len(context3.category_list), 2) 78 | self.assertEqual(context3.category_list, context2.category_list) 79 | self.assertTrue(context3.category_list[0].startswith(_autogen_category_prefix)) 80 | 81 | logger.debug("context3's category list = %r", context3.category_list) 82 | logger.debug("context3's d = %r", context3.d) 83 | logger.debug("context3's lookup maps are = %r", context3.lookup_chain_maps) 84 | 85 | self.assertEqual(context3.get_macro_spec('base'), 86 | context.get_macro_spec('base')) 87 | # self.assertEqual(context3.get_environment_spec('baseenv'), 88 | # context.get_environment_spec('baseenv')) # NO! 89 | self.assertEqual(context3.get_specials_spec('~'), 90 | context.get_specials_spec('~')) 91 | self.assertEqual(context3.test_for_specials('~~~~~~~', pos=0), 92 | context.get_specials_spec('~')) 93 | self.assertEqual(context3.get_macro_spec('more'), extd1['macros'][0]) 94 | self.assertEqual(context3.get_macro_spec('evenmore'), extd2['macros'][0]) 95 | self.assertEqual(context3.get_environment_spec('moreenv'), extd1['environments'][0]) 96 | self.assertEqual(context3.get_environment_spec('baseenv'), extd2['environments'][0]) 97 | self.assertEqual(context3.get_specials_spec('!'), extd1['specials'][0]) 98 | self.assertEqual(context3.get_specials_spec('!!'), extd2['specials'][0]) 99 | self.assertEqual(context3.test_for_specials('!!!!!', pos=0), extd2['specials'][0]) 100 | 101 | 102 | 103 | 104 | 105 | # --- 106 | 107 | if __name__ == '__main__': 108 | logging.basicConfig(level=logging.DEBUG) 109 | unittest.main() 110 | # 111 | -------------------------------------------------------------------------------- /test/test_macrospec_macrocallparser.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import sys 3 | import logging 4 | 5 | 6 | 7 | # from pylatexenc.macrospec._macrocallparser import ( 8 | # LatexMacroCallParser, 9 | # LatexEnvironmentCallParser, 10 | # LatexSpecialsCallParser, 11 | # ) 12 | 13 | # from pylatexenc.latexnodes import ( 14 | # ParsingState 15 | # ) 16 | # from pylatexenc.latexnodes.nodes import * 17 | 18 | 19 | 20 | class TestLatexMacroCallParser(unittest.TestCase): 21 | 22 | # ............. TODO, need to write good tests ................. 23 | 24 | 25 | pass 26 | 27 | 28 | 29 | # --- 30 | 31 | if __name__ == '__main__': 32 | logging.basicConfig(level=logging.DEBUG) 33 | unittest.main() 34 | # 35 | -------------------------------------------------------------------------------- /test/test_pylatexenc.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | ### BEGIN_TEST_PYLATEXENC_SKIP 4 | 5 | import toml 6 | import os.path 7 | 8 | import pylatexenc 9 | 10 | 11 | # thanks https://github.com/python-poetry/poetry/issues/144#issuecomment-877835259 12 | 13 | class TestHardcodedPackageVersion(unittest.TestCase): 14 | 15 | def test_versions_are_in_sync(self): 16 | """Checks if the pyproject.toml and package.__init__.py __version__ are in sync.""" 17 | 18 | path = os.path.join( os.path.dirname(__file__), '..', "pyproject.toml" ) 19 | with open(path) as fpp: 20 | pyproject = toml.loads(fpp.read()) 21 | pyproject_version = pyproject["tool"]["poetry"]["version"] 22 | 23 | package_init_version = pylatexenc.__version__ 24 | 25 | self.assertEqual(package_init_version, pyproject_version) 26 | 27 | 28 | if __name__ == '__main__': 29 | unittest.main() 30 | 31 | ### END_TEST_PYLATEXENC_SKIP 32 | -------------------------------------------------------------------------------- /test/test_util.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from pylatexenc import _util 4 | 5 | 6 | class TestLineNumbersCalculator(unittest.TestCase): 7 | 8 | def test_simple(self): 9 | 10 | s = """\ 11 | one 12 | two 13 | three 14 | four 15 | five 16 | """.lstrip() 17 | 18 | ln = _util.LineNumbersCalculator(s) 19 | 20 | self.assertEqual( ln.pos_to_lineno_colno(0), (1,0) ) 21 | self.assertEqual( ln.pos_to_lineno_colno(1), (1,1) ) 22 | self.assertEqual( ln.pos_to_lineno_colno(2), (1,2) ) 23 | self.assertEqual( ln.pos_to_lineno_colno(3), (1,3) ) 24 | self.assertEqual( ln.pos_to_lineno_colno(4), (2,0) ) 25 | self.assertEqual( ln.pos_to_lineno_colno(5), (2,1) ) 26 | self.assertEqual( ln.pos_to_lineno_colno(6), (2,2) ) 27 | self.assertEqual( ln.pos_to_lineno_colno(7), (2,3) ) 28 | self.assertEqual( ln.pos_to_lineno_colno(8), (3,0) ) 29 | self.assertEqual( ln.pos_to_lineno_colno(9), (3,1) ) 30 | 31 | self.assertEqual( ln.pos_to_lineno_colno(23), (5,4) ) 32 | 33 | def test_as_dict(self): 34 | 35 | s = """\ 36 | one 37 | two 38 | three 39 | four 40 | five 41 | """.lstrip() 42 | 43 | ln = _util.LineNumbersCalculator(s) 44 | 45 | self.assertEqual( ln.pos_to_lineno_colno(9, as_dict=True), 46 | { 'lineno': 3, 47 | 'colno': 1 } ) 48 | -------------------------------------------------------------------------------- /tools/gen_l2t_from_lenc.py: -------------------------------------------------------------------------------- 1 | # 2 | # Inspect latexencode rules to see if there are symbols that we can use for 3 | # latex2text, too 4 | # 5 | 6 | # Py3 only script 7 | import sys 8 | assert sys.version_info > (3,0) 9 | 10 | import unicodedata 11 | 12 | import logging 13 | logging.basicConfig(level=logging.INFO) 14 | logger = logging.getLogger(__name__) 15 | 16 | 17 | from pylatexenc import latexwalker, latex2text, latexencode #, macrospec 18 | 19 | l2t_default_context = latex2text.get_default_latex_context_db() 20 | 21 | def extract_symbol_node(nodelist, uni, latex): 22 | 23 | if len(nodelist) != 1: 24 | # more than one "thing" 25 | logger.warning("Got nodelist with more than one node, skipping (%s): %s = %r", 26 | chr(uni), latex, nodelist) 27 | return 28 | 29 | thenode = nodelist[0] 30 | 31 | if not thenode.isNodeType(latexwalker.LatexMacroNode): 32 | logger.warning("Got node that is not a macro, skipping (%s): %s = %r", 33 | chr(uni), latex, thenode) 34 | return 35 | 36 | if thenode.macroname == 'ensuremath': 37 | # ignore, parse contents instead 38 | if thenode.nodeargd is None or not thenode.nodeargd.argnlist or \ 39 | len(thenode.nodeargd.argnlist) != 1: 40 | logger.warning(r"\ensuremath with no arguments or wrong # of arguments (%s): %s = %r", 41 | chr(uni), latex, nodelist) 42 | return 43 | 44 | argnode = thenode.nodeargd.argnlist[0] 45 | if argnode.isNodeType(latexwalker.LatexGroupNode): 46 | argnodelist = argnode.nodelist 47 | else: 48 | argnodelist = [ argnode ] 49 | 50 | return extract_symbol_node(argnodelist, uni, latex) 51 | 52 | l2t_mspec = l2t_default_context.get_macro_spec(thenode.macroname) 53 | if l2t_mspec is not None and l2t_mspec.macroname: 54 | # macro found, already known 55 | logger.debug("Macro found (%s): %r", chr(uni), thenode) 56 | return 57 | 58 | if thenode.nodeargd and thenode.nodeargd.argnlist: 59 | logger.warning(r"Macro %r for ‘%s’ is not known to latex2text but it has arguments", 60 | thenode, chr(uni)) 61 | return 62 | 63 | # got a symbol macro, go for it: 64 | print(" MacroTextSpec(%r, u'\\N{%s}'), # ‘%s’" % ( 65 | thenode.macroname, unicodedata.name(chr(uni)), chr(uni) 66 | )) 67 | 68 | 69 | for builtin_name in ('defaults', 'unicode-xml'): 70 | 71 | rules = latexencode.get_builtin_conversion_rules(builtin_name) 72 | 73 | logger.info("Reader latexencode defaults %r", builtin_name) 74 | print(" # Rules from latexencode defaults '%s'"%(builtin_name)) 75 | 76 | for rule in rules: 77 | 78 | if rule.rule_type != latexencode.RULE_DICT: 79 | logger.warning("Ignoring non-dict rule type %d", rule.rule_type) 80 | continue 81 | 82 | # inspect rules for symbols that latex2text might not already be aware of 83 | for uni, latex in rule.rule.items(): 84 | try: 85 | nodelist, _, _ = latexwalker.LatexWalker(latex, tolerant_parsing=False).get_latex_nodes() 86 | except latexwalker.LatexWalkerError as e: 87 | logger.warning("Error parsing %r (%s): %s", latex, chr(uni), e) 88 | continue 89 | 90 | extract_symbol_node(nodelist, uni, latex) 91 | -------------------------------------------------------------------------------- /tools/gen_xml_dic.py: -------------------------------------------------------------------------------- 1 | # 2 | # mini-script to generate the pylatexenc.latexencode._uni2latexmap_xml dict mapping 3 | # 4 | import re 5 | import sys 6 | 7 | if sys.version_info.major > 2: 8 | # python 3 9 | unichr = chr 10 | 11 | from xml.etree import ElementTree as ET 12 | 13 | e = ET.parse('unicode.xml') 14 | 15 | d = {} 16 | dnames = {} 17 | 18 | for chxml in e.find('charlist').iter('character'): 19 | Uid = chxml.attrib['id'] 20 | if '-' in Uid: 21 | # composite/multiple characters not supported 22 | continue 23 | charord = int(Uid.lstrip('U'), 16) 24 | latexxml = chxml.find('latex') 25 | if latexxml is None: 26 | continue 27 | latexval = latexxml.text 28 | if latexval == unichr(charord): 29 | # "latex" representation is the same char directly 30 | continue 31 | if charord == 0x20: 32 | # skip space char 33 | continue 34 | if latexval.startswith(r'\ElsevierGlyph') or latexval.startswith(r'\El') \ 35 | or latexval.startswith(r'\ensuremath{\El'): 36 | continue 37 | if re.search(r'\\[a-zA-Z]+\s+$', latexval): 38 | # ends with named macro+space, remove space because 39 | # latexencode.UnicodeToLatexEncoder will handle that with 40 | # replacement_latex_protection 41 | latexval = latexval.rstrip() 42 | d[charord] = latexval 43 | dnames[charord] = chxml.find('description').text 44 | 45 | # dump dictionary into new module file in current working directory 46 | outputfile = '_uni2latexmap_xml.py' 47 | 48 | HEADER = """\ 49 | # -*- coding: utf-8 -*- 50 | # 51 | # Automatically generated from unicode.xml by gen_xml_dic.py 52 | # 53 | 54 | """ 55 | 56 | with open(outputfile, 'w') as f: 57 | f.write(HEADER) 58 | 59 | f.write("uni2latex = {\n") 60 | 61 | for k,v in d.items(): 62 | f.write("0x%04X: %r,\n"%(k, v)) 63 | 64 | f.write("}\n") 65 | 66 | print("Successfully generated file %s"%(outputfile)) 67 | 68 | 69 | # Now see which characters we don't have in our default set of symbols 70 | from pylatexenc.latexencode._uni2latexmap import uni2latex as uni2latex_defaults 71 | 72 | missing_keys = set(d.keys()).difference(set(uni2latex_defaults.keys())) 73 | if missing_keys: 74 | print("#\n# Missing keys added from unicode.xml\n#\n") 75 | for k in sorted(missing_keys): 76 | if "'" not in d[k]: 77 | therepr = "r'"+d[k]+"'" 78 | else: 79 | therepr = repr(d[k]) 80 | thedef = "0x%04X: %s,"%(k, therepr) 81 | print("%-50s# %s [%s]"%(thedef, dnames[k], unichr(k))) 82 | 83 | -------------------------------------------------------------------------------- /tools/unicode.xml.LICENSE: -------------------------------------------------------------------------------- 1 | [This notice should be placed within redistributed or derivative software code 2 | when appropriate. This particular formulation became active on December 31 2002, 3 | superseding the 1998 version.] 4 | 5 | unicode.xml: https://www.w3.org/2003/entities/2007xml/unicode.xml 6 | https://www.w3.org/TR/xml-entity-names/#source 7 | 8 | Copyright © 2015 World Wide Web Consortium, (Massachusetts Institute of 9 | Technology, European Research Consortium for Informatics and Mathematics, 10 | Keio University, Beihang). All Rights Reserved. This work is distributed 11 | under the W3C® Software License [1] in the hope that it will be useful, but 12 | WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY 13 | or FITNESS FOR A PARTICULAR PURPOSE. 14 | 15 | [1] http://www.w3.org/Consortium/Legal/copyright-software 16 | 17 | 18 | ---------- 19 | 20 | Copyright David Carlisle 1999-2015 21 | 22 | Use and distribution of this code are permitted under the terms of the 23 | W3C Software Notice and License. 24 | http://www.w3.org/Consortium/Legal/2002/copyright-software-20021231.html 25 | 26 | ---------- 27 | 28 | LICENSE 29 | 30 | By obtaining, using and/or copying this work, you (the licensee) agree that you 31 | have read, understood, and will comply with the following terms and conditions. 32 | 33 | Permission to copy, modify, and distribute this software and its documentation, 34 | with or without modification, for any purpose and without fee or royalty is 35 | hereby granted, provided that you include the following on ALL copies of the 36 | software and documentation or portions thereof, including modifications: 37 | 38 | - The full text of this NOTICE in a location viewable to users of the 39 | redistributed or derivative work. 40 | 41 | - Any pre-existing intellectual property disclaimers, notices, or terms and 42 | conditions. If none exist, the W3C Software Short Notice should be 43 | included (hypertext is preferred, text is permitted) within the body of 44 | any redistributed or derivative code. 45 | 46 | - Notice of any changes or modifications to the files, including the date 47 | changes were made. (We recommend you provide URIs to the location from 48 | which the code is derived.) 49 | 50 | DISCLAIMERS 51 | 52 | THIS SOFTWARE AND DOCUMENTATION IS PROVIDED "AS IS," AND COPYRIGHT HOLDERS MAKE 53 | NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 54 | TO, WARRANTIES OF MERCHANTABILITY OR FITNESS FOR ANY PARTICULAR PURPOSE OR THAT 55 | THE USE OF THE SOFTWARE OR DOCUMENTATION WILL NOT INFRINGE ANY THIRD PARTY 56 | PATENTS, COPYRIGHTS, TRADEMARKS OR OTHER RIGHTS. 57 | 58 | COPYRIGHT HOLDERS WILL NOT BE LIABLE FOR ANY DIRECT, INDIRECT, SPECIAL OR 59 | CONSEQUENTIAL DAMAGES ARISING OUT OF ANY USE OF THE SOFTWARE OR DOCUMENTATION. 60 | 61 | The name and trademarks of copyright holders may NOT be used in advertising or 62 | publicity pertaining to the software without specific, written prior 63 | permission. Title to copyright in this software and any associated documentation 64 | will at all times remain with copyright holders. 65 | 66 | NOTES 67 | 68 | This version: http://www.w3.org/Consortium/Legal/2002/copyright-software-20021231 69 | 70 | This formulation of W3C's notice and license became active on December 31 71 | 2002. This version removes the copyright ownership notice such that this license 72 | can be used with materials other than those owned by the W3C, reflects that 73 | ERCIM is now a host of the W3C, includes references to this specific dated 74 | version of the license, and removes the ambiguous grant of "use". Otherwise, 75 | this version is the same as the previous version and is written so as to 76 | preserve the Free Software Foundation's assessment of GPL compatibility and 77 | OSI's certification under the Open Source Definition. 78 | --------------------------------------------------------------------------------