├── .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 |
--------------------------------------------------------------------------------