├── .gitattributes ├── .gitignore ├── LICENSE ├── MANIFEST.in ├── README.md ├── conda-forge ├── README.md ├── panflute │ └── meta.yaml └── sugartex │ └── meta.yaml ├── examples ├── README.md ├── examples.md ├── examples.md.md └── examples.pdf ├── setup.cfg ├── setup.py ├── sugartex.md ├── sugartex.pdf ├── sugartex ├── __init__.py ├── _version.py ├── kiwi.py ├── pre_sugartex.py ├── sugartex_filter.py └── sugartex_pandoc_filter.py ├── upload ├── upload.txt └── versioneer.py /.gitattributes: -------------------------------------------------------------------------------- 1 | * text eol=lf 2 | *.* text=auto !eol 3 | *.bat text eol=crlf 4 | *.cmd text eol=crlf 5 | *.sh text eol=lf 6 | 7 | sugartex/_version.py export-subst 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.egg-info 2 | *.whl 3 | *.pyc 4 | build/ 5 | dist/ 6 | __pycache__/ 7 | htmlcov/ 8 | 9 | .coverage 10 | 11 | .RData 12 | .Rhistory 13 | 14 | *.html 15 | _build/ 16 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2018 Peter Zagubisalo 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include sugartex/* 2 | include versioneer.py 3 | include sugartex/_version.py 4 | include LICENSE 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SugarTeX 2 | 3 | SugarTeX is a more readable LaTeX language extension and transcompiler to LaTeX. Designed to be used instead of `$formula$` insertions to markdown. 4 | 5 | See [SugarTeX documentation](https://github.com/kiwi0fruit/sugartex/blob/master/sugartex.md). Examples of input to output conversion see in [this PDF](https://github.com/kiwi0fruit/sugartex/blob/master/examples/examples.pdf?raw=true). 6 | 7 | I use Markdown with Python code blocks for document programming via [Pandoctools](https://github.com/kiwi0fruit/pandoctools) (like R-Markdown). 8 | 9 | Both Python and Markdown are very readable languages. Unfortunately LaTeX is not like this. So I wrote SugaTeX extension+transpiler that is highly readable. In order to achieve this it heavily uses Unicode so that SugarTeX install instructions even have recommended monospace font fallback chains. And more: [SugarTeX Completions](#sugartex-completions-for-atom) Atom package helps write all that Unicode in a moment. 10 | 11 | I am trying to incorporate LaTeX into .md using the Markdown Philosophy of “you should write something that's readable as plain text, without compilation, also”. 12 | 13 | 14 | ## Install 15 | 16 | Install as part of [Pandoctools](https://github.com/kiwi0fruit/pandoctools) - convenient interface and works out of the box. 17 | 18 | Via conda: 19 | 20 | ```bash 21 | conda install -c defaults -c conda-forge sugartex 22 | ``` 23 | 24 | Via pip: 25 | 26 | ```bash 27 | pip install sugartex 28 | ``` 29 | 30 | 31 | ### Atom editor with full Unicode support 32 | 33 | Highly recommended to install [Atom editor](https://atom.io/) as it's the best for markdown. 34 | 35 | Atom is perfect for Unicode rich texts. But you need to install some fonts first. See [**this instruction**](https://github.com/kiwi0fruit/open-fonts/blob/master/README.md#best-monospace) how to install recommended font fallback chains for Unicode support. 36 | 37 | 38 | ### SugarTeX Completions for Atom 39 | 40 | Install [SugarTeX Completions](https://atom.io/packages/sugartex-completions) package for easy typing SugarTeX and lots of other Unicode characters. (it's incompatible with [latex-completions](https://atom.io/packages/latex-completions) package). 41 | 42 | In the [SugarTeX documentation](https://github.com/kiwi0fruit/sugartex/blob/master/sugartex.md) appropriate shortcuts for SugarTeX Completions for Atom are given. 43 | 44 | 45 | ## Usage examples 46 | 47 | Example of input to output conversion is at the end of [this PDF]. 48 | 49 | Windows: 50 | 51 | ```bat 52 | @echo off 53 | chcp 65001 > NUL 54 | set PYTHONIOENCODING=utf-8 55 | set PYTHONUTF8=1 56 | 57 | type doc.md | ^ 58 | pre-sugartex | ^ 59 | pandoc -f markdown --filter sugartex -o doc.md.md 60 | ``` 61 | 62 | Unix (`convert` bash script to use like `./convert doc.md`): 63 | 64 | ```bash 65 | #!/bin/bash 66 | export PYTHONIOENCODING=utf-8 67 | export PYTHONUTF8=1 68 | 69 | cat "$@" | \ 70 | pre-sugartex | \ 71 | pandoc -f markdown --filter sugartex -o "$@.md" 72 | ``` 73 | (or `pandoc -f markdown --filter sugartex --to docx+styles -o "$@.docx"`) 74 | 75 | Or splitting Pandoc reader-writer: 76 | 77 | ```sh 78 | export PYTHONIOENCODING=utf-8 79 | 80 | cat doc.md | \ 81 | pre-sugartex | \ 82 | pandoc -f markdown -t json | \ 83 | sugartex --kiwi | \ 84 | pandoc -f json -o doc.md.md 85 | ``` 86 | 87 | [Panflute](https://github.com/sergiocorreia/panflute) scripts are also installed so you can use it in default Panflute [automation interface in metadata](http://scorreia.com/software/panflute/guide.html#running-filters-automatically) or in recommend [panfl](https://github.com/kiwi0fruit/pandoctools/blob/master/docs/panfl.md) CLI: 88 | 89 | * `panfl sugartex --to markdown`, 90 | * `panfl sugartex.kiwi -t markdown`. 91 | -------------------------------------------------------------------------------- /conda-forge/README.md: -------------------------------------------------------------------------------- 1 | * [sugartex](https://github.com/conda-forge/sugartex-feedstock) 2 | * [panflute](https://github.com/conda-forge/panflute-feedstock) 3 | -------------------------------------------------------------------------------- /conda-forge/panflute/meta.yaml: -------------------------------------------------------------------------------- 1 | {% set name = "panflute" %} 2 | {% set version = "1.11.2" %} 3 | 4 | package: 5 | name: {{ name|lower }} 6 | version: {{ version }} 7 | 8 | source: 9 | url: https://pypi.io/packages/source/{{ name[0] }}/{{ name }}/{{ name }}-{{ version }}.tar.gz 10 | sha256: 61118563a8be3a8e71b9ab3e92c5a21712ed6ce647776914ed978d5f15c673f9 11 | 12 | build: 13 | noarch: python 14 | number: 0 15 | entry_points: 16 | - panflute = panflute:main 17 | - panfl = panflute:panfl 18 | script: "{{ PYTHON }} -m pip install . --no-deps -vv" 19 | 20 | requirements: 21 | host: 22 | - python 23 | - pip 24 | run: 25 | - python 26 | - pyyaml 27 | - future 28 | - shutilwhich 29 | - click 30 | 31 | test: 32 | imports: 33 | - panflute 34 | commands: 35 | - panfl --help 36 | 37 | about: 38 | home: https://github.com/sergiocorreia/panflute 39 | license: BSD-3-Clause 40 | license_family: BSD 41 | license_file: LICENSE 42 | summary: 'Pythonic Pandoc filters' 43 | doc_url: http://scorreia.com/software/panflute/ 44 | dev_url: https://github.com/sergiocorreia/panflute 45 | 46 | extra: 47 | recipe-maintainers: 48 | - kiwi0fruit 49 | -------------------------------------------------------------------------------- /conda-forge/sugartex/meta.yaml: -------------------------------------------------------------------------------- 1 | {% set name = "sugartex" %} 2 | {% set version = "0.1.16" %} 3 | 4 | package: 5 | name: {{ name|lower }} 6 | version: {{ version }} 7 | 8 | source: 9 | url: https://pypi.io/packages/source/{{ name[0] }}/{{ name }}/{{ name }}-{{ version }}.tar.gz 10 | sha256: e14ffbef643b34d6d4cb0de5b57d55e58f16a9afbe7e18e99ab4e33a4f92dd30 11 | 12 | build: 13 | noarch: python 14 | number: 0 15 | entry_points: 16 | - sugartex = sugartex.sugartex_pandoc_filter:cli 17 | - pre-sugartex = sugartex.pre_sugartex:main 18 | script: "{{ PYTHON }} -m pip install . --no-deps -vv" 19 | 20 | requirements: 21 | host: 22 | - python >=3.6 23 | - pip 24 | run: 25 | - python >=3.6 26 | - panflute >=1.11.2 27 | 28 | test: 29 | imports: 30 | - sugartex 31 | commands: 32 | - sugartex --help 33 | - pre-sugartex --help 34 | 35 | about: 36 | home: https://github.com/kiwi0fruit/sugartex 37 | license: MIT 38 | license_family: MIT 39 | license_file: LICENSE 40 | summary: "More readable LaTeX language extension and transcompiler to LaTeX" 41 | doc_url: https://github.com/kiwi0fruit/sugartex 42 | dev_url: https://github.com/kiwi0fruit/sugartex 43 | 44 | extra: 45 | recipe-maintainers: 46 | - kiwi0fruit 47 | -------------------------------------------------------------------------------- /examples/README.md: -------------------------------------------------------------------------------- 1 | The SugarTeX [**examples PDF**](examples.pdf?raw=true). 2 | 3 | Was created via this [Markdown source](examples.md?raw=true). You can find output LaTeX source in [this document](examples.md.md). 4 | 5 | ### Using in Jupyter 6 | 7 | ```py 8 | import IPython.display as ds 9 | import math 10 | from sugartex import stex 11 | 12 | ds.Markdown(stex(f''' 13 | 14 | Some **dynamic** Markdown text with SugarTeX formula: ˎα^˱{math.pi:1.4f}˲ˎ. 15 | 16 | ˎˎ 17 | ˱∇ × [ ⃗B] - 1∕c ∂[ ⃗E]∕∂t ˳= 4π∕c [ ⃗j] ¦# 18 | ∇ ⋅ [ ⃗E]\ ˳= 4πρ ¦ 19 | ∇ × [ ⃗E] + 1∕c ∂[ ⃗B]∕∂t ˳= [ ⃗0] ¦ 20 | ∇ ⋅ [ ⃗B]\ ˳= 0 ˲ 21 | ˎˎ 22 | 23 | 24 | where ˎ[ ⃗B], [ ⃗E], [ ⃗j]: ℝ⁴ → ℝ³ˎ – vector functions of the form 25 | ˎ(t,x,y,z) ↦ [ ⃗f](t,x,y,z), [ ⃗f] = (f_˹x˺, f_˹y˺, f_˹z˺)ˎ. 26 | ''')) 27 | ``` 28 | -------------------------------------------------------------------------------- /examples/examples.md: -------------------------------------------------------------------------------- 1 | --- 2 | eval: False 3 | pandoctools: 4 | profile: Kiwi 5 | # out: "*.*.md" 6 | out: "*.pdf" 7 | --- 8 | 9 | ``` 10 | See @eq:max. 11 | \ˎ\ˎ 12 | ˱∇ × [⠘B] - 1∕c ∂[⠘E]∕∂t ˳= 4π∕c [⠘j] ¦# 13 | ∇ ⋅ [⠘E]\ ˳= 4πρ ¦ 14 | ∇ × [⠘E] + 1∕c ∂[⠘B]∕∂t ˳= [⠘0] ¦ 15 | ∇ ⋅ [⠘B]\ ˳= 0 ˲ 16 | ,\ˎ\ˎ{#eq:max} 17 | 18 | where \ˎ[⠘B], [⠘E], [⠘j]: ℝ⁴ → ℝ³\ˎ – vector functions of the form 19 | \ˎ(t,x,y,z) ↦ [⠘f](t,x,y,z), [⠘f] = (f_˹x˺, f_˹y˺, f_˹z˺)\ˎ. 20 | ``` 21 | See @eq:max. 22 | ˎˎ 23 | ˱∇ × [⠘B] - 1∕c ∂[⠘E]∕∂t ˳= 4π∕c [⠘j] ¦# 24 | ∇ ⋅ [⠘E]\ ˳= 4πρ ¦ 25 | ∇ × [⠘E] + 1∕c ∂[⠘B]∕∂t ˳= [⠘0] ¦ 26 | ∇ ⋅ [⠘B]\ ˳= 0 ˲ 27 | ,ˎˎ{#eq:max} 28 | 29 | where ˎ[⠘B], [⠘E], [⠘j]: ℝ⁴ → ℝ³ˎ – vector functions of the form 30 | ˎ(t,x,y,z) ↦ [⠘f](t,x,y,z), [⠘f] = (f_˹x˺, f_˹y˺, f_˹z˺)ˎ. 31 | 32 | ---- 33 | 34 | 35 | ``` 36 | See @eq:max2. 37 | \ˎ\ˎ 38 | ˱∇ × 𝐁 - 1∕c ∂𝐄∕∂t ˳= 4π∕c 𝐣 ¦# 39 | ∇ ⋅ 𝐄\ ˳= 4πρ ¦ 40 | ∇ × 𝐄 + 1∕c ∂𝐁∕∂t ˳= 𝟎 ¦ 41 | ∇ ⋅ 𝐁\ ˳= 0 ˲ 42 | ,\ˎ\ˎ{#eq:max2} 43 | 44 | where \ˎ𝐁, 𝐄, 𝐣: ℝ⁴ → ℝ³\ˎ – vector functions of the form 45 | \ˎ(t,x,y,z) ↦ 𝐟(t,x,y,z), 𝐟 = (f_˹x˺, f_˹y˺, f_˹z˺)\ˎ. 46 | ``` 47 | See @eq:max2. 48 | ˎˎ 49 | ˱∇ × 𝐁 - 1∕c ∂𝐄∕∂t ˳= 4π∕c 𝐣 ¦# 50 | ∇ ⋅ 𝐄\ ˳= 4πρ ¦ 51 | ∇ × 𝐄 + 1∕c ∂𝐁∕∂t ˳= 𝟎 ¦ 52 | ∇ ⋅ 𝐁\ ˳= 0 ˲ 53 | ,ˎˎ{#eq:max2} 54 | 55 | where ˎ𝐁, 𝐄, 𝐣: ℝ⁴ → ℝ³ˎ – vector functions of the form 56 | ˎ(t,x,y,z) ↦ 𝐟(t,x,y,z), 𝐟 = (f_˹x˺, f_˹y˺, f_˹z˺)ˎ. 57 | 58 | ---- 59 | 60 | ``` 61 | \ˎ\ˎ [⠋A] = [⠋B]˹ᵀ˺ [⠋C] [⠋B] \ˎ\ˎ 62 | 63 | \ˎ\ˎ 𝐀 = 𝐁˹ᵀ˺𝐂 𝐁 \ˎ\ˎ 64 | ``` 65 | ˎˎ [⠋A] = [⠋B]˹ᵀ˺ [⠋C] [⠋B] ˎˎ 66 | 67 | ˎˎ 𝐀 = 𝐁˹ᵀ˺𝐂 𝐁 ˎˎ 68 | 69 | ``` 70 | \ˎ\ˎ 71 | ˱[ x₁₁ ˳x₁₂ ˳x₁₃ ˳… ˳x₁ₙ ¦⠋ 72 | x₂₁ ˳x₂₂ ˳x₂₃ ˳… ˳x₂ₙ ¦ 73 | ⋮ ˳ ⋮ ˳ ⋮ ˳⋱ ˳ ⋮ ¦ 74 | xₚ₁ ˳xₚ₂ ˳xₚ₃ ˳… ˳xₚₙ ]˲ \ˎ\ˎ 75 | ``` 76 | ˎˎ 77 | ˱[ x₁₁ ˳x₁₂ ˳x₁₃ ˳… ˳x₁ₙ ¦⠋ 78 | x₂₁ ˳x₂₂ ˳x₂₃ ˳… ˳x₂ₙ ¦ 79 | ⋮ ˳ ⋮ ˳ ⋮ ˳⋱ ˳ ⋮ ¦ 80 | xₚ₁ ˳xₚ₂ ˳xₚ₃ ˳… ˳xₚₙ ]˲ ˎˎ 81 | 82 | ---- 83 | 84 | ``` 85 | \ˎ\ˎ ˋdefˋB{ 86 | ˱[ ax₀ + by₁ ¦⠋ 87 | ax₁ + by₂ ¦ 88 | ⋮ ¦ 89 | ax_{N-1} + by_{N-1} ]˲ 90 | }¦ 91 | ˋB = a[⠘x] + b[⠘y] \ˎ\ˎ 92 | ``` 93 | ˎˎ ˋdefˋB{ 94 | ˱[ ax₀ + by₁ ¦⠋ 95 | ax₁ + by₂ ¦ 96 | ⋮ ¦ 97 | ax_{N-1} + by_{N-1} ]˲ 98 | }¦ 99 | ˋB = a[ ⃗x] + b[ ⃗y] ˎˎ 100 | 101 | ---- 102 | 103 | ``` 104 | \ˎ\ˎ ˳|x|˳ = {⋲ x˳ ‹if› x≥0 ¦ 105 | -x˳ ‹if› x<0 } \ˎ\ˎ 106 | 107 | \ˎ\ˎ ˹boole˺(x) = {⋲ 1˳ ‹if \ˎx\ˎ is › [ᵐTrue] ¦ 108 | 0˳ ‹if \ˎx\ˎ is › [ᵐFalse] } \ˎ\ˎ 109 | ``` 110 | ˎˎ ˳|x|˳ = {⋲ x˳ ‹if› x≥0 ¦ 111 | -x˳ ‹if› x<0 } ˎˎ 112 | 113 | ˎˎ ˹boole˺(x) = {⋲ 1˳ ‹if ˎxˎ is › [ᵐTrue] ¦ 114 | 0˳ ‹if ˎxˎ is › [ᵐFalse] } ˎˎ 115 | 116 | ---- 117 | 118 | ``` 119 | \ˎ\ˎ ˹lim˺˽x→0 ˱˹sin˺ x˲∕x = 1\ˎ\ˎ 120 | \ˎ\ˎ U_{δ₁ρ₂}^{β₁α₂} \ˎ\ˎ 121 | \ˎ\ˎ √x = 1 + ˱x-1˲∕ᶜ{2 + ˱x-1˲∕ᶜ{2 + ˱x-1˲∕ᶜ{2 + ⋱}}} \ˎ\ˎ 122 | \ˎ\ˎ ˹sin˺² x¨ + ˹cos˺² x¨ = 1 \ˎ\ˎ 123 | ``` 124 | ˎˎ ˹lim˺˽x→0 ˱˹sin˺ x˲∕x = 1ˎˎ 125 | ˎˎ U_{δ₁ρ₂}^{β₁α₂} ˎˎ 126 | ˎˎ √x = 1 + ˱x-1˲∕ᶜ{2 + ˱x-1˲∕ᶜ{2 + ˱x-1˲∕ᶜ{2 + ⋱}}} ˎˎ 127 | ˎˎ ˹sin˺² x¨ + ˹cos˺² x¨ = 1 ˎˎ 128 | 129 | ---- 130 | 131 | ``` 132 | \ˎ\ˎ α₂³∕³√{β₂² + γ₂²} \ˎ\ˎ 133 | \ˎ\ˎ (x + y)² = ∑ₖ₌₀^∞ (n¦ᶜk)xⁿ⁻ᵏyᵏ \ˎ\ˎ 134 | \ˎ\ˎ (n¦ᶜk) = ˱(n¦⠘k)˲, ˱[n¦⠘k]˲ \ˎ\ˎ 135 | ``` 136 | ˎˎ α₂³∕³√{β₂² + γ₂²} ˎˎ 137 | ˎˎ (x + y)² = ∑ₖ₌₀^∞ (n¦ᶜk)xⁿ⁻ᵏyᵏ ˎˎ 138 | ˎˎ (n¦ᶜk) = ˱(n¦⠘k)˲, ˱[n¦⠘k]˲ˎˎ 139 | 140 | ---- 141 | 142 | ``` 143 | \ˎ\ˎ {x + … + x}⏞⎴{k ‹times›} \ˎ\ˎ 144 | \ˎ\ˎ πd²∕4 1∕˳(A+B)˳² = 145 | πd²∕4👻{˳(A)˳²} 1∕˳(A+B)˳² \ˎ\ˎ 146 | \ˎ\ˎ ∑ⁿˍ{0≤i≤N ¦˽ 0≤j≤M} (ij)² + 147 | ∑ⁿˍ{i∈A ¦˽ˡ 0≤j≤M} (ij)² \ˎ\ˎ 148 | ``` 149 | ˎˎ{x + … + x}⏞⎴{k ‹times›}ˎˎ 150 | ˎˎ πd²∕4 1∕˳(A+B)˳² = 151 | πd²∕4👻{˳(A)˳²} 1∕˳(A+B)˳² ˎˎ 152 | ˎˎ ∑ⁿˍ{0≤i≤N ¦˽ 0≤j≤M} (ij)² + 153 | ∑ⁿˍ{i∈A ¦˽ˡ 0≤j≤M} (ij)² ˎˎ 154 | 155 | ---- 156 | 157 | ``` 158 | \ˎ\ˎ ˹erf˺(x) = 1∕√π ∫₋ₓˣ e^{-t²} dt \ˎ\ˎ 159 | \ˎ\ˎ f⁽²⁾(0) = f''(0) = ˳˱d²f∕dx²|˳ₓ₌₀ \ˎ\ˎ 160 | Text \ˎ˳˳(˱a ˳b ¦⠛ᵗ c ˳d˲)˳˳\ˎ and some more text. 161 | ``` 162 | ˎˎ ˹erf˺(x) = 1∕√π ∫₋ₓˣ e^{-t²} dt ˎˎ 163 | ˎˎ f⁽²⁾(0) = f''(0) = ˳˱d²f∕dx²|˳ₓ₌₀ ˎˎ 164 | Text ˎ˳˳(˱a ˳b ¦⠛ᵗ c ˳d˲)˳˳ˎ and some more text. 165 | 166 | ---- 167 | 168 | prefix unary operator `→⎴`: 169 | ``` 170 | \ˎ\ˎ f: x →⎴{‹arrow map›} ˽i x² \ˎ\ˎ 171 | ``` 172 | ˎˎ f: x →⎴{‹arrow map›} ˽i x² ˎˎ 173 | center binary operator `⎴`: 174 | ``` 175 | \ˎ\ˎ f: x → ⎴‹arrow map› ˽i x² \ˎ\ˎ 176 | ``` 177 | ˎˎ f: x → ⎴‹arrow map› ˽i x² ˎˎ 178 | bug because styles also implemented as prefix unary operators (but by design styles should have priority!): 179 | ``` 180 | \ˎ\ˎ f: x →⎴‹arrow map› ˽i x² \ˎ\ˎ 181 | ``` 182 | ˎˎ f: x →⎴‹arrow map› ˽i x² ˎˎ 183 | -------------------------------------------------------------------------------- /examples/examples.md.md: -------------------------------------------------------------------------------- 1 | --- 2 | autoEqnLabels: False 3 | autoSectionLabels: False 4 | ccsDelim: ',' 5 | ccsLabelSep: '---' 6 | ccsTemplate: $$i$$$$ccsLabelSep$$$$t$$ 7 | chapDelim: '.' 8 | chapters: False 9 | chaptersDepth: 1 10 | codeBlockCaptions: False 11 | comments-map: 12 | js: 13 | - '//' 14 | - '/\*' 15 | - '\*/' 16 | py: 17 | - '\#' 18 | - '''''''' 19 | - '''''''' 20 | - '\"\"\"' 21 | - '\"\"\"' 22 | r: 23 | - '\#' 24 | - '''' 25 | - '''' 26 | - '\"' 27 | - '\"' 28 | ts: 29 | - '//' 30 | - '/\*' 31 | - '\*/' 32 | cref: False 33 | crossrefYaml: 'pandoc-crossref.yaml' 34 | eqnLabels: arabic 35 | eqnPrefix: 36 | - 'eq.' 37 | - 'eqns.' 38 | eqnPrefixTemplate: $$p$$ $$i$$ 39 | eval: False 40 | figLabels: arabic 41 | figPrefix: 42 | - 'fig.' 43 | - 'figs.' 44 | figPrefixTemplate: $$p$$ $$i$$ 45 | figureTemplate: $$figureTitle$$ $$i$$$$titleDelim$$ $$t$$ 46 | figureTitle: Figure 47 | kernels-map: 48 | py: python3 49 | r: ir 50 | lastDelim: ',' 51 | linkReferences: False 52 | listingTemplate: $$listingTitle$$ $$i$$$$titleDelim$$ $$t$$ 53 | listingTitle: Listing 54 | listings: False 55 | lofTitle: | 56 | List of Figures 57 | =============== 58 | lolTitle: | 59 | List of Listings 60 | ================ 61 | lotTitle: | 62 | List of Tables 63 | ============== 64 | lstLabels: arabic 65 | lstPrefix: 66 | - 'lst.' 67 | - 'lsts.' 68 | lstPrefixTemplate: $$p$$ $$i$$ 69 | nameInLink: False 70 | numberSections: False 71 | pairDelim: ',' 72 | pandoctools: 73 | out: '*.*.md' 74 | profile: Kiwi 75 | rangeDelim: '\-' 76 | refDelim: ',' 77 | refIndexTemplate: $$i$$$$suf$$ 78 | secHeaderTemplate: $$i$$$$secHeaderDelim$$$$t$$ 79 | secLabels: arabic 80 | secPrefix: 81 | - 'sec.' 82 | - 'secs.' 83 | secPrefixTemplate: $$p$$ $$i$$ 84 | sectionsDepth: 0 85 | styles-map: 86 | py: python 87 | subfigGrid: False 88 | subfigLabels: alpha a 89 | subfigureChildTemplate: $$i$$ 90 | subfigureRefIndexTemplate: '$$i$$$$suf$$ ($$s$$)' 91 | subfigureTemplate: '$$figureTitle$$ $$i$$$$titleDelim$$ $$t$$. $$ccs$$' 92 | tableEqns: False 93 | tableTemplate: $$tableTitle$$ $$i$$$$titleDelim$$ $$t$$ 94 | tableTitle: Table 95 | tblLabels: arabic 96 | tblPrefix: 97 | - 'tbl.' 98 | - 'tbls.' 99 | tblPrefixTemplate: $$p$$ $$i$$ 100 | titleDelim: ':' 101 | --- 102 | 103 | See @eq:max. 104 | ˎˎ 105 | ˱∇ × [⠘B] - 1∕c ∂[⠘E]∕∂t ˳= 4π∕c [⠘j] ¦# 106 | ∇ ⋅ [⠘E]\ ˳= 4πρ ¦ 107 | ∇ × [⠘E] + 1∕c ∂[⠘B]∕∂t ˳= [⠘0] ¦ 108 | ∇ ⋅ [⠘B]\ ˳= 0 ˲ 109 | ,ˎˎ{#eq:max} 110 | 111 | where ˎ[⠘B], [⠘E], [⠘j]: ℝ⁴ → ℝ³ˎ – vector functions of the form 112 | ˎ(t,x,y,z) ↦ [⠘f](t,x,y,z), [⠘f] = (f_˹x˺, f_˹y˺, f_˹z˺)ˎ. 113 | 114 | See eq. 1. [$$ 115 | \begin{aligned}∇ × {\mathbf{B}} - \frac{1}{c} \frac{∂{\mathbf{E}}}{∂t} &= \frac{4π}{c} {\mathbf{j}}\\ 116 | ∇ ⋅ {\mathbf{E}}\ &= 4πρ \\ 117 | ∇ × {\mathbf{E}} + \frac{1}{c} \frac{∂{\mathbf{B}}}{∂t} &= {\mathbf{0}} \\ 118 | ∇ ⋅ {\mathbf{B}}\ &= 0 \end{aligned} 119 | ,\qquad(1)$$]{#eq:max} 120 | 121 | where ${\mathbf{B}},\,{\mathbf{E}},\,{\mathbf{j}}:\,ℝ^{4} → ℝ^{3}$ -- 122 | vector functions of the form 123 | $(t,x,y,z) ↦ {\mathbf{f}}(t,x,y,z),\,{\mathbf{f}} = (f_{\mathrm{x}}, f_{\mathrm{y}}, f_{\mathrm{z}})$. 124 | 125 | ------------------------------------------------------------------------ 126 | 127 | See @eq:max2. 128 | ˎˎ 129 | ˱∇ × 𝐁 - 1∕c ∂𝐄∕∂t ˳= 4π∕c 𝐣 ¦# 130 | ∇ ⋅ 𝐄\ ˳= 4πρ ¦ 131 | ∇ × 𝐄 + 1∕c ∂𝐁∕∂t ˳= 𝟎 ¦ 132 | ∇ ⋅ 𝐁\ ˳= 0 ˲ 133 | ,ˎˎ{#eq:max2} 134 | 135 | where ˎ𝐁, 𝐄, 𝐣: ℝ⁴ → ℝ³ˎ – vector functions of the form 136 | ˎ(t,x,y,z) ↦ 𝐟(t,x,y,z), 𝐟 = (f_˹x˺, f_˹y˺, f_˹z˺)ˎ. 137 | 138 | See eq. 2. [$$ 139 | \begin{aligned}∇ × 𝐁 - \frac{1}{c} \frac{∂𝐄}{∂t} &= \frac{4π}{c} 𝐣\\ 140 | ∇ ⋅ 𝐄\ &= 4πρ \\ 141 | ∇ × 𝐄 + \frac{1}{c} \frac{∂𝐁}{∂t} &= 𝟎 \\ 142 | ∇ ⋅ 𝐁\ &= 0 \end{aligned} 143 | ,\qquad(2)$$]{#eq:max2} 144 | 145 | where $𝐁,\,𝐄,\,𝐣:\,ℝ^{4} → ℝ^{3}$ -- vector functions of the form 146 | $(t,x,y,z) ↦ 𝐟(t,x,y,z),\,𝐟 = (f_{\mathrm{x}}, f_{\mathrm{y}}, f_{\mathrm{z}})$. 147 | 148 | ------------------------------------------------------------------------ 149 | 150 | ˎˎ [⠋A] = [⠋B]˹ᵀ˺ [⠋C] [⠋B] ˎˎ 151 | 152 | ˎˎ 𝐀 = 𝐁˹ᵀ˺𝐂 𝐁 ˎˎ 153 | 154 | $$ {\mathbf{A}} = {\mathbf{B}}^{{\mathrm{T}}} {\mathbf{C}}\,{\mathbf{B}} $$ 155 | 156 | $$ 𝐀 = 𝐁^{{\mathrm{T}}}𝐂\,𝐁 $$ 157 | 158 | ˎˎ 159 | ˱[ x₁₁ ˳x₁₂ ˳x₁₃ ˳… ˳x₁ₙ ¦⠋ 160 | x₂₁ ˳x₂₂ ˳x₂₃ ˳… ˳x₂ₙ ¦ 161 | ⋮ ˳ ⋮ ˳ ⋮ ˳⋱ ˳ ⋮ ¦ 162 | xₚ₁ ˳xₚ₂ ˳xₚ₃ ˳… ˳xₚₙ ]˲ ˎˎ 163 | 164 | $$ 165 | \begin{bmatrix} x_{11} &x_{12} &x_{13} &… &x_{1n}\\ 166 | x_{21} &x_{22} &x_{23} &… &x_{2n} \\ 167 | ⋮ & ⋮ & ⋮ &⋱ & ⋮ \\ 168 | x_{p1} &x_{p2} &x_{p3} &… &x_{pn} \end{bmatrix} $$ 169 | 170 | ------------------------------------------------------------------------ 171 | 172 | ˎˎ ˋdefˋB{ 173 | ˱[ ax₀ + by₁ ¦⠋ 174 | ax₁ + by₂ ¦ 175 | ⋮ ¦ 176 | ax_{N-1} + by_{N-1} ]˲ 177 | }¦ 178 | ˋB = a[⠘x] + b[⠘y] ˎˎ 179 | 180 | $$ \def\B{ 181 | \begin{bmatrix} ax_{0} + by_{1}\\ 182 | ax_{1} + by_{2} \\ 183 | ⋮ \\ 184 | ax_{N-1} + by_{N-1} \end{bmatrix} 185 | }\\ 186 | \B = a{\mathbf{x}} + b{\mathbf{y}} $$ 187 | 188 | ------------------------------------------------------------------------ 189 | 190 | ˎˎ ˳|x|˳ = {⋲ x˳ ‹if› x≥0 ¦ 191 | -x˳ ‹if› x<0 } ˎˎ 192 | 193 | ˎˎ ˹boole˺(x) = {⋲ 1˳ ‹if ˎxˎ is › [ᵐTrue] ¦ 194 | 0˳ ‹if ˎxˎ is › [ᵐFalse] } ˎˎ 195 | 196 | $$ \left\vert{x}\right\vert = \begin{cases} x& {\text{if}} x≥0 \\ 197 | -x& {\text{if}} x<0 \end{cases} $$ 198 | 199 | $$ {\mathrm{boole}}(x) = \begin{cases} 1& {\text{if $x$ is }} {\class{MJX-Monospace}{\mathtt{True}}} \\ 200 | 0& {\text{if $x$ is }} {\class{MJX-Monospace}{\mathtt{False}}} \end{cases} $$ 201 | 202 | ------------------------------------------------------------------------ 203 | 204 | ˎˎ ˹lim˺˽x→0 ˱˹sin˺ x˲∕x = 1ˎˎ 205 | ˎˎ U_{δ₁ρ₂}^{β₁α₂} ˎˎ 206 | ˎˎ √x = 1 + ˱x-1˲∕ᶜ{2 + ˱x-1˲∕ᶜ{2 + ˱x-1˲∕ᶜ{2 + ⋱}}} ˎˎ 207 | ˎˎ ˹sin˺² x¨ + ˹cos˺² x¨ = 1 ˎˎ 208 | 209 | $$ \underset{x→0}{{\mathrm{lim}}} \frac{{{\mathrm{sin}}\,x}}{x} = 1$$ 210 | $$ U_{δ_{1}ρ_{2}}^{β_{1}α_{2}} $$ 211 | $$ \sqrt[]{x} = 1 + \cfrac{{x-1}}{{2 + \cfrac{{x-1}}{{2 + \cfrac{{x-1}}{{2 + ⋱}}}}}} $$ 212 | $$ {\mathrm{sin}}^{2} \ddot{x} + {\mathrm{cos}}^{2} \ddot{x} = 1 $$ 213 | 214 | ------------------------------------------------------------------------ 215 | 216 | ˎˎ α₂³∕³√{β₂² + γ₂²} ˎˎ 217 | ˎˎ (x + y)² = ∑ₖ₌₀^∞ (n¦ᶜk)xⁿ⁻ᵏyᵏ ˎˎ 218 | ˎˎ (n¦ᶜk) = ˱(n¦⠘k)˲, ˱[n¦⠘k]˲ ˎˎ 219 | 220 | $$ \frac{α_{2}^{3}}{\sqrt[3]{{β_{2}^{2} + γ_{2}^{2}}}} $$ 221 | $$ (x + y)^{2} = \sum_{k=0}^∞ \binom{n}{k}x^{n-k}y^{k} $$ 222 | $$ \binom{n}{k} = \genfrac{(}{)}{0pt}{}{n}{k}, \genfrac{[}{]}{0pt}{}{n}{k}$$ 223 | 224 | ------------------------------------------------------------------------ 225 | 226 | ˎˎ {x + … + x}⏞⎴{k ‹times›} ˎˎ 227 | ˎˎ πd²∕4 1∕˳(A+B)˳² = 228 | πd²∕4👻{˳(A)˳²} 1∕˳(A+B)˳² ˎˎ 229 | ˎˎ ∑ⁿˍ{0≤i≤N ¦˽ 0≤j≤M} (ij)² + 230 | ∑ⁿˍ{i∈A ¦˽ˡ 0≤j≤M} (ij)² ˎˎ 231 | 232 | $$\overset{{k {\text{times}}}}{\overbrace{{x + … + x}}}$$ 233 | $$ \frac{πd^{2}}{4} \frac{1}{\left({A+B}\right)^{2}} = 234 | \frac{πd^{2}}{4\vphantom{{\left({A}\right)^{2}}}} \frac{1}{\left({A+B}\right)^{2}} $$ 235 | $$ \sum^{n}_{\substack{0≤i≤N\\0≤j≤M}} (ij)^{2} + 236 | \sum^{n}_{\begin{subarray}{l}i∈A\\0≤j≤M\end{subarray}} (ij)^{2} $$ 237 | 238 | ------------------------------------------------------------------------ 239 | 240 | ˎˎ ˹erf˺(x) = 1∕√π ∫₋ₓˣ e^{-t²} dt ˎˎ 241 | ˎˎ f⁽²⁾(0) = f''(0) = ˳˱d²f∕dx²|˳ₓ₌₀ ˎˎ 242 | Text ˎ˳˳(˱a ˳b ¦⠛ᵗ c ˳d˲)˳˳ˎ and some more text. 243 | 244 | $$ {\mathrm{erf}}(x) = \frac{1}{\sqrt[]{π}} \int_{-x}^{x} e^{-t^{2}} dt $$ 245 | $$ f^{(2)}(0) = f''(0) = \left.{\frac{d^{2}f}{dx^{2}}}\right\vert_{x=0} $$ 246 | Text $\bigl( \begin{smallmatrix}a &b\\c &d\end{smallmatrix}\bigr)$ and 247 | some more text. 248 | 249 | ------------------------------------------------------------------------ 250 | 251 | prefix unary operator `→⎴`: 252 | 253 | ˎˎ f: x →⎴{‹arrow map›} ˽i x² ˎˎ 254 | 255 | $$ f: x \underset{i}{\xrightarrow{{{\text{arrow map}}}}} x^{2} $$ center 256 | binary operator `⎴`: 257 | 258 | ˎˎ f: x → ⎴‹arrow map› ˽i x² ˎˎ 259 | 260 | $$ f: x \underset{i}{\overset{{\text{arrow map}}}{→}} x^{2} $$ bug 261 | because styles also implemented as prefix unary operators (but by design 262 | styles should have priority!): 263 | 264 | ˎˎ f: x →⎴‹arrow map› ˽i x² ˎˎ 265 | 266 | $$ f: x \xrightarrow{‹arrow} \underset{i}{map›} x^{2} $$ 267 | -------------------------------------------------------------------------------- /examples/examples.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kiwi0fruit/sugartex/a80aa33d560f8e93dbdfa984bdf3caf3800ea6c6/examples/examples.pdf -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [versioneer] 2 | VCS = git 3 | style = pep440 4 | versionfile_source = sugartex/_version.py 5 | versionfile_build = sugartex/_version.py 6 | tag_prefix = 7 | #parentdir_prefix = 8 | 9 | [flake8] 10 | ignore = E731 11 | exclude = sugartex/_version.py,versioneer.py 12 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | from os import path 3 | import io 4 | import versioneer 5 | 6 | 7 | here = path.abspath(path.dirname(__file__)) 8 | 9 | with io.open(path.join(here, 'README.md'), encoding='utf-8') as f: 10 | long_description = f.read() 11 | 12 | setup( 13 | name='sugartex', 14 | version=versioneer.get_version(), 15 | cmdclass=versioneer.get_cmdclass(), 16 | 17 | description='More readable LaTeX language extension and transcompiler to LaTeX', 18 | long_description=long_description, 19 | long_description_content_type="text/markdown", 20 | 21 | url='https://github.com/kiwi0fruit/sugartex', 22 | 23 | author='Peter Zagubisalo', 24 | author_email='peter.zagubisalo@gmail.com', 25 | 26 | license='MIT', 27 | 28 | classifiers=[ 29 | 'Development Status :: 3 - Alpha', 30 | 'Intended Audience :: Developers', 31 | 'Topic :: Software Development :: Build Tools', 32 | 'License :: OSI Approved :: MIT License', 33 | 34 | 'Programming Language :: Python :: 3', 35 | 'Programming Language :: Python :: 3.6', 36 | ], 37 | 38 | # keywords='sample setuptools development', 39 | packages=find_packages(exclude=['docs', 'tests']), 40 | python_requires='>=3.6', 41 | install_requires=['panflute>=1.11.2'], 42 | 43 | include_package_data=True, 44 | package_data={ 45 | 'sugartex': ['sugartex/*.py'], 46 | }, 47 | entry_points={ 48 | 'console_scripts': [ 49 | 'sugartex=sugartex.sugartex_pandoc_filter:cli', 50 | 'pre-sugartex=sugartex.pre_sugartex:main', 51 | ], 52 | }, 53 | ) 54 | -------------------------------------------------------------------------------- /sugartex.md: -------------------------------------------------------------------------------- 1 | --- 2 | pandoctools: 3 | profile: Kiwi 4 | out: "*.pdf" 5 | eval: False 6 | --- 7 | 8 | # SugarTeX 9 | 10 | SugarTeX is a more readable LaTeX language extension and a transcompiler to LaTeX. 11 | 12 | See [PDF version of this documentation](sugartex.pdf?raw=true) - it nicely renders all Unicode characters. See original Markdown version [here](https://github.com/kiwi0fruit/sugartex/blob/master/sugartex.md). 13 | 14 | 15 | # Contents 16 | 17 | * [Command line interfaces](#command-line-interfaces) 18 | * [Tweaking SugarTeX](#tweaking-sugartex) 19 | * [SugarTeX replacements and operators](#sugartex-replacements-and-operators) 20 | * [Math delimiters](#math-delimiters) 21 | * [New escape character](#new-escape-character) 22 | * [Brackets](#brackets) 23 | * [Simple pre-replacements](#simple-pre-replacements) 24 | * [Superscripts and Subscripts](#superscripts-and-subscripts) 25 | * [Regular expressions pre-replacements](#regular-expressions-pre-replacements) 26 | * [Nullary operators](#nullary-operators) 27 | * [Prefix unary operators](#prefix-unary-operators) 28 | * [Styles](#styles) 29 | * [Styles with special brackets](#styles-with-special-brackets) 30 | * [Greedy prefix unary operators](#greedy-prefix-unary-operators) 31 | * [Standard prefix unary operators](#standard-prefix-unary-operators) 32 | * [Postfix unary operators](#postfix-unary-operators) 33 | * [Center binary operators](#center-binary-operators) 34 | * [Matrices](#matrices) 35 | * [General fractions without bars](#general-fractions-without-bars) 36 | * [Greedy center binary operators](#greedy-center-binary-operators) 37 | * [Standard center binary operators](#standard-center-binary-operators) 38 | * [Regular expressions loop replacements](#regular-expressions-loop-replacements) 39 | * [Regular expressions post-replacements](#regular-expressions-post-replacements) 40 | * [Simple post-replacements](#simple-post-replacements) 41 | * [Escapable characters](#escapable-characters) 42 | * [Examples](#examples) 43 | * [Caveats of SugarTeX to docx conversion with free software only](#caveats-of-sugartex-to-docx-conversion-with-free-software-only) 44 | 45 | 46 | # Command line interfaces 47 | 48 | 1. `sugartex`: 49 | 50 | ``` 51 | Usage: sugartex [OPTIONS] [TO] 52 | 53 | Reads from stdin and writes to stdout. Can have single argument/option only. 54 | When no args or the arg is not from options then run Pandoc SugarTeX filter 55 | that iterates over math blocks. 56 | 57 | Options: 58 | --kiwi Same as above but with kiwi flavor, 59 | --help Show this message and exit. 60 | ``` 61 | 62 | 2. `pre-sugartex`: 63 | 64 | ``` 65 | Usage: pre-sugartex [OPTIONS] 66 | 67 | Reads from stdin and writes to stdout. 68 | When no options: only replace 69 | U+02CE Modifier Letter Low Grave Accent 70 | (that looks like low '`') with $ 71 | 72 | Options: 73 | --all Full SugarTeX replace with regexp, 74 | --kiwi Same as above but with kiwi flavor, 75 | --help Show this message and exit. 76 | ``` 77 | 78 | 79 | [Panflute](https://github.com/sergiocorreia/panflute) scripts are also installed so you can use it in default Panflute [automation interface in metadata](http://scorreia.com/software/panflute/guide.html#running-filters-automatically) or recommended [`panfl` CLI](https://github.com/kiwi0fruit/pandoctools/blob/master/docs/panfl.md): 80 | 81 | * `panfl sugartex --to markdown`, 82 | * `panfl sugartex.kiwi -t markdown`. 83 | 84 | Examples. Windows: 85 | 86 | ```batch 87 | chcp 65001 > NUL 88 | set PYTHONIOENCODING=utf-8 89 | 90 | type doc.md | ^ 91 | pre-sugartex | ^ 92 | pandoc -f markdown --filter sugartex -o doc.md.md 93 | ``` 94 | 95 | Unix: 96 | 97 | ```bash 98 | export PYTHONIOENCODING=utf-8 99 | 100 | cat doc.md | \ 101 | pre-sugartex | \ 102 | pandoc -f markdown --filter sugartex -o doc.md.md 103 | ``` 104 | 105 | Or splitting Pandoc reader-writer: 106 | 107 | ```batch 108 | chcp 65001 > NUL 109 | set PYTHONIOENCODING=utf-8 110 | 111 | type doc.md | ^ 112 | pre-sugartex | ^ 113 | pandoc -f markdown -t json | ^ 114 | sugartex --kiwi | ^ 115 | pandoc -f json -o doc.md.md 116 | ``` 117 | 118 | 119 | # Tweaking SugarTeX 120 | 121 | SugarTeX is written in python and has a tweakable architecture. As you can see in [this filter](sugartex/sugartex_pandoc_filter.py) tweaks can be made in between: 122 | ```py 123 | sugartex = SugarTeX(ready=False) 124 | ... 125 | sugartex.ready() 126 | ``` 127 | 128 | Attributes of instance of `SugarTeX` class can be changed. See them in defining of `SugarTeX` class and in it's `__init__` method [here](sugartex/sugartex_filter.py). List of attributes: 129 | 130 | * `.brackets` 131 | * `.brackets_types` 132 | * `.simple_pre` 133 | * `.superscripts` 134 | * `.subscripts` 135 | * `.regex_pre` 136 | * `.null_ops` (class `NullOps`) 137 | * `.pref_un_ops` (class `PrefUnOps`), including: 138 | * `.styles` (class `Styles`) 139 | * `.other_styles` (class `OtherStyles`) 140 | * `.pref_un_greedy` (class `PrefUnGreedy`) 141 | * `.postf_un_ops` (class `PostfUnOps`) 142 | * `.bin_centr_ops` (class `BinCentrOps`), including: 143 | * `.matrices` (class `Matrices`) 144 | * `.bin_centr_greedy` (class `BinCentrGreedy`) 145 | * `.loop_regexps` 146 | * `.regex_post` 147 | * `.simple_post` 148 | * `.escapes` 149 | 150 | 151 | # SugarTeX replacements and operators 152 | 153 | Many replacements use amsmath macros. 154 | 155 | ## Math delimiters 156 | 157 | In default use-case SugarTeX first preprocesses text replacing `\ˎ` with `$` (modifier letter low grave accent U+02CE). Can be escaped: `\\ˎ` 158 | 159 | ***SugarTeX Completions for Atom***: 160 | 161 | * `\ˎ` ← `` \_` ``, 162 | * `\ˎ` ← `\$`. 163 | 164 | 165 | ## New escape character 166 | 167 | In SugarTeX the default escape character is `\`. But it's a special symbol in LaTeX. In cases when `\` would work as escaping character you can use `` ` `` or `ˋ` (modifier letter grave accent). At the end it will be replaced with `\`. Note that `` ` `` and `ˋ` **won't** work as escape characters. 168 | 169 | ***SugarTeX Completions for Atom***: 170 | 171 | * `ˋ` ← `` \` `` (modifier letter grave accent). 172 | 173 | 174 | ## Brackets 175 | 176 | Independently replace brackets: 177 | 178 | * `˳(` → `\left({` and `)˳` → `}\right)` (modifier letter low ring U+02F3), 179 | * `˳˳(` → `\bigl(` and `)˳˳` → `\bigr)`, 180 | * `˳ˌ(` → `\Bigl(` and `)˳ˌ` → `\Bigr)`, 181 | * `ˌ˳(` → `\biggl(` and `)ˌ˳` → `\biggr)`, 182 | * `ˌˌ(` → `\Biggl(` and `)ˌˌ` → `\Biggr)` (modifier letter low vertical line U+02CC). 183 | 184 | Instead of `(` and `)` can be other brackets: 185 | 186 | * `[` → `[` and `]` → `]`, 187 | * `(` → `(` and `)` → `)`, 188 | * `{` → `\{` and `}` → `\}`, 189 | * `│` → `\vert` (box drawings light vertical U+2502, for math in markdown tables), 190 | * `|` → `\vert`, 191 | * `‖` → `\Vert` (double vertical line U+2016), 192 | * `˱` → `.` and `˲` → `.` (modifier letter low left/right arrowhead U+02F1/U+02F2), 193 | * `⟨` → `\langle` and `⟩` → `\rangle` (mathematical left/right angle bracket U+27E8/27E9), 194 | * `⌊` → `\lfloor` and `⌋` → `\rfloor` (left/right floor U+230A/U+230B), 195 | * `⌈` → `\lceil` and `⌉` → `\rceil` (left/right ceiling U+2308/U+2309. 196 | 197 | ***SugarTeX Completions for Atom***: 198 | 199 | Use these shortcuts for fast Unicode typing in Atom: 200 | 201 | * `˳` ← `\&`, 202 | * `˳` ← `\_o\small`, 203 | * `ˌ` ← `\_'\small`. 204 | * `│` ← `\|`, 205 | * `‖` ← `\||`, 206 | * `˱` ← `\_<`, 207 | * `˲` ← `\_>`, 208 | * `˱˲` ← `\_<>`, 209 | * `⟨` ← `\<\`, 210 | * `⟩` ← `\>\`, 211 | * `⟨⟩` ← `\<>\`, 212 | * `⌊` ← `\lfloor`, 213 | * `⌋` ← `\rfloor`, 214 | * `⌈` ← `\lceil`, 215 | * `⌉` ← `\rceil`. 216 | 217 | 218 | ## Simple pre-replacements 219 | 220 | * `∛` → `3√` (cube root U+221B), 221 | * `∜` → `4√` (fourth root U+221C), 222 | * ` ` → `\,` (thin space U+2009). 223 | 224 | ***SugarTeX Completions for Atom***: 225 | 226 | * ` ` ← `\,` (thin space), 227 | * ` ` ← `\],[` (thin space), 228 | * `√` ← `\^1/2`, 229 | * `∛` ← `\^1/3`, 230 | * `∜` ← `\^1/4`. 231 | 232 | 233 | ## Superscripts and Subscripts 234 | 235 | Groups of superscript Unicode characters like `¹²³` are replaced with `^{123}`. Unless they are escaped with `\` or followed by `√`: 236 | 237 | * `\¹²³√` → `¹23√` (square root U+221A), 238 | * `\¹²³` → `¹^{23}`, 239 | * `¹²³ᵃᵇᶜ` → `^{123abc}`. 240 | 241 | Same is for groups of subscript Unicode characters: 242 | 243 | * `\₁₂₃` → `₁_{23}`. 244 | * `₁₂₃ₖₗₘ` → `_{123klm}`. 245 | 246 | List of supported characters can be found in the beginning of the SugarTeX [source code](https://github.com/kiwi0fruit/pandoctools/blob/master/pandoctools/sugartex/sugartex.py). 247 | 248 | **UPDATE** 249 | 250 | Now `‹›` and `˹˺` from [Styles with special brackets](#styles-with-special-brackets) end up inside `_{}`/`^{}`, like: `A‹ₐₑ›` → `A_{‹ae›}`. Does not work if there are non-subscript/superscript characters inside `‹›`/`˹˺`, like: `A‹ᵃe›` → `A‹^{a}e›`. 251 | 252 | ***SugarTeX Completions for Atom***: 253 | 254 | * `₁` ← `\_1`, 255 | * `ₐ` ← `\_a`, 256 | * `¹` ← `\^1`, 257 | * `ᵃ` ← `\^a`. 258 | 259 | 260 | ## Regular expressions pre-replacements 261 | 262 | Nothing. But can be tweaked. 263 | 264 | 265 | ## Nullary operators 266 | 267 | Big operators replacements: 268 | 269 | * `∑` → `\sum` (n-ary summation U+2211), 270 | * `∑:` → `\sum\nolimits`, 271 | * `∑⢈` → `\sum\limits` (braille pattern dots-48 U+2888). 272 | 273 | Supported symbols for limits: 274 | 275 | * `⢈`, `⡁` → `\limits` (braille pattern dots-48/dots-17 U+2888/U+2841), 276 | * `:`, `⠆`, `⠰` → `\nolimits` (braille pattern dots-23/dots-56 U+2806/U+2830). 277 | 278 | Supported big operators: 279 | 280 | * `∑` → `\sum`, 281 | * `∏` → `\prod`, 282 | * `∫` → `\int`, 283 | * `∬` → `\iint`, 284 | * `∭` → `\iiint`, 285 | * `⨌` → `\iiiint`, 286 | * `∮` → `\oint`. 287 | 288 | Who knows what I was thinking about by adding them here instead of Regular expressions replacements... 289 | 290 | ***SugarTeX Completions for Atom***: 291 | 292 | * `⢈` ← `\:`, 293 | * `⠰` ← `\:\small`, 294 | * `∑` ← `\sum`, 295 | * `∏` ← `\prod`, 296 | * `∫` ← `\int`, 297 | * `∬` ← `\iint`, 298 | * `∭` ← `\iiint`, 299 | * `⨌` ← `\iiiint`, 300 | * `∮` ← `\oint`. 301 | 302 | 303 | ## Prefix unary operators 304 | 305 | ### Styles 306 | 307 | Text inside standard brackets (`()`, `[]`, `{}`) with special prefix is replaced with style operator. For example: 308 | 309 | `[ʳtext]` or `[^{r}text]` → `\mathrm{text}`. 310 | 311 | First SugarTeX finds opening part like `[^{r}` then searches for the first non-escaped closing part `]` that is not inside `{}` or `˱˲` – SugarTeX counts opening and closing `{}˱˲` (`˱˲` would later be replaced with `{}` so both are counted together). For example: 312 | 313 | `(ʳsome{te)(t})` → `\mathrm{some{te)(t}}`. 314 | 315 | List of available styles: 316 | 317 | * `{ʳtext}` / `{^{r}text}` → `\mathrm{text}` (**math regular**), 318 | * `{ⁱx}` / `{^{i}x}` → `\mathit{x}` (**math italic**), 319 | * `{ᵇx}` / `{^{b}x}` → `\mathbf{x}` (**math bold**), 320 | * `{ᵝx}` / `{^{β}x}` → `\boldsymbol{x}` (**math bold italic**), 321 | * `{ᵐtext}` / `{^{m}text}` → `\mathtt{text}` (**math monospace**), 322 | * `{ᶜA}` / `{^{c}A}` → `\mathcal{A}` (**math calligraphic**, 323 | no cyrillic support, see Monotype Corsiva), 324 | * `{ᵗtext}` / `{^{t}text}` → `\text{text}` (**text**), 325 | * `{ᵗⁱtext}` / `{^{ti}text}` → `\textit{text}` (**text italic**), 326 | * `{ᵗᵇtext}` / `{^{tb}text}` → `\textbf{text}` (**text bold**), 327 | * `{ᵗᵝtext}` / `{^{tβ}text}` → `\textit{\textbf{text}}` (**text bold italic**), 328 | * `{ ⃗x}` / `{⃗x}` → `\mathbf{x}` (**vector bold notation**, 329 | combining right arrow above U+20D7, first one is 'space' +\ \ ⃗ ), 330 | * `{⠘x}` / `{⠃x}` → `\mathbf{x}` (**vector bold notation**, 331 | braille pattern dots-45/dots-12 U+2818/U+2803 [right upper 2/left upper 2]), 332 | * `{⠋A}` / `{⠛A}` → `\mathbf{A}` (**matrix bold notation**, 333 | braille pattern dots-124/dots-1245 U+280B/U+281B). 334 | 335 | ***SugarTeX Completions for Atom***: 336 | 337 | * ← `\^->`, 338 | * `⠘` ← `\^:`, 339 | * `⠛` ← `\^::`, 340 | * `⠛` ← `\array`, 341 | * `⠋` ← `\^:.\rot-90`, 342 | * `⠋` ← `\matrix`. 343 | 344 | 345 | ### Styles with special brackets 346 | 347 | * `‹ᵝtext›` / `‹^{β}text›` → `\textit{\textbf{text}}` (**text bold italic**), 348 | * `‹ⁱtext›` / `‹^{i}text›` → `\textit{text}` (**text italic**), 349 | * `‹ᵇtext›` / `‹^{b}text›` → `\textbf{text}` (**text bold**), 350 | * `‹text›` → `\text{text}` (**text regular**, 351 | single left/right-pointing angle quotation mark U+2039/U+203A), 352 | * `˹text˺` → `\mathrm{text}` (**math regular**, 353 | modifier letter begin/end high tone U+02F9/U+02FA). 354 | 355 | ***SugarTeX Completions for Atom***: 356 | 357 | * `‹` ← `\<`, 358 | * `›` ← `\>`, 359 | * `‹›` ← `\<>`, 360 | * `‹›` ← `\text`, 361 | * `˹˺` ← `\^r\small`, 362 | * `˹˺` ← `\regular`. 363 | 364 | 365 | ### Greedy prefix unary operators 366 | 367 | * `{⋲ smth}` / `˱⋲ smth˲` → `\begin{cases} smth\end{cases}` (**piecewise**, element of with long horizontal stroke U+22F2). 368 | 369 | ``` 370 | \ˎ\ˎ 371 | ˳|x|˳ = {⋲ x˳ ‹if› x≥0 ¦ 372 | -x˳ ‹if› x<0 } 373 | \ˎ\ˎ 374 | ``` 375 | 376 | SugarTeX finds non-escaped `{⋲` or `˱⋲` first then searches for non-escaped `}` or `˲` that is not inside `{}` or `˱˲` – SugarTeX counts opening and closing `{}˱˲` (`˱˲` would later be replaced with `{}` so both are counted together). 377 | 378 | ***SugarTeX Completions for Atom***: 379 | 380 | * `⋲` ← `\-e`, 381 | * `⋲` ← `\-E`. 382 | 383 | 384 | ### Standard prefix unary operators 385 | 386 | * `⧼matrix a` → `\begin{matrix} a` 387 | (left-pointing curved angle bracket U+29FC), 388 | * `👻 A² a` → `\vphantom{A^2} a` 389 | (**invisible characters that adjust height**, ghost U+1F47B), 390 | * `→⎴ text a` → `\xrightarrow{text} a` 391 | (**arrow with text above that adjusts to the text length**, rightwards arrow U+2192, top square bracket U+23B4), 392 | * `←⎴˱long text˲ a` → `\xleftarrow{{long text}} a` 393 | (leftwards arrow U+2190). 394 | 395 | SugarTeX finds non-escaped `⧼ *` first (for example) then searches for a place before non-escaped `}`, `˲`, space, newline or end of the string that is not inside `{}` or `˱˲` – SugarTeX counts opening and closing `{}˱˲` (`˱˲` would later be replaced with `{}` so both are counted together). 396 | 397 | ***SugarTeX Completions for Atom***: 398 | 399 | * `⧼` ← `\<\alt2`, 400 | * `⧽` ← `\>\alt2`, 401 | * `⧼⧽` ← `\<>\alt2`, 402 | * `👻` ← `\ghost`, 403 | * `⎴` ← `\^^`, 404 | * `⎴` ← `\^]\rot90`, 405 | * `→` ← `\->`, 406 | * `←` ← `\<-`. 407 | 408 | 409 | ## Postfix unary operators 410 | 411 | * `a x ⃗` → `a \vec{x }` (**vector**, 412 | combining right arrow above U+20D7), 413 | * `a x ⃑` → `a \overrightarrow{x }` (**arrow above**, 414 | combining right harpoon above U+20D1), 415 | * `a x^` → `a \widehat{x}` 416 | **warning**: works only if the next character after `^` is `}`, `˲`, newline or end of the string, 417 | * `a xˆ` → `a \hat{x}` (modifier letter circumflex accent U+02C6), 418 | * `a x¯` → `a \bar{x}` (macron U+00AF), 419 | * `a x‾` → `a \overline{x}` (overline U+203E), 420 | * `a x˙` → `a \dot{x}` (dot above U+02D9), 421 | * `a x¨` → `a \ddot{x}` (diaeresis U+00A8), 422 | * `x + y+z⏞` → `x + \overbrace{y+z}` 423 | (top curly bracket U+23DE), 424 | * `x + {y + z}⏟` → `x + \underbrace{{y + z}}` 425 | (bottom curly bracket U+23DF), 426 | * `a xˍ` → `a \underline{x}` 427 | **warning**: works only if the next character after `ˍ` is `}`, `˲`, newline or end of the string (modifier letter low macron U+02CD), 428 | * `a matrix⧽` → `a \end{matrix}` 429 | (right-pointing curved angle bracket U+29FD), 430 | 431 | SugarTeX finds non-escaped \*⧽ first (for example) then before it searches for a place after non-escaped `{`, `˱`, space, newline or start of the string that is not inside `{}` or `˱˲` – SugarTeX counts opening and closing `{}˱˲` (`˱˲` would later be replaced with `{}` so both are counted together). 432 | 433 | **In combination with styles:** 434 | 435 | When combining **one-character** postfix unary operators with styles the order in which operators are applied changes: 436 | 437 | `[ᵇx ⃗]` → `\vec{\mathbf{x }}` 438 | 439 | ***SugarTeX Completions for Atom***: 440 | 441 | * ← `\^->`, 442 | * ← `\^->\har`, 443 | * `ˆ` ← `\^\small`, 444 | * `¯` ← `\^_\small` (macron), 445 | * `¯` ← `\^-\small` (macron), 446 | * `‾` ← `\^_` (overline), 447 | * `˙` ← `\^.`, 448 | * `¨` ← `\^..`, 449 | * `⏞` ← `\^}\rot90`, 450 | * `⏟` ← `\_}\rot-90`, 451 | * `ˍ` ← `\_`, 452 | * `⧼` ← `\<\alt2`, 453 | * `⧽` ← `\>\alt2`, 454 | * `⧼⧽` ← `\<>\alt2`. 455 | 456 | 457 | ## Center binary operators 458 | 459 | ### Matrices 460 | 461 | Family of `*matrix` amsmath macros is given by `¦⠋` operator (broken bar U+00A6, braille pattern dots-124 U+280B): 462 | 463 | `˱[a ˳b ¦⠋ c ˳d]˲` → 464 |  `\begin{bmatrix}a ˳b¦c ˳d\end{bmatrix}` → 465 |  `\begin{bmatrix}a &b\\c &d\end{bmatrix}` 466 | 467 | All brackets: 468 | 469 | * `˱a ˳b ¦⠋ c ˳d˲` → `...matrix...` (**no brackets**, 470 | modifier letter low left/right arrowhead U+02F1/U+02F2), 471 | * `{a ˳b ¦⠋ c ˳d}` → `...Bmatrix...` (**curly brackets**), 472 | * `˱(a ˳b ¦⠋ c ˳d)˲`/`{(a ˳b ¦⠋ c ˳d)}` → `...pmatrix...`, 473 | * `˱[a ˳b ¦⠋ c ˳d]˲`/`{[a ˳b ¦⠋ c ˳d]}` → `...bmatrix...`, 474 | * `˱│a ˳b ¦⠋ c ˳d│˲`/`{│a ˳b ¦⠋ c ˳d│}`/ 475 | `˱|a ˳b ¦⠋ c ˳d|˲`/`{|a ˳b ¦⠋ c ˳d|}` → `...vmatrix...` 476 | (box drawings light vertical U+2502, for math in markdown tables), 477 | * `˱‖a ˳b ¦⠋ c ˳d‖˲`/`{‖a ˳b ¦⠋ c ˳d‖}` → `...Vmatrix...` 478 | (double vertical line U+2016). 479 | 480 | SugarTeX finds non-escaped binary operator separator `¦⠋` first then: 481 | 482 | * searches for a place after non-escaped `{` or `˱` that is not inside `{}` or `˱˲`, 483 | * searches for a place before non-escaped `}` or `˲` that is not inside `{}` or `˱˲`, 484 | * it also figures out bracket type properly, 485 | * this way it finds two arguments (SugarTeX counts opening and closing `{}˱˲`, `˱˲` would later be replaced with `{}` so both are counted together). 486 | 487 | ***SugarTeX Completions for Atom***: 488 | 489 | * `˳` ← `\&`, 490 | * `˳` ← `\_o\small`, 491 | * `│` ← `\|`, 492 | * `‖` ← `\||`, 493 | * `˱` ← `\_<`, 494 | * `˲` ← `\_>`, 495 | * `˱˲` ← `\_<>`, 496 | * `¦` ← `\\`, 497 | * `¦` ← `\--\rot90`, 498 | * `⠋` ← `\^:.\rot-90`, 499 | * `⠋` ← `\matrix`. 500 | 501 | 502 | ### General fractions without bars 503 | 504 | Fractions works almost the same as Matrices - they add brackets and stack arguments: first arg is atop of the second arg. But with dome differences: 505 | 506 | * they use `¦⠘` or `¦⠃` as a separator (broken bar U+00A6, braille pattern dots-45 U+2818 / dots-12 U+2803), 507 | * cannot handle more than one line break (so two args only), 508 | * they use `\genfrac` amsmath macro, 509 | * they can have size modifiers after `¦⠘`: 510 | * `ᵈ`/`^{d}` - display mode, 511 | * `ᵗ`/`^{t}` - text mode, 512 | * `ˢ`/`^{s}` - smaller, 513 | * `ˣˢ`/`^{xs}` - extra small, 514 | * left and right brackets can be different. 515 | 516 | Examples: 517 | 518 | * `˱(x¦⠘ᵗy)˲`, 519 | * `˱[x¦⠘y]˲`, 520 | * `{x¦⠘y}` (**curly brackets**), 521 | * `˱x¦⠘y˲` (**no brackets**, modifier letter low left/right arrowhead U+02F1/U+02F2), 522 | * `˱|x¦⠘y|˲`, `˱│x¦⠘y│˲` (box drawings light vertical U+2502, for math in markdown tables), 523 | * `˱‖x¦⠘ᵈy‖˲` (double vertical line U+2016). 524 | 525 | Arguments search algorithm is the same as for matrices. 526 | 527 | ***SugarTeX Completions for Atom***: 528 | 529 | * `│` ← `\|`, 530 | * `‖` ← `\||`, 531 | * `˱` ← `\_<`, 532 | * `˲` ← `\_>`, 533 | * `˱˲` ← `\_<>`, 534 | * `¦` ← `\\`, 535 | * `¦` ← `\--\rot90`, 536 | * `⠘` ← `\^:`. 537 | 538 | 539 | ### Greedy center binary operators 540 | 541 | Arguments search algorithm is the same as for matrices (except it now does not have brackets). 542 | 543 | 1) `˱smth1 ¦⠛ᵗ smth2˲` → 544 |  `\begin{smallmatrix}smth1¦smth2\end{smallmatrix}`, 545 | (Braille Pattern Dots-1245 U+281B). 546 | 547 | ``` 548 | \ˎ˳˳(˱a ˳b ¦⠛ᵗ c ˳d˲)˳˳\ˎ 549 | ``` 550 | 551 | 2) `˱smth1 ¦⠛ smth2˲` → 552 |  `\begin{array}smth1¦smth2\end{array}`, 553 | (Braille Pattern Dots-1245 U+281B). 554 | 555 | ``` 556 | \ˎ\ˎ 557 | ˳[˱ ˱cccc|c˲ 558 | x₁₁ ˳x₁₂ ˳x₁₃ ˳… ˳x₁ₙ ¦⠛ 559 | x₂₁ ˳x₂₂ ˳x₂₃ ˳… ˳x₂ₙ ¦ 560 | ⋮ ˳ ⋮ ˳ ⋮ ˳⋱ ˳ ⋮ ¦ 561 | xₚ₁ ˳xₚ₂ ˳xₚ₃ ˳… ˳xₚₙ ˲]˳ 562 | \ˎ\ˎ 563 | ``` 564 | 565 | 3) `˱smth1 ¦# smth2˲` → 566 |  `\begin{aligned}smth1¦smth2\end{aligned}`, 567 | 568 | ``` 569 | \ˎ\ˎ 570 | ˳|x|˳ = ˳{˱ x˳ ‹if› x≥0 ¦# 571 | -x˳ ‹if› x<0 ˲ ˲˳ 572 | \ˎ\ˎ 573 | ``` 574 | 575 | 4) `˱smth1 ¦˽ smth2˲` / `˱smth1 ¦⎵ smth2˲` → 576 |  `\substack{smth1¦smth2}`, 577 | (modifier letter shelf U+02FD / bottom square bracket U+23B5) 578 | 579 | ``` 580 | \ˎ\ˎ ∑ⁿˍ{0≤i≤N ¦˽ 0≤j≤M} (ij)³ \ˎ\ˎ 581 | ``` 582 | 583 | 5) `˱smth1 ¦˽ˡ smth2˲` / `˱smth1 ¦⎵ˡ smth2˲` → 584 |  `\begin{subarray}{l}smth1¦smth2\end{subarray}`, 585 | (modifier letter shelf U+02FD / bottom square bracket U+23B5) 586 | 587 | ``` 588 | \ˎ\ˎ ∑ⁿˍ{0≤i≤N ¦˽ˡ 0≤j≤M} (ij)³ \ˎ\ˎ 589 | ``` 590 | 591 | Instead of `ˡ` (left) it can also be `ᶜ` (center) or `ʳ` (right). 592 | 593 | ***SugarTeX Completions for Atom***: 594 | 595 | * `⠛` ← `\^::`, 596 | * `˽` ← `\__`, 597 | * `˽` ← `\_]\rot-90`, 598 | * `⎵` ← `\_]\rot-90\alt`, 599 | * `¦` ← `\\`, 600 | * `¦` ← `\--\rot90`. 601 | 602 | 603 | ### Standard center binary operators 604 | 605 | #### Fractions 606 | 607 | * `x∕y` → `\frac{x}{y}` (division slash U+2215), 608 | * `1+x∕y` → `\frac{1+x}{y}`, 609 | * `1 + {x + z}∕y` → `1 + \frac{{x + z}}{y}`, 610 | * `x∕ᵈy` → `\dfrac{x}{y}`, 611 | * `x∕ᵗy` → `\tfrac{x}{y}`, 612 | * `x∕ᶜy` → `\cfrac{x}{y}`, 613 | * `x∕ˢy` and `x∕ˣˢy` are the same as `x∕ᵗy` but smaller and use `\genfrac` macros. Bar thickness can be set this way: `{0.5px}x∕ˢy`. 614 | 615 | #### Roots, overset, underset 616 | 617 | * `√64` → `\sqrt[]{64}` (square root U+221A), 618 | * `⁶√64` → `\sqrt[6]{64}`, 619 | * `1 + ⁶√64` → `1 + \sqrt[6]{64}`, 620 | * `˹lim˺˽x→0` / `˹lim˺⎵x→0` → `\underset{x→0}{˹lim˺}` (modifier letter shelf U+02FD / bottom square bracket U+23B5), 621 | * `{x + … + x}⏞⎴{k ‹times›}` → 622 | `\overset{{k ‹times›}}{{x + … + x}⏞}` (top square bracket U+23B4). 623 | 624 | #### Binomial coefficients 625 | 626 | * `(i¦ᶜn)` → `\binom{i}{n}`, 627 | * `(i¦ᶜᵈn)` → `\dbinom{i}{n}` (display), 628 | * `(i¦ᶜᵗn)` → `\tbinom{i}{n}` (text). 629 | 630 | In this case SugarTeX finds non-escaped binary operator separator `¦ᶜ` first then searches for `(` and `)`. Other stop symbols do not work. 631 | 632 | SugarTeX finds non-escaped binary operator separator (like `∕`) first then: 633 | 634 | * searches for a place after non-escaped `{`, `˱`, space, newline or start of the string that is not inside `{}` or `˱˲`, 635 | * searches for a place before non-escaped `}`, `˲`, space, newline or end of the string that is not inside `{}` or `˱˲`, 636 | * this way it finds two arguments (SugarTeX counts opening and closing `{}˱˲`, `˱˲` would later be replaced with `{}` so both are counted together). 637 | 638 | ***SugarTeX Completions for Atom***: 639 | 640 | * `˽` ← `\__`, 641 | * `˽` ← `\_]\rot-90`, 642 | * `⎵` ← `\_]\rot-90\alt`, 643 | * `⎴` ← `\^^`, 644 | * `⎴` ← `\^]\rot90`, 645 | * `∕` ← `\/`, 646 | * `√` ← `\^1/2`, 647 | * `¦` ← `\\`, 648 | * `¦` ← `\--\rot90`. 649 | 650 | 651 | ## Regular expressions loop replacements 652 | 653 | Nothing. But can be tweaked. 654 | 655 | 656 | ## Regular expressions post-replacements 657 | 658 | Nothing. But can be tweaked. 659 | 660 | 661 | ## Simple post-replacements 662 | 663 | * `¦` → `\\` (broken bar U+00A6, this should be after other `¦` replacements), 664 | * `˳` → `&` (modifier letter low ring U+02F3, this should be after brackets and other `˳` replacements), 665 | * `˱` → `{` and `˲` → `}` (modifier letter low left/right arrowhead U+02F1/U+02F2), 666 | * `ˍ` → `_` (modifier letter low macron U+02CD), 667 | * `` ` `` → `\`, 668 | * `ˋ` → `\` (modifier letter grave accent U+02CB), 669 | * `↕^{d}` → `\displaystyle` (up down arrow U+2195), 670 | * `↕^{t}` → `\textstyle`, 671 | * `↕^{s}` → `\scriptstyle`, 672 | * `↕^{xs}` → `\scriptscriptstyle`, 673 | * Superscripts and Subscripts replacements give: 674 | * `↕ᵈ` → `\displaystyle`, 675 | * `↕ᵗ` → `\textstyle`, 676 | * `↕ˢ` → `\scriptstyle`, 677 | * `↕ˣˢ` → `\scriptscriptstyle`. 678 | 679 | ***SugarTeX Completions for Atom***: 680 | 681 | * `¦` ← `\\`, 682 | * `¦` ← `\--\rot90`, 683 | * `˳` ← `\&`, 684 | * `˳` ← `\_o\small`, 685 | * `˱` ← `\_<`, 686 | * `˲` ← `\_>`, 687 | * `˱˲` ← `\_<>`, 688 | * `ˍ` ← `\_`, 689 | * `ˋ` ← `` \` `` (modifier letter grave accent). 690 | * `↕` ← `\<->\rot90`. 691 | 692 | 693 | ## Escapable characters 694 | 695 | All one-character replacements from: 696 | 697 | * Prefix unary operators, 698 | * Postfix unary operators, 699 | * Center binary operators, 700 | * Nullary operators, 701 | * Simple pre-replacements, 702 | * Simple post-replacements, 703 | 704 | and `⋲`, `›`, `˺`, `↕`, `ˌ` 705 | 706 | (element of with long horizontal stroke U+22F2, single right-pointing angle quotation mark U+203A, modifier letter end high tone U+02FA, up down arrow U+2195, modifier letter low vertical line U+02CC) 707 | 708 | are escapable with `\`. 709 | 710 | 711 | # Examples 712 | 713 | You can find SugarTeX examples [**in this document**](https://github.com/kiwi0fruit/sugartex/tree/master/examples) (SugarTeX code + rendered formulas). 714 | 715 | 716 | # Caveats of SugarTeX to docx conversion with free software only 717 | 718 | [Math support in pandoc](https://github.com/kiwi0fruit/open-fonts/blob/master/language_variants_and_math_support.md#pandoc). 719 | -------------------------------------------------------------------------------- /sugartex.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kiwi0fruit/sugartex/a80aa33d560f8e93dbdfa984bdf3caf3800ea6c6/sugartex.pdf -------------------------------------------------------------------------------- /sugartex/__init__.py: -------------------------------------------------------------------------------- 1 | from .sugartex_filter import SugarTeX # noqa 2 | from .pre_sugartex import ( 3 | sugartex, 4 | sugartex_replace_all, sugartex_replace_all as stex, 5 | sugartex_preprocess, sugartex_preprocess as pre 6 | ) # noqa 7 | from .sugartex_pandoc_filter import main # noqa 8 | 9 | from ._version import get_versions 10 | __version__ = get_versions()['version'] 11 | del get_versions 12 | -------------------------------------------------------------------------------- /sugartex/_version.py: -------------------------------------------------------------------------------- 1 | 2 | # This file helps to compute a version number in source trees obtained from 3 | # git-archive tarball (such as those provided by githubs download-from-tag 4 | # feature). Distribution tarballs (built by setup.py sdist) and build 5 | # directories (produced by setup.py build) will contain a much shorter file 6 | # that just contains the computed version number. 7 | 8 | # This file is released into the public domain. Generated by 9 | # versioneer-0.16 (https://github.com/warner/python-versioneer) 10 | 11 | """Git implementation of _version.py.""" 12 | 13 | import errno 14 | import os 15 | import re 16 | import subprocess 17 | import sys 18 | 19 | 20 | def get_keywords(): 21 | """Get the keywords needed to look up the version information.""" 22 | # these strings will be replaced by git during git-archive. 23 | # setup.py/versioneer.py will grep for the variable names, so they must 24 | # each be defined on a line of their own. _version.py will just call 25 | # get_keywords(). 26 | git_refnames = " (HEAD -> master)" 27 | git_full = "a80aa33d560f8e93dbdfa984bdf3caf3800ea6c6" 28 | keywords = {"refnames": git_refnames, "full": git_full} 29 | return keywords 30 | 31 | 32 | class VersioneerConfig: 33 | """Container for Versioneer configuration parameters.""" 34 | 35 | 36 | def get_config(): 37 | """Create, populate and return the VersioneerConfig() object.""" 38 | # these strings are filled in when 'setup.py versioneer' creates 39 | # _version.py 40 | cfg = VersioneerConfig() 41 | cfg.VCS = "git" 42 | cfg.style = "pep440" 43 | cfg.tag_prefix = "" 44 | cfg.parentdir_prefix = "None" 45 | cfg.versionfile_source = "sugartex/_version.py" 46 | cfg.verbose = False 47 | return cfg 48 | 49 | 50 | class NotThisMethod(Exception): 51 | """Exception raised if a method is not valid for the current scenario.""" 52 | 53 | 54 | LONG_VERSION_PY = {} 55 | HANDLERS = {} 56 | 57 | 58 | def register_vcs_handler(vcs, method): # decorator 59 | """Decorator to mark a method as the handler for a particular VCS.""" 60 | def decorate(f): 61 | """Store f in HANDLERS[vcs][method].""" 62 | if vcs not in HANDLERS: 63 | HANDLERS[vcs] = {} 64 | HANDLERS[vcs][method] = f 65 | return f 66 | return decorate 67 | 68 | 69 | def run_command(commands, args, cwd=None, verbose=False, hide_stderr=False): 70 | """Call the given command(s).""" 71 | assert isinstance(commands, list) 72 | p = None 73 | for c in commands: 74 | try: 75 | dispcmd = str([c] + args) 76 | # remember shell=False, so use git.cmd on windows, not just git 77 | p = subprocess.Popen([c] + args, cwd=cwd, stdout=subprocess.PIPE, 78 | stderr=(subprocess.PIPE if hide_stderr 79 | else None)) 80 | break 81 | except EnvironmentError: 82 | e = sys.exc_info()[1] 83 | if e.errno == errno.ENOENT: 84 | continue 85 | if verbose: 86 | print("unable to run %s" % dispcmd) 87 | print(e) 88 | return None 89 | else: 90 | if verbose: 91 | print("unable to find command, tried %s" % (commands,)) 92 | return None 93 | stdout = p.communicate()[0].strip() 94 | if sys.version_info[0] >= 3: 95 | stdout = stdout.decode() 96 | if p.returncode != 0: 97 | if verbose: 98 | print("unable to run %s (error)" % dispcmd) 99 | return None 100 | return stdout 101 | 102 | 103 | def versions_from_parentdir(parentdir_prefix, root, verbose): 104 | """Try to determine the version from the parent directory name. 105 | 106 | Source tarballs conventionally unpack into a directory that includes 107 | both the project name and a version string. 108 | """ 109 | dirname = os.path.basename(root) 110 | if not dirname.startswith(parentdir_prefix): 111 | if verbose: 112 | print("guessing rootdir is '%s', but '%s' doesn't start with " 113 | "prefix '%s'" % (root, dirname, parentdir_prefix)) 114 | raise NotThisMethod("rootdir doesn't start with parentdir_prefix") 115 | return {"version": dirname[len(parentdir_prefix):], 116 | "full-revisionid": None, 117 | "dirty": False, "error": None} 118 | 119 | 120 | @register_vcs_handler("git", "get_keywords") 121 | def git_get_keywords(versionfile_abs): 122 | """Extract version information from the given file.""" 123 | # the code embedded in _version.py can just fetch the value of these 124 | # keywords. When used from setup.py, we don't want to import _version.py, 125 | # so we do it with a regexp instead. This function is not used from 126 | # _version.py. 127 | keywords = {} 128 | try: 129 | f = open(versionfile_abs, "r") 130 | for line in f.readlines(): 131 | if line.strip().startswith("git_refnames ="): 132 | mo = re.search(r'=\s*"(.*)"', line) 133 | if mo: 134 | keywords["refnames"] = mo.group(1) 135 | if line.strip().startswith("git_full ="): 136 | mo = re.search(r'=\s*"(.*)"', line) 137 | if mo: 138 | keywords["full"] = mo.group(1) 139 | f.close() 140 | except EnvironmentError: 141 | pass 142 | return keywords 143 | 144 | 145 | @register_vcs_handler("git", "keywords") 146 | def git_versions_from_keywords(keywords, tag_prefix, verbose): 147 | """Get version information from git keywords.""" 148 | if not keywords: 149 | raise NotThisMethod("no keywords at all, weird") 150 | refnames = keywords["refnames"].strip() 151 | if refnames.startswith("$Format"): 152 | if verbose: 153 | print("keywords are unexpanded, not using") 154 | raise NotThisMethod("unexpanded keywords, not a git-archive tarball") 155 | refs = set([r.strip() for r in refnames.strip("()").split(",")]) 156 | # starting in git-1.8.3, tags are listed as "tag: foo-1.0" instead of 157 | # just "foo-1.0". If we see a "tag: " prefix, prefer those. 158 | TAG = "tag: " 159 | tags = set([r[len(TAG):] for r in refs if r.startswith(TAG)]) 160 | if not tags: 161 | # Either we're using git < 1.8.3, or there really are no tags. We use 162 | # a heuristic: assume all version tags have a digit. The old git %d 163 | # expansion behaves like git log --decorate=short and strips out the 164 | # refs/heads/ and refs/tags/ prefixes that would let us distinguish 165 | # between branches and tags. By ignoring refnames without digits, we 166 | # filter out many common branch names like "release" and 167 | # "stabilization", as well as "HEAD" and "master". 168 | tags = set([r for r in refs if re.search(r'\d', r)]) 169 | if verbose: 170 | print("discarding '%s', no digits" % ",".join(refs-tags)) 171 | if verbose: 172 | print("likely tags: %s" % ",".join(sorted(tags))) 173 | for ref in sorted(tags): 174 | # sorting will prefer e.g. "2.0" over "2.0rc1" 175 | if ref.startswith(tag_prefix): 176 | r = ref[len(tag_prefix):] 177 | if verbose: 178 | print("picking %s" % r) 179 | return {"version": r, 180 | "full-revisionid": keywords["full"].strip(), 181 | "dirty": False, "error": None 182 | } 183 | # no suitable tags, so version is "0+unknown", but full hex is still there 184 | if verbose: 185 | print("no suitable tags, using unknown + full revision id") 186 | return {"version": "0+unknown", 187 | "full-revisionid": keywords["full"].strip(), 188 | "dirty": False, "error": "no suitable tags"} 189 | 190 | 191 | @register_vcs_handler("git", "pieces_from_vcs") 192 | def git_pieces_from_vcs(tag_prefix, root, verbose, run_command=run_command): 193 | """Get version from 'git describe' in the root of the source tree. 194 | 195 | This only gets called if the git-archive 'subst' keywords were *not* 196 | expanded, and _version.py hasn't already been rewritten with a short 197 | version string, meaning we're inside a checked out source tree. 198 | """ 199 | if not os.path.exists(os.path.join(root, ".git")): 200 | if verbose: 201 | print("no .git in %s" % root) 202 | raise NotThisMethod("no .git directory") 203 | 204 | GITS = ["git"] 205 | if sys.platform == "win32": 206 | GITS = ["git.cmd", "git.exe"] 207 | # if there is a tag matching tag_prefix, this yields TAG-NUM-gHEX[-dirty] 208 | # if there isn't one, this yields HEX[-dirty] (no NUM) 209 | describe_out = run_command(GITS, ["describe", "--tags", "--dirty", 210 | "--always", "--long", 211 | "--match", "%s*" % tag_prefix], 212 | cwd=root) 213 | # --long was added in git-1.5.5 214 | if describe_out is None: 215 | raise NotThisMethod("'git describe' failed") 216 | describe_out = describe_out.strip() 217 | full_out = run_command(GITS, ["rev-parse", "HEAD"], cwd=root) 218 | if full_out is None: 219 | raise NotThisMethod("'git rev-parse' failed") 220 | full_out = full_out.strip() 221 | 222 | pieces = {} 223 | pieces["long"] = full_out 224 | pieces["short"] = full_out[:7] # maybe improved later 225 | pieces["error"] = None 226 | 227 | # parse describe_out. It will be like TAG-NUM-gHEX[-dirty] or HEX[-dirty] 228 | # TAG might have hyphens. 229 | git_describe = describe_out 230 | 231 | # look for -dirty suffix 232 | dirty = git_describe.endswith("-dirty") 233 | pieces["dirty"] = dirty 234 | if dirty: 235 | git_describe = git_describe[:git_describe.rindex("-dirty")] 236 | 237 | # now we have TAG-NUM-gHEX or HEX 238 | 239 | if "-" in git_describe: 240 | # TAG-NUM-gHEX 241 | mo = re.search(r'^(.+)-(\d+)-g([0-9a-f]+)$', git_describe) 242 | if not mo: 243 | # unparseable. Maybe git-describe is misbehaving? 244 | pieces["error"] = ("unable to parse git-describe output: '%s'" 245 | % describe_out) 246 | return pieces 247 | 248 | # tag 249 | full_tag = mo.group(1) 250 | if not full_tag.startswith(tag_prefix): 251 | if verbose: 252 | fmt = "tag '%s' doesn't start with prefix '%s'" 253 | print(fmt % (full_tag, tag_prefix)) 254 | pieces["error"] = ("tag '%s' doesn't start with prefix '%s'" 255 | % (full_tag, tag_prefix)) 256 | return pieces 257 | pieces["closest-tag"] = full_tag[len(tag_prefix):] 258 | 259 | # distance: number of commits since tag 260 | pieces["distance"] = int(mo.group(2)) 261 | 262 | # commit: short hex revision ID 263 | pieces["short"] = mo.group(3) 264 | 265 | else: 266 | # HEX: no tags 267 | pieces["closest-tag"] = None 268 | count_out = run_command(GITS, ["rev-list", "HEAD", "--count"], 269 | cwd=root) 270 | pieces["distance"] = int(count_out) # total number of commits 271 | 272 | return pieces 273 | 274 | 275 | def plus_or_dot(pieces): 276 | """Return a + if we don't already have one, else return a .""" 277 | if "+" in pieces.get("closest-tag", ""): 278 | return "." 279 | return "+" 280 | 281 | 282 | def render_pep440(pieces): 283 | """Build up version string, with post-release "local version identifier". 284 | 285 | Our goal: TAG[+DISTANCE.gHEX[.dirty]] . Note that if you 286 | get a tagged build and then dirty it, you'll get TAG+0.gHEX.dirty 287 | 288 | Exceptions: 289 | 1: no tags. git_describe was just HEX. 0+untagged.DISTANCE.gHEX[.dirty] 290 | """ 291 | if pieces["closest-tag"]: 292 | rendered = pieces["closest-tag"] 293 | if pieces["distance"] or pieces["dirty"]: 294 | rendered += plus_or_dot(pieces) 295 | rendered += "%d.g%s" % (pieces["distance"], pieces["short"]) 296 | if pieces["dirty"]: 297 | rendered += ".dirty" 298 | else: 299 | # exception #1 300 | rendered = "0+untagged.%d.g%s" % (pieces["distance"], 301 | pieces["short"]) 302 | if pieces["dirty"]: 303 | rendered += ".dirty" 304 | return rendered 305 | 306 | 307 | def render_pep440_pre(pieces): 308 | """TAG[.post.devDISTANCE] -- No -dirty. 309 | 310 | Exceptions: 311 | 1: no tags. 0.post.devDISTANCE 312 | """ 313 | if pieces["closest-tag"]: 314 | rendered = pieces["closest-tag"] 315 | if pieces["distance"]: 316 | rendered += ".post.dev%d" % pieces["distance"] 317 | else: 318 | # exception #1 319 | rendered = "0.post.dev%d" % pieces["distance"] 320 | return rendered 321 | 322 | 323 | def render_pep440_post(pieces): 324 | """TAG[.postDISTANCE[.dev0]+gHEX] . 325 | 326 | The ".dev0" means dirty. Note that .dev0 sorts backwards 327 | (a dirty tree will appear "older" than the corresponding clean one), 328 | but you shouldn't be releasing software with -dirty anyways. 329 | 330 | Exceptions: 331 | 1: no tags. 0.postDISTANCE[.dev0] 332 | """ 333 | if pieces["closest-tag"]: 334 | rendered = pieces["closest-tag"] 335 | if pieces["distance"] or pieces["dirty"]: 336 | rendered += ".post%d" % pieces["distance"] 337 | if pieces["dirty"]: 338 | rendered += ".dev0" 339 | rendered += plus_or_dot(pieces) 340 | rendered += "g%s" % pieces["short"] 341 | else: 342 | # exception #1 343 | rendered = "0.post%d" % pieces["distance"] 344 | if pieces["dirty"]: 345 | rendered += ".dev0" 346 | rendered += "+g%s" % pieces["short"] 347 | return rendered 348 | 349 | 350 | def render_pep440_old(pieces): 351 | """TAG[.postDISTANCE[.dev0]] . 352 | 353 | The ".dev0" means dirty. 354 | 355 | Eexceptions: 356 | 1: no tags. 0.postDISTANCE[.dev0] 357 | """ 358 | if pieces["closest-tag"]: 359 | rendered = pieces["closest-tag"] 360 | if pieces["distance"] or pieces["dirty"]: 361 | rendered += ".post%d" % pieces["distance"] 362 | if pieces["dirty"]: 363 | rendered += ".dev0" 364 | else: 365 | # exception #1 366 | rendered = "0.post%d" % pieces["distance"] 367 | if pieces["dirty"]: 368 | rendered += ".dev0" 369 | return rendered 370 | 371 | 372 | def render_git_describe(pieces): 373 | """TAG[-DISTANCE-gHEX][-dirty]. 374 | 375 | Like 'git describe --tags --dirty --always'. 376 | 377 | Exceptions: 378 | 1: no tags. HEX[-dirty] (note: no 'g' prefix) 379 | """ 380 | if pieces["closest-tag"]: 381 | rendered = pieces["closest-tag"] 382 | if pieces["distance"]: 383 | rendered += "-%d-g%s" % (pieces["distance"], pieces["short"]) 384 | else: 385 | # exception #1 386 | rendered = pieces["short"] 387 | if pieces["dirty"]: 388 | rendered += "-dirty" 389 | return rendered 390 | 391 | 392 | def render_git_describe_long(pieces): 393 | """TAG-DISTANCE-gHEX[-dirty]. 394 | 395 | Like 'git describe --tags --dirty --always -long'. 396 | The distance/hash is unconditional. 397 | 398 | Exceptions: 399 | 1: no tags. HEX[-dirty] (note: no 'g' prefix) 400 | """ 401 | if pieces["closest-tag"]: 402 | rendered = pieces["closest-tag"] 403 | rendered += "-%d-g%s" % (pieces["distance"], pieces["short"]) 404 | else: 405 | # exception #1 406 | rendered = pieces["short"] 407 | if pieces["dirty"]: 408 | rendered += "-dirty" 409 | return rendered 410 | 411 | 412 | def render(pieces, style): 413 | """Render the given version pieces into the requested style.""" 414 | if pieces["error"]: 415 | return {"version": "unknown", 416 | "full-revisionid": pieces.get("long"), 417 | "dirty": None, 418 | "error": pieces["error"]} 419 | 420 | if not style or style == "default": 421 | style = "pep440" # the default 422 | 423 | if style == "pep440": 424 | rendered = render_pep440(pieces) 425 | elif style == "pep440-pre": 426 | rendered = render_pep440_pre(pieces) 427 | elif style == "pep440-post": 428 | rendered = render_pep440_post(pieces) 429 | elif style == "pep440-old": 430 | rendered = render_pep440_old(pieces) 431 | elif style == "git-describe": 432 | rendered = render_git_describe(pieces) 433 | elif style == "git-describe-long": 434 | rendered = render_git_describe_long(pieces) 435 | else: 436 | raise ValueError("unknown style '%s'" % style) 437 | 438 | return {"version": rendered, "full-revisionid": pieces["long"], 439 | "dirty": pieces["dirty"], "error": None} 440 | 441 | 442 | def get_versions(): 443 | """Get version information or return default if unable to do so.""" 444 | # I am in _version.py, which lives at ROOT/VERSIONFILE_SOURCE. If we have 445 | # __file__, we can work backwards from there to the root. Some 446 | # py2exe/bbfreeze/non-CPython implementations don't do __file__, in which 447 | # case we can only use expanded keywords. 448 | 449 | cfg = get_config() 450 | verbose = cfg.verbose 451 | 452 | try: 453 | return git_versions_from_keywords(get_keywords(), cfg.tag_prefix, 454 | verbose) 455 | except NotThisMethod: 456 | pass 457 | 458 | try: 459 | root = os.path.realpath(__file__) 460 | # versionfile_source is the relative path from the top of the source 461 | # tree (where the .git directory might live) to this file. Invert 462 | # this to find the root from __file__. 463 | for i in cfg.versionfile_source.split('/'): 464 | root = os.path.dirname(root) 465 | except NameError: 466 | return {"version": "0+unknown", "full-revisionid": None, 467 | "dirty": None, 468 | "error": "unable to find root of source tree"} 469 | 470 | try: 471 | pieces = git_pieces_from_vcs(cfg.tag_prefix, root, verbose) 472 | return render(pieces, cfg.style) 473 | except NotThisMethod: 474 | pass 475 | 476 | try: 477 | if cfg.parentdir_prefix: 478 | return versions_from_parentdir(cfg.parentdir_prefix, root, verbose) 479 | except NotThisMethod: 480 | pass 481 | 482 | return {"version": "0+unknown", "full-revisionid": None, 483 | "dirty": None, 484 | "error": "unable to compute version"} 485 | -------------------------------------------------------------------------------- /sugartex/kiwi.py: -------------------------------------------------------------------------------- 1 | from .sugartex_pandoc_filter import main, kiwi_hack 2 | 3 | kiwi_hack() 4 | 5 | if __name__ == '__main__': 6 | main() 7 | -------------------------------------------------------------------------------- /sugartex/pre_sugartex.py: -------------------------------------------------------------------------------- 1 | """ 2 | CLI wrapper for sugartex_preprosess function: 3 | (source: str) -> str 4 | """ 5 | import sys 6 | import re 7 | from .sugartex_filter import SugarTeX 8 | import time 9 | 10 | SESSION_ID = '§' + str(int(round(time.time() * 1000)))[-4:] + '§' 11 | 12 | 13 | def sugartex_preprocess(source: str) -> str: 14 | """ 15 | Preprocess text for SugarTeX Pandoc filter. 16 | Replaces 'ˎ' with `$` (except `\ˎ`), replaces `\ˎ` with `ˎ` 17 | """ 18 | rep = {r'\ˎ': 'ˎ', 'ˎ': '$'} 19 | return re.sub(r'\\ˎ|ˎ', lambda m: rep[m.group(0)], source) 20 | 21 | 22 | sugartex = SugarTeX(ready=False) 23 | 24 | 25 | def sugartex_replace_all(string): 26 | """ 27 | Replace all with SugarTeX. 28 | Runs ``sugartex_preprocess`` then iterates via regex and 29 | replaces each math between '$...$'. 30 | """ 31 | string = sugartex_preprocess(string).replace(r'\$', SESSION_ID) 32 | return re.sub( 33 | r'(?<=\$)[^$]*(?=\$)', 34 | lambda m: sugartex.replace(m.group(0)), 35 | string 36 | ).replace(SESSION_ID, r'\$') 37 | 38 | 39 | def main(): 40 | """ 41 | Usage: pre-sugartex [OPTIONS] 42 | 43 | Reads from stdin and writes to stdout. 44 | When no options: only replace 45 | U+02CE Modifier Letter Low Grave Accent 46 | (that looks like low '`') with $ 47 | 48 | Options: 49 | --all Full SugarTeX replace with regexp, 50 | --kiwi Same as above but with kiwi flavor, 51 | --help Show this message and exit. 52 | """ 53 | if len(sys.argv) > 1: 54 | arg1 = sys.argv[1] 55 | if arg1 == '--all' or arg1 == '--kiwi': 56 | if arg1 == '--kiwi': 57 | sugartex.mjx_hack() 58 | # sugartex.subscripts['ᵩ'] = 'ψ' # Consolas font specific 59 | # sugartex.superscripts['ᵠ'] = 'ψ' # Consolas font specific 60 | sugartex.ready() 61 | sys.stdout.write(sugartex_replace_all(sys.stdin.read())) 62 | elif arg1.lower() == '--help': 63 | print(str(main.__doc__).replace('\n ', '\n')) 64 | else: 65 | raise Exception("Invalid first argument: " + arg1) 66 | else: 67 | sys.stdout.write(sugartex_preprocess(sys.stdin.read())) 68 | 69 | 70 | if __name__ == '__main__': 71 | main() 72 | -------------------------------------------------------------------------------- /sugartex/sugartex_filter.py: -------------------------------------------------------------------------------- 1 | import re 2 | import copy 3 | from collections import OrderedDict 4 | from typing import Type, Callable, Iterable 5 | from itertools import chain 6 | 7 | 8 | # SugarTeX language extension: 9 | # ---------------------------- 10 | # Superscripts and subscripts: 11 | # all are escapable 12 | # ₀₁₂₃₄₅₆₇₈₉ₐₑₕᵢⱼₖₗₘₙₒₚᵣₛₜᵤᵥₓᵦᵧᵨᵩᵪ₊₋₌₍₎ 13 | _subscripts = { 14 | '₀': '0', 15 | '₁': '1', 16 | '₂': '2', 17 | '₃': '3', 18 | '₄': '4', 19 | '₅': '5', 20 | '₆': '6', 21 | '₇': '7', 22 | '₈': '8', 23 | '₉': '9', 24 | 'ₐ': 'a', # no b c d 25 | 'ₑ': 'e', # no f g 26 | 'ₕ': 'h', 27 | 'ᵢ': 'i', 28 | 'ⱼ': 'j', 29 | 'ₖ': 'k', 30 | 'ₗ': 'l', 31 | 'ₘ': 'm', 32 | 'ₙ': 'n', 33 | 'ₒ': 'o', 34 | 'ₚ': 'p', # no q 35 | 'ᵣ': 'r', 36 | 'ₛ': 's', 37 | 'ₜ': 't', 38 | 'ᵤ': 'u', 39 | 'ᵥ': 'v', # no w 40 | 'ₓ': 'x', # no y z 41 | 'ᵦ': 'β', 42 | 'ᵧ': 'γ', 43 | 'ᵨ': 'ρ', 44 | # 'ᵩ': 'ψ', # problems in consolas font ψ<>φ, ᵠ<>ᶲ, ᵩ<>φ 45 | 'ᵪ': 'χ', 46 | '₊': '+', 47 | '₋': '-', 48 | '₌': '=', 49 | '₍': '(', 50 | '₎': ')', 51 | } 52 | # all are escapable 53 | # ⁰¹²³⁴⁵⁶⁷⁸⁹ᵃᵇᶜᵈᵉᶠᵍʰⁱʲᵏˡᵐⁿᵒᵖʳˢᵗᵘᵛʷˣʸᶻᵅᵝᵞᵟᵋᶿᶥᶲᵠᵡ⁺⁻⁼⁽⁾ᴬᴮᴰᴱᴳᴴᴵᶦᴶᴷᴸᶫᴹᴺᶰᴼᴾᴿᵀᵁᶸⱽᵂ 54 | _superscripts = { 55 | '⁰': '0', 56 | '¹': '1', 57 | '²': '2', 58 | '³': '3', 59 | '⁴': '4', 60 | '⁵': '5', 61 | '⁶': '6', 62 | '⁷': '7', 63 | '⁸': '8', 64 | '⁹': '9', 65 | 'ᵃ': 'a', 66 | 'ᵇ': 'b', 67 | 'ᶜ': 'c', 68 | 'ᵈ': 'd', 69 | 'ᵉ': 'e', 70 | 'ᶠ': 'f', 71 | 'ᵍ': 'g', 72 | 'ʰ': 'h', 73 | 'ⁱ': 'i', 74 | 'ʲ': 'j', 75 | 'ᵏ': 'k', 76 | 'ˡ': 'l', 77 | 'ᵐ': 'm', 78 | 'ⁿ': 'n', 79 | 'ᵒ': 'o', 80 | 'ᵖ': 'p', # no q 81 | 'ʳ': 'r', 82 | 'ˢ': 's', 83 | 'ᵗ': 't', 84 | 'ᵘ': 'u', 85 | 'ᵛ': 'v', 86 | 'ʷ': 'w', 87 | 'ˣ': 'x', 88 | 'ʸ': 'y', 89 | 'ᶻ': 'z', 90 | 'ᵅ': 'α', 91 | 'ᵝ': 'β', 92 | 'ᵞ': 'γ', 93 | 'ᵟ': 'δ', 94 | 'ᵋ': 'ε', 95 | 'ᶿ': 'θ', 96 | 'ᶥ': 'ι', 97 | 'ᶲ': 'φ', 98 | # 'ᵠ': 'ψ', # problems in consolas font ψ<>φ, ᵠ<>ᶲ, ᵩ<>φ 99 | 'ᵡ': 'χ', 100 | '⁺': '+', 101 | '⁻': '-', 102 | '⁼': '=', 103 | '⁽': '(', 104 | '⁾': ')', 105 | 'ᴬ': 'A', 106 | 'ᴮ': 'B', # no C 107 | 'ᴰ': 'D', 108 | 'ᴱ': 'E', # no F 109 | 'ᴳ': 'G', 110 | 'ᴴ': 'H', 111 | 'ᴵ': 'I', 112 | 'ᶦ': 'I', 113 | 'ᴶ': 'J', 114 | 'ᴷ': 'K', 115 | 'ᴸ': 'L', 116 | 'ᶫ': 'L', 117 | 'ᴹ': 'M', 118 | 'ᴺ': 'N', 119 | 'ᶰ': 'N', 120 | 'ᴼ': 'O', 121 | 'ᴾ': 'P', # no Q 122 | 'ᴿ': 'R', # no S 123 | 'ᵀ': 'T', 124 | 'ᵁ': 'U', 125 | 'ᶸ': 'U', 126 | 'ⱽ': 'V', 127 | 'ᵂ': 'W', # no X Y Z 128 | } 129 | 130 | _brackets = [ 131 | (('[', r'['), (']', r']')), 132 | (('(', r'('), (')', r')')), 133 | (('{', r'\{'), ('}', r'\}')), 134 | (('│', r'\vert'), ('│', r'\vert')), # for math in markdown tables 135 | (('|', r'\vert'), ('|', r'\vert')), 136 | (('‖', r'\Vert'), ('‖', r'\Vert')), 137 | (('˱', r'.'), ('˲', r'.')), 138 | (('⟨', r'\langle'), ('⟩', r'\rangle')), 139 | (('⌊', r'\lfloor'), ('⌋', r'\rfloor')), 140 | (('⌈', r'\lceil'), ('⌉', r'\rceil')), 141 | ] 142 | _brackets_types = [ 143 | (('˳˳{}', r'\bigl{} '), ('{}˳˳', r'\bigr{}')), 144 | (('˳ˌ{}', r'\Bigl{} '), ('{}˳ˌ', r'\Bigr{}')), 145 | (('ˌ˳{}', r'\biggl{} '), ('{}ˌ˳', r'\biggr{}')), 146 | (('ˌˌ{}', r'\Biggl{} '), ('{}ˌˌ', r'\Biggr{}')), 147 | (('˳{}', r'\left{}{{'), ('{}˳', r'}}\right{}')) 148 | ] # ˳ alt+L + mlori, ˌ alt+L + molo vline 149 | 150 | # language=PythonRegExp 151 | _default_pref = r'^|(?<=\n)|(?<=^[ ˱{])|(?<=[^\\][ ˱{])' # language=PythonRegExp 152 | _default_postf = r'$|(?=\n)|(? str: 156 | ops = list(ops_) 157 | singles = re.escape(''.join(op for op in ops if len(op) == 1)) # language=PythonRegExp 158 | singles = [r'[{}]'.format(singles)] if (singles != '') else [] 159 | longs = [op for op in ops if len(op) > 1] 160 | longs.sort(key=lambda s: -len(s)) 161 | longs = [re.escape(op) for op in longs] 162 | return '|'.join(longs + singles) 163 | 164 | 165 | def _search_regex(ops: dict, regex_pat: str): 166 | """ 167 | Search order: 168 | * specified regexps 169 | * operators sorted from longer to shorter 170 | """ 171 | custom_regexps = list(filter(None, [dic['regex'] for op, dic in ops.items() if 'regex' in dic])) 172 | op_names = [op for op, dic in ops.items() if 'regex' not in dic] 173 | regex = [regex_pat.format(_ops_regex(op_names))] if len(op_names) > 0 else [] 174 | return re.compile('|'.join(custom_regexps + regex)) 175 | 176 | 177 | class Styles: 178 | """Math styles with brackets (prefix unary operators)""" 179 | # language=PythonRegExp 180 | regex_pat = r'(? str: 185 | return self.postf_pat.format(re.escape(close_br), postf_un_ops) 186 | 187 | @staticmethod 188 | def pat(pat: str): # has 2 slots: text, postf_un_op 189 | return lambda t: ('{{' + pat + '{}}}').format(t[0], t[1] if (t[1] is not None) else '') 190 | 191 | brackets = [('{', '}'), ('[', ']'), ('(', ')')] 192 | 193 | styles = OrderedDict([ # should have only one slot 194 | ('^{r}', r'\mathrm{{{}}}'), # regular 195 | ('^{i}', r'\mathit{{{}}}'), # italic 196 | ('^{b}', r'\mathbf{{{}}}'), # bold 197 | (' ⃗', r'\mathbf{{{}}}'), # vector bold notation 198 | ('⃗', r'\mathbf{{{}}}'), # vector bold notation 199 | ('⠃', r'\mathbf{{{}}}'), # vector bold notation 200 | ('⠘', r'\mathbf{{{}}}'), # vector bold notation 201 | ('⠛', r'\mathbf{{{}}}'), # matrix bold notation 202 | ('⠋', r'\mathbf{{{}}}'), # matrix bold notation 203 | ('^{β}', r'\boldsymbol{{{}}}'), # bold italic 204 | ('^{m}', r'\mathtt{{{}}}'), # monospace 205 | ('^{c}', r'\mathcal{{{}}}'), # calligraphic # no cyrillic support (see Monotype Corsiva) 206 | ('^{t}', r'\text{{{}}}'), 207 | ('^{ti}', r'\textit{{{}}}'), 208 | ('^{tb}', r'\textbf{{{}}}'), 209 | ('^{tβ}', r'\textit{{\textbf{{{}}}}}'), 210 | ]) 211 | mjx_bi = r'\class{{MJX-BoldItalic}}{{\mathbf{{{}}}}}' # mathjax hack 212 | mjx_m = r'\class{{MJX-Monospace}}{{\mathtt{{{}}}}}' # mathjax hack 213 | mpl_bi = r'\mathsf{{{}}}' # matplotlib hack 214 | styles_bak = copy.deepcopy(styles) 215 | 216 | def spec(self, postf_un_ops: str) -> list: 217 | """Return prefix unary operators list""" 218 | spec = [(l + op, {'pat': self.pat(pat), 219 | 'postf': self.postf(r, postf_un_ops), 220 | 'regex': None}) 221 | for op, pat in self.styles.items() 222 | for l, r in self.brackets] 223 | spec[0][1]['regex'] = self.regex_pat.format( 224 | _ops_regex(l for l, r in self.brackets), 225 | _ops_regex(self.styles.keys()) 226 | ) 227 | return spec 228 | 229 | 230 | class PrefUnGreedy: 231 | """ 232 | Greedy prefix unary operators like `{⋲` or `˱⋲`: 233 | ˎ\sign(x) = {⋲ 1 if x>0 ¦ 0 if x=0 ¦ -1 if x<0}ˎ 234 | """ 235 | # language=PythonRegExp 236 | postf = r'(? list: 243 | """Returns prefix unary operators list. 244 | Sets only one regex for all items in the dict.""" 245 | spec = [item 246 | for op, pat in self.ops.items() 247 | for item in [('{' + op, {'pat': pat, 'postf': self.postf, 'regex': None}), 248 | ('˱' + op, {'pat': pat, 'postf': self.postf, 'regex': None})] 249 | ] 250 | spec[0][1]['regex'] = self.regex_pat.format(_ops_regex(self.ops.keys())) 251 | return spec 252 | 253 | 254 | class OtherStyles: 255 | """ 256 | Other styles (prefix unary operators) 257 | ops = { 258 | captured regex group: { 259 | 'pat': .format() pattern OR func -> str, 260 | 'postf': postfix regex string 261 | 'regex': str or None # optional 262 | # if not provided - it will be made from operator key 263 | # if None - it will be ignored completely (useful when you specify only one regex) 264 | } 265 | } 266 | 'pat' should have only one slot 267 | """ 268 | postf_pat = Styles.postf_pat 269 | postf = Styles.postf 270 | pat = staticmethod(Styles.pat) # language=PythonRegExp 271 | regex_pat = r'(? list: 283 | spec = [(op, {'pat': self.pat(dic['pat']), 284 | 'postf': self.postf(dic['postf'], postf_un_ops), 285 | 'regex': None}) 286 | for op, dic in self.styles.items()] 287 | spec[0][1]['regex'] = self.regex_pat.format(_ops_regex(self.styles.keys())) 288 | return spec 289 | 290 | 291 | class PrefUnOps: 292 | """ 293 | Prefix unary operators. 294 | ops = { 295 | captured regex group: { 296 | 'pat': .format() pattern OR func -> str, 297 | 'postf': postfix regex string # optional 298 | 'regex': str or None # optional 299 | # if not provided - it will be made from operator key 300 | # if None - it will be ignored completely (useful when you specify only one regex) 301 | } 302 | } 303 | 'pat' should have only one slot 304 | """ 305 | postf = _default_postf # language=PythonRegExp 306 | regex_pat = r'(? str, 353 | 'pref': prefix regex string, # optional 354 | 'regex': str or None # optional 355 | # if not provided - it will be made from operator key 356 | # if None - it will be ignored completely (useful when you specify only one regex) 357 | } 358 | } 359 | 'pat' should have only one slot 360 | """ 361 | pref = _default_pref # language=PythonRegExp 362 | regex_pat = r'(? 366 | ('^', {'pat': r'\widehat{{{}}}', # language=PythonRegExp 367 | 'regex': r'(? str: 390 | """Regex-escaped string with all one-symbol operators""" 391 | return re.escape(''.join((key for key in self.ops.keys() if len(key) == 1))) 392 | 393 | 394 | class Matrices: 395 | """Matrix operators (and fractions without bar) (center binary operators)""" 396 | # language=PythonRegExp 397 | pref = r'(?}}{{{1}}}{{{2}}}' 421 | 422 | @staticmethod 423 | def pat(pat: str, dic: dict): # has 4 slots: left bracket id, term1, term2, right bracket id 424 | return lambda t: pat.format(dic[t[1] if t[1] != '' else t[0]], t[2], t[3], dic[t[4] if t[4] != '' else t[5]]) 425 | 426 | def spec(self) -> list: 427 | frac_spec = [(op + style, {'pat': self.pat(self.frac_pat.replace('<>', self.frac_styles[style]), 428 | self.frac_dic), 429 | 'pref': self.pref, 'postf': self.postf}) 430 | for op in self.frac_ops 431 | for style in self.frac_styles.keys()] 432 | mat_spec = [(op, {'pat': self.pat(self.mat_pat, self.mat_dic), 433 | 'pref': self.pref, 'postf': self.postf}) 434 | for op in self.mat_ops] 435 | return mat_spec + frac_spec 436 | 437 | 438 | class BinCentrGreedy: 439 | """ 440 | Greedy center binary operators like `¦#`: 441 | ˎsign(x) = ˳{˱1 if x>0 ¦# 0 if x=0 ¦ -1 if x<0˲˲˳ˎ 442 | """ 443 | # language=PythonRegExp 444 | pref = r'(? list: 461 | spec = [(op, {'pat': pat, 'pref': self.pref, 'postf': self.postf}) 462 | for op, pat in self.ops.items()] 463 | return spec 464 | 465 | 466 | class BinCentrOps: 467 | """ 468 | Center binary operators. 469 | ops = { 470 | captured center group: { 471 | 'pat': .format() pattern OR lambda func -> str, 472 | 'pre': prefix regex string, # optional 473 | 'post': postfix regex string # optional 474 | 'regex': str or None # optional 475 | # if not provided - it will be made from operator key 476 | # if None - it will be ignored completely (useful when you specify only one regex) 477 | } 478 | } 479 | """ 480 | pref = _default_pref 481 | postf = _default_postf # language=PythonRegExp 482 | regex_pat = r'(? str) instead of .format() pattern (it's argument would 590 | be the tuple mentioned). 591 | """ 592 | max_iter = 10 593 | max_while = 1000 594 | # all brackets and simple replacements are applied via one big regex (OR `|` joined) 595 | # so they cannot be stacked 596 | brackets = _brackets 597 | brackets_types = _brackets_types 598 | simple_pre = OrderedDict([ # Order should not matter! 599 | ('∛', ' 3√'), 600 | ('∜', ' 4√'), 601 | (' ', r'\,'), # thin space 602 | ]) 603 | superscripts = _superscripts 604 | subscripts = _subscripts 605 | regex_pre = [] 606 | 607 | # language=PythonRegExp 608 | loop_regexps = [] # = [[regex string, replace],] 609 | 610 | regex_post = [] 611 | simple_post = OrderedDict([ # Order should not matter! 612 | ('¦', '\\\\'), # IMPORTANT: this should be after other `¦` replacements 613 | ('˳', '&'), # IMPORTANT: this should be after brackets and other `˳` replacements 614 | ('˱', '{'), 615 | ('˲', '}'), 616 | ('ˍ', '_'), 617 | ('`', '\\'), 618 | ('ˋ', '\\'), # modifier letter grave accent 619 | ('↕^{d}', r'\displaystyle'), 620 | ('↕^{t}', r'\textstyle'), 621 | ('↕^{s}', r'\scriptstyle'), 622 | ('↕^{xs}', r'\scriptscriptstyle'), 623 | ]) 624 | 625 | # Escapes: 626 | # ------------------------------ 627 | escapes = ['⋲', '›', '˺', '↕', 'ˌ'] 628 | escapes_regex = None # it's assigned later 629 | 630 | def __init__(self, ready: bool=True): 631 | self.pref_un_ops = PrefUnOps() 632 | self.postf_un_ops = PostfUnOps() 633 | self.bin_centr_ops = BinCentrOps() 634 | self.null_ops = NullOps() 635 | 636 | self.readied = False 637 | if ready: 638 | self.ready() 639 | 640 | def ready(self): 641 | self.readied = True 642 | 643 | self.bin_centr_ops.fill() 644 | self.postf_un_ops.fill() 645 | styles_postf = self.postf_un_ops.one_symbol_ops_str() 646 | self.pref_un_ops.fill(styles_postf) 647 | self.null_ops.fill() 648 | # Brackets + simple pre replacements: 649 | self.simple_pre = OrderedDict(self._brackets_to_list() + list(self.simple_pre.items())) 650 | # Superscripts and subscripts + pre regexps: 651 | self.regex_pre = [self._su_scripts_regex()] + self.regex_pre 652 | # Escape characters: 653 | self.escapes += [s for s in chain(self.pref_un_ops.ops.keys(), 654 | self.postf_un_ops.ops.keys(), 655 | self.bin_centr_ops.ops.keys(), 656 | self.null_ops.ops.keys(), 657 | self.simple_pre.keys(), 658 | self.simple_post.keys()) if len(s) == 1] 659 | self.escapes = OrderedDict((s, None) for s in self.escapes).keys() 660 | self.escapes_regex = re.compile(r'\\({})'.format(_ops_regex(self.escapes))) 661 | 662 | @staticmethod 663 | def _dict_replace(dic: dict or Type[OrderedDict], source: str) -> str: 664 | if len(dic) > 0 and source != '': 665 | rep = {re.escape(key): val for key, val in dic.items()} 666 | regex = re.compile(r'(? list: 699 | return [ 700 | (key, val) 701 | for lpat, rpat in self.brackets_types 702 | for l, r in self.brackets 703 | for key, val in ((lpat[0].format(l[0]), lpat[1].format(l[1])), 704 | (rpat[0].format(r[0]), rpat[1].format(r[1]))) 705 | ] 706 | 707 | @staticmethod 708 | def _local_map(match, loc: str = 'lr') -> list: 709 | """ 710 | :param match: 711 | :param loc: str 712 | "l" or "r" or "lr" 713 | turns on/off left/right local area calculation 714 | :return: list 715 | list of the same size as the string + 2 716 | it's the local map that counted { and } 717 | list can contain: None or int>=0 718 | from the left of the operator match: 719 | in `b}a` if a:0 then }:0 and b:1 720 | in `b{a` if a:0 then {:0 and b:-1(None) 721 | from the right of the operator match: 722 | in `a{b` if a:0 then {:0 and b:1 723 | in `a}b` if a:0 then }:0 and b:-1(None) 724 | Map for +1 (needed for r'$') and -1 (needed for r'^') 725 | characters is also stored: +1 -> +1, -1 -> +2 726 | """ 727 | s = match.string 728 | map_ = [None] * (len(s) + 2) 729 | if loc == 'l' or loc == 'lr': 730 | balance = 0 731 | for i in reversed(range(0, match.start())): 732 | map_[i] = balance 733 | c, prev = s[i], (s[i - 1] if i > 0 else '') 734 | if (c == '}' or c == '˲') and prev != '\\': 735 | balance += 1 736 | elif (c == '{' or c == '˱') and prev != '\\': 737 | balance -= 1 738 | if balance < 0: 739 | break 740 | map_[-1] = balance 741 | if loc == 'r' or loc == 'lr': 742 | balance = 0 743 | for i in range(match.end(), len(s)): 744 | map_[i] = balance 745 | c, prev = s[i], s[i - 1] 746 | if (c == '{' or c == '˱') and prev != '\\': 747 | balance += 1 748 | elif (c == '}' or c == '˲') and prev != '\\': 749 | balance -= 1 750 | if balance < 0: 751 | break 752 | map_[len(s)] = balance 753 | return map_ 754 | 755 | def _operators_replace(self, string: str) -> str: 756 | """ 757 | Searches for first unary or binary operator (via self.op_regex 758 | that has only one group that contain operator) 759 | then replaces it (or escapes it if brackets do not match). 760 | Everything until: 761 | * space ' ' 762 | * begin/end of the string 763 | * bracket from outer scope (like '{a/b}': term1=a term2=b) 764 | is considered a term (contents of matching brackets '{}' are 765 | ignored). 766 | 767 | Attributes 768 | ---------- 769 | string: str 770 | string to replace 771 | """ 772 | # noinspection PyShadowingNames 773 | def replace(string: str, start: int, end: int, substring: str) -> str: 774 | return string[0:start] + substring + string[end:len(string)] 775 | 776 | # noinspection PyShadowingNames 777 | def sub_pat(pat: Callable[[list], str] or str, terms: list) -> str: 778 | if isinstance(pat, str): 779 | return pat.format(*terms) 780 | else: 781 | return pat(terms) 782 | 783 | count = 0 784 | 785 | def check(): 786 | nonlocal count 787 | count += 1 788 | if count > self.max_while: 789 | raise RuntimeError('Presumably while loop is stuck') 790 | 791 | # noinspection PyShadowingNames 792 | def null_replace(match) -> str: 793 | regex_terms = [gr for gr in match.groups() if gr is not None] 794 | op = regex_terms[0] 795 | terms = regex_terms[1:] 796 | return sub_pat(self.null_ops.ops[op]['pat'], terms) 797 | 798 | string = self.null_ops.regex.sub(null_replace, string) 799 | 800 | for ops, loc in [(self.pref_un_ops, 'r'), (self.postf_un_ops, 'l'), 801 | (self.bin_centr_ops, 'lr')]: 802 | count = 0 803 | match = ops.regex.search(string) 804 | while match: 805 | check() 806 | regex_terms = [gr for gr in match.groups() if gr is not None] 807 | op = regex_terms[0] 808 | loc_map = self._local_map(match, loc) 809 | lmatch, rmatch = None, None 810 | if loc == 'l' or loc == 'lr': 811 | for m in ops.ops[op]['pref'].finditer(string): 812 | if m.end() <= match.start() and loc_map[m.end() - 1] == 0: 813 | lmatch = m 814 | if lmatch is None: 815 | string = replace(string, match.start(), match.end(), match.group(0).replace(op, '\\' + op)) 816 | match = ops.regex.search(string) 817 | continue 818 | else: 819 | term1 = string[lmatch.end():match.start()] 820 | if loc == 'r' or loc == 'lr': 821 | for m in ops.ops[op]['postf'].finditer(string): 822 | if m.start() >= match.end() and loc_map[m.start()] == 0: 823 | rmatch = m 824 | break 825 | if rmatch is None: 826 | string = replace(string, match.start(), match.end(), match.group(0).replace(op, '\\' + op)) 827 | match = ops.regex.search(string) 828 | continue 829 | else: 830 | term2 = string[match.end():rmatch.start()] 831 | if loc == 'l': 832 | # noinspection PyUnboundLocalVariable 833 | terms = list(lmatch.groups()) + [term1] + regex_terms[1:] 834 | start, end = lmatch.start(), match.end() 835 | elif loc == 'r': 836 | # noinspection PyUnboundLocalVariable 837 | terms = regex_terms[1:] + [term2] + list(rmatch.groups()) 838 | start, end = match.start(), rmatch.end() 839 | elif loc == 'lr': 840 | terms = list(lmatch.groups()) + [term1] + regex_terms[1:] + [term2] + list(rmatch.groups()) 841 | start, end = lmatch.start(), rmatch.end() 842 | else: # this never happen 843 | terms = regex_terms[1:] 844 | start, end = match.start(), match.end() 845 | 846 | string = replace(string, start, end, sub_pat(ops.ops[op]['pat'], terms)) 847 | match = ops.regex.search(string) 848 | 849 | return string 850 | 851 | def mjx_hack(self): 852 | s = self.pref_un_ops.styles 853 | s2 = self.pref_un_ops.other_styles 854 | s.styles['^{β}'] = s.mjx_bi 855 | s.styles['^{m}'] = s.mjx_m 856 | s.styles['^{tβ}'] = s2.mjx_tbi 857 | s2.styles['‹^{β}']['pat'] = s2.mjx_tbi 858 | 859 | def mpl_hack(self): 860 | s = self.pref_un_ops.styles 861 | s.styles['^{β}'] = s.mpl_bi 862 | 863 | def replace(self, src: str) -> str: 864 | """ 865 | Extends LaTeX syntax via regex preprocess 866 | :param src: str 867 | LaTeX string 868 | :return: str 869 | New LaTeX string 870 | """ 871 | if not self.readied: 872 | self.ready() 873 | 874 | # Brackets + simple pre replacements: 875 | src = self._dict_replace(self.simple_pre, src) 876 | 877 | # Superscripts and subscripts + pre regexps: 878 | for regex, replace in self.regex_pre: 879 | src = regex.sub(replace, src) 880 | 881 | # Unary and binary operators: 882 | src = self._operators_replace(src) 883 | 884 | # Loop regexps: 885 | src_prev = src 886 | for i in range(self.max_iter): 887 | for regex, replace in self.loop_regexps: 888 | src = regex.sub(replace, src) 889 | if src_prev == src: 890 | break 891 | else: 892 | src_prev = src 893 | 894 | # Post regexps: 895 | for regex, replace in self.regex_post: 896 | src = regex.sub(replace, src) 897 | 898 | # Simple post replacements: 899 | src = self._dict_replace(self.simple_post, src) 900 | 901 | # Escape characters: 902 | src = self.escapes_regex.sub(r'\1', src) 903 | 904 | return src 905 | -------------------------------------------------------------------------------- /sugartex/sugartex_pandoc_filter.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import panflute as pf 3 | from .sugartex_filter import SugarTeX 4 | 5 | sugartex = SugarTeX(ready=False) 6 | 7 | 8 | def kiwi_hack(): 9 | sugartex.mjx_hack() 10 | # sugartex.subscripts['ᵩ'] = 'ψ' # Consolas font specific 11 | # sugartex.superscripts['ᵠ'] = 'ψ' # Consolas font specific 12 | 13 | 14 | # noinspection PyUnusedLocal 15 | def action(elem, doc): 16 | if isinstance(elem, pf.Math): 17 | elem.text = sugartex.replace(elem.text) 18 | 19 | 20 | def main(doc=None): 21 | sugartex.ready() 22 | return pf.run_filter(action, doc=doc) 23 | 24 | 25 | def cli(): 26 | """ 27 | Usage: sugartex [OPTIONS] [TO] 28 | 29 | Reads from stdin and writes to stdout. Can have single argument/option only. 30 | When no args or the arg is not from options then run Pandoc SugarTeX filter 31 | that iterates over math blocks. 32 | 33 | Options: 34 | --kiwi Same as above but with kiwi flavor, 35 | --help Show this message and exit. 36 | """ 37 | if len(sys.argv) > 1: 38 | if sys.argv[1] == '--kiwi': 39 | kiwi_hack() 40 | elif sys.argv[1].lower() == '--help': 41 | print(str(cli.__doc__).replace('\n ', '\n')) 42 | return None 43 | main() 44 | 45 | 46 | if __name__ == '__main__': 47 | cli() 48 | -------------------------------------------------------------------------------- /upload: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | cd "$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" 3 | 4 | tagname="0.1.16" 5 | git tag -d "$tagname" 6 | git push --delete origin "$tagname" 7 | git tag -a "$tagname" 8 | git push --tags 9 | 10 | python setup.py sdist 11 | if [[ "$(uname -s)" == MINGW* ]]; then 12 | winpty twine upload dist/* --skip-existing 13 | else 14 | twine upload dist/* --skip-existing; fi 15 | 16 | rm -rf ./dist 17 | -------------------------------------------------------------------------------- /upload.txt: -------------------------------------------------------------------------------- 1 | Install to conda environment: 2 | 3 | * twine 4 | 5 | conda install twine 6 | 7 | * On Windows install Git together with Bash. 8 | 9 | 10 | Scripts: 11 | 12 | To run the script open Bash terminal, then type . (dot with space), then 13 | drag and drop the script to the Bash terminal (the script will change CWD itself). 14 | 15 | * open terminal with activated environment 16 | * to upload package to pypi change version in `upload`, push changes, run `upload` 17 | 18 | 19 | Hint: Easily create shortcut to activated environment via Shortcutter: 20 | 21 | conda install -c defaults -c conda-forge shortcutter 22 | # pip install shortcutter 23 | shortcutter --terminal 24 | 25 | * To start Bash on windows type `%b%` in the terminal created via Shortcutter 26 | -------------------------------------------------------------------------------- /versioneer.py: -------------------------------------------------------------------------------- 1 | 2 | # Version: 0.16 3 | 4 | """The Versioneer - like a rocketeer, but for versions. 5 | 6 | The Versioneer 7 | ============== 8 | 9 | * like a rocketeer, but for versions! 10 | * https://github.com/warner/python-versioneer 11 | * Brian Warner 12 | * License: Public Domain 13 | * Compatible With: python2.6, 2.7, 3.3, 3.4, 3.5, and pypy 14 | * [![Latest Version] 15 | (https://pypip.in/version/versioneer/badge.svg?style=flat) 16 | ](https://pypi.python.org/pypi/versioneer/) 17 | * [![Build Status] 18 | (https://travis-ci.org/warner/python-versioneer.png?branch=master) 19 | ](https://travis-ci.org/warner/python-versioneer) 20 | 21 | This is a tool for managing a recorded version number in distutils-based 22 | python projects. The goal is to remove the tedious and error-prone "update 23 | the embedded version string" step from your release process. Making a new 24 | release should be as easy as recording a new tag in your version-control 25 | system, and maybe making new tarballs. 26 | 27 | 28 | ## Quick Install 29 | 30 | * `pip install versioneer` to somewhere to your $PATH 31 | * add a `[versioneer]` section to your setup.cfg (see below) 32 | * run `versioneer install` in your source tree, commit the results 33 | 34 | ## Version Identifiers 35 | 36 | Source trees come from a variety of places: 37 | 38 | * a version-control system checkout (mostly used by developers) 39 | * a nightly tarball, produced by build automation 40 | * a snapshot tarball, produced by a web-based VCS browser, like github's 41 | "tarball from tag" feature 42 | * a release tarball, produced by "setup.py sdist", distributed through PyPI 43 | 44 | Within each source tree, the version identifier (either a string or a number, 45 | this tool is format-agnostic) can come from a variety of places: 46 | 47 | * ask the VCS tool itself, e.g. "git describe" (for checkouts), which knows 48 | about recent "tags" and an absolute revision-id 49 | * the name of the directory into which the tarball was unpacked 50 | * an expanded VCS keyword ($Id$, etc) 51 | * a `_version.py` created by some earlier build step 52 | 53 | For released software, the version identifier is closely related to a VCS 54 | tag. Some projects use tag names that include more than just the version 55 | string (e.g. "myproject-1.2" instead of just "1.2"), in which case the tool 56 | needs to strip the tag prefix to extract the version identifier. For 57 | unreleased software (between tags), the version identifier should provide 58 | enough information to help developers recreate the same tree, while also 59 | giving them an idea of roughly how old the tree is (after version 1.2, before 60 | version 1.3). Many VCS systems can report a description that captures this, 61 | for example `git describe --tags --dirty --always` reports things like 62 | "0.7-1-g574ab98-dirty" to indicate that the checkout is one revision past the 63 | 0.7 tag, has a unique revision id of "574ab98", and is "dirty" (it has 64 | uncommitted changes. 65 | 66 | The version identifier is used for multiple purposes: 67 | 68 | * to allow the module to self-identify its version: `myproject.__version__` 69 | * to choose a name and prefix for a 'setup.py sdist' tarball 70 | 71 | ## Theory of Operation 72 | 73 | Versioneer works by adding a special `_version.py` file into your source 74 | tree, where your `__init__.py` can import it. This `_version.py` knows how to 75 | dynamically ask the VCS tool for version information at import time. 76 | 77 | `_version.py` also contains `$Revision$` markers, and the installation 78 | process marks `_version.py` to have this marker rewritten with a tag name 79 | during the `git archive` command. As a result, generated tarballs will 80 | contain enough information to get the proper version. 81 | 82 | To allow `setup.py` to compute a version too, a `versioneer.py` is added to 83 | the top level of your source tree, next to `setup.py` and the `setup.cfg` 84 | that configures it. This overrides several distutils/setuptools commands to 85 | compute the version when invoked, and changes `setup.py build` and `setup.py 86 | sdist` to replace `_version.py` with a small static file that contains just 87 | the generated version data. 88 | 89 | ## Installation 90 | 91 | First, decide on values for the following configuration variables: 92 | 93 | * `VCS`: the version control system you use. Currently accepts "git". 94 | 95 | * `style`: the style of version string to be produced. See "Styles" below for 96 | details. Defaults to "pep440", which looks like 97 | `TAG[+DISTANCE.gSHORTHASH[.dirty]]`. 98 | 99 | * `versionfile_source`: 100 | 101 | A project-relative pathname into which the generated version strings should 102 | be written. This is usually a `_version.py` next to your project's main 103 | `__init__.py` file, so it can be imported at runtime. If your project uses 104 | `src/myproject/__init__.py`, this should be `src/myproject/_version.py`. 105 | This file should be checked in to your VCS as usual: the copy created below 106 | by `setup.py setup_versioneer` will include code that parses expanded VCS 107 | keywords in generated tarballs. The 'build' and 'sdist' commands will 108 | replace it with a copy that has just the calculated version string. 109 | 110 | This must be set even if your project does not have any modules (and will 111 | therefore never import `_version.py`), since "setup.py sdist" -based trees 112 | still need somewhere to record the pre-calculated version strings. Anywhere 113 | in the source tree should do. If there is a `__init__.py` next to your 114 | `_version.py`, the `setup.py setup_versioneer` command (described below) 115 | will append some `__version__`-setting assignments, if they aren't already 116 | present. 117 | 118 | * `versionfile_build`: 119 | 120 | Like `versionfile_source`, but relative to the build directory instead of 121 | the source directory. These will differ when your setup.py uses 122 | 'package_dir='. If you have `package_dir={'myproject': 'src/myproject'}`, 123 | then you will probably have `versionfile_build='myproject/_version.py'` and 124 | `versionfile_source='src/myproject/_version.py'`. 125 | 126 | If this is set to None, then `setup.py build` will not attempt to rewrite 127 | any `_version.py` in the built tree. If your project does not have any 128 | libraries (e.g. if it only builds a script), then you should use 129 | `versionfile_build = None`. To actually use the computed version string, 130 | your `setup.py` will need to override `distutils.command.build_scripts` 131 | with a subclass that explicitly inserts a copy of 132 | `versioneer.get_version()` into your script file. See 133 | `test/demoapp-script-only/setup.py` for an example. 134 | 135 | * `tag_prefix`: 136 | 137 | a string, like 'PROJECTNAME-', which appears at the start of all VCS tags. 138 | If your tags look like 'myproject-1.2.0', then you should use 139 | tag_prefix='myproject-'. If you use unprefixed tags like '1.2.0', this 140 | should be an empty string, using either `tag_prefix=` or `tag_prefix=''`. 141 | 142 | * `parentdir_prefix`: 143 | 144 | a optional string, frequently the same as tag_prefix, which appears at the 145 | start of all unpacked tarball filenames. If your tarball unpacks into 146 | 'myproject-1.2.0', this should be 'myproject-'. To disable this feature, 147 | just omit the field from your `setup.cfg`. 148 | 149 | This tool provides one script, named `versioneer`. That script has one mode, 150 | "install", which writes a copy of `versioneer.py` into the current directory 151 | and runs `versioneer.py setup` to finish the installation. 152 | 153 | To versioneer-enable your project: 154 | 155 | * 1: Modify your `setup.cfg`, adding a section named `[versioneer]` and 156 | populating it with the configuration values you decided earlier (note that 157 | the option names are not case-sensitive): 158 | 159 | ```` 160 | [versioneer] 161 | VCS = git 162 | style = pep440 163 | versionfile_source = src/myproject/_version.py 164 | versionfile_build = myproject/_version.py 165 | tag_prefix = 166 | parentdir_prefix = myproject- 167 | ```` 168 | 169 | * 2: Run `versioneer install`. This will do the following: 170 | 171 | * copy `versioneer.py` into the top of your source tree 172 | * create `_version.py` in the right place (`versionfile_source`) 173 | * modify your `__init__.py` (if one exists next to `_version.py`) to define 174 | `__version__` (by calling a function from `_version.py`) 175 | * modify your `MANIFEST.in` to include both `versioneer.py` and the 176 | generated `_version.py` in sdist tarballs 177 | 178 | `versioneer install` will complain about any problems it finds with your 179 | `setup.py` or `setup.cfg`. Run it multiple times until you have fixed all 180 | the problems. 181 | 182 | * 3: add a `import versioneer` to your setup.py, and add the following 183 | arguments to the setup() call: 184 | 185 | version=versioneer.get_version(), 186 | cmdclass=versioneer.get_cmdclass(), 187 | 188 | * 4: commit these changes to your VCS. To make sure you won't forget, 189 | `versioneer install` will mark everything it touched for addition using 190 | `git add`. Don't forget to add `setup.py` and `setup.cfg` too. 191 | 192 | ## Post-Installation Usage 193 | 194 | Once established, all uses of your tree from a VCS checkout should get the 195 | current version string. All generated tarballs should include an embedded 196 | version string (so users who unpack them will not need a VCS tool installed). 197 | 198 | If you distribute your project through PyPI, then the release process should 199 | boil down to two steps: 200 | 201 | * 1: git tag 1.0 202 | * 2: python setup.py register sdist upload 203 | 204 | If you distribute it through github (i.e. users use github to generate 205 | tarballs with `git archive`), the process is: 206 | 207 | * 1: git tag 1.0 208 | * 2: git push; git push --tags 209 | 210 | Versioneer will report "0+untagged.NUMCOMMITS.gHASH" until your tree has at 211 | least one tag in its history. 212 | 213 | ## Version-String Flavors 214 | 215 | Code which uses Versioneer can learn about its version string at runtime by 216 | importing `_version` from your main `__init__.py` file and running the 217 | `get_versions()` function. From the "outside" (e.g. in `setup.py`), you can 218 | import the top-level `versioneer.py` and run `get_versions()`. 219 | 220 | Both functions return a dictionary with different flavors of version 221 | information: 222 | 223 | * `['version']`: A condensed version string, rendered using the selected 224 | style. This is the most commonly used value for the project's version 225 | string. The default "pep440" style yields strings like `0.11`, 226 | `0.11+2.g1076c97`, or `0.11+2.g1076c97.dirty`. See the "Styles" section 227 | below for alternative styles. 228 | 229 | * `['full-revisionid']`: detailed revision identifier. For Git, this is the 230 | full SHA1 commit id, e.g. "1076c978a8d3cfc70f408fe5974aa6c092c949ac". 231 | 232 | * `['dirty']`: a boolean, True if the tree has uncommitted changes. Note that 233 | this is only accurate if run in a VCS checkout, otherwise it is likely to 234 | be False or None 235 | 236 | * `['error']`: if the version string could not be computed, this will be set 237 | to a string describing the problem, otherwise it will be None. It may be 238 | useful to throw an exception in setup.py if this is set, to avoid e.g. 239 | creating tarballs with a version string of "unknown". 240 | 241 | Some variants are more useful than others. Including `full-revisionid` in a 242 | bug report should allow developers to reconstruct the exact code being tested 243 | (or indicate the presence of local changes that should be shared with the 244 | developers). `version` is suitable for display in an "about" box or a CLI 245 | `--version` output: it can be easily compared against release notes and lists 246 | of bugs fixed in various releases. 247 | 248 | The installer adds the following text to your `__init__.py` to place a basic 249 | version in `YOURPROJECT.__version__`: 250 | 251 | from ._version import get_versions 252 | __version__ = get_versions()['version'] 253 | del get_versions 254 | 255 | ## Styles 256 | 257 | The setup.cfg `style=` configuration controls how the VCS information is 258 | rendered into a version string. 259 | 260 | The default style, "pep440", produces a PEP440-compliant string, equal to the 261 | un-prefixed tag name for actual releases, and containing an additional "local 262 | version" section with more detail for in-between builds. For Git, this is 263 | TAG[+DISTANCE.gHEX[.dirty]] , using information from `git describe --tags 264 | --dirty --always`. For example "0.11+2.g1076c97.dirty" indicates that the 265 | tree is like the "1076c97" commit but has uncommitted changes (".dirty"), and 266 | that this commit is two revisions ("+2") beyond the "0.11" tag. For released 267 | software (exactly equal to a known tag), the identifier will only contain the 268 | stripped tag, e.g. "0.11". 269 | 270 | Other styles are available. See details.md in the Versioneer source tree for 271 | descriptions. 272 | 273 | ## Debugging 274 | 275 | Versioneer tries to avoid fatal errors: if something goes wrong, it will tend 276 | to return a version of "0+unknown". To investigate the problem, run `setup.py 277 | version`, which will run the version-lookup code in a verbose mode, and will 278 | display the full contents of `get_versions()` (including the `error` string, 279 | which may help identify what went wrong). 280 | 281 | ## Updating Versioneer 282 | 283 | To upgrade your project to a new release of Versioneer, do the following: 284 | 285 | * install the new Versioneer (`pip install -U versioneer` or equivalent) 286 | * edit `setup.cfg`, if necessary, to include any new configuration settings 287 | indicated by the release notes 288 | * re-run `versioneer install` in your source tree, to replace 289 | `SRC/_version.py` 290 | * commit any changed files 291 | 292 | ### Upgrading to 0.16 293 | 294 | Nothing special. 295 | 296 | ### Upgrading to 0.15 297 | 298 | Starting with this version, Versioneer is configured with a `[versioneer]` 299 | section in your `setup.cfg` file. Earlier versions required the `setup.py` to 300 | set attributes on the `versioneer` module immediately after import. The new 301 | version will refuse to run (raising an exception during import) until you 302 | have provided the necessary `setup.cfg` section. 303 | 304 | In addition, the Versioneer package provides an executable named 305 | `versioneer`, and the installation process is driven by running `versioneer 306 | install`. In 0.14 and earlier, the executable was named 307 | `versioneer-installer` and was run without an argument. 308 | 309 | ### Upgrading to 0.14 310 | 311 | 0.14 changes the format of the version string. 0.13 and earlier used 312 | hyphen-separated strings like "0.11-2-g1076c97-dirty". 0.14 and beyond use a 313 | plus-separated "local version" section strings, with dot-separated 314 | components, like "0.11+2.g1076c97". PEP440-strict tools did not like the old 315 | format, but should be ok with the new one. 316 | 317 | ### Upgrading from 0.11 to 0.12 318 | 319 | Nothing special. 320 | 321 | ### Upgrading from 0.10 to 0.11 322 | 323 | You must add a `versioneer.VCS = "git"` to your `setup.py` before re-running 324 | `setup.py setup_versioneer`. This will enable the use of additional 325 | version-control systems (SVN, etc) in the future. 326 | 327 | ## Future Directions 328 | 329 | This tool is designed to make it easily extended to other version-control 330 | systems: all VCS-specific components are in separate directories like 331 | src/git/ . The top-level `versioneer.py` script is assembled from these 332 | components by running make-versioneer.py . In the future, make-versioneer.py 333 | will take a VCS name as an argument, and will construct a version of 334 | `versioneer.py` that is specific to the given VCS. It might also take the 335 | configuration arguments that are currently provided manually during 336 | installation by editing setup.py . Alternatively, it might go the other 337 | direction and include code from all supported VCS systems, reducing the 338 | number of intermediate scripts. 339 | 340 | 341 | ## License 342 | 343 | To make Versioneer easier to embed, all its code is dedicated to the public 344 | domain. The `_version.py` that it creates is also in the public domain. 345 | Specifically, both are released under the Creative Commons "Public Domain 346 | Dedication" license (CC0-1.0), as described in 347 | https://creativecommons.org/publicdomain/zero/1.0/ . 348 | 349 | """ 350 | 351 | from __future__ import print_function 352 | try: 353 | import configparser 354 | except ImportError: 355 | import ConfigParser as configparser 356 | import errno 357 | import json 358 | import os 359 | import re 360 | import subprocess 361 | import sys 362 | 363 | 364 | class VersioneerConfig: 365 | """Container for Versioneer configuration parameters.""" 366 | 367 | 368 | def get_root(): 369 | """Get the project root directory. 370 | 371 | We require that all commands are run from the project root, i.e. the 372 | directory that contains setup.py, setup.cfg, and versioneer.py . 373 | """ 374 | root = os.path.realpath(os.path.abspath(os.getcwd())) 375 | setup_py = os.path.join(root, "setup.py") 376 | versioneer_py = os.path.join(root, "versioneer.py") 377 | if not (os.path.exists(setup_py) or os.path.exists(versioneer_py)): 378 | # allow 'python path/to/setup.py COMMAND' 379 | root = os.path.dirname(os.path.realpath(os.path.abspath(sys.argv[0]))) 380 | setup_py = os.path.join(root, "setup.py") 381 | versioneer_py = os.path.join(root, "versioneer.py") 382 | if not (os.path.exists(setup_py) or os.path.exists(versioneer_py)): 383 | err = ("Versioneer was unable to run the project root directory. " 384 | "Versioneer requires setup.py to be executed from " 385 | "its immediate directory (like 'python setup.py COMMAND'), " 386 | "or in a way that lets it use sys.argv[0] to find the root " 387 | "(like 'python path/to/setup.py COMMAND').") 388 | raise VersioneerBadRootError(err) 389 | try: 390 | # Certain runtime workflows (setup.py install/develop in a setuptools 391 | # tree) execute all dependencies in a single python process, so 392 | # "versioneer" may be imported multiple times, and python's shared 393 | # module-import table will cache the first one. So we can't use 394 | # os.path.dirname(__file__), as that will find whichever 395 | # versioneer.py was first imported, even in later projects. 396 | me = os.path.realpath(os.path.abspath(__file__)) 397 | if os.path.splitext(me)[0] != os.path.splitext(versioneer_py)[0]: 398 | print("Warning: build in %s is using versioneer.py from %s" 399 | % (os.path.dirname(me), versioneer_py)) 400 | except NameError: 401 | pass 402 | return root 403 | 404 | 405 | def get_config_from_root(root): 406 | """Read the project setup.cfg file to determine Versioneer config.""" 407 | # This might raise EnvironmentError (if setup.cfg is missing), or 408 | # configparser.NoSectionError (if it lacks a [versioneer] section), or 409 | # configparser.NoOptionError (if it lacks "VCS="). See the docstring at 410 | # the top of versioneer.py for instructions on writing your setup.cfg . 411 | setup_cfg = os.path.join(root, "setup.cfg") 412 | parser = configparser.SafeConfigParser() 413 | with open(setup_cfg, "r") as f: 414 | parser.readfp(f) 415 | VCS = parser.get("versioneer", "VCS") # mandatory 416 | 417 | def get(parser, name): 418 | if parser.has_option("versioneer", name): 419 | return parser.get("versioneer", name) 420 | return None 421 | cfg = VersioneerConfig() 422 | cfg.VCS = VCS 423 | cfg.style = get(parser, "style") or "" 424 | cfg.versionfile_source = get(parser, "versionfile_source") 425 | cfg.versionfile_build = get(parser, "versionfile_build") 426 | cfg.tag_prefix = get(parser, "tag_prefix") 427 | if cfg.tag_prefix in ("''", '""'): 428 | cfg.tag_prefix = "" 429 | cfg.parentdir_prefix = get(parser, "parentdir_prefix") 430 | cfg.verbose = get(parser, "verbose") 431 | return cfg 432 | 433 | 434 | class NotThisMethod(Exception): 435 | """Exception raised if a method is not valid for the current scenario.""" 436 | 437 | # these dictionaries contain VCS-specific tools 438 | LONG_VERSION_PY = {} 439 | HANDLERS = {} 440 | 441 | 442 | def register_vcs_handler(vcs, method): # decorator 443 | """Decorator to mark a method as the handler for a particular VCS.""" 444 | def decorate(f): 445 | """Store f in HANDLERS[vcs][method].""" 446 | if vcs not in HANDLERS: 447 | HANDLERS[vcs] = {} 448 | HANDLERS[vcs][method] = f 449 | return f 450 | return decorate 451 | 452 | 453 | def run_command(commands, args, cwd=None, verbose=False, hide_stderr=False): 454 | """Call the given command(s).""" 455 | assert isinstance(commands, list) 456 | p = None 457 | for c in commands: 458 | try: 459 | dispcmd = str([c] + args) 460 | # remember shell=False, so use git.cmd on windows, not just git 461 | p = subprocess.Popen([c] + args, cwd=cwd, stdout=subprocess.PIPE, 462 | stderr=(subprocess.PIPE if hide_stderr 463 | else None)) 464 | break 465 | except EnvironmentError: 466 | e = sys.exc_info()[1] 467 | if e.errno == errno.ENOENT: 468 | continue 469 | if verbose: 470 | print("unable to run %s" % dispcmd) 471 | print(e) 472 | return None 473 | else: 474 | if verbose: 475 | print("unable to find command, tried %s" % (commands,)) 476 | return None 477 | stdout = p.communicate()[0].strip() 478 | if sys.version_info[0] >= 3: 479 | stdout = stdout.decode() 480 | if p.returncode != 0: 481 | if verbose: 482 | print("unable to run %s (error)" % dispcmd) 483 | return None 484 | return stdout 485 | LONG_VERSION_PY['git'] = ''' 486 | # This file helps to compute a version number in source trees obtained from 487 | # git-archive tarball (such as those provided by githubs download-from-tag 488 | # feature). Distribution tarballs (built by setup.py sdist) and build 489 | # directories (produced by setup.py build) will contain a much shorter file 490 | # that just contains the computed version number. 491 | 492 | # This file is released into the public domain. Generated by 493 | # versioneer-0.16 (https://github.com/warner/python-versioneer) 494 | 495 | """Git implementation of _version.py.""" 496 | 497 | import errno 498 | import os 499 | import re 500 | import subprocess 501 | import sys 502 | 503 | 504 | def get_keywords(): 505 | """Get the keywords needed to look up the version information.""" 506 | # these strings will be replaced by git during git-archive. 507 | # setup.py/versioneer.py will grep for the variable names, so they must 508 | # each be defined on a line of their own. _version.py will just call 509 | # get_keywords(). 510 | git_refnames = "%(DOLLAR)sFormat:%%d%(DOLLAR)s" 511 | git_full = "%(DOLLAR)sFormat:%%H%(DOLLAR)s" 512 | keywords = {"refnames": git_refnames, "full": git_full} 513 | return keywords 514 | 515 | 516 | class VersioneerConfig: 517 | """Container for Versioneer configuration parameters.""" 518 | 519 | 520 | def get_config(): 521 | """Create, populate and return the VersioneerConfig() object.""" 522 | # these strings are filled in when 'setup.py versioneer' creates 523 | # _version.py 524 | cfg = VersioneerConfig() 525 | cfg.VCS = "git" 526 | cfg.style = "%(STYLE)s" 527 | cfg.tag_prefix = "%(TAG_PREFIX)s" 528 | cfg.parentdir_prefix = "%(PARENTDIR_PREFIX)s" 529 | cfg.versionfile_source = "%(VERSIONFILE_SOURCE)s" 530 | cfg.verbose = False 531 | return cfg 532 | 533 | 534 | class NotThisMethod(Exception): 535 | """Exception raised if a method is not valid for the current scenario.""" 536 | 537 | 538 | LONG_VERSION_PY = {} 539 | HANDLERS = {} 540 | 541 | 542 | def register_vcs_handler(vcs, method): # decorator 543 | """Decorator to mark a method as the handler for a particular VCS.""" 544 | def decorate(f): 545 | """Store f in HANDLERS[vcs][method].""" 546 | if vcs not in HANDLERS: 547 | HANDLERS[vcs] = {} 548 | HANDLERS[vcs][method] = f 549 | return f 550 | return decorate 551 | 552 | 553 | def run_command(commands, args, cwd=None, verbose=False, hide_stderr=False): 554 | """Call the given command(s).""" 555 | assert isinstance(commands, list) 556 | p = None 557 | for c in commands: 558 | try: 559 | dispcmd = str([c] + args) 560 | # remember shell=False, so use git.cmd on windows, not just git 561 | p = subprocess.Popen([c] + args, cwd=cwd, stdout=subprocess.PIPE, 562 | stderr=(subprocess.PIPE if hide_stderr 563 | else None)) 564 | break 565 | except EnvironmentError: 566 | e = sys.exc_info()[1] 567 | if e.errno == errno.ENOENT: 568 | continue 569 | if verbose: 570 | print("unable to run %%s" %% dispcmd) 571 | print(e) 572 | return None 573 | else: 574 | if verbose: 575 | print("unable to find command, tried %%s" %% (commands,)) 576 | return None 577 | stdout = p.communicate()[0].strip() 578 | if sys.version_info[0] >= 3: 579 | stdout = stdout.decode() 580 | if p.returncode != 0: 581 | if verbose: 582 | print("unable to run %%s (error)" %% dispcmd) 583 | return None 584 | return stdout 585 | 586 | 587 | def versions_from_parentdir(parentdir_prefix, root, verbose): 588 | """Try to determine the version from the parent directory name. 589 | 590 | Source tarballs conventionally unpack into a directory that includes 591 | both the project name and a version string. 592 | """ 593 | dirname = os.path.basename(root) 594 | if not dirname.startswith(parentdir_prefix): 595 | if verbose: 596 | print("guessing rootdir is '%%s', but '%%s' doesn't start with " 597 | "prefix '%%s'" %% (root, dirname, parentdir_prefix)) 598 | raise NotThisMethod("rootdir doesn't start with parentdir_prefix") 599 | return {"version": dirname[len(parentdir_prefix):], 600 | "full-revisionid": None, 601 | "dirty": False, "error": None} 602 | 603 | 604 | @register_vcs_handler("git", "get_keywords") 605 | def git_get_keywords(versionfile_abs): 606 | """Extract version information from the given file.""" 607 | # the code embedded in _version.py can just fetch the value of these 608 | # keywords. When used from setup.py, we don't want to import _version.py, 609 | # so we do it with a regexp instead. This function is not used from 610 | # _version.py. 611 | keywords = {} 612 | try: 613 | f = open(versionfile_abs, "r") 614 | for line in f.readlines(): 615 | if line.strip().startswith("git_refnames ="): 616 | mo = re.search(r'=\s*"(.*)"', line) 617 | if mo: 618 | keywords["refnames"] = mo.group(1) 619 | if line.strip().startswith("git_full ="): 620 | mo = re.search(r'=\s*"(.*)"', line) 621 | if mo: 622 | keywords["full"] = mo.group(1) 623 | f.close() 624 | except EnvironmentError: 625 | pass 626 | return keywords 627 | 628 | 629 | @register_vcs_handler("git", "keywords") 630 | def git_versions_from_keywords(keywords, tag_prefix, verbose): 631 | """Get version information from git keywords.""" 632 | if not keywords: 633 | raise NotThisMethod("no keywords at all, weird") 634 | refnames = keywords["refnames"].strip() 635 | if refnames.startswith("$Format"): 636 | if verbose: 637 | print("keywords are unexpanded, not using") 638 | raise NotThisMethod("unexpanded keywords, not a git-archive tarball") 639 | refs = set([r.strip() for r in refnames.strip("()").split(",")]) 640 | # starting in git-1.8.3, tags are listed as "tag: foo-1.0" instead of 641 | # just "foo-1.0". If we see a "tag: " prefix, prefer those. 642 | TAG = "tag: " 643 | tags = set([r[len(TAG):] for r in refs if r.startswith(TAG)]) 644 | if not tags: 645 | # Either we're using git < 1.8.3, or there really are no tags. We use 646 | # a heuristic: assume all version tags have a digit. The old git %%d 647 | # expansion behaves like git log --decorate=short and strips out the 648 | # refs/heads/ and refs/tags/ prefixes that would let us distinguish 649 | # between branches and tags. By ignoring refnames without digits, we 650 | # filter out many common branch names like "release" and 651 | # "stabilization", as well as "HEAD" and "master". 652 | tags = set([r for r in refs if re.search(r'\d', r)]) 653 | if verbose: 654 | print("discarding '%%s', no digits" %% ",".join(refs-tags)) 655 | if verbose: 656 | print("likely tags: %%s" %% ",".join(sorted(tags))) 657 | for ref in sorted(tags): 658 | # sorting will prefer e.g. "2.0" over "2.0rc1" 659 | if ref.startswith(tag_prefix): 660 | r = ref[len(tag_prefix):] 661 | if verbose: 662 | print("picking %%s" %% r) 663 | return {"version": r, 664 | "full-revisionid": keywords["full"].strip(), 665 | "dirty": False, "error": None 666 | } 667 | # no suitable tags, so version is "0+unknown", but full hex is still there 668 | if verbose: 669 | print("no suitable tags, using unknown + full revision id") 670 | return {"version": "0+unknown", 671 | "full-revisionid": keywords["full"].strip(), 672 | "dirty": False, "error": "no suitable tags"} 673 | 674 | 675 | @register_vcs_handler("git", "pieces_from_vcs") 676 | def git_pieces_from_vcs(tag_prefix, root, verbose, run_command=run_command): 677 | """Get version from 'git describe' in the root of the source tree. 678 | 679 | This only gets called if the git-archive 'subst' keywords were *not* 680 | expanded, and _version.py hasn't already been rewritten with a short 681 | version string, meaning we're inside a checked out source tree. 682 | """ 683 | if not os.path.exists(os.path.join(root, ".git")): 684 | if verbose: 685 | print("no .git in %%s" %% root) 686 | raise NotThisMethod("no .git directory") 687 | 688 | GITS = ["git"] 689 | if sys.platform == "win32": 690 | GITS = ["git.cmd", "git.exe"] 691 | # if there is a tag matching tag_prefix, this yields TAG-NUM-gHEX[-dirty] 692 | # if there isn't one, this yields HEX[-dirty] (no NUM) 693 | describe_out = run_command(GITS, ["describe", "--tags", "--dirty", 694 | "--always", "--long", 695 | "--match", "%%s*" %% tag_prefix], 696 | cwd=root) 697 | # --long was added in git-1.5.5 698 | if describe_out is None: 699 | raise NotThisMethod("'git describe' failed") 700 | describe_out = describe_out.strip() 701 | full_out = run_command(GITS, ["rev-parse", "HEAD"], cwd=root) 702 | if full_out is None: 703 | raise NotThisMethod("'git rev-parse' failed") 704 | full_out = full_out.strip() 705 | 706 | pieces = {} 707 | pieces["long"] = full_out 708 | pieces["short"] = full_out[:7] # maybe improved later 709 | pieces["error"] = None 710 | 711 | # parse describe_out. It will be like TAG-NUM-gHEX[-dirty] or HEX[-dirty] 712 | # TAG might have hyphens. 713 | git_describe = describe_out 714 | 715 | # look for -dirty suffix 716 | dirty = git_describe.endswith("-dirty") 717 | pieces["dirty"] = dirty 718 | if dirty: 719 | git_describe = git_describe[:git_describe.rindex("-dirty")] 720 | 721 | # now we have TAG-NUM-gHEX or HEX 722 | 723 | if "-" in git_describe: 724 | # TAG-NUM-gHEX 725 | mo = re.search(r'^(.+)-(\d+)-g([0-9a-f]+)$', git_describe) 726 | if not mo: 727 | # unparseable. Maybe git-describe is misbehaving? 728 | pieces["error"] = ("unable to parse git-describe output: '%%s'" 729 | %% describe_out) 730 | return pieces 731 | 732 | # tag 733 | full_tag = mo.group(1) 734 | if not full_tag.startswith(tag_prefix): 735 | if verbose: 736 | fmt = "tag '%%s' doesn't start with prefix '%%s'" 737 | print(fmt %% (full_tag, tag_prefix)) 738 | pieces["error"] = ("tag '%%s' doesn't start with prefix '%%s'" 739 | %% (full_tag, tag_prefix)) 740 | return pieces 741 | pieces["closest-tag"] = full_tag[len(tag_prefix):] 742 | 743 | # distance: number of commits since tag 744 | pieces["distance"] = int(mo.group(2)) 745 | 746 | # commit: short hex revision ID 747 | pieces["short"] = mo.group(3) 748 | 749 | else: 750 | # HEX: no tags 751 | pieces["closest-tag"] = None 752 | count_out = run_command(GITS, ["rev-list", "HEAD", "--count"], 753 | cwd=root) 754 | pieces["distance"] = int(count_out) # total number of commits 755 | 756 | return pieces 757 | 758 | 759 | def plus_or_dot(pieces): 760 | """Return a + if we don't already have one, else return a .""" 761 | if "+" in pieces.get("closest-tag", ""): 762 | return "." 763 | return "+" 764 | 765 | 766 | def render_pep440(pieces): 767 | """Build up version string, with post-release "local version identifier". 768 | 769 | Our goal: TAG[+DISTANCE.gHEX[.dirty]] . Note that if you 770 | get a tagged build and then dirty it, you'll get TAG+0.gHEX.dirty 771 | 772 | Exceptions: 773 | 1: no tags. git_describe was just HEX. 0+untagged.DISTANCE.gHEX[.dirty] 774 | """ 775 | if pieces["closest-tag"]: 776 | rendered = pieces["closest-tag"] 777 | if pieces["distance"] or pieces["dirty"]: 778 | rendered += plus_or_dot(pieces) 779 | rendered += "%%d.g%%s" %% (pieces["distance"], pieces["short"]) 780 | if pieces["dirty"]: 781 | rendered += ".dirty" 782 | else: 783 | # exception #1 784 | rendered = "0+untagged.%%d.g%%s" %% (pieces["distance"], 785 | pieces["short"]) 786 | if pieces["dirty"]: 787 | rendered += ".dirty" 788 | return rendered 789 | 790 | 791 | def render_pep440_pre(pieces): 792 | """TAG[.post.devDISTANCE] -- No -dirty. 793 | 794 | Exceptions: 795 | 1: no tags. 0.post.devDISTANCE 796 | """ 797 | if pieces["closest-tag"]: 798 | rendered = pieces["closest-tag"] 799 | if pieces["distance"]: 800 | rendered += ".post.dev%%d" %% pieces["distance"] 801 | else: 802 | # exception #1 803 | rendered = "0.post.dev%%d" %% pieces["distance"] 804 | return rendered 805 | 806 | 807 | def render_pep440_post(pieces): 808 | """TAG[.postDISTANCE[.dev0]+gHEX] . 809 | 810 | The ".dev0" means dirty. Note that .dev0 sorts backwards 811 | (a dirty tree will appear "older" than the corresponding clean one), 812 | but you shouldn't be releasing software with -dirty anyways. 813 | 814 | Exceptions: 815 | 1: no tags. 0.postDISTANCE[.dev0] 816 | """ 817 | if pieces["closest-tag"]: 818 | rendered = pieces["closest-tag"] 819 | if pieces["distance"] or pieces["dirty"]: 820 | rendered += ".post%%d" %% pieces["distance"] 821 | if pieces["dirty"]: 822 | rendered += ".dev0" 823 | rendered += plus_or_dot(pieces) 824 | rendered += "g%%s" %% pieces["short"] 825 | else: 826 | # exception #1 827 | rendered = "0.post%%d" %% pieces["distance"] 828 | if pieces["dirty"]: 829 | rendered += ".dev0" 830 | rendered += "+g%%s" %% pieces["short"] 831 | return rendered 832 | 833 | 834 | def render_pep440_old(pieces): 835 | """TAG[.postDISTANCE[.dev0]] . 836 | 837 | The ".dev0" means dirty. 838 | 839 | Eexceptions: 840 | 1: no tags. 0.postDISTANCE[.dev0] 841 | """ 842 | if pieces["closest-tag"]: 843 | rendered = pieces["closest-tag"] 844 | if pieces["distance"] or pieces["dirty"]: 845 | rendered += ".post%%d" %% pieces["distance"] 846 | if pieces["dirty"]: 847 | rendered += ".dev0" 848 | else: 849 | # exception #1 850 | rendered = "0.post%%d" %% pieces["distance"] 851 | if pieces["dirty"]: 852 | rendered += ".dev0" 853 | return rendered 854 | 855 | 856 | def render_git_describe(pieces): 857 | """TAG[-DISTANCE-gHEX][-dirty]. 858 | 859 | Like 'git describe --tags --dirty --always'. 860 | 861 | Exceptions: 862 | 1: no tags. HEX[-dirty] (note: no 'g' prefix) 863 | """ 864 | if pieces["closest-tag"]: 865 | rendered = pieces["closest-tag"] 866 | if pieces["distance"]: 867 | rendered += "-%%d-g%%s" %% (pieces["distance"], pieces["short"]) 868 | else: 869 | # exception #1 870 | rendered = pieces["short"] 871 | if pieces["dirty"]: 872 | rendered += "-dirty" 873 | return rendered 874 | 875 | 876 | def render_git_describe_long(pieces): 877 | """TAG-DISTANCE-gHEX[-dirty]. 878 | 879 | Like 'git describe --tags --dirty --always -long'. 880 | The distance/hash is unconditional. 881 | 882 | Exceptions: 883 | 1: no tags. HEX[-dirty] (note: no 'g' prefix) 884 | """ 885 | if pieces["closest-tag"]: 886 | rendered = pieces["closest-tag"] 887 | rendered += "-%%d-g%%s" %% (pieces["distance"], pieces["short"]) 888 | else: 889 | # exception #1 890 | rendered = pieces["short"] 891 | if pieces["dirty"]: 892 | rendered += "-dirty" 893 | return rendered 894 | 895 | 896 | def render(pieces, style): 897 | """Render the given version pieces into the requested style.""" 898 | if pieces["error"]: 899 | return {"version": "unknown", 900 | "full-revisionid": pieces.get("long"), 901 | "dirty": None, 902 | "error": pieces["error"]} 903 | 904 | if not style or style == "default": 905 | style = "pep440" # the default 906 | 907 | if style == "pep440": 908 | rendered = render_pep440(pieces) 909 | elif style == "pep440-pre": 910 | rendered = render_pep440_pre(pieces) 911 | elif style == "pep440-post": 912 | rendered = render_pep440_post(pieces) 913 | elif style == "pep440-old": 914 | rendered = render_pep440_old(pieces) 915 | elif style == "git-describe": 916 | rendered = render_git_describe(pieces) 917 | elif style == "git-describe-long": 918 | rendered = render_git_describe_long(pieces) 919 | else: 920 | raise ValueError("unknown style '%%s'" %% style) 921 | 922 | return {"version": rendered, "full-revisionid": pieces["long"], 923 | "dirty": pieces["dirty"], "error": None} 924 | 925 | 926 | def get_versions(): 927 | """Get version information or return default if unable to do so.""" 928 | # I am in _version.py, which lives at ROOT/VERSIONFILE_SOURCE. If we have 929 | # __file__, we can work backwards from there to the root. Some 930 | # py2exe/bbfreeze/non-CPython implementations don't do __file__, in which 931 | # case we can only use expanded keywords. 932 | 933 | cfg = get_config() 934 | verbose = cfg.verbose 935 | 936 | try: 937 | return git_versions_from_keywords(get_keywords(), cfg.tag_prefix, 938 | verbose) 939 | except NotThisMethod: 940 | pass 941 | 942 | try: 943 | root = os.path.realpath(__file__) 944 | # versionfile_source is the relative path from the top of the source 945 | # tree (where the .git directory might live) to this file. Invert 946 | # this to find the root from __file__. 947 | for i in cfg.versionfile_source.split('/'): 948 | root = os.path.dirname(root) 949 | except NameError: 950 | return {"version": "0+unknown", "full-revisionid": None, 951 | "dirty": None, 952 | "error": "unable to find root of source tree"} 953 | 954 | try: 955 | pieces = git_pieces_from_vcs(cfg.tag_prefix, root, verbose) 956 | return render(pieces, cfg.style) 957 | except NotThisMethod: 958 | pass 959 | 960 | try: 961 | if cfg.parentdir_prefix: 962 | return versions_from_parentdir(cfg.parentdir_prefix, root, verbose) 963 | except NotThisMethod: 964 | pass 965 | 966 | return {"version": "0+unknown", "full-revisionid": None, 967 | "dirty": None, 968 | "error": "unable to compute version"} 969 | ''' 970 | 971 | 972 | @register_vcs_handler("git", "get_keywords") 973 | def git_get_keywords(versionfile_abs): 974 | """Extract version information from the given file.""" 975 | # the code embedded in _version.py can just fetch the value of these 976 | # keywords. When used from setup.py, we don't want to import _version.py, 977 | # so we do it with a regexp instead. This function is not used from 978 | # _version.py. 979 | keywords = {} 980 | try: 981 | f = open(versionfile_abs, "r") 982 | for line in f.readlines(): 983 | if line.strip().startswith("git_refnames ="): 984 | mo = re.search(r'=\s*"(.*)"', line) 985 | if mo: 986 | keywords["refnames"] = mo.group(1) 987 | if line.strip().startswith("git_full ="): 988 | mo = re.search(r'=\s*"(.*)"', line) 989 | if mo: 990 | keywords["full"] = mo.group(1) 991 | f.close() 992 | except EnvironmentError: 993 | pass 994 | return keywords 995 | 996 | 997 | @register_vcs_handler("git", "keywords") 998 | def git_versions_from_keywords(keywords, tag_prefix, verbose): 999 | """Get version information from git keywords.""" 1000 | if not keywords: 1001 | raise NotThisMethod("no keywords at all, weird") 1002 | refnames = keywords["refnames"].strip() 1003 | if refnames.startswith("$Format"): 1004 | if verbose: 1005 | print("keywords are unexpanded, not using") 1006 | raise NotThisMethod("unexpanded keywords, not a git-archive tarball") 1007 | refs = set([r.strip() for r in refnames.strip("()").split(",")]) 1008 | # starting in git-1.8.3, tags are listed as "tag: foo-1.0" instead of 1009 | # just "foo-1.0". If we see a "tag: " prefix, prefer those. 1010 | TAG = "tag: " 1011 | tags = set([r[len(TAG):] for r in refs if r.startswith(TAG)]) 1012 | if not tags: 1013 | # Either we're using git < 1.8.3, or there really are no tags. We use 1014 | # a heuristic: assume all version tags have a digit. The old git %d 1015 | # expansion behaves like git log --decorate=short and strips out the 1016 | # refs/heads/ and refs/tags/ prefixes that would let us distinguish 1017 | # between branches and tags. By ignoring refnames without digits, we 1018 | # filter out many common branch names like "release" and 1019 | # "stabilization", as well as "HEAD" and "master". 1020 | tags = set([r for r in refs if re.search(r'\d', r)]) 1021 | if verbose: 1022 | print("discarding '%s', no digits" % ",".join(refs-tags)) 1023 | if verbose: 1024 | print("likely tags: %s" % ",".join(sorted(tags))) 1025 | for ref in sorted(tags): 1026 | # sorting will prefer e.g. "2.0" over "2.0rc1" 1027 | if ref.startswith(tag_prefix): 1028 | r = ref[len(tag_prefix):] 1029 | if verbose: 1030 | print("picking %s" % r) 1031 | return {"version": r, 1032 | "full-revisionid": keywords["full"].strip(), 1033 | "dirty": False, "error": None 1034 | } 1035 | # no suitable tags, so version is "0+unknown", but full hex is still there 1036 | if verbose: 1037 | print("no suitable tags, using unknown + full revision id") 1038 | return {"version": "0+unknown", 1039 | "full-revisionid": keywords["full"].strip(), 1040 | "dirty": False, "error": "no suitable tags"} 1041 | 1042 | 1043 | @register_vcs_handler("git", "pieces_from_vcs") 1044 | def git_pieces_from_vcs(tag_prefix, root, verbose, run_command=run_command): 1045 | """Get version from 'git describe' in the root of the source tree. 1046 | 1047 | This only gets called if the git-archive 'subst' keywords were *not* 1048 | expanded, and _version.py hasn't already been rewritten with a short 1049 | version string, meaning we're inside a checked out source tree. 1050 | """ 1051 | if not os.path.exists(os.path.join(root, ".git")): 1052 | if verbose: 1053 | print("no .git in %s" % root) 1054 | raise NotThisMethod("no .git directory") 1055 | 1056 | GITS = ["git"] 1057 | if sys.platform == "win32": 1058 | GITS = ["git.cmd", "git.exe"] 1059 | # if there is a tag matching tag_prefix, this yields TAG-NUM-gHEX[-dirty] 1060 | # if there isn't one, this yields HEX[-dirty] (no NUM) 1061 | describe_out = run_command(GITS, ["describe", "--tags", "--dirty", 1062 | "--always", "--long", 1063 | "--match", "%s*" % tag_prefix], 1064 | cwd=root) 1065 | # --long was added in git-1.5.5 1066 | if describe_out is None: 1067 | raise NotThisMethod("'git describe' failed") 1068 | describe_out = describe_out.strip() 1069 | full_out = run_command(GITS, ["rev-parse", "HEAD"], cwd=root) 1070 | if full_out is None: 1071 | raise NotThisMethod("'git rev-parse' failed") 1072 | full_out = full_out.strip() 1073 | 1074 | pieces = {} 1075 | pieces["long"] = full_out 1076 | pieces["short"] = full_out[:7] # maybe improved later 1077 | pieces["error"] = None 1078 | 1079 | # parse describe_out. It will be like TAG-NUM-gHEX[-dirty] or HEX[-dirty] 1080 | # TAG might have hyphens. 1081 | git_describe = describe_out 1082 | 1083 | # look for -dirty suffix 1084 | dirty = git_describe.endswith("-dirty") 1085 | pieces["dirty"] = dirty 1086 | if dirty: 1087 | git_describe = git_describe[:git_describe.rindex("-dirty")] 1088 | 1089 | # now we have TAG-NUM-gHEX or HEX 1090 | 1091 | if "-" in git_describe: 1092 | # TAG-NUM-gHEX 1093 | mo = re.search(r'^(.+)-(\d+)-g([0-9a-f]+)$', git_describe) 1094 | if not mo: 1095 | # unparseable. Maybe git-describe is misbehaving? 1096 | pieces["error"] = ("unable to parse git-describe output: '%s'" 1097 | % describe_out) 1098 | return pieces 1099 | 1100 | # tag 1101 | full_tag = mo.group(1) 1102 | if not full_tag.startswith(tag_prefix): 1103 | if verbose: 1104 | fmt = "tag '%s' doesn't start with prefix '%s'" 1105 | print(fmt % (full_tag, tag_prefix)) 1106 | pieces["error"] = ("tag '%s' doesn't start with prefix '%s'" 1107 | % (full_tag, tag_prefix)) 1108 | return pieces 1109 | pieces["closest-tag"] = full_tag[len(tag_prefix):] 1110 | 1111 | # distance: number of commits since tag 1112 | pieces["distance"] = int(mo.group(2)) 1113 | 1114 | # commit: short hex revision ID 1115 | pieces["short"] = mo.group(3) 1116 | 1117 | else: 1118 | # HEX: no tags 1119 | pieces["closest-tag"] = None 1120 | count_out = run_command(GITS, ["rev-list", "HEAD", "--count"], 1121 | cwd=root) 1122 | pieces["distance"] = int(count_out) # total number of commits 1123 | 1124 | return pieces 1125 | 1126 | 1127 | def do_vcs_install(manifest_in, versionfile_source, ipy): 1128 | """Git-specific installation logic for Versioneer. 1129 | 1130 | For Git, this means creating/changing .gitattributes to mark _version.py 1131 | for export-time keyword substitution. 1132 | """ 1133 | GITS = ["git"] 1134 | if sys.platform == "win32": 1135 | GITS = ["git.cmd", "git.exe"] 1136 | files = [manifest_in, versionfile_source] 1137 | if ipy: 1138 | files.append(ipy) 1139 | try: 1140 | me = __file__ 1141 | if me.endswith(".pyc") or me.endswith(".pyo"): 1142 | me = os.path.splitext(me)[0] + ".py" 1143 | versioneer_file = os.path.relpath(me) 1144 | except NameError: 1145 | versioneer_file = "versioneer.py" 1146 | files.append(versioneer_file) 1147 | present = False 1148 | try: 1149 | f = open(".gitattributes", "r") 1150 | for line in f.readlines(): 1151 | if line.strip().startswith(versionfile_source): 1152 | if "export-subst" in line.strip().split()[1:]: 1153 | present = True 1154 | f.close() 1155 | except EnvironmentError: 1156 | pass 1157 | if not present: 1158 | f = open(".gitattributes", "a+") 1159 | f.write("%s export-subst\n" % versionfile_source) 1160 | f.close() 1161 | files.append(".gitattributes") 1162 | run_command(GITS, ["add", "--"] + files) 1163 | 1164 | 1165 | def versions_from_parentdir(parentdir_prefix, root, verbose): 1166 | """Try to determine the version from the parent directory name. 1167 | 1168 | Source tarballs conventionally unpack into a directory that includes 1169 | both the project name and a version string. 1170 | """ 1171 | dirname = os.path.basename(root) 1172 | if not dirname.startswith(parentdir_prefix): 1173 | if verbose: 1174 | print("guessing rootdir is '%s', but '%s' doesn't start with " 1175 | "prefix '%s'" % (root, dirname, parentdir_prefix)) 1176 | raise NotThisMethod("rootdir doesn't start with parentdir_prefix") 1177 | return {"version": dirname[len(parentdir_prefix):], 1178 | "full-revisionid": None, 1179 | "dirty": False, "error": None} 1180 | 1181 | SHORT_VERSION_PY = """ 1182 | # This file was generated by 'versioneer.py' (0.16) from 1183 | # revision-control system data, or from the parent directory name of an 1184 | # unpacked source archive. Distribution tarballs contain a pre-generated copy 1185 | # of this file. 1186 | 1187 | import json 1188 | import sys 1189 | 1190 | version_json = ''' 1191 | %s 1192 | ''' # END VERSION_JSON 1193 | 1194 | 1195 | def get_versions(): 1196 | return json.loads(version_json) 1197 | """ 1198 | 1199 | 1200 | def versions_from_file(filename): 1201 | """Try to determine the version from _version.py if present.""" 1202 | try: 1203 | with open(filename) as f: 1204 | contents = f.read() 1205 | except EnvironmentError: 1206 | raise NotThisMethod("unable to read _version.py") 1207 | mo = re.search(r"version_json = '''\n(.*)''' # END VERSION_JSON", 1208 | contents, re.M | re.S) 1209 | if not mo: 1210 | raise NotThisMethod("no version_json in _version.py") 1211 | return json.loads(mo.group(1)) 1212 | 1213 | 1214 | def write_to_version_file(filename, versions): 1215 | """Write the given version number to the given _version.py file.""" 1216 | os.unlink(filename) 1217 | contents = json.dumps(versions, sort_keys=True, 1218 | indent=1, separators=(",", ": ")) 1219 | with open(filename, "w") as f: 1220 | f.write(SHORT_VERSION_PY % contents) 1221 | 1222 | print("set %s to '%s'" % (filename, versions["version"])) 1223 | 1224 | 1225 | def plus_or_dot(pieces): 1226 | """Return a + if we don't already have one, else return a .""" 1227 | if "+" in pieces.get("closest-tag", ""): 1228 | return "." 1229 | return "+" 1230 | 1231 | 1232 | def render_pep440(pieces): 1233 | """Build up version string, with post-release "local version identifier". 1234 | 1235 | Our goal: TAG[+DISTANCE.gHEX[.dirty]] . Note that if you 1236 | get a tagged build and then dirty it, you'll get TAG+0.gHEX.dirty 1237 | 1238 | Exceptions: 1239 | 1: no tags. git_describe was just HEX. 0+untagged.DISTANCE.gHEX[.dirty] 1240 | """ 1241 | if pieces["closest-tag"]: 1242 | rendered = pieces["closest-tag"] 1243 | if pieces["distance"] or pieces["dirty"]: 1244 | rendered += plus_or_dot(pieces) 1245 | rendered += "%d.g%s" % (pieces["distance"], pieces["short"]) 1246 | if pieces["dirty"]: 1247 | rendered += ".dirty" 1248 | else: 1249 | # exception #1 1250 | rendered = "0+untagged.%d.g%s" % (pieces["distance"], 1251 | pieces["short"]) 1252 | if pieces["dirty"]: 1253 | rendered += ".dirty" 1254 | return rendered 1255 | 1256 | 1257 | def render_pep440_pre(pieces): 1258 | """TAG[.post.devDISTANCE] -- No -dirty. 1259 | 1260 | Exceptions: 1261 | 1: no tags. 0.post.devDISTANCE 1262 | """ 1263 | if pieces["closest-tag"]: 1264 | rendered = pieces["closest-tag"] 1265 | if pieces["distance"]: 1266 | rendered += ".post.dev%d" % pieces["distance"] 1267 | else: 1268 | # exception #1 1269 | rendered = "0.post.dev%d" % pieces["distance"] 1270 | return rendered 1271 | 1272 | 1273 | def render_pep440_post(pieces): 1274 | """TAG[.postDISTANCE[.dev0]+gHEX] . 1275 | 1276 | The ".dev0" means dirty. Note that .dev0 sorts backwards 1277 | (a dirty tree will appear "older" than the corresponding clean one), 1278 | but you shouldn't be releasing software with -dirty anyways. 1279 | 1280 | Exceptions: 1281 | 1: no tags. 0.postDISTANCE[.dev0] 1282 | """ 1283 | if pieces["closest-tag"]: 1284 | rendered = pieces["closest-tag"] 1285 | if pieces["distance"] or pieces["dirty"]: 1286 | rendered += ".post%d" % pieces["distance"] 1287 | if pieces["dirty"]: 1288 | rendered += ".dev0" 1289 | rendered += plus_or_dot(pieces) 1290 | rendered += "g%s" % pieces["short"] 1291 | else: 1292 | # exception #1 1293 | rendered = "0.post%d" % pieces["distance"] 1294 | if pieces["dirty"]: 1295 | rendered += ".dev0" 1296 | rendered += "+g%s" % pieces["short"] 1297 | return rendered 1298 | 1299 | 1300 | def render_pep440_old(pieces): 1301 | """TAG[.postDISTANCE[.dev0]] . 1302 | 1303 | The ".dev0" means dirty. 1304 | 1305 | Eexceptions: 1306 | 1: no tags. 0.postDISTANCE[.dev0] 1307 | """ 1308 | if pieces["closest-tag"]: 1309 | rendered = pieces["closest-tag"] 1310 | if pieces["distance"] or pieces["dirty"]: 1311 | rendered += ".post%d" % pieces["distance"] 1312 | if pieces["dirty"]: 1313 | rendered += ".dev0" 1314 | else: 1315 | # exception #1 1316 | rendered = "0.post%d" % pieces["distance"] 1317 | if pieces["dirty"]: 1318 | rendered += ".dev0" 1319 | return rendered 1320 | 1321 | 1322 | def render_git_describe(pieces): 1323 | """TAG[-DISTANCE-gHEX][-dirty]. 1324 | 1325 | Like 'git describe --tags --dirty --always'. 1326 | 1327 | Exceptions: 1328 | 1: no tags. HEX[-dirty] (note: no 'g' prefix) 1329 | """ 1330 | if pieces["closest-tag"]: 1331 | rendered = pieces["closest-tag"] 1332 | if pieces["distance"]: 1333 | rendered += "-%d-g%s" % (pieces["distance"], pieces["short"]) 1334 | else: 1335 | # exception #1 1336 | rendered = pieces["short"] 1337 | if pieces["dirty"]: 1338 | rendered += "-dirty" 1339 | return rendered 1340 | 1341 | 1342 | def render_git_describe_long(pieces): 1343 | """TAG-DISTANCE-gHEX[-dirty]. 1344 | 1345 | Like 'git describe --tags --dirty --always -long'. 1346 | The distance/hash is unconditional. 1347 | 1348 | Exceptions: 1349 | 1: no tags. HEX[-dirty] (note: no 'g' prefix) 1350 | """ 1351 | if pieces["closest-tag"]: 1352 | rendered = pieces["closest-tag"] 1353 | rendered += "-%d-g%s" % (pieces["distance"], pieces["short"]) 1354 | else: 1355 | # exception #1 1356 | rendered = pieces["short"] 1357 | if pieces["dirty"]: 1358 | rendered += "-dirty" 1359 | return rendered 1360 | 1361 | 1362 | def render(pieces, style): 1363 | """Render the given version pieces into the requested style.""" 1364 | if pieces["error"]: 1365 | return {"version": "unknown", 1366 | "full-revisionid": pieces.get("long"), 1367 | "dirty": None, 1368 | "error": pieces["error"]} 1369 | 1370 | if not style or style == "default": 1371 | style = "pep440" # the default 1372 | 1373 | if style == "pep440": 1374 | rendered = render_pep440(pieces) 1375 | elif style == "pep440-pre": 1376 | rendered = render_pep440_pre(pieces) 1377 | elif style == "pep440-post": 1378 | rendered = render_pep440_post(pieces) 1379 | elif style == "pep440-old": 1380 | rendered = render_pep440_old(pieces) 1381 | elif style == "git-describe": 1382 | rendered = render_git_describe(pieces) 1383 | elif style == "git-describe-long": 1384 | rendered = render_git_describe_long(pieces) 1385 | else: 1386 | raise ValueError("unknown style '%s'" % style) 1387 | 1388 | return {"version": rendered, "full-revisionid": pieces["long"], 1389 | "dirty": pieces["dirty"], "error": None} 1390 | 1391 | 1392 | class VersioneerBadRootError(Exception): 1393 | """The project root directory is unknown or missing key files.""" 1394 | 1395 | 1396 | def get_versions(verbose=False): 1397 | """Get the project version from whatever source is available. 1398 | 1399 | Returns dict with two keys: 'version' and 'full'. 1400 | """ 1401 | if "versioneer" in sys.modules: 1402 | # see the discussion in cmdclass.py:get_cmdclass() 1403 | del sys.modules["versioneer"] 1404 | 1405 | root = get_root() 1406 | cfg = get_config_from_root(root) 1407 | 1408 | assert cfg.VCS is not None, "please set [versioneer]VCS= in setup.cfg" 1409 | handlers = HANDLERS.get(cfg.VCS) 1410 | assert handlers, "unrecognized VCS '%s'" % cfg.VCS 1411 | verbose = verbose or cfg.verbose 1412 | assert cfg.versionfile_source is not None, \ 1413 | "please set versioneer.versionfile_source" 1414 | assert cfg.tag_prefix is not None, "please set versioneer.tag_prefix" 1415 | 1416 | versionfile_abs = os.path.join(root, cfg.versionfile_source) 1417 | 1418 | # extract version from first of: _version.py, VCS command (e.g. 'git 1419 | # describe'), parentdir. This is meant to work for developers using a 1420 | # source checkout, for users of a tarball created by 'setup.py sdist', 1421 | # and for users of a tarball/zipball created by 'git archive' or github's 1422 | # download-from-tag feature or the equivalent in other VCSes. 1423 | 1424 | get_keywords_f = handlers.get("get_keywords") 1425 | from_keywords_f = handlers.get("keywords") 1426 | if get_keywords_f and from_keywords_f: 1427 | try: 1428 | keywords = get_keywords_f(versionfile_abs) 1429 | ver = from_keywords_f(keywords, cfg.tag_prefix, verbose) 1430 | if verbose: 1431 | print("got version from expanded keyword %s" % ver) 1432 | return ver 1433 | except NotThisMethod: 1434 | pass 1435 | 1436 | try: 1437 | ver = versions_from_file(versionfile_abs) 1438 | if verbose: 1439 | print("got version from file %s %s" % (versionfile_abs, ver)) 1440 | return ver 1441 | except NotThisMethod: 1442 | pass 1443 | 1444 | from_vcs_f = handlers.get("pieces_from_vcs") 1445 | if from_vcs_f: 1446 | try: 1447 | pieces = from_vcs_f(cfg.tag_prefix, root, verbose) 1448 | ver = render(pieces, cfg.style) 1449 | if verbose: 1450 | print("got version from VCS %s" % ver) 1451 | return ver 1452 | except NotThisMethod: 1453 | pass 1454 | 1455 | try: 1456 | if cfg.parentdir_prefix: 1457 | ver = versions_from_parentdir(cfg.parentdir_prefix, root, verbose) 1458 | if verbose: 1459 | print("got version from parentdir %s" % ver) 1460 | return ver 1461 | except NotThisMethod: 1462 | pass 1463 | 1464 | if verbose: 1465 | print("unable to compute version") 1466 | 1467 | return {"version": "0+unknown", "full-revisionid": None, 1468 | "dirty": None, "error": "unable to compute version"} 1469 | 1470 | 1471 | def get_version(): 1472 | """Get the short version string for this project.""" 1473 | return get_versions()["version"] 1474 | 1475 | 1476 | def get_cmdclass(): 1477 | """Get the custom setuptools/distutils subclasses used by Versioneer.""" 1478 | if "versioneer" in sys.modules: 1479 | del sys.modules["versioneer"] 1480 | # this fixes the "python setup.py develop" case (also 'install' and 1481 | # 'easy_install .'), in which subdependencies of the main project are 1482 | # built (using setup.py bdist_egg) in the same python process. Assume 1483 | # a main project A and a dependency B, which use different versions 1484 | # of Versioneer. A's setup.py imports A's Versioneer, leaving it in 1485 | # sys.modules by the time B's setup.py is executed, causing B to run 1486 | # with the wrong versioneer. Setuptools wraps the sub-dep builds in a 1487 | # sandbox that restores sys.modules to it's pre-build state, so the 1488 | # parent is protected against the child's "import versioneer". By 1489 | # removing ourselves from sys.modules here, before the child build 1490 | # happens, we protect the child from the parent's versioneer too. 1491 | # Also see https://github.com/warner/python-versioneer/issues/52 1492 | 1493 | cmds = {} 1494 | 1495 | # we add "version" to both distutils and setuptools 1496 | from distutils.core import Command 1497 | 1498 | class cmd_version(Command): 1499 | description = "report generated version string" 1500 | user_options = [] 1501 | boolean_options = [] 1502 | 1503 | def initialize_options(self): 1504 | pass 1505 | 1506 | def finalize_options(self): 1507 | pass 1508 | 1509 | def run(self): 1510 | vers = get_versions(verbose=True) 1511 | print("Version: %s" % vers["version"]) 1512 | print(" full-revisionid: %s" % vers.get("full-revisionid")) 1513 | print(" dirty: %s" % vers.get("dirty")) 1514 | if vers["error"]: 1515 | print(" error: %s" % vers["error"]) 1516 | cmds["version"] = cmd_version 1517 | 1518 | # we override "build_py" in both distutils and setuptools 1519 | # 1520 | # most invocation pathways end up running build_py: 1521 | # distutils/build -> build_py 1522 | # distutils/install -> distutils/build ->.. 1523 | # setuptools/bdist_wheel -> distutils/install ->.. 1524 | # setuptools/bdist_egg -> distutils/install_lib -> build_py 1525 | # setuptools/install -> bdist_egg ->.. 1526 | # setuptools/develop -> ? 1527 | 1528 | # we override different "build_py" commands for both environments 1529 | if "setuptools" in sys.modules: 1530 | from setuptools.command.build_py import build_py as _build_py 1531 | else: 1532 | from distutils.command.build_py import build_py as _build_py 1533 | 1534 | class cmd_build_py(_build_py): 1535 | def run(self): 1536 | root = get_root() 1537 | cfg = get_config_from_root(root) 1538 | versions = get_versions() 1539 | _build_py.run(self) 1540 | # now locate _version.py in the new build/ directory and replace 1541 | # it with an updated value 1542 | if cfg.versionfile_build: 1543 | target_versionfile = os.path.join(self.build_lib, 1544 | cfg.versionfile_build) 1545 | print("UPDATING %s" % target_versionfile) 1546 | write_to_version_file(target_versionfile, versions) 1547 | cmds["build_py"] = cmd_build_py 1548 | 1549 | if "cx_Freeze" in sys.modules: # cx_freeze enabled? 1550 | from cx_Freeze.dist import build_exe as _build_exe 1551 | 1552 | class cmd_build_exe(_build_exe): 1553 | def run(self): 1554 | root = get_root() 1555 | cfg = get_config_from_root(root) 1556 | versions = get_versions() 1557 | target_versionfile = cfg.versionfile_source 1558 | print("UPDATING %s" % target_versionfile) 1559 | write_to_version_file(target_versionfile, versions) 1560 | 1561 | _build_exe.run(self) 1562 | os.unlink(target_versionfile) 1563 | with open(cfg.versionfile_source, "w") as f: 1564 | LONG = LONG_VERSION_PY[cfg.VCS] 1565 | f.write(LONG % 1566 | {"DOLLAR": "$", 1567 | "STYLE": cfg.style, 1568 | "TAG_PREFIX": cfg.tag_prefix, 1569 | "PARENTDIR_PREFIX": cfg.parentdir_prefix, 1570 | "VERSIONFILE_SOURCE": cfg.versionfile_source, 1571 | }) 1572 | cmds["build_exe"] = cmd_build_exe 1573 | del cmds["build_py"] 1574 | 1575 | # we override different "sdist" commands for both environments 1576 | if "setuptools" in sys.modules: 1577 | from setuptools.command.sdist import sdist as _sdist 1578 | else: 1579 | from distutils.command.sdist import sdist as _sdist 1580 | 1581 | class cmd_sdist(_sdist): 1582 | def run(self): 1583 | versions = get_versions() 1584 | self._versioneer_generated_versions = versions 1585 | # unless we update this, the command will keep using the old 1586 | # version 1587 | self.distribution.metadata.version = versions["version"] 1588 | return _sdist.run(self) 1589 | 1590 | def make_release_tree(self, base_dir, files): 1591 | root = get_root() 1592 | cfg = get_config_from_root(root) 1593 | _sdist.make_release_tree(self, base_dir, files) 1594 | # now locate _version.py in the new base_dir directory 1595 | # (remembering that it may be a hardlink) and replace it with an 1596 | # updated value 1597 | target_versionfile = os.path.join(base_dir, cfg.versionfile_source) 1598 | print("UPDATING %s" % target_versionfile) 1599 | write_to_version_file(target_versionfile, 1600 | self._versioneer_generated_versions) 1601 | cmds["sdist"] = cmd_sdist 1602 | 1603 | return cmds 1604 | 1605 | 1606 | CONFIG_ERROR = """ 1607 | setup.cfg is missing the necessary Versioneer configuration. You need 1608 | a section like: 1609 | 1610 | [versioneer] 1611 | VCS = git 1612 | style = pep440 1613 | versionfile_source = src/myproject/_version.py 1614 | versionfile_build = myproject/_version.py 1615 | tag_prefix = 1616 | parentdir_prefix = myproject- 1617 | 1618 | You will also need to edit your setup.py to use the results: 1619 | 1620 | import versioneer 1621 | setup(version=versioneer.get_version(), 1622 | cmdclass=versioneer.get_cmdclass(), ...) 1623 | 1624 | Please read the docstring in ./versioneer.py for configuration instructions, 1625 | edit setup.cfg, and re-run the installer or 'python versioneer.py setup'. 1626 | """ 1627 | 1628 | SAMPLE_CONFIG = """ 1629 | # See the docstring in versioneer.py for instructions. Note that you must 1630 | # re-run 'versioneer.py setup' after changing this section, and commit the 1631 | # resulting files. 1632 | 1633 | [versioneer] 1634 | #VCS = git 1635 | #style = pep440 1636 | #versionfile_source = 1637 | #versionfile_build = 1638 | #tag_prefix = 1639 | #parentdir_prefix = 1640 | 1641 | """ 1642 | 1643 | INIT_PY_SNIPPET = """ 1644 | from ._version import get_versions 1645 | __version__ = get_versions()['version'] 1646 | del get_versions 1647 | """ 1648 | 1649 | 1650 | def do_setup(): 1651 | """Main VCS-independent setup function for installing Versioneer.""" 1652 | root = get_root() 1653 | try: 1654 | cfg = get_config_from_root(root) 1655 | except (EnvironmentError, configparser.NoSectionError, 1656 | configparser.NoOptionError) as e: 1657 | if isinstance(e, (EnvironmentError, configparser.NoSectionError)): 1658 | print("Adding sample versioneer config to setup.cfg", 1659 | file=sys.stderr) 1660 | with open(os.path.join(root, "setup.cfg"), "a") as f: 1661 | f.write(SAMPLE_CONFIG) 1662 | print(CONFIG_ERROR, file=sys.stderr) 1663 | return 1 1664 | 1665 | print(" creating %s" % cfg.versionfile_source) 1666 | with open(cfg.versionfile_source, "w") as f: 1667 | LONG = LONG_VERSION_PY[cfg.VCS] 1668 | f.write(LONG % {"DOLLAR": "$", 1669 | "STYLE": cfg.style, 1670 | "TAG_PREFIX": cfg.tag_prefix, 1671 | "PARENTDIR_PREFIX": cfg.parentdir_prefix, 1672 | "VERSIONFILE_SOURCE": cfg.versionfile_source, 1673 | }) 1674 | 1675 | ipy = os.path.join(os.path.dirname(cfg.versionfile_source), 1676 | "__init__.py") 1677 | if os.path.exists(ipy): 1678 | try: 1679 | with open(ipy, "r") as f: 1680 | old = f.read() 1681 | except EnvironmentError: 1682 | old = "" 1683 | if INIT_PY_SNIPPET not in old: 1684 | print(" appending to %s" % ipy) 1685 | with open(ipy, "a") as f: 1686 | f.write(INIT_PY_SNIPPET) 1687 | else: 1688 | print(" %s unmodified" % ipy) 1689 | else: 1690 | print(" %s doesn't exist, ok" % ipy) 1691 | ipy = None 1692 | 1693 | # Make sure both the top-level "versioneer.py" and versionfile_source 1694 | # (PKG/_version.py, used by runtime code) are in MANIFEST.in, so 1695 | # they'll be copied into source distributions. Pip won't be able to 1696 | # install the package without this. 1697 | manifest_in = os.path.join(root, "MANIFEST.in") 1698 | simple_includes = set() 1699 | try: 1700 | with open(manifest_in, "r") as f: 1701 | for line in f: 1702 | if line.startswith("include "): 1703 | for include in line.split()[1:]: 1704 | simple_includes.add(include) 1705 | except EnvironmentError: 1706 | pass 1707 | # That doesn't cover everything MANIFEST.in can do 1708 | # (http://docs.python.org/2/distutils/sourcedist.html#commands), so 1709 | # it might give some false negatives. Appending redundant 'include' 1710 | # lines is safe, though. 1711 | if "versioneer.py" not in simple_includes: 1712 | print(" appending 'versioneer.py' to MANIFEST.in") 1713 | with open(manifest_in, "a") as f: 1714 | f.write("include versioneer.py\n") 1715 | else: 1716 | print(" 'versioneer.py' already in MANIFEST.in") 1717 | if cfg.versionfile_source not in simple_includes: 1718 | print(" appending versionfile_source ('%s') to MANIFEST.in" % 1719 | cfg.versionfile_source) 1720 | with open(manifest_in, "a") as f: 1721 | f.write("include %s\n" % cfg.versionfile_source) 1722 | else: 1723 | print(" versionfile_source already in MANIFEST.in") 1724 | 1725 | # Make VCS-specific changes. For git, this means creating/changing 1726 | # .gitattributes to mark _version.py for export-time keyword 1727 | # substitution. 1728 | do_vcs_install(manifest_in, cfg.versionfile_source, ipy) 1729 | return 0 1730 | 1731 | 1732 | def scan_setup_py(): 1733 | """Validate the contents of setup.py against Versioneer's expectations.""" 1734 | found = set() 1735 | setters = False 1736 | errors = 0 1737 | with open("setup.py", "r") as f: 1738 | for line in f.readlines(): 1739 | if "import versioneer" in line: 1740 | found.add("import") 1741 | if "versioneer.get_cmdclass()" in line: 1742 | found.add("cmdclass") 1743 | if "versioneer.get_version()" in line: 1744 | found.add("get_version") 1745 | if "versioneer.VCS" in line: 1746 | setters = True 1747 | if "versioneer.versionfile_source" in line: 1748 | setters = True 1749 | if len(found) != 3: 1750 | print("") 1751 | print("Your setup.py appears to be missing some important items") 1752 | print("(but I might be wrong). Please make sure it has something") 1753 | print("roughly like the following:") 1754 | print("") 1755 | print(" import versioneer") 1756 | print(" setup( version=versioneer.get_version(),") 1757 | print(" cmdclass=versioneer.get_cmdclass(), ...)") 1758 | print("") 1759 | errors += 1 1760 | if setters: 1761 | print("You should remove lines like 'versioneer.VCS = ' and") 1762 | print("'versioneer.versionfile_source = ' . This configuration") 1763 | print("now lives in setup.cfg, and should be removed from setup.py") 1764 | print("") 1765 | errors += 1 1766 | return errors 1767 | 1768 | if __name__ == "__main__": 1769 | cmd = sys.argv[1] 1770 | if cmd == "setup": 1771 | errors = do_setup() 1772 | errors += scan_setup_py() 1773 | if errors: 1774 | sys.exit(1) 1775 | --------------------------------------------------------------------------------