├── .github └── workflows │ └── ci.yml ├── .travis.yml ├── AUTHORS ├── CONTRIBUTORS ├── LICENSE ├── README.md ├── ast ├── ast.go ├── ast_test.go ├── walk.go └── walk_test.go ├── ci └── run-tests.go ├── cmd └── mtex-render │ ├── README.md │ ├── fonts.go │ ├── gio.go │ ├── main.go │ └── testdata │ ├── cli.png │ └── gui.png ├── drawtex ├── canvas.go ├── drawimg │ ├── drawimg.go │ ├── drawimg_test.go │ └── testdata │ │ ├── gofont_delta_golden.png │ │ ├── gofont_func_golden.png │ │ ├── gofont_sqrt_golden.png │ │ ├── gofont_sqrt_over_2pi_golden.png │ │ ├── liberation_delta_golden.png │ │ ├── liberation_func_golden.png │ │ ├── liberation_sqrt_golden.png │ │ ├── liberation_sqrt_over_2pi_golden.png │ │ ├── lmroman_delta_golden.png │ │ ├── lmroman_func_golden.png │ │ ├── lmroman_sqrt_golden.png │ │ ├── lmroman_sqrt_over_2pi_golden.png │ │ ├── stix_delta_golden.png │ │ ├── stix_func_golden.png │ │ ├── stix_sqrt_golden.png │ │ └── stix_sqrt_over_2pi_golden.png └── drawpdf │ └── drawpdf.go ├── font ├── font.go ├── liberation │ └── liberation.go ├── lm │ └── lm.go └── ttf │ ├── ttf.go │ └── ttf_test.go ├── go.mod ├── go.sum ├── internal ├── fakebackend │ ├── fakebackend.go │ ├── fakebackend_fonts_gen.go │ ├── fakebackend_kerns_gen.go │ ├── fakebackend_test.go │ ├── fakebackend_xheight_gen.go │ └── gen-fakebackend.go └── tex2unicode │ ├── utf8.go │ └── utf8_test.go ├── latex.go ├── macros.go ├── mtex ├── README.md ├── macros.go ├── mtex.go ├── parser.go ├── parser_test.go ├── render.go ├── render_test.go ├── symbols │ ├── gen-symbols.go │ ├── set.go │ ├── symbols.go │ ├── symbols_gen.go │ └── symbols_test.go └── testdata │ └── mtex-example.png ├── parser.go ├── parser_test.go ├── scanner.go ├── scanner_test.go ├── tex ├── box.go ├── box_test.go ├── state.go ├── tex.go ├── tex_test.go └── utils.go └── token ├── kind_string.go ├── token.go └── token_test.go /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | schedule: 9 | - cron: '0 2 * * 1' 10 | 11 | env: 12 | GOPROXY: "https://proxy.golang.org" 13 | COVERAGE: "-coverpkg=github.com/go-latex/latex/..." 14 | 15 | jobs: 16 | 17 | build: 18 | name: Build 19 | strategy: 20 | matrix: 21 | go-version: [1.22.x, 1.21.x] 22 | platform: [ubuntu-latest, macos-latest, windows-latest] 23 | runs-on: ${{ matrix.platform }} 24 | steps: 25 | - name: Install Go 26 | uses: actions/setup-go@v4 27 | with: 28 | go-version: ${{ matrix.go-version }} 29 | 30 | - name: Cache-Go 31 | uses: actions/cache@v3 32 | with: 33 | path: | 34 | ~/go/pkg/mod # Module download cache 35 | ~/.cache/go-build # Build cache (Linux) 36 | ~/Library/Caches/go-build # Build cache (Mac) 37 | '%LocalAppData%\go-build' # Build cache (Windows) 38 | key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} 39 | restore-keys: | 40 | ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} 41 | 42 | - name: Checkout code 43 | uses: actions/checkout@v3 44 | 45 | - name: Install Linux packages 46 | if: matrix.platform == 'ubuntu-latest' 47 | run: | 48 | sudo apt-get update 49 | sudo apt-get install -qq pkg-config libwayland-dev libx11-dev libx11-xcb-dev libxkbcommon-dev libxkbcommon-x11-dev libgles2-mesa-dev libegl1-mesa-dev libffi-dev libxcursor-dev xvfb xdotool 50 | # start a virtual frame buffer 51 | Xvfb :99 -screen 0 1920x1024x24 & 52 | 53 | - name: Build-Linux 54 | if: matrix.platform == 'ubuntu-latest' 55 | run: | 56 | go install -v $TAGS ./... 57 | 58 | - name: Build-Windows 59 | if: matrix.platform == 'windows-latest' 60 | run: | 61 | go install -v $TAGS ./... 62 | 63 | - name: Build-Darwin 64 | if: matrix.platform == 'macos-latest' 65 | run: | 66 | go install -v $TAGS ./... 67 | 68 | - name: Test Linux 69 | if: matrix.platform == 'ubuntu-latest' 70 | run: | 71 | go run ./ci/run-tests.go $TAGS -race $COVERAGE 72 | 73 | - name: Test Windows 74 | if: matrix.platform == 'windows-latest' 75 | run: | 76 | go run ./ci/run-tests.go $TAGS 77 | 78 | - name: Test Darwin 79 | if: matrix.platform == 'macos-latest' 80 | run: | 81 | go run ./ci/run-tests.go $TAGS 82 | 83 | - name: static-check 84 | uses: dominikh/staticcheck-action@v1 85 | with: 86 | install-go: false 87 | cache-key: ${{ matrix.platform }} 88 | version: "2023.1.5" 89 | 90 | - name: Upload-Coverage 91 | if: matrix.platform == 'ubuntu-latest' 92 | uses: codecov/codecov-action@v3 93 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | go_import_path: github.com/go-latex/latex 3 | 4 | language: go 5 | 6 | go: 7 | - 1.14.x 8 | - 1.13.x 9 | - master 10 | 11 | os: 12 | - linux 13 | 14 | arch: 15 | - amd64 16 | 17 | env: 18 | global: 19 | - GO111MODULE=on 20 | - GOFLAGS="-mod=readonly" 21 | 22 | cache: 23 | directories: 24 | - $HOME/.cache/go-build 25 | - $HOME/gopath/pkg/mod 26 | 27 | git: 28 | depth: 1 29 | autocrlf: input 30 | 31 | matrix: 32 | fast_finish: true 33 | allow_failures: 34 | - go: master 35 | 36 | script: 37 | - go install -v ./... 38 | - go run ./ci/run-tests.go -coverpkg=github.com/go-latex/latex/... -race 39 | 40 | after_success: 41 | - bash <(curl -s https://codecov.io/bash) 42 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | # This is the official list of go-latex authors for copyright purposes. 2 | # This file is distinct from the CONTRIBUTORS files. 3 | # See the latter for an explanation. 4 | 5 | # Names should be added to this file as 6 | # Name or Organization 7 | # The email address is not required for organizations. 8 | 9 | # Please keep the list sorted. 10 | 11 | Google Inc 12 | Sebastien Binet 13 | -------------------------------------------------------------------------------- /CONTRIBUTORS: -------------------------------------------------------------------------------- 1 | # This is the official list of people who can contribute 2 | # (and typically have contributed) code to the go-latex 3 | # project. 4 | # 5 | # The AUTHORS file lists the copyright holders; this file 6 | # lists people. For example, Google employees would be listed here 7 | # but not in AUTHORS, because Google would hold the copyright. 8 | # 9 | # When adding J Random Contributor's name to this file, 10 | # either J's name or J's organization's name should be 11 | # added to the AUTHORS file. 12 | # 13 | # Names should be added to this file like so: 14 | # Name 15 | # 16 | # Please keep the list sorted. 17 | 18 | Dan Lorenc 19 | Sebastien Binet 20 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright ©2020 The go-latex Authors. All rights reserved. 2 | 3 | Redistribution and use in source and binary forms, with or without 4 | modification, are permitted provided that the following conditions are met: 5 | * Redistributions of source code must retain the above copyright 6 | notice, this list of conditions and the following disclaimer. 7 | * Redistributions in binary form must reproduce the above copyright 8 | notice, this list of conditions and the following disclaimer in the 9 | documentation and/or other materials provided with the distribution. 10 | * Neither the name of the go-latex project nor the names of its authors and 11 | contributors may be used to endorse or promote products derived from this 12 | software without specific prior written permission. 13 | 14 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 15 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 16 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 17 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 18 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 19 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 20 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 21 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 22 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 23 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # latex 2 | 3 | **ARCHIVED**. Please use [latex](https://codeberg.org/go-latex/latex) 4 | 5 | -------------------------------------------------------------------------------- /ast/ast.go: -------------------------------------------------------------------------------- 1 | // Copyright ©2020 The go-latex Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | // Package ast declares the types used to represent syntax trees for 6 | // LaTeX documents. 7 | package ast // import "github.com/go-latex/latex/ast" 8 | 9 | import ( 10 | "fmt" 11 | "io" 12 | 13 | "github.com/go-latex/latex/token" 14 | ) 15 | 16 | // Node is a node in a LaTeX document. 17 | type Node interface { 18 | Pos() token.Pos // position of first character belonging to the node. 19 | End() token.Pos // position of first character immediately after the node. 20 | 21 | isNode() 22 | } 23 | 24 | // List is a collection of nodes. 25 | type List []Node 26 | 27 | func (x List) isNode() {} 28 | func (x List) Pos() token.Pos { 29 | if len(x) == 0 { 30 | return -1 31 | } 32 | return x[0].Pos() 33 | } 34 | 35 | func (x List) End() token.Pos { 36 | if len(x) == 0 { 37 | return -1 38 | } 39 | return x[len(x)-1].End() 40 | } 41 | 42 | // Macro is a LaTeX macro. 43 | // ex: 44 | // \sqrt{a} 45 | // \frac{num}{den} 46 | type Macro struct { 47 | Name *Ident 48 | Args List 49 | } 50 | 51 | func (x *Macro) isNode() {} 52 | func (x *Macro) Pos() token.Pos { return x.Name.Pos() } 53 | func (x *Macro) End() token.Pos { 54 | if len(x.Args) > 0 { 55 | return x.Args[len(x.Args)-1].End() 56 | } 57 | return x.Name.End() 58 | } 59 | 60 | // Arg is an argument of a macro. 61 | // ex: 62 | // {a} in \sqrt{a} 63 | type Arg struct { 64 | Lbrace token.Pos // position of '{' 65 | List List // or stmt? 66 | Rbrace token.Pos // position of '}' 67 | } 68 | 69 | func (x *Arg) Pos() token.Pos { return x.Lbrace } 70 | func (x *Arg) End() token.Pos { return x.Rbrace } 71 | func (x *Arg) isNode() {} 72 | 73 | // OptArg is an optional argument of a macro 74 | // ex: 75 | // [n] in \sqrt[n]{a} 76 | type OptArg struct { 77 | Lbrack token.Pos // position of '[' 78 | List List 79 | Rbrack token.Pos // position of ']' 80 | } 81 | 82 | func (x *OptArg) Pos() token.Pos { return x.Lbrack } 83 | func (x *OptArg) End() token.Pos { return x.Rbrack } 84 | func (x *OptArg) isNode() {} 85 | 86 | type Ident struct { 87 | NamePos token.Pos // identifier position 88 | Name string // identifier name 89 | } 90 | 91 | func (x *Ident) Pos() token.Pos { return x.NamePos } 92 | func (x *Ident) End() token.Pos { return token.Pos(int(x.NamePos) + len(x.Name)) } 93 | func (x *Ident) isNode() {} 94 | 95 | // MathExpr is a math expression. 96 | // ex: 97 | // $f(x) \doteq \sqrt[n]{x}$ 98 | // \[ x^n + y^n = z^n \] 99 | type MathExpr struct { 100 | Delim string // delimiter used for this math expression. 101 | Left token.Pos // position of opening '$', '\(', '\[' or '\begin{math}' 102 | List List 103 | Right token.Pos // position of closing '$', '\)', '\]' or '\end{math}' 104 | } 105 | 106 | func (x *MathExpr) isNode() {} 107 | func (x *MathExpr) Pos() token.Pos { return x.Left } 108 | func (x *MathExpr) End() token.Pos { return x.Right } 109 | 110 | type Word struct { 111 | WordPos token.Pos 112 | Text string 113 | } 114 | 115 | func (x *Word) isNode() {} 116 | func (x *Word) Pos() token.Pos { return x.WordPos } 117 | func (x *Word) End() token.Pos { return token.Pos(int(x.WordPos) + len(x.Text)) } 118 | 119 | type Literal struct { 120 | LitPos token.Pos 121 | Text string 122 | } 123 | 124 | func (x *Literal) isNode() {} 125 | func (x *Literal) Pos() token.Pos { return x.LitPos } 126 | func (x *Literal) End() token.Pos { return token.Pos(int(x.LitPos) + len(x.Text)) } 127 | 128 | type Symbol struct { 129 | SymPos token.Pos 130 | Text string 131 | } 132 | 133 | func (x *Symbol) isNode() {} 134 | func (x *Symbol) Pos() token.Pos { return x.SymPos } 135 | func (x *Symbol) End() token.Pos { return token.Pos(int(x.SymPos) + len(x.Text)) } 136 | 137 | // Sub is a subscript node. 138 | // 139 | // e.g.: \sum_{i=0} 140 | type Sub struct { 141 | UnderPos token.Pos 142 | Node Node 143 | } 144 | 145 | func (x *Sub) isNode() {} 146 | func (x *Sub) Pos() token.Pos { return x.UnderPos } 147 | func (x *Sub) End() token.Pos { return x.Node.End() } 148 | 149 | // Sup is a superscript node. 150 | // 151 | // e.g.: \sum^{n} 152 | type Sup struct { 153 | HatPos token.Pos 154 | Node Node 155 | } 156 | 157 | func (x *Sup) isNode() {} 158 | func (x *Sup) Pos() token.Pos { return x.HatPos } 159 | func (x *Sup) End() token.Pos { return x.Node.End() } 160 | 161 | // Print prints node to w. 162 | func Print(o io.Writer, node Node) { 163 | switch node := node.(type) { 164 | case *Arg: 165 | fmt.Fprintf(o, "{") 166 | for i, n := range node.List { 167 | if i > 0 { 168 | fmt.Fprintf(o, ", ") 169 | } 170 | Print(o, n) 171 | } 172 | fmt.Fprintf(o, "}") 173 | 174 | case *Ident: 175 | fmt.Fprintf(o, "ast.Ident{%q}", node.Name) 176 | 177 | case *Macro: 178 | fmt.Fprintf(o, "ast.Macro{%q", node.Name.Name) 179 | switch len(node.Args) { 180 | case 0: 181 | // no-op 182 | default: 183 | fmt.Fprintf(o, ", Args:") 184 | for i, n := range node.Args { 185 | if i > 0 { 186 | fmt.Fprintf(o, ", ") 187 | } 188 | Print(o, n) 189 | } 190 | } 191 | fmt.Fprintf(o, "}") 192 | case *MathExpr: 193 | fmt.Fprintf(o, "ast.MathExpr{") 194 | switch len(node.List) { 195 | case 0: 196 | // no-op 197 | default: 198 | fmt.Fprintf(o, "List:") 199 | for i, n := range node.List { 200 | if i > 0 { 201 | fmt.Fprintf(o, ", ") 202 | } 203 | Print(o, n) 204 | } 205 | } 206 | fmt.Fprintf(o, "}") 207 | case *OptArg: 208 | fmt.Fprintf(o, "[") 209 | for i, n := range node.List { 210 | if i > 0 { 211 | fmt.Fprintf(o, ", ") 212 | } 213 | Print(o, n) 214 | } 215 | fmt.Fprintf(o, "]") 216 | case *Word: 217 | fmt.Fprintf(o, "ast.Word{%q}", node.Text) 218 | case *Literal: 219 | fmt.Fprintf(o, "ast.Lit{%q}", node.Text) 220 | case List: 221 | fmt.Fprintf(o, "ast.List{") 222 | for i, n := range node { 223 | if i > 0 { 224 | fmt.Fprintf(o, ", ") 225 | } 226 | Print(o, n) 227 | } 228 | fmt.Fprintf(o, "}") 229 | 230 | case *Sub: 231 | fmt.Fprintf(o, "ast.Sub{") 232 | Print(o, node.Node) 233 | fmt.Fprintf(o, "}") 234 | 235 | case *Sup: 236 | fmt.Fprintf(o, "ast.Sup{") 237 | Print(o, node.Node) 238 | fmt.Fprintf(o, "}") 239 | 240 | case *Symbol: 241 | fmt.Fprintf(o, "ast.Symbol{%q}", node.Text) 242 | 243 | // case *Op: 244 | // fmt.Fprintf(o, "ast.Op{%q}", node.Text) 245 | 246 | case nil: 247 | fmt.Fprintf(o, "") 248 | 249 | default: 250 | panic(fmt.Errorf("unknown node %T", node)) 251 | } 252 | } 253 | 254 | var ( 255 | _ Node = (*List)(nil) 256 | _ Node = (*Arg)(nil) 257 | _ Node = (*Ident)(nil) 258 | _ Node = (*Macro)(nil) 259 | _ Node = (*MathExpr)(nil) 260 | _ Node = (*OptArg)(nil) 261 | _ Node = (*Word)(nil) 262 | _ Node = (*Literal)(nil) 263 | _ Node = (*Sup)(nil) 264 | _ Node = (*Sub)(nil) 265 | _ Node = (*Symbol)(nil) 266 | ) 267 | -------------------------------------------------------------------------------- /ast/ast_test.go: -------------------------------------------------------------------------------- 1 | // Copyright ©2020 The go-latex Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package ast 6 | 7 | import ( 8 | "strings" 9 | "testing" 10 | 11 | "github.com/go-latex/latex/token" 12 | ) 13 | 14 | func TestPrint(t *testing.T) { 15 | for _, tc := range []struct { 16 | node Node 17 | want string 18 | pos token.Pos 19 | }{ 20 | { 21 | node: nil, 22 | want: "", 23 | }, 24 | { 25 | node: &Macro{ 26 | Name: &Ident{ 27 | NamePos: 42, 28 | Name: `\cos`, 29 | }, 30 | Args: nil, 31 | }, 32 | pos: 42, 33 | want: `ast.Macro{"\\cos"}`, 34 | }, 35 | { 36 | node: &Macro{ 37 | Name: &Ident{ 38 | NamePos: 42, 39 | Name: `\sqrt`, 40 | }, 41 | Args: List{ 42 | &Arg{List: List{&Word{Text: "x"}}}, 43 | }, 44 | }, 45 | pos: 42, 46 | want: `ast.Macro{"\\sqrt", Args:{ast.Word{"x"}}}`, 47 | }, 48 | { 49 | node: &Macro{ 50 | Name: &Ident{ 51 | NamePos: 42, 52 | Name: `\sqrt`, 53 | }, 54 | Args: List{ 55 | &OptArg{List: List{&Word{Text: "n"}}}, 56 | &Arg{List: List{&Word{Text: "x"}}}, 57 | }, 58 | }, 59 | pos: 42, 60 | want: `ast.Macro{"\\sqrt", Args:[ast.Word{"n"}], {ast.Word{"x"}}}`, 61 | }, 62 | { 63 | node: &Word{Text: "hello"}, 64 | want: `ast.Word{"hello"}`, 65 | }, 66 | { 67 | node: &Symbol{Text: "$"}, 68 | want: `ast.Symbol{"$"}`, 69 | }, 70 | { 71 | node: &Literal{Text: "10"}, 72 | want: `ast.Lit{"10"}`, 73 | }, 74 | { 75 | node: &Sup{Node: &Literal{Text: "10"}}, 76 | want: `ast.Sup{ast.Lit{"10"}}`, 77 | }, 78 | { 79 | node: &Sub{Node: &Literal{Text: "10"}}, 80 | want: `ast.Sub{ast.Lit{"10"}}`, 81 | }, 82 | { 83 | node: List{&Literal{Text: "1"}, &Literal{Text: "2"}}, 84 | want: `ast.List{ast.Lit{"1"}, ast.Lit{"2"}}`, 85 | }, 86 | { 87 | node: &MathExpr{ 88 | List: List{ 89 | &Literal{Text: "1"}, 90 | &Word{Text: "x"}, 91 | }, 92 | }, 93 | want: `ast.MathExpr{List:ast.Lit{"1"}, ast.Word{"x"}}`, 94 | }, 95 | { 96 | node: &Ident{Name: `\cos`}, 97 | want: `ast.Ident{"\\cos"}`, 98 | }, 99 | } { 100 | t.Run("", func(t *testing.T) { 101 | o := new(strings.Builder) 102 | Print(o, tc.node) 103 | 104 | if got, want := o.String(), tc.want; got != want { 105 | t.Fatalf("error:\ngot=%v\nwant=%v", got, want) 106 | } 107 | 108 | if tc.node == nil { 109 | return 110 | } 111 | 112 | tc.node.isNode() 113 | if got, want := tc.node.Pos(), tc.pos; got != want { 114 | t.Fatalf("invalid node position: got=%v, want=%v", got, want) 115 | } 116 | _ = tc.node.End() 117 | }) 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /ast/walk.go: -------------------------------------------------------------------------------- 1 | // Copyright ©2020 The go-latex Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package ast 6 | 7 | import "fmt" 8 | 9 | // A Visitor's Visit method is invoked for each node encountered by Walk. 10 | // If the result visitor w is not nil, Walk visits each of the children 11 | // of node with the visitor w, followed by a call of w.Visit(nil). 12 | type Visitor interface { 13 | Visit(node Node) (w Visitor) 14 | } 15 | 16 | // Walk traverses an AST in depth-first order: It starts by calling 17 | // v.Visit(node); node must not be nil. If the visitor w returned by 18 | // v.Visit(node) is not nil, Walk is invoked recursively with visitor 19 | // w for each of the non-nil children of node, followed by a call of 20 | // w.Visit(nil). 21 | func Walk(v Visitor, node Node) { 22 | if v = v.Visit(node); v == nil { 23 | return 24 | } 25 | 26 | switch n := node.(type) { 27 | case List: 28 | for _, x := range n { 29 | Walk(v, x) 30 | } 31 | 32 | case *Macro: 33 | if n.Name != nil { 34 | Walk(v, n.Name) 35 | } 36 | walkNodes(v, n.Args) 37 | 38 | case *Arg: 39 | walkNodes(v, n.List) 40 | 41 | case *OptArg: 42 | walkNodes(v, n.List) 43 | 44 | case *Ident: 45 | // nothing to do. 46 | 47 | case *MathExpr: 48 | walkNodes(v, n.List) 49 | 50 | case *Word, *Literal, *Symbol: 51 | // nothing to do. 52 | 53 | case *Sub: 54 | Walk(v, n.Node) 55 | 56 | case *Sup: 57 | Walk(v, n.Node) 58 | 59 | default: 60 | panic(fmt.Errorf("unknown ast node %#v (type=%T)", n, n)) 61 | } 62 | 63 | v.Visit(nil) 64 | } 65 | 66 | func walkNodes(v Visitor, nodes []Node) { 67 | for _, x := range nodes { 68 | Walk(v, x) 69 | } 70 | } 71 | 72 | type inspector func(Node) bool 73 | 74 | func (f inspector) Visit(node Node) Visitor { 75 | if f(node) { 76 | return f 77 | } 78 | return nil 79 | } 80 | 81 | // Inspect traverses an AST in depth-first order: It starts by calling 82 | // f(node); node must not be nil. If f returns true, Inspect invokes f 83 | // recursively for each of the non-nil children of node, followed by a 84 | // call of f(nil). 85 | // 86 | func Inspect(node Node, f func(Node) bool) { 87 | Walk(inspector(f), node) 88 | } 89 | -------------------------------------------------------------------------------- /ast/walk_test.go: -------------------------------------------------------------------------------- 1 | // Copyright ©2020 The go-latex Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package ast 6 | 7 | import ( 8 | "fmt" 9 | "io" 10 | "strings" 11 | "testing" 12 | ) 13 | 14 | func TestWalk(t *testing.T) { 15 | for _, tc := range []struct { 16 | node Node 17 | want string 18 | }{ 19 | { 20 | node: List{ 21 | &Word{Text: "hello"}, 22 | &Word{Text: "world"}, 23 | }, 24 | want: "ast.List *ast.Word *ast.Word ", 25 | }, 26 | { 27 | node: &Macro{ 28 | Name: &Ident{Name: "sqrt"}, 29 | Args: []Node{ 30 | &OptArg{ 31 | List: []Node{ 32 | &Word{Text: "n"}, 33 | }, 34 | }, 35 | &Arg{ 36 | List: []Node{ 37 | &Literal{Text: "2"}, 38 | &Word{Text: "x"}, 39 | }, 40 | }, 41 | }, 42 | }, 43 | want: "*ast.Macro *ast.Ident *ast.OptArg *ast.Word *ast.Arg *ast.Literal *ast.Word ", 44 | }, 45 | { 46 | node: &MathExpr{ 47 | Delim: "$", 48 | List: []Node{ 49 | &Literal{Text: "2"}, 50 | &Symbol{Text: "+"}, 51 | &Word{Text: "x"}, 52 | }, 53 | }, 54 | want: "*ast.MathExpr *ast.Literal *ast.Symbol *ast.Word ", 55 | }, 56 | { 57 | node: &Sub{ 58 | Node: &Word{Text: "i"}, 59 | }, 60 | want: "*ast.Sub *ast.Word ", 61 | }, 62 | { 63 | node: &Sup{ 64 | Node: &Literal{Text: "2"}, 65 | }, 66 | want: "*ast.Sup *ast.Literal ", 67 | }, 68 | } { 69 | t.Run("", func(t *testing.T) { 70 | o := new(strings.Builder) 71 | v := &sprinter{o} 72 | Walk(v, tc.node) 73 | got := strings.TrimSpace(o.String()) 74 | if got != tc.want { 75 | t.Fatalf("invalid walk:\ngot= %v\nwant=%v", got, tc.want) 76 | } 77 | }) 78 | } 79 | } 80 | 81 | type sprinter struct { 82 | w io.Writer 83 | } 84 | 85 | func (p *sprinter) Visit(n Node) Visitor { 86 | fmt.Fprintf(p.w, "%T ", n) 87 | return p 88 | } 89 | 90 | func TestInspect(t *testing.T) { 91 | for _, tc := range []struct { 92 | node Node 93 | want string 94 | }{ 95 | { 96 | node: List{ 97 | &Word{Text: "hello"}, 98 | &Word{Text: "world"}, 99 | }, 100 | want: "ast.List *ast.Word *ast.Word ", 101 | }, 102 | { 103 | node: &Macro{ 104 | Name: &Ident{Name: "sqrt"}, 105 | Args: []Node{ 106 | &OptArg{ 107 | List: []Node{ 108 | &Word{Text: "n"}, 109 | }, 110 | }, 111 | &Arg{ 112 | List: []Node{ 113 | &Literal{Text: "2"}, 114 | &Word{Text: "x"}, 115 | }, 116 | }, 117 | }, 118 | }, 119 | want: "*ast.Macro *ast.Ident *ast.OptArg *ast.Word *ast.Arg *ast.Literal *ast.Word ", 120 | }, 121 | { 122 | node: &MathExpr{ 123 | Delim: "$", 124 | List: []Node{ 125 | &Literal{Text: "2"}, 126 | &Symbol{Text: "+"}, 127 | &Word{Text: "x"}, 128 | }, 129 | }, 130 | want: "*ast.MathExpr *ast.Literal *ast.Symbol *ast.Word ", 131 | }, 132 | { 133 | node: &Sub{ 134 | Node: &Word{Text: "i"}, 135 | }, 136 | want: "*ast.Sub *ast.Word ", 137 | }, 138 | { 139 | node: &Sup{ 140 | Node: &Literal{Text: "2"}, 141 | }, 142 | want: "*ast.Sup *ast.Literal ", 143 | }, 144 | } { 145 | t.Run("", func(t *testing.T) { 146 | o := new(strings.Builder) 147 | Inspect(tc.node, func(n Node) bool { 148 | fmt.Fprintf(o, "%T ", n) 149 | return true 150 | }) 151 | got := strings.TrimSpace(o.String()) 152 | if got != tc.want { 153 | t.Fatalf("invalid inspect:\ngot= %v\nwant=%v", got, tc.want) 154 | } 155 | }) 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /ci/run-tests.go: -------------------------------------------------------------------------------- 1 | // Copyright ©2020 The go-latex Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | //go:build ignore 6 | // +build ignore 7 | 8 | package main 9 | 10 | import ( 11 | "bufio" 12 | "bytes" 13 | "flag" 14 | "fmt" 15 | "log" 16 | "os" 17 | "os/exec" 18 | "strings" 19 | "time" 20 | ) 21 | 22 | func main() { 23 | log.SetPrefix("ci: ") 24 | log.SetFlags(0) 25 | 26 | start := time.Now() 27 | defer func() { 28 | log.Printf("elapsed time: %v\n", time.Since(start)) 29 | }() 30 | 31 | var ( 32 | race = flag.Bool("race", false, "enable race detector") 33 | cover = flag.String("coverpkg", "", "apply coverage analysis in each test to packages matching the patterns.") 34 | tags = flag.String("tags", "", "build tags") 35 | verbose = flag.Bool("v", false, "enable verbose output") 36 | ) 37 | 38 | flag.Parse() 39 | 40 | pkgs, err := pkgList() 41 | if err != nil { 42 | log.Fatal(err) 43 | } 44 | 45 | f, err := os.Create("coverage.txt") 46 | if err != nil { 47 | log.Fatal(err) 48 | } 49 | defer f.Close() 50 | 51 | args := []string{"test"} 52 | 53 | if *verbose || *cover != "" || *race { 54 | args = append(args, "-v") 55 | } 56 | if *cover != "" { 57 | args = append(args, "-coverprofile=profile.out", "-covermode=atomic", "-coverpkg="+*cover) 58 | } 59 | if *tags != "" { 60 | args = append(args, "-tags="+*tags) 61 | } 62 | switch { 63 | case *race: 64 | args = append(args, "-race", "-timeout=20m") 65 | default: 66 | args = append(args, "-timeout=10m") 67 | } 68 | args = append(args, "") 69 | 70 | for _, pkg := range pkgs { 71 | args[len(args)-1] = pkg 72 | cmd := exec.Command("go", args...) 73 | cmd.Stdin = os.Stdin 74 | cmd.Stdout = os.Stdout 75 | cmd.Stderr = os.Stderr 76 | err := cmd.Run() 77 | if err != nil { 78 | log.Fatal(err) 79 | } 80 | if *cover != "" { 81 | profile, err := os.ReadFile("profile.out") 82 | if err != nil { 83 | log.Fatal(err) 84 | } 85 | _, err = f.Write(profile) 86 | if err != nil { 87 | log.Fatal(err) 88 | } 89 | os.Remove("profile.out") 90 | } 91 | } 92 | 93 | err = f.Close() 94 | if err != nil { 95 | log.Fatal(err) 96 | } 97 | } 98 | 99 | func pkgList() ([]string, error) { 100 | out := new(bytes.Buffer) 101 | cmd := exec.Command("go", "list", "./...") 102 | cmd.Stdout = out 103 | cmd.Stderr = os.Stderr 104 | cmd.Stdin = os.Stdin 105 | 106 | err := cmd.Run() 107 | if err != nil { 108 | return nil, fmt.Errorf("could not get package list: %w", err) 109 | } 110 | 111 | var pkgs []string 112 | scan := bufio.NewScanner(out) 113 | for scan.Scan() { 114 | pkg := scan.Text() 115 | if strings.Contains(pkg, "vendor") { 116 | continue 117 | } 118 | pkgs = append(pkgs, pkg) 119 | } 120 | 121 | return pkgs, nil 122 | } 123 | -------------------------------------------------------------------------------- /cmd/mtex-render/README.md: -------------------------------------------------------------------------------- 1 | # mtex-render 2 | 3 | `mtex-render` is a simple command that renders a LaTeX equation to a PDF or PNG document: 4 | 5 | ``` 6 | $> mtex-render -h 7 | Usage of mtex-render: 8 | -dpi float 9 | dots-per-inch to use (default 72) 10 | -font-size float 11 | font size to use (default 12) 12 | -gui 13 | enable GUI mode 14 | -o string 15 | path to output file (default "out.png") 16 | 17 | $> mtex-render -dpi 250 -font-size 42 -o foo.png "$\frac{2\pi}{\sqrt{x+\partial x}}$" 18 | ``` 19 | 20 | ![img-cli](https://github.com/go-latex/latex/raw/main/cmd/mtex-render/testdata/cli.png) 21 | 22 | ## GUI 23 | 24 | `mtex-render` also provides a Gio-based GUI: 25 | 26 | ``` 27 | $> mtex-render -gui 28 | ``` 29 | 30 | ![img-gui](https://github.com/go-latex/latex/raw/main/cmd/mtex-render/testdata/gui.png) 31 | 32 | -------------------------------------------------------------------------------- /cmd/mtex-render/fonts.go: -------------------------------------------------------------------------------- 1 | // Copyright ©2020 The go-latex Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package main 6 | 7 | import ( 8 | "log" 9 | "strings" 10 | 11 | "gioui.org/font/opentype" 12 | "gioui.org/text" 13 | lmromanbold "github.com/go-fonts/latin-modern/lmroman10bold" 14 | lmromanbolditalic "github.com/go-fonts/latin-modern/lmroman10bolditalic" 15 | lmromanitalic "github.com/go-fonts/latin-modern/lmroman10italic" 16 | lmromanregular "github.com/go-fonts/latin-modern/lmroman10regular" 17 | "github.com/go-fonts/liberation/liberationserifbold" 18 | "github.com/go-fonts/liberation/liberationserifbolditalic" 19 | "github.com/go-fonts/liberation/liberationserifitalic" 20 | "github.com/go-fonts/liberation/liberationserifregular" 21 | 22 | "github.com/go-latex/latex/font/liberation" 23 | "github.com/go-latex/latex/font/lm" 24 | "github.com/go-latex/latex/font/ttf" 25 | ) 26 | 27 | func liberationFonts() *ttf.Fonts { 28 | return liberation.Fonts() 29 | } 30 | 31 | func lmromanFonts() *ttf.Fonts { 32 | return lm.Fonts() 33 | } 34 | 35 | func registerFont(fnt text.Font, name string, raw []byte) text.FontFace { 36 | face, err := opentype.Parse(raw) 37 | if err != nil { 38 | log.Fatalf("could not parse fonts: %+v", err) 39 | } 40 | 41 | if strings.Contains(name, "-") { 42 | i := strings.Index(name, "-") 43 | name = name[:i] 44 | } 45 | fnt.Typeface = text.Typeface(name) 46 | return text.FontFace{ 47 | Font: fnt, 48 | Face: face, 49 | } 50 | } 51 | 52 | func liberationCollection() []text.FontFace { 53 | var coll []text.FontFace 54 | 55 | coll = append(coll, 56 | registerFont( 57 | text.Font{}, 58 | "Liberation", 59 | liberationserifregular.TTF, 60 | ), 61 | registerFont( 62 | text.Font{Weight: text.Bold}, 63 | "Liberation", 64 | liberationserifbold.TTF, 65 | ), 66 | registerFont( 67 | text.Font{Style: text.Italic}, 68 | "Liberation", 69 | liberationserifitalic.TTF, 70 | ), 71 | registerFont( 72 | text.Font{Weight: text.Bold, Style: text.Italic}, 73 | "Liberation", 74 | liberationserifbolditalic.TTF, 75 | ), 76 | ) 77 | return coll 78 | } 79 | 80 | func latinmodernCollection() []text.FontFace { 81 | var coll []text.FontFace 82 | 83 | coll = append(coll, 84 | registerFont( 85 | text.Font{}, 86 | "LatinModern-Regular", 87 | lmromanregular.TTF, 88 | ), 89 | registerFont( 90 | text.Font{Weight: text.Bold}, 91 | "LatinModern-Bold", 92 | lmromanbold.TTF, 93 | ), 94 | registerFont( 95 | text.Font{Style: text.Italic}, 96 | "LatinModern-Italic", 97 | lmromanitalic.TTF, 98 | ), 99 | registerFont( 100 | text.Font{Weight: text.Bold, Style: text.Italic}, 101 | "LatinModern-BoldItalic", 102 | lmromanbolditalic.TTF, 103 | ), 104 | ) 105 | return coll 106 | } 107 | -------------------------------------------------------------------------------- /cmd/mtex-render/gio.go: -------------------------------------------------------------------------------- 1 | // Copyright ©2020 The go-latex Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package main 6 | 7 | import ( 8 | "bytes" 9 | "fmt" 10 | "image" 11 | "image/color" 12 | "image/png" 13 | "log" 14 | "os" 15 | 16 | "gioui.org/app" 17 | "gioui.org/f32" 18 | "gioui.org/font/gofont" 19 | "gioui.org/gpu/headless" 20 | "gioui.org/io/key" 21 | "gioui.org/io/system" 22 | "gioui.org/layout" 23 | "gioui.org/op" 24 | "gioui.org/op/clip" 25 | "gioui.org/op/paint" 26 | "gioui.org/text" 27 | "gioui.org/unit" 28 | "gioui.org/widget" 29 | "gioui.org/widget/material" 30 | "golang.org/x/image/font" 31 | "golang.org/x/image/font/sfnt" 32 | "golang.org/x/image/math/fixed" 33 | 34 | "github.com/go-latex/latex/drawtex" 35 | "github.com/go-latex/latex/drawtex/drawimg" 36 | "github.com/go-latex/latex/font/ttf" 37 | "github.com/go-latex/latex/mtex" 38 | ) 39 | 40 | const useLiberation = true 41 | 42 | func runGio() { 43 | ui := NewUI() 44 | go func() { 45 | win := app.NewWindow( 46 | app.Title("mtex"), 47 | app.Size(unit.Dp(ui.width), unit.Dp(ui.height)), 48 | ) 49 | err := ui.Run(win) 50 | if err != nil { 51 | log.Fatal(err) 52 | } 53 | os.Exit(0) 54 | }() 55 | app.Main() 56 | } 57 | 58 | type UI struct { 59 | Theme *material.Theme 60 | 61 | Button widget.Clickable 62 | Editor widget.Editor 63 | 64 | expr string 65 | 66 | width float32 67 | height float32 68 | screen bool 69 | 70 | Image image.Image 71 | } 72 | 73 | func NewUI() *UI { 74 | ui := &UI{ 75 | Theme: material.NewTheme(gofont.Collection()), 76 | Image: image.NewRGBA(image.Rect(0, 0, 1, 1)), 77 | width: 800, 78 | height: 900, 79 | } 80 | ui.expr = `$\sqrt{x + y}$` 81 | ui.expr = `$f(x) = \frac{\sqrt{x +20}}{2\pi} +\hbar \sum y\partial y$` 82 | ui.Editor.SetText(ui.expr) 83 | return ui 84 | } 85 | 86 | func (ui *UI) Run(win *app.Window) error { 87 | defer win.Close() 88 | 89 | var ops op.Ops 90 | for e := range win.Events() { 91 | switch e := e.(type) { 92 | case system.DestroyEvent: 93 | return e.Err 94 | case key.Event: 95 | switch e.Name { 96 | case key.NameEscape: 97 | return nil 98 | case key.NameEnter, key.NameReturn: 99 | if e.Modifiers.Contain(key.ModCtrl) && e.State == key.Press { 100 | ui.expr = ui.Editor.Text() 101 | win.Invalidate() 102 | } 103 | case "F11": 104 | if e.State == key.Press { 105 | ui.screen = true 106 | win.Invalidate() 107 | } 108 | } 109 | 110 | case system.FrameEvent: 111 | gtx := layout.NewContext(&ops, e) 112 | ui.Layout(gtx) 113 | e.Frame(gtx.Ops) 114 | if ui.screen { 115 | ui.screen = false 116 | ui.screenshot(gtx.Ops) 117 | } 118 | } 119 | } 120 | 121 | return nil 122 | } 123 | 124 | var ( 125 | margin = unit.Dp(10) 126 | list = &layout.List{ 127 | Axis: layout.Vertical, 128 | } 129 | ) 130 | 131 | type ( 132 | D = layout.Dimensions 133 | C = layout.Context 134 | ) 135 | 136 | func (ui *UI) screenshot(ops *op.Ops) { 137 | win, err := headless.NewWindow(int(ui.width), int(ui.height)) 138 | if err != nil { 139 | return 140 | } 141 | 142 | err = win.Frame(ops) 143 | if err != nil { 144 | return 145 | } 146 | 147 | img, err := win.Screenshot() 148 | if err != nil { 149 | return 150 | } 151 | 152 | f, err := os.Create("ooo.png") 153 | if err != nil { 154 | return 155 | } 156 | defer f.Close() 157 | 158 | _ = png.Encode(f, img) 159 | } 160 | 161 | func (ui *UI) Layout(gtx C) D { 162 | widgets := []layout.Widget{ 163 | material.H3(ui.Theme, "Math-TeX renderer").Layout, 164 | func(gtx C) D { 165 | gtx.Constraints.Max.Y = gtx.Px(unit.Dp(200)) 166 | ed := material.Editor(ui.Theme, &ui.Editor, "") 167 | ed.TextSize = ed.TextSize.Scale(1.5) 168 | return widget.Border{ 169 | Color: color.NRGBA{A: 107}, 170 | CornerRadius: unit.Dp(4), 171 | Width: unit.Dp(2), 172 | }.Layout(gtx, ed.Layout) 173 | }, 174 | func(gtx C) D { 175 | return layout.UniformInset(margin).Layout(gtx, func(gtx C) D { 176 | for range ui.Button.Clicks() { 177 | ui.expr = ui.Editor.Text() 178 | } 179 | return material.Button(ui.Theme, &ui.Button, "Render").Layout(gtx) 180 | }) 181 | }, 182 | material.H5(ui.Theme, "Img renderer").Layout, 183 | func(gtx C) D { 184 | return layout.UniformInset(margin).Layout(gtx, func(gtx C) D { 185 | _ = ui.imgRender() 186 | return widget.Border{ 187 | Color: color.NRGBA{A: 107}, 188 | CornerRadius: unit.Dp(4), 189 | Width: unit.Dp(2), 190 | }.Layout( 191 | gtx, 192 | widget.Image{ 193 | Src: paint.NewImageOp(ui.Image), 194 | }.Layout, 195 | ) 196 | }) 197 | }, 198 | material.H5(ui.Theme, "Gio renderer").Layout, 199 | func(gtx C) D { 200 | return layout.UniformInset(margin).Layout(gtx, func(gtx C) D { 201 | _ = ui.render(gtx) 202 | max := gtx.Constraints.Constrain(gtx.Constraints.Max) 203 | return D{ 204 | Size: max, 205 | } 206 | }) 207 | }, 208 | } 209 | 210 | return list.Layout(gtx, len(widgets), func(gtx C, i int) D { 211 | return layout.UniformInset(unit.Dp(16)).Layout(gtx, widgets[i]) 212 | }) 213 | } 214 | 215 | func (ui *UI) render(gtx C) error { 216 | return ui.gioRender(gtx) 217 | } 218 | 219 | func (ui *UI) imgRender() error { 220 | const dpi = 256 221 | o := new(bytes.Buffer) 222 | dst := drawimg.NewRenderer(o) 223 | fnt := lmromanFonts() 224 | if useLiberation { 225 | fnt = liberationFonts() 226 | } 227 | err := mtex.Render( 228 | dst, 229 | ui.expr, 230 | float64(ui.Theme.TextSize.V), 231 | dpi, 232 | fnt, 233 | ) 234 | if err != nil { 235 | return err 236 | } 237 | ui.Image, err = png.Decode(o) 238 | if err != nil { 239 | return err 240 | } 241 | return nil 242 | } 243 | 244 | func (ui *UI) gioRender(gtx C) error { 245 | const dpi = 256 246 | dst := newGioRenderer(gtx, ui.Theme, ui.expr) 247 | err := mtex.Render( 248 | dst, 249 | ui.expr, 250 | float64(ui.Theme.TextSize.V), 251 | dpi, 252 | dst.ft, 253 | ) 254 | if err != nil { 255 | return err 256 | } 257 | return nil 258 | } 259 | 260 | type gioRenderer struct { 261 | gtx layout.Context 262 | col color.NRGBA 263 | th *material.Theme 264 | ft *ttf.Fonts 265 | 266 | offset f32.Point 267 | } 268 | 269 | func newGioRenderer(gtx C, th *material.Theme, txt string) *gioRenderer { 270 | fonts := latinmodernCollection() 271 | r := gioRenderer{ 272 | gtx: gtx, 273 | col: color.NRGBA{A: 255}, 274 | th: material.NewTheme(fonts), 275 | ft: lmromanFonts(), 276 | } 277 | if useLiberation { 278 | fonts = liberationCollection() 279 | r.th = material.NewTheme(fonts) 280 | r.ft = liberationFonts() 281 | } 282 | r.th.TextSize = th.TextSize 283 | 284 | ppem := fixed.Int26_6(r.ft.Rm.UnitsPerEm()) 285 | 286 | met, err := r.ft.Rm.Metrics(new(sfnt.Buffer), ppem, font.HintingNone) 287 | if err != nil { 288 | panic(fmt.Errorf("could not extract font extents: %+v", err)) 289 | } 290 | scale := float32(th.TextSize.V) / float32(ppem) 291 | 292 | // FIXME(sbinet): find out where these -1 offsets come from. 293 | r.offset = f32.Pt(-1, -scale*float32(met.Height-met.Ascent)-1) 294 | return &r 295 | } 296 | 297 | func (r *gioRenderer) Render(width, height, dpi float64, c *drawtex.Canvas) error { 298 | 299 | if false { 300 | stk := op.Save(r.gtx.Ops) 301 | var p clip.Path 302 | p.Begin(r.gtx.Ops) 303 | p.MoveTo(f32.Point{}) 304 | p.LineTo(r.pt(width*dpi, 0)) 305 | p.LineTo(r.pt(width*dpi, height*dpi)) 306 | p.LineTo(r.pt(0, height*dpi)) 307 | p.Close() 308 | clip.Stroke{ 309 | Path: p.End(), 310 | Style: clip.StrokeStyle{ 311 | Width: 2, 312 | }, 313 | }.Op().Add(r.gtx.Ops) 314 | paint.Fill(r.gtx.Ops, color.NRGBA{R: 255, A: 255}) 315 | stk.Load() 316 | } 317 | 318 | dpi /= 72 319 | for _, opTex := range c.Ops() { 320 | switch opTex := opTex.(type) { 321 | case drawtex.GlyphOp: 322 | r.drawGlyph(dpi, opTex) 323 | case drawtex.RectOp: 324 | r.drawRect(dpi, opTex) 325 | default: 326 | panic(fmt.Errorf("unknown drawtex op %T", opTex)) 327 | } 328 | } 329 | return nil 330 | } 331 | 332 | func (r *gioRenderer) drawGlyph(dpi float64, tex drawtex.GlyphOp) { 333 | defer op.Save(r.gtx.Ops).Load() 334 | x := tex.X * dpi 335 | y := (tex.Y - tex.Glyph.Size) * dpi 336 | op.Offset(r.pt(x, y)).Add(r.gtx.Ops) 337 | lbl := material.Label( 338 | r.th, 339 | unit.Px(float32(tex.Glyph.Size*dpi)), 340 | tex.Glyph.Symbol, 341 | ) 342 | if tex.Glyph.Metrics.Slanted { 343 | lbl.Font.Style = text.Italic 344 | } 345 | lbl.Color = r.col 346 | lbl.Alignment = text.Start 347 | lbl.Layout(r.gtx) 348 | } 349 | 350 | func (r *gioRenderer) drawRect(dpi float64, tex drawtex.RectOp) { 351 | defer op.Save(r.gtx.Ops).Load() 352 | 353 | var p clip.Path 354 | p.Begin(r.gtx.Ops) 355 | p.MoveTo(r.pt(tex.X1*dpi, tex.Y1*dpi).Add(r.offset)) 356 | p.LineTo(r.pt(tex.X2*dpi, tex.Y1*dpi).Add(r.offset)) 357 | p.LineTo(r.pt(tex.X2*dpi, tex.Y2*dpi).Add(r.offset)) 358 | p.LineTo(r.pt(tex.X1*dpi, tex.Y2*dpi).Add(r.offset)) 359 | p.Close() 360 | clip.Outline{ 361 | Path: p.End(), 362 | }.Op().Add(r.gtx.Ops) 363 | 364 | paint.Fill(r.gtx.Ops, r.col) 365 | } 366 | 367 | func (*gioRenderer) pt(x, y float64) f32.Point { 368 | return f32.Point{ 369 | X: float32(x), 370 | Y: float32(y), 371 | } 372 | } 373 | 374 | var ( 375 | _ mtex.Renderer = (*gioRenderer)(nil) 376 | ) 377 | -------------------------------------------------------------------------------- /cmd/mtex-render/main.go: -------------------------------------------------------------------------------- 1 | // Copyright ©2020 The go-latex Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | // Command mtex-render renders a LaTeX math expression to a PNG, PDF, ... file. 6 | // 7 | // Example: 8 | // 9 | // $> mtex-render "\$\\sqrt{x}\$" 10 | package main 11 | 12 | import ( 13 | "flag" 14 | "log" 15 | "os" 16 | 17 | "github.com/go-latex/latex/drawtex/drawimg" 18 | "github.com/go-latex/latex/mtex" 19 | ) 20 | 21 | func main() { 22 | 23 | log.SetPrefix("mtex: ") 24 | log.SetFlags(0) 25 | 26 | var ( 27 | dpi = flag.Float64("dpi", 72, "dots-per-inch to use") 28 | size = flag.Float64("font-size", 12, "font size to use") 29 | out = flag.String("o", "out.png", "path to output file") 30 | gui = flag.Bool("gui", false, "enable GUI mode") 31 | ) 32 | 33 | flag.Parse() 34 | 35 | if *gui { 36 | runGio() 37 | return 38 | } 39 | 40 | if flag.NArg() != 1 { 41 | flag.Usage() 42 | log.Fatalf("missing math expression to render") 43 | } 44 | 45 | expr := flag.Arg(0) 46 | 47 | log.Printf("rendering math expression: %q", expr) 48 | 49 | f, err := os.Create(*out) 50 | if err != nil { 51 | log.Fatalf("could not create output file: %+v", err) 52 | } 53 | defer f.Close() 54 | 55 | fnts := lmromanFonts() 56 | if useLiberation { 57 | fnts = liberationFonts() 58 | } 59 | 60 | dst := drawimg.NewRenderer(f) 61 | err = mtex.Render(dst, expr, *size, *dpi, fnts) 62 | if err != nil { 63 | log.Fatalf("could not render math expression %q: %+v", expr, err) 64 | } 65 | 66 | err = f.Close() 67 | if err != nil { 68 | log.Fatalf("could not close output file: %+v", err) 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /cmd/mtex-render/testdata/cli.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/go-latex/latex/2790903426af0a03fad34348f8d9dd2a92617ad7/cmd/mtex-render/testdata/cli.png -------------------------------------------------------------------------------- /cmd/mtex-render/testdata/gui.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/go-latex/latex/2790903426af0a03fad34348f8d9dd2a92617ad7/cmd/mtex-render/testdata/gui.png -------------------------------------------------------------------------------- /drawtex/canvas.go: -------------------------------------------------------------------------------- 1 | // Copyright ©2020 The go-latex Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | // Package drawtex describes the graphics interface for drawing LaTeX. 6 | package drawtex // import "github.com/go-latex/latex/drawtex" 7 | 8 | import ( 9 | "github.com/go-latex/latex/font" 10 | "golang.org/x/image/font/sfnt" 11 | ) 12 | 13 | type Canvas struct { 14 | ops []Op 15 | } 16 | 17 | func New() *Canvas { 18 | return &Canvas{} 19 | } 20 | 21 | func (c *Canvas) RenderGlyph(x, y float64, infos Glyph) { 22 | c.ops = append(c.ops, GlyphOp{x, y, infos}) 23 | } 24 | 25 | func (c *Canvas) RenderRectFilled(x1, y1, x2, y2 float64) { 26 | c.ops = append(c.ops, RectOp{x1, y1, x2, y2}) 27 | } 28 | 29 | func (c *Canvas) Ops() []Op { return c.ops } 30 | 31 | type Op interface { 32 | isOp() 33 | } 34 | 35 | type GlyphOp struct { 36 | X, Y float64 37 | Glyph Glyph 38 | } 39 | 40 | func (GlyphOp) isOp() {} 41 | 42 | type RectOp struct { 43 | X1, Y1 float64 44 | X2, Y2 float64 45 | } 46 | 47 | func (RectOp) isOp() {} 48 | 49 | type Glyph struct { 50 | Font *sfnt.Font 51 | Size float64 52 | Postscript string 53 | Metrics font.Metrics 54 | Symbol string 55 | Num sfnt.GlyphIndex 56 | Offset float64 57 | } 58 | 59 | var ( 60 | _ Op = (*GlyphOp)(nil) 61 | _ Op = (*RectOp)(nil) 62 | ) 63 | -------------------------------------------------------------------------------- /drawtex/drawimg/drawimg.go: -------------------------------------------------------------------------------- 1 | // Copyright ©2020 The go-latex Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | // Package drawimg implements a canvas for img. 6 | package drawimg // import "github.com/go-latex/latex/drawtex/drawimg" 7 | 8 | import ( 9 | "fmt" 10 | "image" 11 | "image/color" 12 | "image/draw" 13 | "image/png" 14 | "io" 15 | "math" 16 | 17 | "git.sr.ht/~sbinet/gg" 18 | "github.com/go-latex/latex/drawtex" 19 | "github.com/go-latex/latex/mtex" 20 | "golang.org/x/image/font" 21 | "golang.org/x/image/font/opentype" 22 | ) 23 | 24 | type Renderer struct { 25 | w io.Writer 26 | } 27 | 28 | func NewRenderer(w io.Writer) *Renderer { 29 | return &Renderer{w: w} 30 | } 31 | 32 | func (r *Renderer) Render(width, height, dpi float64, c *drawtex.Canvas) error { 33 | var ( 34 | w = width * dpi 35 | h = height * dpi 36 | ctx = gg.NewContext(int(math.Ceil(w)), int(math.Ceil(h))) 37 | ) 38 | // log.Printf("write: w=%g, h=%g", w, h) 39 | 40 | if false { 41 | draw.Draw(ctx.Image().(draw.Image), ctx.Image().Bounds(), &image.Uniform{color.White}, image.Point{}, draw.Src) 42 | } 43 | 44 | ctx.SetColor(color.Black) 45 | 46 | for _, op := range c.Ops() { 47 | switch op := op.(type) { 48 | case drawtex.GlyphOp: 49 | drawGlyph(ctx, dpi, op) 50 | case drawtex.RectOp: 51 | drawRect(ctx, dpi, op) 52 | default: 53 | panic(fmt.Errorf("unknown drawtex op %T", op)) 54 | } 55 | } 56 | 57 | return png.Encode(r.w, ctx.Image()) 58 | } 59 | 60 | func drawGlyph(ctx *gg.Context, dpi float64, op drawtex.GlyphOp) { 61 | face, err := opentype.NewFace(op.Glyph.Font, &opentype.FaceOptions{ 62 | DPI: dpi, 63 | Size: op.Glyph.Size, 64 | Hinting: font.HintingNone, 65 | }) 66 | if err != nil { 67 | panic(fmt.Errorf("could not open font face for glyph %q: %+v", 68 | op.Glyph.Symbol, err, 69 | )) 70 | } 71 | defer face.Close() 72 | ctx.SetFontFace(face) 73 | 74 | dpi /= 72 75 | 76 | x := op.X * dpi 77 | y := op.Y * dpi 78 | // log.Printf("draw-glyph: %q w=%g, h=%g x=%g, y=%g, size=%v", 79 | // op.Glyph.Symbol, 80 | // w, h, x, y, op.Glyph.Size, 81 | // ) 82 | ctx.DrawString(op.Glyph.Symbol, x, y) 83 | } 84 | 85 | func drawRect(ctx *gg.Context, dpi float64, op drawtex.RectOp) { 86 | dpi /= 72 87 | ctx.NewSubPath() 88 | ctx.MoveTo(op.X1*dpi, op.Y1*dpi) 89 | ctx.LineTo(op.X2*dpi, op.Y1*dpi) 90 | ctx.LineTo(op.X2*dpi, op.Y2*dpi) 91 | ctx.LineTo(op.X1*dpi, op.Y2*dpi) 92 | ctx.LineTo(op.X1*dpi, op.Y1*dpi) 93 | ctx.ClosePath() 94 | ctx.Fill() 95 | // log.Printf("draw-rect: pt1=(%g, %g) -> (%g, %g)", op.X1, op.Y1, op.X2, op.Y2) 96 | } 97 | 98 | var ( 99 | _ mtex.Renderer = (*Renderer)(nil) 100 | ) 101 | -------------------------------------------------------------------------------- /drawtex/drawimg/drawimg_test.go: -------------------------------------------------------------------------------- 1 | // Copyright ©2020 The go-latex Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | //go:build linux 6 | 7 | package drawimg_test 8 | 9 | import ( 10 | "bytes" 11 | "os" 12 | "testing" 13 | 14 | "github.com/go-fonts/latin-modern/lmroman12bold" 15 | "github.com/go-fonts/latin-modern/lmroman12italic" 16 | "github.com/go-fonts/latin-modern/lmroman12regular" 17 | "github.com/go-fonts/liberation/liberationsansbold" 18 | "github.com/go-fonts/liberation/liberationsansbolditalic" 19 | "github.com/go-fonts/liberation/liberationsansitalic" 20 | "github.com/go-fonts/liberation/liberationsansregular" 21 | "github.com/go-fonts/stix/stix2mathregular" 22 | "github.com/go-fonts/stix/stix2textbold" 23 | "github.com/go-fonts/stix/stix2textbolditalic" 24 | "github.com/go-fonts/stix/stix2textitalic" 25 | "github.com/go-fonts/stix/stix2textregular" 26 | "github.com/go-latex/latex/drawtex/drawimg" 27 | "github.com/go-latex/latex/font/ttf" 28 | "github.com/go-latex/latex/mtex" 29 | "golang.org/x/image/font/sfnt" 30 | ) 31 | 32 | func TestRenderer(t *testing.T) { 33 | const ( 34 | size = 12 35 | dpi = 256 36 | ) 37 | 38 | load := func(name string) []byte { 39 | name = "testdata/" + name + "_golden.png" 40 | raw, err := os.ReadFile(name) 41 | if err != nil { 42 | t.Fatalf("could not read file %q: %+v", name, err) 43 | } 44 | return raw 45 | } 46 | 47 | fonts := map[string]*ttf.Fonts{ 48 | "gofont": nil, 49 | "lmroman": lmromanFonts(t), 50 | "stix": stixFonts(t), 51 | "liberation": liberationFonts(t), 52 | } 53 | 54 | for _, tc := range []struct { 55 | name string 56 | expr string 57 | }{ 58 | { 59 | name: "func", 60 | expr: `$f(x)=ax+b$`, 61 | }, 62 | { 63 | name: "sqrt", 64 | expr: `$\sqrt{x}$`, 65 | }, 66 | { 67 | name: "sqrt_over_2pi", 68 | expr: `$\frac{\sqrt{x+20}}{2\pi}$`, 69 | }, 70 | { 71 | name: "delta", 72 | expr: `$\delta x \neq \frac{\sqrt{x+20}}{2\Delta}$`, 73 | }, 74 | } { 75 | t.Run(tc.name, func(t *testing.T) { 76 | for _, font := range []string{ 77 | "gofont", 78 | "lmroman", 79 | "stix", 80 | "liberation", 81 | } { 82 | t.Run(font, func(t *testing.T) { 83 | out := new(bytes.Buffer) 84 | dst := drawimg.NewRenderer(out) 85 | err := mtex.Render(dst, tc.expr, size, dpi, fonts[font]) 86 | if err != nil { 87 | t.Fatalf("could not render expression %q: %+v", tc.expr, err) 88 | } 89 | 90 | name := font + "_" + tc.name 91 | if got, want := out.Bytes(), load(name); !bytes.Equal(got, want) { 92 | err := os.WriteFile("testdata/"+name+".png", got, 0644) 93 | if err != nil { 94 | t.Fatalf("could not create output file: %+v", err) 95 | } 96 | t.Fatal("files differ") 97 | } 98 | }) 99 | } 100 | }) 101 | } 102 | } 103 | 104 | func lmromanFonts(t *testing.T) *ttf.Fonts { 105 | rm, err := sfnt.Parse(lmroman12regular.TTF) 106 | if err != nil { 107 | t.Fatalf("could not parse fonts: %+v", err) 108 | } 109 | 110 | it, err := sfnt.Parse(lmroman12italic.TTF) 111 | if err != nil { 112 | t.Fatalf("could not parse fonts: %+v", err) 113 | } 114 | 115 | bf, err := sfnt.Parse(lmroman12bold.TTF) 116 | if err != nil { 117 | t.Fatalf("could not parse fonts: %+v", err) 118 | } 119 | 120 | return &ttf.Fonts{ 121 | Default: rm, 122 | Rm: rm, 123 | It: it, 124 | Bf: bf, 125 | BfIt: nil, 126 | } 127 | } 128 | 129 | func stixFonts(t *testing.T) *ttf.Fonts { 130 | rm, err := sfnt.Parse(stix2mathregular.TTF) 131 | if err != nil { 132 | t.Fatalf("could not parse fonts: %+v", err) 133 | } 134 | 135 | def, err := sfnt.Parse(stix2textregular.TTF) 136 | if err != nil { 137 | t.Fatalf("could not parse fonts: %+v", err) 138 | } 139 | 140 | it, err := sfnt.Parse(stix2textitalic.TTF) 141 | if err != nil { 142 | t.Fatalf("could not parse fonts: %+v", err) 143 | } 144 | 145 | bf, err := sfnt.Parse(stix2textbold.TTF) 146 | if err != nil { 147 | t.Fatalf("could not parse fonts: %+v", err) 148 | } 149 | 150 | bfit, err := sfnt.Parse(stix2textbolditalic.TTF) 151 | if err != nil { 152 | t.Fatalf("could not parse fonts: %+v", err) 153 | } 154 | 155 | return &ttf.Fonts{ 156 | Default: def, 157 | Rm: rm, 158 | It: it, 159 | Bf: bf, 160 | BfIt: bfit, 161 | } 162 | } 163 | 164 | func liberationFonts(t *testing.T) *ttf.Fonts { 165 | rm, err := sfnt.Parse(liberationsansregular.TTF) 166 | if err != nil { 167 | t.Fatalf("could not parse fonts: %+v", err) 168 | } 169 | 170 | it, err := sfnt.Parse(liberationsansitalic.TTF) 171 | if err != nil { 172 | t.Fatalf("could not parse fonts: %+v", err) 173 | } 174 | 175 | bf, err := sfnt.Parse(liberationsansbold.TTF) 176 | if err != nil { 177 | t.Fatalf("could not parse fonts: %+v", err) 178 | } 179 | 180 | bfit, err := sfnt.Parse(liberationsansbolditalic.TTF) 181 | if err != nil { 182 | t.Fatalf("could not parse fonts: %+v", err) 183 | } 184 | 185 | return &ttf.Fonts{ 186 | Default: rm, 187 | Rm: rm, 188 | It: it, 189 | Bf: bf, 190 | BfIt: bfit, 191 | } 192 | } 193 | -------------------------------------------------------------------------------- /drawtex/drawimg/testdata/gofont_delta_golden.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/go-latex/latex/2790903426af0a03fad34348f8d9dd2a92617ad7/drawtex/drawimg/testdata/gofont_delta_golden.png -------------------------------------------------------------------------------- /drawtex/drawimg/testdata/gofont_func_golden.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/go-latex/latex/2790903426af0a03fad34348f8d9dd2a92617ad7/drawtex/drawimg/testdata/gofont_func_golden.png -------------------------------------------------------------------------------- /drawtex/drawimg/testdata/gofont_sqrt_golden.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/go-latex/latex/2790903426af0a03fad34348f8d9dd2a92617ad7/drawtex/drawimg/testdata/gofont_sqrt_golden.png -------------------------------------------------------------------------------- /drawtex/drawimg/testdata/gofont_sqrt_over_2pi_golden.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/go-latex/latex/2790903426af0a03fad34348f8d9dd2a92617ad7/drawtex/drawimg/testdata/gofont_sqrt_over_2pi_golden.png -------------------------------------------------------------------------------- /drawtex/drawimg/testdata/liberation_delta_golden.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/go-latex/latex/2790903426af0a03fad34348f8d9dd2a92617ad7/drawtex/drawimg/testdata/liberation_delta_golden.png -------------------------------------------------------------------------------- /drawtex/drawimg/testdata/liberation_func_golden.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/go-latex/latex/2790903426af0a03fad34348f8d9dd2a92617ad7/drawtex/drawimg/testdata/liberation_func_golden.png -------------------------------------------------------------------------------- /drawtex/drawimg/testdata/liberation_sqrt_golden.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/go-latex/latex/2790903426af0a03fad34348f8d9dd2a92617ad7/drawtex/drawimg/testdata/liberation_sqrt_golden.png -------------------------------------------------------------------------------- /drawtex/drawimg/testdata/liberation_sqrt_over_2pi_golden.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/go-latex/latex/2790903426af0a03fad34348f8d9dd2a92617ad7/drawtex/drawimg/testdata/liberation_sqrt_over_2pi_golden.png -------------------------------------------------------------------------------- /drawtex/drawimg/testdata/lmroman_delta_golden.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/go-latex/latex/2790903426af0a03fad34348f8d9dd2a92617ad7/drawtex/drawimg/testdata/lmroman_delta_golden.png -------------------------------------------------------------------------------- /drawtex/drawimg/testdata/lmroman_func_golden.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/go-latex/latex/2790903426af0a03fad34348f8d9dd2a92617ad7/drawtex/drawimg/testdata/lmroman_func_golden.png -------------------------------------------------------------------------------- /drawtex/drawimg/testdata/lmroman_sqrt_golden.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/go-latex/latex/2790903426af0a03fad34348f8d9dd2a92617ad7/drawtex/drawimg/testdata/lmroman_sqrt_golden.png -------------------------------------------------------------------------------- /drawtex/drawimg/testdata/lmroman_sqrt_over_2pi_golden.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/go-latex/latex/2790903426af0a03fad34348f8d9dd2a92617ad7/drawtex/drawimg/testdata/lmroman_sqrt_over_2pi_golden.png -------------------------------------------------------------------------------- /drawtex/drawimg/testdata/stix_delta_golden.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/go-latex/latex/2790903426af0a03fad34348f8d9dd2a92617ad7/drawtex/drawimg/testdata/stix_delta_golden.png -------------------------------------------------------------------------------- /drawtex/drawimg/testdata/stix_func_golden.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/go-latex/latex/2790903426af0a03fad34348f8d9dd2a92617ad7/drawtex/drawimg/testdata/stix_func_golden.png -------------------------------------------------------------------------------- /drawtex/drawimg/testdata/stix_sqrt_golden.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/go-latex/latex/2790903426af0a03fad34348f8d9dd2a92617ad7/drawtex/drawimg/testdata/stix_sqrt_golden.png -------------------------------------------------------------------------------- /drawtex/drawimg/testdata/stix_sqrt_over_2pi_golden.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/go-latex/latex/2790903426af0a03fad34348f8d9dd2a92617ad7/drawtex/drawimg/testdata/stix_sqrt_over_2pi_golden.png -------------------------------------------------------------------------------- /drawtex/drawpdf/drawpdf.go: -------------------------------------------------------------------------------- 1 | // Copyright ©2020 The go-latex Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | // Package drawpdf implements a canvas for PDF. 6 | package drawpdf // import "github.com/go-latex/latex/drawtex/drawpdf" 7 | 8 | import ( 9 | "log" 10 | 11 | "github.com/go-latex/latex/drawtex" 12 | pdf "github.com/go-pdf/fpdf" 13 | ) 14 | 15 | func Write(fname string, w, h float64, c *drawtex.Canvas) error { 16 | doc := pdf.NewCustom(&pdf.InitType{ 17 | UnitStr: "pt", 18 | Size: pdf.SizeType{Wd: w, Ht: h}, 19 | }) 20 | doc.AddPage() 21 | 22 | for _, op := range c.Ops() { 23 | switch op := op.(type) { 24 | case drawtex.GlyphOp: 25 | log.Printf(">>> %T: %#v", op, op) 26 | drawGlyph(doc, op) 27 | case drawtex.RectOp: 28 | log.Printf(">>> %T: %#v", op, op) 29 | drawRect(doc, op) 30 | default: 31 | log.Panicf("unknown drawtex op %T", op) 32 | } 33 | } 34 | return doc.OutputFileAndClose(fname) 35 | } 36 | 37 | func drawGlyph(doc *pdf.Fpdf, op drawtex.GlyphOp) {} 38 | func drawRect(doc *pdf.Fpdf, op drawtex.RectOp) {} 39 | -------------------------------------------------------------------------------- /font/font.go: -------------------------------------------------------------------------------- 1 | // Copyright ©2020 The go-latex Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | // Package font holds types to handle and abstract away font management. 6 | package font 7 | 8 | // Font represents a font. 9 | type Font struct { 10 | Name string // Name is the LaTeX name of the font (regular, default, it, ...) 11 | Type string // Type is the LaTeX class of the font (it, rm, ...) 12 | Size float64 // Size is the font size in points. 13 | } 14 | 15 | // Backend is the interface that allows to render math expressions. 16 | type Backend interface { 17 | // RenderGlyphs renders the glyph g at the reference point (x,y). 18 | RenderGlyph(x, y float64, font Font, symbol string, dpi float64) 19 | 20 | // RenderRectFilled draws a filled black rectangle from (x1,y1) to (x2,y2). 21 | RenderRectFilled(x1, y1, x2, y2 float64) 22 | 23 | // Kern returns the kerning distance between two symbols. 24 | Kern(ft1 Font, sym1 string, ft2 Font, sym2 string, dpi float64) float64 25 | 26 | // Metrics returns the metrics. 27 | Metrics(symbol string, font Font, dpi float64, math bool) Metrics 28 | 29 | // XHeight returns the xheight for the given font and dpi. 30 | XHeight(font Font, dpi float64) float64 31 | 32 | // UnderlineThickness returns the line thickness that matches the given font. 33 | // It is used as a base unit for drawing lines such as in a fraction or radical. 34 | UnderlineThickness(font Font, dpi float64) float64 35 | } 36 | 37 | // Metrics represents the metrics of a glyph in a given font. 38 | type Metrics struct { 39 | Advance float64 // Advance distance of the glyph, in points. 40 | Height float64 // Height of the glyph in points. 41 | Width float64 // Width of the glyph in points. 42 | 43 | // Ink rectangle of the glyph. 44 | XMin, XMax, YMin, YMax float64 45 | 46 | // Iceberg is the distance from the baseline to the top of the glyph. 47 | // Iceberg corresponds to TeX's definition of "height". 48 | Iceberg float64 49 | 50 | // Slanted indicates whether the glyph is slanted. 51 | Slanted bool 52 | } 53 | -------------------------------------------------------------------------------- /font/liberation/liberation.go: -------------------------------------------------------------------------------- 1 | // Copyright ©2021 The go-latex Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | // Package liberation provides a ttf.Fonts value populated with Liberation fonts. 6 | package liberation // import "github.com/go-latex/latex/font/liberation" 7 | 8 | import ( 9 | "log" 10 | "sync" 11 | 12 | "github.com/go-fonts/liberation/liberationserifbold" 13 | "github.com/go-fonts/liberation/liberationserifbolditalic" 14 | "github.com/go-fonts/liberation/liberationserifitalic" 15 | "github.com/go-fonts/liberation/liberationserifregular" 16 | "golang.org/x/image/font/sfnt" 17 | 18 | "github.com/go-latex/latex/font/ttf" 19 | ) 20 | 21 | var ( 22 | once sync.Once 23 | fnts *ttf.Fonts 24 | ) 25 | 26 | // Fonts returns a ttf.Fonts value populated with Liberation fonts. 27 | func Fonts() *ttf.Fonts { 28 | once.Do(func() { 29 | rm, err := sfnt.Parse(liberationserifregular.TTF) 30 | if err != nil { 31 | log.Panicf("could not parse fonts: %+v", err) 32 | } 33 | 34 | it, err := sfnt.Parse(liberationserifitalic.TTF) 35 | if err != nil { 36 | log.Panicf("could not parse fonts: %+v", err) 37 | } 38 | 39 | bf, err := sfnt.Parse(liberationserifbold.TTF) 40 | if err != nil { 41 | log.Panicf("could not parse fonts: %+v", err) 42 | } 43 | 44 | bfit, err := sfnt.Parse(liberationserifbolditalic.TTF) 45 | if err != nil { 46 | log.Panicf("could not parse fonts: %+v", err) 47 | } 48 | 49 | fnts = &ttf.Fonts{ 50 | Default: rm, 51 | Rm: rm, 52 | It: it, 53 | Bf: bf, 54 | BfIt: bfit, 55 | } 56 | }) 57 | return fnts 58 | } 59 | -------------------------------------------------------------------------------- /font/lm/lm.go: -------------------------------------------------------------------------------- 1 | // Copyright ©2021 The go-latex Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | // Package lm provides a ttf.Fonts value populated with latin-modern, 6 | // a LaTeX-looking font. 7 | package lm // import "github.com/go-latex/latex/font/lm" 8 | 9 | import ( 10 | "log" 11 | "sync" 12 | 13 | lmromanbold "github.com/go-fonts/latin-modern/lmroman10bold" 14 | lmromanbolditalic "github.com/go-fonts/latin-modern/lmroman10bolditalic" 15 | lmromanitalic "github.com/go-fonts/latin-modern/lmroman10italic" 16 | lmromanregular "github.com/go-fonts/latin-modern/lmroman10regular" 17 | "golang.org/x/image/font/sfnt" 18 | 19 | "github.com/go-latex/latex/font/ttf" 20 | ) 21 | 22 | var ( 23 | once sync.Once 24 | fnts *ttf.Fonts 25 | ) 26 | 27 | // Fonts returns a ttf.Fonts value populated with latin-modern fonts. 28 | func Fonts() *ttf.Fonts { 29 | once.Do(func() { 30 | rm, err := sfnt.Parse(lmromanregular.TTF) 31 | if err != nil { 32 | log.Panicf("could not parse fonts: %+v", err) 33 | } 34 | 35 | it, err := sfnt.Parse(lmromanitalic.TTF) 36 | if err != nil { 37 | log.Panicf("could not parse fonts: %+v", err) 38 | } 39 | 40 | bf, err := sfnt.Parse(lmromanbold.TTF) 41 | if err != nil { 42 | log.Panicf("could not parse fonts: %+v", err) 43 | } 44 | 45 | bfit, err := sfnt.Parse(lmromanbolditalic.TTF) 46 | if err != nil { 47 | log.Panicf("could not parse fonts: %+v", err) 48 | } 49 | 50 | fnts = &ttf.Fonts{ 51 | Default: rm, 52 | Rm: rm, 53 | It: it, 54 | Bf: bf, 55 | BfIt: bfit, 56 | } 57 | }) 58 | 59 | return fnts 60 | } 61 | -------------------------------------------------------------------------------- /font/ttf/ttf.go: -------------------------------------------------------------------------------- 1 | // Copyright ©2020 The go-latex Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | // Package ttf provides a truetype font Backend 6 | package ttf // import "github.com/go-latex/latex/font/ttf" 7 | 8 | import ( 9 | "errors" 10 | "fmt" 11 | "unicode" 12 | 13 | "github.com/go-latex/latex/drawtex" 14 | "github.com/go-latex/latex/font" 15 | "github.com/go-latex/latex/internal/tex2unicode" 16 | stdfont "golang.org/x/image/font" 17 | "golang.org/x/image/font/gofont/gobold" 18 | "golang.org/x/image/font/gofont/gobolditalic" 19 | "golang.org/x/image/font/gofont/goitalic" 20 | "golang.org/x/image/font/gofont/goregular" 21 | "golang.org/x/image/font/opentype" 22 | "golang.org/x/image/font/sfnt" 23 | "golang.org/x/image/math/fixed" 24 | ) 25 | 26 | type Fonts struct { 27 | Default *sfnt.Font 28 | 29 | Rm *sfnt.Font 30 | It *sfnt.Font 31 | Bf *sfnt.Font 32 | BfIt *sfnt.Font 33 | } 34 | 35 | type Backend struct { 36 | canvas *drawtex.Canvas 37 | glyphs map[ttfKey]ttfVal 38 | fonts map[string]*sfnt.Font 39 | } 40 | 41 | func New(cnv *drawtex.Canvas) *Backend { 42 | return NewFrom(cnv, &defaultFonts) 43 | } 44 | 45 | func NewFrom(cnv *drawtex.Canvas, fnts *Fonts) *Backend { 46 | be := &Backend{ 47 | canvas: cnv, 48 | glyphs: make(map[ttfKey]ttfVal), 49 | fonts: make(map[string]*sfnt.Font), 50 | } 51 | 52 | be.fonts["default"] = fnts.Default 53 | be.fonts["regular"] = fnts.Rm 54 | be.fonts["rm"] = fnts.Rm 55 | be.fonts["it"] = fnts.It 56 | be.fonts["bf"] = fnts.Bf 57 | 58 | return be 59 | } 60 | 61 | // RenderGlyphs renders the glyph g at the reference point (x,y). 62 | func (be *Backend) RenderGlyph(x, y float64, font font.Font, symbol string, dpi float64) { 63 | glyph := be.getInfo(symbol, font, dpi, true) 64 | be.canvas.RenderGlyph(x, y, drawtex.Glyph{ 65 | Font: glyph.font, 66 | Size: glyph.size, 67 | Postscript: glyph.postscript, 68 | Metrics: glyph.metrics, 69 | Symbol: string(glyph.rune), 70 | Num: glyph.glyph, 71 | Offset: glyph.offset, 72 | }) 73 | } 74 | 75 | // RenderRectFilled draws a filled black rectangle from (x1,y1) to (x2,y2). 76 | func (be *Backend) RenderRectFilled(x1, y1, x2, y2 float64) { 77 | be.canvas.RenderRectFilled(x1, y1, x2, y2) 78 | } 79 | 80 | // Metrics returns the metrics. 81 | func (be *Backend) Metrics(symbol string, fnt font.Font, dpi float64, math bool) font.Metrics { 82 | return be.getInfo(symbol, fnt, dpi, math).metrics 83 | } 84 | 85 | func (be *Backend) getInfo(symbol string, fnt font.Font, dpi float64, math bool) ttfVal { 86 | key := ttfKey{symbol, fnt, dpi} 87 | val, ok := be.glyphs[key] 88 | if ok { 89 | return val 90 | } 91 | 92 | var ( 93 | buf sfnt.Buffer 94 | hinting = hintingNone 95 | ) 96 | 97 | ft, rn, _ /*symbol*/, fontSize, slanted := be.getGlyph(symbol, fnt, math) 98 | 99 | postscript, err := ft.Name(&buf, sfnt.NameIDPostScript) 100 | if err != nil { 101 | panic(fmt.Errorf("could not retrieve postscript name of font: %+v", err)) 102 | } 103 | 104 | idx, err := ft.GlyphIndex(&buf, rn) 105 | if err != nil { 106 | panic(fmt.Errorf("could not retrieve glyph index for %q: %+v", rn, err)) 107 | } 108 | 109 | symName, err := ft.GlyphName(&buf, idx) 110 | if err != nil { 111 | panic(fmt.Errorf("could not retrieve glyph name of %q: %+v", rn, err)) 112 | } 113 | 114 | var ppem = int(ft.UnitsPerEm() * 6) 115 | _, err = ft.LoadGlyph(&buf, idx, fixed.I(ppem), nil) 116 | if err != nil { 117 | panic(fmt.Errorf("could not load glyph %q: %+v", rn, err)) 118 | } 119 | 120 | adv, err := ft.GlyphAdvance(&buf, idx, fixed.I(ppem), hinting) 121 | if err != nil { 122 | panic(fmt.Errorf("could not retrieve glyph advance for %q: %+v", rn, err)) 123 | } 124 | 125 | fupe := fixed.Int26_6(ft.UnitsPerEm()) 126 | _, err = ft.LoadGlyph(&buf, idx, fupe, nil) 127 | if err != nil { 128 | panic(fmt.Errorf("could not load glyph %q: %+v", rn, err)) 129 | } 130 | 131 | bnds, _, err := ft.GlyphBounds(&buf, idx, fixed.I(12), hinting) 132 | if err != nil { 133 | panic(err) 134 | } 135 | 136 | var ( 137 | scale = fontSize / 12 138 | xmin = scale * float64(bnds.Min.X) / 64 139 | xmax = scale * float64(bnds.Max.X) / 64 140 | ymin = scale * float64(-bnds.Max.Y) / 64 // FIXME 141 | ymax = scale * float64(-bnds.Min.Y) / 64 // FIXME 142 | width = xmax - xmin 143 | height = ymax - ymin 144 | ) 145 | 146 | offset := 0.0 147 | if postscript == "Cmex10" { 148 | offset = height/2 + (fnt.Size / 3 * dpi / 72) 149 | } 150 | 151 | me := font.Metrics{ 152 | Advance: float64(adv) / 65536 * fnt.Size / 12, 153 | Height: height, 154 | Width: width, 155 | XMin: xmin, 156 | XMax: xmax, 157 | YMin: ymin + offset, 158 | YMax: ymax + offset, 159 | Iceberg: ymax + offset, 160 | Slanted: slanted, 161 | } 162 | 163 | be.glyphs[key] = ttfVal{ 164 | font: ft, 165 | size: fnt.Size, 166 | postscript: postscript, 167 | metrics: me, 168 | symbolName: symName, 169 | rune: rn, 170 | glyph: idx, 171 | offset: offset, 172 | } 173 | return be.glyphs[key] 174 | } 175 | 176 | // XHeight returns the xheight for the given font and dpi. 177 | func (be *Backend) XHeight(fnt font.Font, dpi float64) float64 { 178 | ft := be.getFont(fnt.Type) 179 | face, err := opentype.NewFace(ft, &opentype.FaceOptions{ 180 | DPI: dpi, 181 | Size: fnt.Size, 182 | Hinting: stdfont.HintingNone, 183 | }) 184 | if err != nil { 185 | panic(fmt.Errorf("could not open font face for font=%s,%g,%s: %+v", 186 | fnt.Name, fnt.Size, fnt.Type, err, 187 | )) 188 | } 189 | defer face.Close() 190 | 191 | return float64(-face.Metrics().XHeight) / 64 192 | } 193 | 194 | const ( 195 | hintingNone = stdfont.HintingNone 196 | //hintingFull = stdfont.HintingFull 197 | ) 198 | 199 | func (be *Backend) getGlyph(symbol string, font font.Font, math bool) (*sfnt.Font, rune, string, float64, bool) { 200 | var ( 201 | fontType = font.Type 202 | idx = tex2unicode.Index(symbol, math) 203 | ) 204 | 205 | // only characters in the "Letter" class should be italicized in "it" mode. 206 | // Greek capital letters should be roman. 207 | if font.Type == "it" && idx < 0x10000 { 208 | if !unicode.Is(unicode.L, idx) { 209 | fontType = "rm" 210 | } 211 | } 212 | slanted := (fontType == "it") || be.isSlanted(symbol) 213 | ft := be.getFont(fontType) 214 | if ft == nil { 215 | panic("could not find TTF font for [" + fontType + "]") 216 | } 217 | 218 | // FIXME(sbinet): 219 | // \sigma -> sigma, A->A, \infty->infinity, \nabla->gradient 220 | // etc... 221 | symbolName := symbol 222 | return ft, idx, symbolName, font.Size, slanted 223 | } 224 | 225 | func (*Backend) isSlanted(symbol string) bool { 226 | switch symbol { 227 | case `\int`, `\oint`: 228 | return true 229 | default: 230 | return false 231 | } 232 | } 233 | 234 | func (be *Backend) getFont(fontType string) *sfnt.Font { 235 | return be.fonts[fontType] 236 | } 237 | 238 | // UnderlineThickness returns the line thickness that matches the given font. 239 | // It is used as a base unit for drawing lines such as in a fraction or radical. 240 | func (*Backend) UnderlineThickness(font font.Font, dpi float64) float64 { 241 | // theoretically, we could grab the underline thickness from the font 242 | // metrics. 243 | // but that information is just too un-reliable. 244 | // so, it is hardcoded. 245 | return (0.75 / 12 * font.Size * dpi) / 72 246 | } 247 | 248 | // Kern returns the kerning distance between two symbols. 249 | func (be *Backend) Kern(ft1 font.Font, sym1 string, ft2 font.Font, sym2 string, dpi float64) float64 { 250 | if ft1.Name == ft2.Name && ft1.Size == ft2.Size { 251 | const math = true 252 | info1 := be.getInfo(sym1, ft1, dpi, math) 253 | info2 := be.getInfo(sym2, ft2, dpi, math) 254 | scale := fixed.Int26_6(info1.font.UnitsPerEm()) 255 | var buf sfnt.Buffer 256 | k, err := info1.font.Kern(&buf, info1.glyph, info2.glyph, scale, hintingNone) 257 | if err != nil { 258 | if errors.Is(err, sfnt.ErrNotFound) { 259 | return 0 260 | } 261 | panic(fmt.Errorf("could not compute kerning for %q/%q: %+v", 262 | sym1, sym2, err, 263 | )) 264 | } 265 | return float64(k) / 64 266 | } 267 | return 0 268 | } 269 | 270 | type ttfKey struct { 271 | symbol string 272 | font font.Font 273 | dpi float64 274 | } 275 | 276 | type ttfVal struct { 277 | font *sfnt.Font 278 | size float64 279 | postscript string 280 | metrics font.Metrics 281 | symbolName string 282 | rune rune 283 | glyph sfnt.GlyphIndex 284 | offset float64 285 | } 286 | 287 | var defaultFonts = Fonts{ 288 | Rm: mustParseTTF(goregular.TTF), 289 | It: mustParseTTF(goitalic.TTF), 290 | Bf: mustParseTTF(gobold.TTF), 291 | BfIt: mustParseTTF(gobolditalic.TTF), 292 | } 293 | 294 | func mustParseTTF(raw []byte) *sfnt.Font { 295 | ft, err := sfnt.Parse(raw) 296 | if err != nil { 297 | panic(fmt.Errorf("could not parse raw TTF data: %+v", err)) 298 | } 299 | return ft 300 | } 301 | 302 | func init() { 303 | defaultFonts.Default = defaultFonts.Rm 304 | } 305 | 306 | var ( 307 | _ font.Backend = (*Backend)(nil) 308 | ) 309 | -------------------------------------------------------------------------------- /font/ttf/ttf_test.go: -------------------------------------------------------------------------------- 1 | // Copyright ©2020 The go-latex Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package ttf 6 | 7 | import ( 8 | "fmt" 9 | "testing" 10 | 11 | "github.com/go-fonts/dejavu/dejavusans" 12 | "github.com/go-fonts/dejavu/dejavusansoblique" 13 | "github.com/go-latex/latex/font" 14 | "github.com/go-latex/latex/internal/fakebackend" 15 | "golang.org/x/image/font/sfnt" 16 | ) 17 | 18 | func TestDejaVuBackend(t *testing.T) { 19 | var ( 20 | be = newBackend() 21 | ref = fakebackend.New() 22 | ) 23 | for _, sym := range []string{ 24 | "A", 25 | // "B", 26 | // "a", 27 | // "g", 28 | "z", 29 | "Z", 30 | // "I", 31 | "T", 32 | "i", 33 | "t", 34 | // `\sum`, 35 | // `\sigma`, 36 | } { 37 | for _, math := range []bool{ 38 | true, 39 | false, 40 | } { 41 | for _, descr := range []font.Font{ 42 | {Name: "default", Size: 12, Type: "rm"}, 43 | //{Name: "default", Size: 10, Type: "rm"}, 44 | //{Name: "it", Size: 12, Type: "it"}, 45 | //{Name: "it", Size: 10, Type: "it"}, 46 | } { 47 | t.Run(fmt.Sprintf("Metrics/%s-math=%v-%s-%g-%s", sym, math, descr.Name, descr.Size, descr.Type), func(t *testing.T) { 48 | got := be.Metrics(sym, descr, 72, math) 49 | if got, want := got, ref.Metrics(sym, descr, 72, math); got != want { 50 | t.Fatalf("invalid metrics.\ngot= %#v\nwant=%#v\n", got, want) 51 | } 52 | }) 53 | t.Run(fmt.Sprintf("XHeight/%s-math=%v-%s-%g-%s", sym, math, descr.Name, descr.Size, descr.Type), func(t *testing.T) { 54 | got := be.XHeight(descr, 72) 55 | if got, want := got, ref.XHeight(descr, 72); got != want { 56 | t.Fatalf("invalid xheight.\ngot= %#v\nwant=%#v\n", got, want) 57 | } 58 | }) 59 | } 60 | } 61 | } 62 | } 63 | 64 | func newBackend() *Backend { 65 | be := &Backend{ 66 | glyphs: make(map[ttfKey]ttfVal), 67 | fonts: make(map[string]*sfnt.Font), 68 | } 69 | 70 | ftmap := map[string][]byte{ 71 | "default": dejavusans.TTF, 72 | "regular": dejavusans.TTF, 73 | "rm": dejavusans.TTF, 74 | "it": dejavusansoblique.TTF, 75 | } 76 | for k, raw := range ftmap { 77 | ft, err := sfnt.Parse(raw) 78 | if err != nil { 79 | panic(fmt.Errorf("could not parse %q: %+v", k, err)) 80 | } 81 | be.fonts[k] = ft 82 | } 83 | 84 | return be 85 | } 86 | 87 | func TestGofontBackend(t *testing.T) { 88 | be := New(nil) 89 | { 90 | fnt := font.Font{Name: "default", Size: 12, Type: "regular"} 91 | got := be.Metrics("A", fnt, 72, true) 92 | want := font.Metrics{Advance: 8.00390625, Height: 8.671875, Width: 7.75, XMin: 0.109375, XMax: 7.859375, YMin: 0, YMax: 8.671875, Iceberg: 8.671875, Slanted: false} 93 | if got != want { 94 | t.Fatalf("got=%#v\nwant=%#v", got, want) 95 | } 96 | } 97 | { 98 | fnt := font.Font{Name: "it", Size: 12, Type: "it"} 99 | got := be.Metrics("A", fnt, 72, true) 100 | want := font.Metrics{Advance: 8.1328125, Height: 8.671875, Width: 7.75, XMin: 0.171875, XMax: 7.921875, YMin: 0, YMax: 8.671875, Iceberg: 8.671875, Slanted: true} 101 | if got != want { 102 | t.Fatalf("got=%#v\nwant=%#v", got, want) 103 | } 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/go-latex/latex 2 | 3 | go 1.21 4 | 5 | require ( 6 | gioui.org v0.0.0-20210822154628-43a7030f6e0b 7 | git.sr.ht/~sbinet/gg v0.5.0 8 | github.com/go-fonts/dejavu v0.3.4 9 | github.com/go-fonts/latin-modern v0.3.3 10 | github.com/go-fonts/liberation v0.3.3 11 | github.com/go-fonts/stix v0.2.2 12 | github.com/go-pdf/fpdf v0.9.0 13 | golang.org/x/image v0.18.0 14 | ) 15 | 16 | require ( 17 | gioui.org/cpu v0.0.0-20210817075930-8d6a761490d2 // indirect 18 | gioui.org/shader v1.0.0 // indirect 19 | github.com/campoy/embedmd v1.0.0 // indirect 20 | github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect 21 | github.com/pmezard/go-difflib v1.0.0 // indirect 22 | golang.org/x/exp/shiny v0.0.0-20240707233637-46b078467d37 // indirect 23 | golang.org/x/sys v0.22.0 // indirect 24 | golang.org/x/text v0.16.0 // indirect 25 | ) 26 | -------------------------------------------------------------------------------- /internal/fakebackend/fakebackend.go: -------------------------------------------------------------------------------- 1 | // Copyright ©2020 The go-latex Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | // Package fakebackend provides a fake tex Backend for testing purposes. 6 | package fakebackend // import "github.com/go-latex/latex/internal/fakebackend" 7 | 8 | //go:generate go run ./gen-fakebackend.go 9 | 10 | import ( 11 | "fmt" 12 | 13 | "github.com/go-latex/latex/font" 14 | ) 15 | 16 | type dbXHs map[xhKey]float64 17 | type dbFonts map[fontKey]font.Metrics 18 | type dbKerns map[kernKey]float64 19 | 20 | var ( 21 | fontsDb dbFonts 22 | xhsDb dbXHs 23 | kernsDb dbKerns 24 | ) 25 | 26 | type Backend struct { 27 | fonts dbFonts 28 | xhs dbXHs 29 | kerns dbKerns 30 | } 31 | 32 | func New() *Backend { 33 | return &Backend{ 34 | fonts: fontsDb, 35 | xhs: xhsDb, 36 | kerns: kernsDb, 37 | } 38 | } 39 | 40 | // RenderGlyphs renders the glyph g at the reference point (x,y). 41 | func (be *Backend) RenderGlyph(x, y float64, font font.Font, symbol string, dpi float64) { 42 | //panic("not implemented") 43 | } 44 | 45 | // RenderRectFilled draws a filled black rectangle from (x1,y1) to (x2,y2). 46 | func (be *Backend) RenderRectFilled(x1, y1, x2, y2 float64) { 47 | //panic("not implemented") 48 | } 49 | 50 | // Metrics returns the metrics. 51 | func (be *Backend) Metrics(symbol string, font font.Font, dpi float64, math bool) font.Metrics { 52 | if dpi != 72 { 53 | panic(fmt.Errorf("no pre-generated metrics for dpi=%v", dpi)) 54 | } 55 | 56 | key := fontKey{symbol, font, math} 57 | metrics, ok := be.fonts[key] 58 | if !ok { 59 | panic(fmt.Errorf("no pre-generated metrics for %#v", key)) 60 | } 61 | 62 | return metrics 63 | } 64 | 65 | // XHeight returns the xheight for the given font and dpi. 66 | func (be *Backend) XHeight(font font.Font, dpi float64) float64 { 67 | key := xhKey{font.Name, font.Size, dpi} 68 | xh, ok := be.xhs[key] 69 | if !ok { 70 | panic(fmt.Errorf("no pre-generated xheight for %#v", key)) 71 | } 72 | 73 | return xh 74 | } 75 | 76 | // UnderlineThickness returns the line thickness that matches the given font. 77 | // It is used as a base unit for drawing lines such as in a fraction or radical. 78 | func (be *Backend) UnderlineThickness(font font.Font, dpi float64) float64 { 79 | // theoretically, we could grab the underline thickness from the font 80 | // metrics. 81 | // but that information is just too un-reliable. 82 | // so, it is hardcoded. 83 | return (0.75 / 12 * font.Size * dpi) / 72 84 | } 85 | 86 | // Kern returns the kerning distance between two symbols. 87 | func (be *Backend) Kern(ft1 font.Font, sym1 string, ft2 font.Font, sym2 string, dpi float64) float64 { 88 | if ft1 == ft2 { 89 | return 0 90 | } 91 | 92 | kern, ok := be.kerns[kernKey{ft1, sym1, sym2}] 93 | if !ok { 94 | return 0 95 | // panic(fmt.Errorf( 96 | // "no pre-generated kerning for ft1=%v, sym1=%q, ft2=%v, sym2=%q", 97 | // ft1, sym1, ft2, sym2, 98 | // )) 99 | } 100 | return kern 101 | } 102 | 103 | type fontKey struct { 104 | symbol string 105 | font font.Font 106 | math bool 107 | } 108 | 109 | type xhKey struct { 110 | font string 111 | size float64 112 | dpi float64 113 | } 114 | 115 | type kernKey struct { 116 | //f1, f2 tex.Font 117 | font font.Font 118 | s1, s2 string 119 | } 120 | 121 | var ( 122 | _ font.Backend = (*Backend)(nil) 123 | ) 124 | -------------------------------------------------------------------------------- /internal/fakebackend/fakebackend_kerns_gen.go: -------------------------------------------------------------------------------- 1 | // Autogenerated. DO NOT EDIT. 2 | 3 | package fakebackend 4 | 5 | import "github.com/go-latex/latex/font" 6 | 7 | func init() { 8 | kernsDb = dbKerns{ 9 | kernKey{font: font.Font{Name: "default", Size: 10, Type: "regular"}, s1: "A", s2: "V"}: 0, 10 | kernKey{font: font.Font{Name: "default", Size: 10, Type: "regular"}, s1: "V", s2: "A"}: 0, 11 | kernKey{font: font.Font{Name: "default", Size: 10, Type: "regular"}, s1: "A", s2: "é"}: 0, 12 | kernKey{font: font.Font{Name: "default", Size: 10, Type: "regular"}, s1: "é", s2: "A"}: 0, 13 | kernKey{font: font.Font{Name: "default", Size: 10, Type: "regular"}, s1: "V", s2: "é"}: 0, 14 | kernKey{font: font.Font{Name: "default", Size: 10, Type: "regular"}, s1: "é", s2: "V"}: 0, 15 | kernKey{font: font.Font{Name: "default", Size: 10, Type: "regular"}, s1: "h", s2: "e"}: 0, 16 | kernKey{font: font.Font{Name: "default", Size: 10, Type: "regular"}, s1: "e", s2: "h"}: 0, 17 | kernKey{font: font.Font{Name: "default", Size: 10, Type: "regular"}, s1: "l", s2: "e"}: 0, 18 | kernKey{font: font.Font{Name: "default", Size: 10, Type: "regular"}, s1: "e", s2: "l"}: 0, 19 | kernKey{font: font.Font{Name: "default", Size: 10, Type: "regular"}, s1: "l", s2: "l"}: 0, 20 | kernKey{font: font.Font{Name: "default", Size: 10, Type: "regular"}, s1: "l", s2: "l"}: 0, 21 | kernKey{font: font.Font{Name: "default", Size: 10, Type: "regular"}, s1: "l", s2: "o"}: 0, 22 | kernKey{font: font.Font{Name: "default", Size: 10, Type: "regular"}, s1: "o", s2: "l"}: 0, 23 | kernKey{font: font.Font{Name: "default", Size: 10, Type: "regular"}, s1: "é", s2: "é"}: 0, 24 | kernKey{font: font.Font{Name: "default", Size: 10, Type: "regular"}, s1: "é", s2: "é"}: 0, 25 | kernKey{font: font.Font{Name: "default", Size: 10, Type: "regular"}, s1: "f", s2: "i"}: 0, 26 | kernKey{font: font.Font{Name: "default", Size: 10, Type: "regular"}, s1: "i", s2: "f"}: 0, 27 | kernKey{font: font.Font{Name: "default", Size: 10, Type: "regular"}, s1: " ", s2: "i"}: 0, 28 | kernKey{font: font.Font{Name: "default", Size: 10, Type: "regular"}, s1: "i", s2: " "}: 0, 29 | kernKey{font: font.Font{Name: "default", Size: 10, Type: "regular"}, s1: "i", s2: "s"}: 0, 30 | kernKey{font: font.Font{Name: "default", Size: 10, Type: "regular"}, s1: "s", s2: "i"}: 0, 31 | kernKey{font: font.Font{Name: "default", Size: 10, Type: "regular"}, s1: " ", s2: "s"}: 0, 32 | kernKey{font: font.Font{Name: "default", Size: 10, Type: "regular"}, s1: "s", s2: " "}: 0, 33 | kernKey{font: font.Font{Name: "default", Size: 10, Type: "regular"}, s1: "A", s2: "\\sigma"}: 0, 34 | kernKey{font: font.Font{Name: "default", Size: 10, Type: "regular"}, s1: "\\sigma", s2: "A"}: 0, 35 | kernKey{font: font.Font{Name: "default", Size: 10, Type: "regular"}, s1: "a", s2: "\\sigma"}: 0, 36 | kernKey{font: font.Font{Name: "default", Size: 10, Type: "regular"}, s1: "\\sigma", s2: "a"}: 0, 37 | kernKey{font: font.Font{Name: "default", Size: 10, Type: "regular"}, s1: "é", s2: "\\sigma"}: 0, 38 | kernKey{font: font.Font{Name: "default", Size: 10, Type: "regular"}, s1: "\\sigma", s2: "é"}: 0, 39 | kernKey{font: font.Font{Name: "default", Size: 10, Type: "regular"}, s1: " ", s2: "\\sigma"}: 0, 40 | kernKey{font: font.Font{Name: "default", Size: 10, Type: "regular"}, s1: "\\sigma", s2: " "}: 0, 41 | kernKey{font: font.Font{Name: "default", Size: 10, Type: "regular"}, s1: "\\sum", s2: "\\sigma"}: 0, 42 | kernKey{font: font.Font{Name: "default", Size: 10, Type: "regular"}, s1: "\\sigma", s2: "\\sum"}: 0, 43 | kernKey{font: font.Font{Name: "default", Size: 10, Type: "regular"}, s1: "1", s2: "."}: 0, 44 | kernKey{font: font.Font{Name: "default", Size: 10, Type: "regular"}, s1: ".", s2: "1"}: 0, 45 | kernKey{font: font.Font{Name: "default", Size: 10, Type: "regular"}, s1: "2", s2: "."}: 0, 46 | kernKey{font: font.Font{Name: "default", Size: 10, Type: "regular"}, s1: ".", s2: "2"}: -0.5, 47 | kernKey{font: font.Font{Name: "default", Size: 12, Type: "regular"}, s1: "A", s2: "V"}: 0, 48 | kernKey{font: font.Font{Name: "default", Size: 12, Type: "regular"}, s1: "V", s2: "A"}: 0, 49 | kernKey{font: font.Font{Name: "default", Size: 12, Type: "regular"}, s1: "A", s2: "é"}: 0, 50 | kernKey{font: font.Font{Name: "default", Size: 12, Type: "regular"}, s1: "é", s2: "A"}: 0, 51 | kernKey{font: font.Font{Name: "default", Size: 12, Type: "regular"}, s1: "V", s2: "é"}: 0, 52 | kernKey{font: font.Font{Name: "default", Size: 12, Type: "regular"}, s1: "é", s2: "V"}: 0, 53 | kernKey{font: font.Font{Name: "default", Size: 12, Type: "regular"}, s1: "h", s2: "e"}: 0, 54 | kernKey{font: font.Font{Name: "default", Size: 12, Type: "regular"}, s1: "e", s2: "h"}: 0, 55 | kernKey{font: font.Font{Name: "default", Size: 12, Type: "regular"}, s1: "l", s2: "e"}: 0, 56 | kernKey{font: font.Font{Name: "default", Size: 12, Type: "regular"}, s1: "e", s2: "l"}: 0, 57 | kernKey{font: font.Font{Name: "default", Size: 12, Type: "regular"}, s1: "l", s2: "l"}: 0, 58 | kernKey{font: font.Font{Name: "default", Size: 12, Type: "regular"}, s1: "l", s2: "l"}: 0, 59 | kernKey{font: font.Font{Name: "default", Size: 12, Type: "regular"}, s1: "l", s2: "o"}: 0, 60 | kernKey{font: font.Font{Name: "default", Size: 12, Type: "regular"}, s1: "o", s2: "l"}: 0, 61 | kernKey{font: font.Font{Name: "default", Size: 12, Type: "regular"}, s1: "é", s2: "é"}: 0, 62 | kernKey{font: font.Font{Name: "default", Size: 12, Type: "regular"}, s1: "é", s2: "é"}: 0, 63 | kernKey{font: font.Font{Name: "default", Size: 12, Type: "regular"}, s1: "f", s2: "i"}: 0, 64 | kernKey{font: font.Font{Name: "default", Size: 12, Type: "regular"}, s1: "i", s2: "f"}: 0, 65 | kernKey{font: font.Font{Name: "default", Size: 12, Type: "regular"}, s1: " ", s2: "i"}: 0, 66 | kernKey{font: font.Font{Name: "default", Size: 12, Type: "regular"}, s1: "i", s2: " "}: 0, 67 | kernKey{font: font.Font{Name: "default", Size: 12, Type: "regular"}, s1: "i", s2: "s"}: 0, 68 | kernKey{font: font.Font{Name: "default", Size: 12, Type: "regular"}, s1: "s", s2: "i"}: 0, 69 | kernKey{font: font.Font{Name: "default", Size: 12, Type: "regular"}, s1: " ", s2: "s"}: 0, 70 | kernKey{font: font.Font{Name: "default", Size: 12, Type: "regular"}, s1: "s", s2: " "}: 0, 71 | kernKey{font: font.Font{Name: "default", Size: 12, Type: "regular"}, s1: "A", s2: "\\sigma"}: 0, 72 | kernKey{font: font.Font{Name: "default", Size: 12, Type: "regular"}, s1: "\\sigma", s2: "A"}: 0, 73 | kernKey{font: font.Font{Name: "default", Size: 12, Type: "regular"}, s1: "a", s2: "\\sigma"}: 0, 74 | kernKey{font: font.Font{Name: "default", Size: 12, Type: "regular"}, s1: "\\sigma", s2: "a"}: 0, 75 | kernKey{font: font.Font{Name: "default", Size: 12, Type: "regular"}, s1: "é", s2: "\\sigma"}: 0, 76 | kernKey{font: font.Font{Name: "default", Size: 12, Type: "regular"}, s1: "\\sigma", s2: "é"}: 0, 77 | kernKey{font: font.Font{Name: "default", Size: 12, Type: "regular"}, s1: " ", s2: "\\sigma"}: 0, 78 | kernKey{font: font.Font{Name: "default", Size: 12, Type: "regular"}, s1: "\\sigma", s2: " "}: 0, 79 | kernKey{font: font.Font{Name: "default", Size: 12, Type: "regular"}, s1: "\\sum", s2: "\\sigma"}: 0, 80 | kernKey{font: font.Font{Name: "default", Size: 12, Type: "regular"}, s1: "\\sigma", s2: "\\sum"}: 0, 81 | kernKey{font: font.Font{Name: "default", Size: 12, Type: "regular"}, s1: "1", s2: "."}: 0, 82 | kernKey{font: font.Font{Name: "default", Size: 12, Type: "regular"}, s1: ".", s2: "1"}: 0, 83 | kernKey{font: font.Font{Name: "default", Size: 12, Type: "regular"}, s1: "2", s2: "."}: 0, 84 | kernKey{font: font.Font{Name: "default", Size: 12, Type: "regular"}, s1: ".", s2: "2"}: -0.625, 85 | kernKey{font: font.Font{Name: "default", Size: 10, Type: "rm"}, s1: "A", s2: "V"}: 0, 86 | kernKey{font: font.Font{Name: "default", Size: 10, Type: "rm"}, s1: "V", s2: "A"}: 0, 87 | kernKey{font: font.Font{Name: "default", Size: 10, Type: "rm"}, s1: "A", s2: "é"}: 0, 88 | kernKey{font: font.Font{Name: "default", Size: 10, Type: "rm"}, s1: "é", s2: "A"}: 0, 89 | kernKey{font: font.Font{Name: "default", Size: 10, Type: "rm"}, s1: "V", s2: "é"}: 0, 90 | kernKey{font: font.Font{Name: "default", Size: 10, Type: "rm"}, s1: "é", s2: "V"}: 0, 91 | kernKey{font: font.Font{Name: "default", Size: 10, Type: "rm"}, s1: "h", s2: "e"}: 0, 92 | kernKey{font: font.Font{Name: "default", Size: 10, Type: "rm"}, s1: "e", s2: "h"}: 0, 93 | kernKey{font: font.Font{Name: "default", Size: 10, Type: "rm"}, s1: "l", s2: "e"}: 0, 94 | kernKey{font: font.Font{Name: "default", Size: 10, Type: "rm"}, s1: "e", s2: "l"}: 0, 95 | kernKey{font: font.Font{Name: "default", Size: 10, Type: "rm"}, s1: "l", s2: "l"}: 0, 96 | kernKey{font: font.Font{Name: "default", Size: 10, Type: "rm"}, s1: "l", s2: "l"}: 0, 97 | kernKey{font: font.Font{Name: "default", Size: 10, Type: "rm"}, s1: "l", s2: "o"}: 0, 98 | kernKey{font: font.Font{Name: "default", Size: 10, Type: "rm"}, s1: "o", s2: "l"}: 0, 99 | kernKey{font: font.Font{Name: "default", Size: 10, Type: "rm"}, s1: "é", s2: "é"}: 0, 100 | kernKey{font: font.Font{Name: "default", Size: 10, Type: "rm"}, s1: "é", s2: "é"}: 0, 101 | kernKey{font: font.Font{Name: "default", Size: 10, Type: "rm"}, s1: "f", s2: "i"}: 0, 102 | kernKey{font: font.Font{Name: "default", Size: 10, Type: "rm"}, s1: "i", s2: "f"}: 0, 103 | kernKey{font: font.Font{Name: "default", Size: 10, Type: "rm"}, s1: " ", s2: "i"}: 0, 104 | kernKey{font: font.Font{Name: "default", Size: 10, Type: "rm"}, s1: "i", s2: " "}: 0, 105 | kernKey{font: font.Font{Name: "default", Size: 10, Type: "rm"}, s1: "i", s2: "s"}: 0, 106 | kernKey{font: font.Font{Name: "default", Size: 10, Type: "rm"}, s1: "s", s2: "i"}: 0, 107 | kernKey{font: font.Font{Name: "default", Size: 10, Type: "rm"}, s1: " ", s2: "s"}: 0, 108 | kernKey{font: font.Font{Name: "default", Size: 10, Type: "rm"}, s1: "s", s2: " "}: 0, 109 | kernKey{font: font.Font{Name: "default", Size: 10, Type: "rm"}, s1: "A", s2: "\\sigma"}: 0, 110 | kernKey{font: font.Font{Name: "default", Size: 10, Type: "rm"}, s1: "\\sigma", s2: "A"}: 0, 111 | kernKey{font: font.Font{Name: "default", Size: 10, Type: "rm"}, s1: "a", s2: "\\sigma"}: 0, 112 | kernKey{font: font.Font{Name: "default", Size: 10, Type: "rm"}, s1: "\\sigma", s2: "a"}: 0, 113 | kernKey{font: font.Font{Name: "default", Size: 10, Type: "rm"}, s1: "é", s2: "\\sigma"}: 0, 114 | kernKey{font: font.Font{Name: "default", Size: 10, Type: "rm"}, s1: "\\sigma", s2: "é"}: 0, 115 | kernKey{font: font.Font{Name: "default", Size: 10, Type: "rm"}, s1: " ", s2: "\\sigma"}: 0, 116 | kernKey{font: font.Font{Name: "default", Size: 10, Type: "rm"}, s1: "\\sigma", s2: " "}: 0, 117 | kernKey{font: font.Font{Name: "default", Size: 10, Type: "rm"}, s1: "\\sum", s2: "\\sigma"}: 0, 118 | kernKey{font: font.Font{Name: "default", Size: 10, Type: "rm"}, s1: "\\sigma", s2: "\\sum"}: 0, 119 | kernKey{font: font.Font{Name: "default", Size: 10, Type: "rm"}, s1: "1", s2: "."}: 0, 120 | kernKey{font: font.Font{Name: "default", Size: 10, Type: "rm"}, s1: ".", s2: "1"}: 0, 121 | kernKey{font: font.Font{Name: "default", Size: 10, Type: "rm"}, s1: "2", s2: "."}: 0, 122 | kernKey{font: font.Font{Name: "default", Size: 10, Type: "rm"}, s1: ".", s2: "2"}: -0.5, 123 | kernKey{font: font.Font{Name: "default", Size: 12, Type: "rm"}, s1: "A", s2: "V"}: 0, 124 | kernKey{font: font.Font{Name: "default", Size: 12, Type: "rm"}, s1: "V", s2: "A"}: 0, 125 | kernKey{font: font.Font{Name: "default", Size: 12, Type: "rm"}, s1: "A", s2: "é"}: 0, 126 | kernKey{font: font.Font{Name: "default", Size: 12, Type: "rm"}, s1: "é", s2: "A"}: 0, 127 | kernKey{font: font.Font{Name: "default", Size: 12, Type: "rm"}, s1: "V", s2: "é"}: 0, 128 | kernKey{font: font.Font{Name: "default", Size: 12, Type: "rm"}, s1: "é", s2: "V"}: 0, 129 | kernKey{font: font.Font{Name: "default", Size: 12, Type: "rm"}, s1: "h", s2: "e"}: 0, 130 | kernKey{font: font.Font{Name: "default", Size: 12, Type: "rm"}, s1: "e", s2: "h"}: 0, 131 | kernKey{font: font.Font{Name: "default", Size: 12, Type: "rm"}, s1: "l", s2: "e"}: 0, 132 | kernKey{font: font.Font{Name: "default", Size: 12, Type: "rm"}, s1: "e", s2: "l"}: 0, 133 | kernKey{font: font.Font{Name: "default", Size: 12, Type: "rm"}, s1: "l", s2: "l"}: 0, 134 | kernKey{font: font.Font{Name: "default", Size: 12, Type: "rm"}, s1: "l", s2: "l"}: 0, 135 | kernKey{font: font.Font{Name: "default", Size: 12, Type: "rm"}, s1: "l", s2: "o"}: 0, 136 | kernKey{font: font.Font{Name: "default", Size: 12, Type: "rm"}, s1: "o", s2: "l"}: 0, 137 | kernKey{font: font.Font{Name: "default", Size: 12, Type: "rm"}, s1: "é", s2: "é"}: 0, 138 | kernKey{font: font.Font{Name: "default", Size: 12, Type: "rm"}, s1: "é", s2: "é"}: 0, 139 | kernKey{font: font.Font{Name: "default", Size: 12, Type: "rm"}, s1: "f", s2: "i"}: 0, 140 | kernKey{font: font.Font{Name: "default", Size: 12, Type: "rm"}, s1: "i", s2: "f"}: 0, 141 | kernKey{font: font.Font{Name: "default", Size: 12, Type: "rm"}, s1: " ", s2: "i"}: 0, 142 | kernKey{font: font.Font{Name: "default", Size: 12, Type: "rm"}, s1: "i", s2: " "}: 0, 143 | kernKey{font: font.Font{Name: "default", Size: 12, Type: "rm"}, s1: "i", s2: "s"}: 0, 144 | kernKey{font: font.Font{Name: "default", Size: 12, Type: "rm"}, s1: "s", s2: "i"}: 0, 145 | kernKey{font: font.Font{Name: "default", Size: 12, Type: "rm"}, s1: " ", s2: "s"}: 0, 146 | kernKey{font: font.Font{Name: "default", Size: 12, Type: "rm"}, s1: "s", s2: " "}: 0, 147 | kernKey{font: font.Font{Name: "default", Size: 12, Type: "rm"}, s1: "A", s2: "\\sigma"}: 0, 148 | kernKey{font: font.Font{Name: "default", Size: 12, Type: "rm"}, s1: "\\sigma", s2: "A"}: 0, 149 | kernKey{font: font.Font{Name: "default", Size: 12, Type: "rm"}, s1: "a", s2: "\\sigma"}: 0, 150 | kernKey{font: font.Font{Name: "default", Size: 12, Type: "rm"}, s1: "\\sigma", s2: "a"}: 0, 151 | kernKey{font: font.Font{Name: "default", Size: 12, Type: "rm"}, s1: "é", s2: "\\sigma"}: 0, 152 | kernKey{font: font.Font{Name: "default", Size: 12, Type: "rm"}, s1: "\\sigma", s2: "é"}: 0, 153 | kernKey{font: font.Font{Name: "default", Size: 12, Type: "rm"}, s1: " ", s2: "\\sigma"}: 0, 154 | kernKey{font: font.Font{Name: "default", Size: 12, Type: "rm"}, s1: "\\sigma", s2: " "}: 0, 155 | kernKey{font: font.Font{Name: "default", Size: 12, Type: "rm"}, s1: "\\sum", s2: "\\sigma"}: 0, 156 | kernKey{font: font.Font{Name: "default", Size: 12, Type: "rm"}, s1: "\\sigma", s2: "\\sum"}: 0, 157 | kernKey{font: font.Font{Name: "default", Size: 12, Type: "rm"}, s1: "1", s2: "."}: 0, 158 | kernKey{font: font.Font{Name: "default", Size: 12, Type: "rm"}, s1: ".", s2: "1"}: 0, 159 | kernKey{font: font.Font{Name: "default", Size: 12, Type: "rm"}, s1: "2", s2: "."}: 0, 160 | kernKey{font: font.Font{Name: "default", Size: 12, Type: "rm"}, s1: ".", s2: "2"}: -0.625, 161 | kernKey{font: font.Font{Name: "it", Size: 10, Type: "it"}, s1: "A", s2: "V"}: 0, 162 | kernKey{font: font.Font{Name: "it", Size: 10, Type: "it"}, s1: "V", s2: "A"}: 0, 163 | kernKey{font: font.Font{Name: "it", Size: 10, Type: "it"}, s1: "A", s2: "é"}: 0, 164 | kernKey{font: font.Font{Name: "it", Size: 10, Type: "it"}, s1: "é", s2: "A"}: 0, 165 | kernKey{font: font.Font{Name: "it", Size: 10, Type: "it"}, s1: "V", s2: "é"}: 0, 166 | kernKey{font: font.Font{Name: "it", Size: 10, Type: "it"}, s1: "é", s2: "V"}: 0, 167 | kernKey{font: font.Font{Name: "it", Size: 10, Type: "it"}, s1: "h", s2: "e"}: 0, 168 | kernKey{font: font.Font{Name: "it", Size: 10, Type: "it"}, s1: "e", s2: "h"}: 0, 169 | kernKey{font: font.Font{Name: "it", Size: 10, Type: "it"}, s1: "l", s2: "e"}: 0, 170 | kernKey{font: font.Font{Name: "it", Size: 10, Type: "it"}, s1: "e", s2: "l"}: 0, 171 | kernKey{font: font.Font{Name: "it", Size: 10, Type: "it"}, s1: "l", s2: "l"}: 0, 172 | kernKey{font: font.Font{Name: "it", Size: 10, Type: "it"}, s1: "l", s2: "l"}: 0, 173 | kernKey{font: font.Font{Name: "it", Size: 10, Type: "it"}, s1: "l", s2: "o"}: 0, 174 | kernKey{font: font.Font{Name: "it", Size: 10, Type: "it"}, s1: "o", s2: "l"}: 0, 175 | kernKey{font: font.Font{Name: "it", Size: 10, Type: "it"}, s1: "é", s2: "é"}: 0, 176 | kernKey{font: font.Font{Name: "it", Size: 10, Type: "it"}, s1: "é", s2: "é"}: 0, 177 | kernKey{font: font.Font{Name: "it", Size: 10, Type: "it"}, s1: "f", s2: "i"}: 0, 178 | kernKey{font: font.Font{Name: "it", Size: 10, Type: "it"}, s1: "i", s2: "f"}: 0, 179 | kernKey{font: font.Font{Name: "it", Size: 10, Type: "it"}, s1: " ", s2: "i"}: 0, 180 | kernKey{font: font.Font{Name: "it", Size: 10, Type: "it"}, s1: "i", s2: " "}: 0, 181 | kernKey{font: font.Font{Name: "it", Size: 10, Type: "it"}, s1: "i", s2: "s"}: 0, 182 | kernKey{font: font.Font{Name: "it", Size: 10, Type: "it"}, s1: "s", s2: "i"}: 0, 183 | kernKey{font: font.Font{Name: "it", Size: 10, Type: "it"}, s1: " ", s2: "s"}: 0, 184 | kernKey{font: font.Font{Name: "it", Size: 10, Type: "it"}, s1: "s", s2: " "}: 0, 185 | kernKey{font: font.Font{Name: "it", Size: 10, Type: "it"}, s1: "A", s2: "\\sigma"}: 0, 186 | kernKey{font: font.Font{Name: "it", Size: 10, Type: "it"}, s1: "\\sigma", s2: "A"}: 0, 187 | kernKey{font: font.Font{Name: "it", Size: 10, Type: "it"}, s1: "a", s2: "\\sigma"}: 0, 188 | kernKey{font: font.Font{Name: "it", Size: 10, Type: "it"}, s1: "\\sigma", s2: "a"}: 0, 189 | kernKey{font: font.Font{Name: "it", Size: 10, Type: "it"}, s1: "é", s2: "\\sigma"}: 0, 190 | kernKey{font: font.Font{Name: "it", Size: 10, Type: "it"}, s1: "\\sigma", s2: "é"}: 0, 191 | kernKey{font: font.Font{Name: "it", Size: 10, Type: "it"}, s1: " ", s2: "\\sigma"}: 0, 192 | kernKey{font: font.Font{Name: "it", Size: 10, Type: "it"}, s1: "\\sigma", s2: " "}: 0, 193 | kernKey{font: font.Font{Name: "it", Size: 10, Type: "it"}, s1: "\\sum", s2: "\\sigma"}: 0, 194 | kernKey{font: font.Font{Name: "it", Size: 10, Type: "it"}, s1: "\\sigma", s2: "\\sum"}: 0, 195 | kernKey{font: font.Font{Name: "it", Size: 10, Type: "it"}, s1: "1", s2: "."}: 0, 196 | kernKey{font: font.Font{Name: "it", Size: 10, Type: "it"}, s1: ".", s2: "1"}: 0, 197 | kernKey{font: font.Font{Name: "it", Size: 10, Type: "it"}, s1: "2", s2: "."}: 0, 198 | kernKey{font: font.Font{Name: "it", Size: 10, Type: "it"}, s1: ".", s2: "2"}: -0.5, 199 | kernKey{font: font.Font{Name: "it", Size: 12, Type: "it"}, s1: "A", s2: "V"}: 0, 200 | kernKey{font: font.Font{Name: "it", Size: 12, Type: "it"}, s1: "V", s2: "A"}: 0, 201 | kernKey{font: font.Font{Name: "it", Size: 12, Type: "it"}, s1: "A", s2: "é"}: 0, 202 | kernKey{font: font.Font{Name: "it", Size: 12, Type: "it"}, s1: "é", s2: "A"}: 0, 203 | kernKey{font: font.Font{Name: "it", Size: 12, Type: "it"}, s1: "V", s2: "é"}: 0, 204 | kernKey{font: font.Font{Name: "it", Size: 12, Type: "it"}, s1: "é", s2: "V"}: 0, 205 | kernKey{font: font.Font{Name: "it", Size: 12, Type: "it"}, s1: "h", s2: "e"}: 0, 206 | kernKey{font: font.Font{Name: "it", Size: 12, Type: "it"}, s1: "e", s2: "h"}: 0, 207 | kernKey{font: font.Font{Name: "it", Size: 12, Type: "it"}, s1: "l", s2: "e"}: 0, 208 | kernKey{font: font.Font{Name: "it", Size: 12, Type: "it"}, s1: "e", s2: "l"}: 0, 209 | kernKey{font: font.Font{Name: "it", Size: 12, Type: "it"}, s1: "l", s2: "l"}: 0, 210 | kernKey{font: font.Font{Name: "it", Size: 12, Type: "it"}, s1: "l", s2: "l"}: 0, 211 | kernKey{font: font.Font{Name: "it", Size: 12, Type: "it"}, s1: "l", s2: "o"}: 0, 212 | kernKey{font: font.Font{Name: "it", Size: 12, Type: "it"}, s1: "o", s2: "l"}: 0, 213 | kernKey{font: font.Font{Name: "it", Size: 12, Type: "it"}, s1: "é", s2: "é"}: 0, 214 | kernKey{font: font.Font{Name: "it", Size: 12, Type: "it"}, s1: "é", s2: "é"}: 0, 215 | kernKey{font: font.Font{Name: "it", Size: 12, Type: "it"}, s1: "f", s2: "i"}: 0, 216 | kernKey{font: font.Font{Name: "it", Size: 12, Type: "it"}, s1: "i", s2: "f"}: 0, 217 | kernKey{font: font.Font{Name: "it", Size: 12, Type: "it"}, s1: " ", s2: "i"}: 0, 218 | kernKey{font: font.Font{Name: "it", Size: 12, Type: "it"}, s1: "i", s2: " "}: 0, 219 | kernKey{font: font.Font{Name: "it", Size: 12, Type: "it"}, s1: "i", s2: "s"}: 0, 220 | kernKey{font: font.Font{Name: "it", Size: 12, Type: "it"}, s1: "s", s2: "i"}: 0, 221 | kernKey{font: font.Font{Name: "it", Size: 12, Type: "it"}, s1: " ", s2: "s"}: 0, 222 | kernKey{font: font.Font{Name: "it", Size: 12, Type: "it"}, s1: "s", s2: " "}: 0, 223 | kernKey{font: font.Font{Name: "it", Size: 12, Type: "it"}, s1: "A", s2: "\\sigma"}: 0, 224 | kernKey{font: font.Font{Name: "it", Size: 12, Type: "it"}, s1: "\\sigma", s2: "A"}: 0, 225 | kernKey{font: font.Font{Name: "it", Size: 12, Type: "it"}, s1: "a", s2: "\\sigma"}: 0, 226 | kernKey{font: font.Font{Name: "it", Size: 12, Type: "it"}, s1: "\\sigma", s2: "a"}: 0, 227 | kernKey{font: font.Font{Name: "it", Size: 12, Type: "it"}, s1: "é", s2: "\\sigma"}: 0, 228 | kernKey{font: font.Font{Name: "it", Size: 12, Type: "it"}, s1: "\\sigma", s2: "é"}: 0, 229 | kernKey{font: font.Font{Name: "it", Size: 12, Type: "it"}, s1: " ", s2: "\\sigma"}: 0, 230 | kernKey{font: font.Font{Name: "it", Size: 12, Type: "it"}, s1: "\\sigma", s2: " "}: 0, 231 | kernKey{font: font.Font{Name: "it", Size: 12, Type: "it"}, s1: "\\sum", s2: "\\sigma"}: 0, 232 | kernKey{font: font.Font{Name: "it", Size: 12, Type: "it"}, s1: "\\sigma", s2: "\\sum"}: 0, 233 | kernKey{font: font.Font{Name: "it", Size: 12, Type: "it"}, s1: "1", s2: "."}: 0, 234 | kernKey{font: font.Font{Name: "it", Size: 12, Type: "it"}, s1: ".", s2: "1"}: 0, 235 | kernKey{font: font.Font{Name: "it", Size: 12, Type: "it"}, s1: "2", s2: "."}: 0, 236 | kernKey{font: font.Font{Name: "it", Size: 12, Type: "it"}, s1: ".", s2: "2"}: -0.625, 237 | } 238 | } 239 | -------------------------------------------------------------------------------- /internal/fakebackend/fakebackend_test.go: -------------------------------------------------------------------------------- 1 | // Copyright ©2020 The go-latex Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package fakebackend 6 | 7 | import ( 8 | "testing" 9 | 10 | "github.com/go-latex/latex/font" 11 | ) 12 | 13 | func TestBackend(t *testing.T) { 14 | be := New() 15 | { 16 | fnt := font.Font{Name: "default", Size: 12, Type: "regular"} 17 | got := be.Metrics("A", fnt, 72, true) 18 | want := font.Metrics{Advance: 8.208984375, Height: 8.75, Width: 8.015625, XMin: 0.09375, XMax: 8.109375, YMin: 0, YMax: 8.75, Iceberg: 8.75, Slanted: false} 19 | if got != want { 20 | t.Fatalf("got=%#v\nwant=%#v", got, want) 21 | } 22 | } 23 | { 24 | fnt := font.Font{Name: "it", Size: 12, Type: "it"} 25 | got := be.Metrics("A", fnt, 72, true) 26 | want := font.Metrics{Advance: 8.208984375, Height: 8.75, Width: 8.015625, XMin: -0.640625, XMax: 7.390625, YMin: 0, YMax: 8.75, Iceberg: 8.75, Slanted: true} 27 | if got != want { 28 | t.Fatalf("got=%#v\nwant=%#v", got, want) 29 | } 30 | } 31 | { 32 | fnt := font.Font{Name: "default", Size: 12, Type: "regular"} 33 | got := be.Metrics(`\oint`, fnt, 72, true) 34 | want := font.Metrics{Advance: 8.8359375, Height: 16.453125, Width: 6.90625, XMin: 0.96875, XMax: 7.875, YMin: -3.609375, YMax: 12.84375, Iceberg: 12.84375, Slanted: true} 35 | if got != want { 36 | t.Fatalf("got=%#v\nwant=%#v", got, want) 37 | } 38 | } 39 | 40 | { 41 | fnt := font.Font{Name: "default", Size: 12, Type: "regular"} 42 | got := be.XHeight(fnt, 72) 43 | want := 6.5625 44 | if got != want { 45 | t.Fatalf("got=%#v\nwant=%#v", got, want) 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /internal/fakebackend/fakebackend_xheight_gen.go: -------------------------------------------------------------------------------- 1 | // Autogenerated. DO NOT EDIT. 2 | 3 | package fakebackend 4 | 5 | func init() { 6 | xhsDb = dbXHs{ 7 | xhKey{"default", 10, 72}: 5.46875, 8 | xhKey{"default", 12, 72}: 6.5625, 9 | xhKey{"regular", 10, 72}: 5.46875, 10 | xhKey{"regular", 12, 72}: 6.5625, 11 | xhKey{"rm", 10, 72}: 5.46875, 12 | xhKey{"rm", 12, 72}: 6.5625, 13 | xhKey{"it", 10, 72}: 5.46875, 14 | xhKey{"it", 12, 72}: 6.5625, 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /internal/fakebackend/gen-fakebackend.go: -------------------------------------------------------------------------------- 1 | // Copyright ©2020 The go-latex Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | //go:build ignore 6 | // +build ignore 7 | 8 | package main 9 | 10 | import ( 11 | "bytes" 12 | "encoding/json" 13 | "fmt" 14 | "log" 15 | "os" 16 | "os/exec" 17 | ) 18 | 19 | func main() { 20 | genFonts() 21 | genXHs() 22 | genKerns() 23 | } 24 | 25 | func genFonts() { 26 | buf := new(bytes.Buffer) 27 | cmd := exec.Command("python", "-c", pyFonts) 28 | cmd.Stdout = buf 29 | cmd.Stderr = os.Stderr 30 | 31 | err := cmd.Run() 32 | if err != nil { 33 | log.Fatalf("could not run python script: %+v", err) 34 | } 35 | 36 | var db []struct { 37 | FontName string `json:"font_name"` 38 | FontType string `json:"font_type"` 39 | Math bool `json:"math"` 40 | Size float64 `json:"size"` 41 | Symbol string `json:"symbol"` 42 | Metrics Metrics `json:"metrics"` 43 | } 44 | 45 | err = json.NewDecoder(buf).Decode(&db) 46 | if err != nil { 47 | log.Fatalf("could not decode json: %+v", err) 48 | } 49 | 50 | out, err := os.Create("fakebackend_fonts_gen.go") 51 | if err != nil { 52 | log.Fatal(err) 53 | } 54 | 55 | fmt.Fprintf(out, `// Autogenerated. DO NOT EDIT. 56 | 57 | package fakebackend 58 | 59 | import "github.com/go-latex/latex/font" 60 | 61 | func init() { 62 | fontsDb = dbFonts{ 63 | `) 64 | 65 | for _, v := range db { 66 | fmt.Fprintf( 67 | out, 68 | "fontKey{symbol: %q, math: %v, font: font.Font{Name:%q, Size:%v, Type:%q}}:", 69 | v.Symbol, v.Math, v.FontName, v.Size, v.FontType, 70 | ) 71 | 72 | fmt.Fprintf( 73 | out, 74 | "font.Metrics{Advance: %g, Height: %g, Width: %g, XMin: %g, XMax: %g, YMin: %g, YMax: %g, Iceberg: %g, Slanted: %v},\n", 75 | v.Metrics.Advance, 76 | v.Metrics.Height, 77 | v.Metrics.Width, 78 | v.Metrics.XMin, 79 | v.Metrics.XMax, 80 | v.Metrics.YMin, 81 | v.Metrics.YMax, 82 | v.Metrics.Iceberg, 83 | v.Metrics.Slanted, 84 | ) 85 | } 86 | 87 | fmt.Fprintf(out, "\t}\n}\n") 88 | 89 | err = out.Close() 90 | if err != nil { 91 | log.Fatal(err) 92 | } 93 | } 94 | 95 | type Metrics struct { 96 | Advance float64 `json:"advance"` 97 | Height float64 `json:"height"` 98 | Width float64 `json:"width"` 99 | XMin float64 `json:"xmin"` 100 | XMax float64 `json:"xmax"` 101 | YMin float64 `json:"ymin"` 102 | YMax float64 `json:"ymax"` 103 | Iceberg float64 `json:"iceberg"` 104 | Slanted bool `json:"slanted"` 105 | } 106 | 107 | const pyFonts = ` 108 | import sys 109 | import string 110 | import json 111 | import matplotlib.mathtext as mtex 112 | 113 | dejavu = mtex.DejaVuSansFonts(mtex.FontProperties(), mtex.MathtextBackendPdf()) 114 | 115 | math = [True, False] 116 | sizes = [10,12] 117 | fonts = [ 118 | ("default", "regular"), 119 | ("default", "rm"), 120 | ("default", "it"), 121 | ("rm", "rm"), 122 | ("it", "it") 123 | ] 124 | symbols = list(string.ascii_letters) + list(string.digits) + \ 125 | ["\\"+k for k in mtex.tex2uni.keys()] + [ 126 | "é", 127 | " ", "=", "+", "-", "(", ")", "{", "}", "<", ">", "." 128 | ] 129 | 130 | db = [] 131 | for math in [True, False]: 132 | for font in fonts: 133 | fontName = font[0] 134 | fontClass = font[1] 135 | for size in sizes: 136 | for sym in symbols: 137 | m = dejavu.get_metrics(fontName, fontClass, sym, size, 72, math) 138 | db.append({ 139 | 'font_name': font[0], 'font_type': font[1], 140 | 'math':math, 'size': size, 141 | 'symbol': sym, 142 | 'metrics': { 143 | 'advance': m.advance, 144 | 'height': m.height, 145 | 'width': m.width, 146 | 'xmin': m.xmin, 147 | 'xmax': m.xmax, 148 | 'ymin': m.ymin, 149 | 'ymax': m.ymax, 150 | 'iceberg': m.iceberg, 151 | 'slanted': m.slanted, 152 | }, 153 | }) 154 | #print(f"sym: {sym}=> {m}") 155 | 156 | ## extra sizes for \binom, \sqrt 157 | for math in [True, False]: 158 | for font in fonts: 159 | fontName = font[0] 160 | fontClass = font[1] 161 | for size in [12.840350877192982, 12.68796992481203]: 162 | for sym in ["(",")", r"\__sqrt__"]: 163 | m = dejavu.get_metrics(fontName, fontClass, sym, size, 72, math) 164 | db.append({ 165 | 'font_name': font[0], 'font_type': font[1], 166 | 'math':math, 'size': size, 167 | 'symbol': sym, 168 | 'metrics': { 169 | 'advance': m.advance, 170 | 'height': m.height, 171 | 'width': m.width, 172 | 'xmin': m.xmin, 173 | 'xmax': m.xmax, 174 | 'ymin': m.ymin, 175 | 'ymax': m.ymax, 176 | 'iceberg': m.iceberg, 177 | 'slanted': m.slanted, 178 | }, 179 | }) 180 | #print(f"sym: {sym}=> {m}") 181 | 182 | with open("testdata/metrics-dejavu-sans.json", "w") as f: 183 | json.dump(db, f, indent=' ') 184 | pass 185 | json.dump(db, sys.stdout) 186 | sys.stdout.flush() 187 | ` 188 | 189 | func genXHs() { 190 | buf := new(bytes.Buffer) 191 | cmd := exec.Command("python", "-c", pyXH) 192 | cmd.Stdout = buf 193 | 194 | err := cmd.Run() 195 | if err != nil { 196 | log.Fatalf("could not run python script: %+v", err) 197 | } 198 | 199 | var db []struct { 200 | FontName string `json:"font_name"` 201 | Size float64 `json:"size"` 202 | DPI float64 `json:"dpi"` 203 | XHeight float64 `json:"xheight"` 204 | } 205 | 206 | err = json.NewDecoder(buf).Decode(&db) 207 | if err != nil { 208 | log.Fatalf("could not decode json: %+v", err) 209 | } 210 | 211 | out, err := os.Create("fakebackend_xheight_gen.go") 212 | if err != nil { 213 | log.Fatal(err) 214 | } 215 | 216 | fmt.Fprintf(out, `// Autogenerated. DO NOT EDIT. 217 | 218 | package fakebackend 219 | 220 | func init() { 221 | xhsDb = dbXHs{ 222 | `) 223 | 224 | for _, v := range db { 225 | fmt.Fprintf( 226 | out, 227 | "\t\txhKey{%q, %v, %v}: %g,\n", 228 | v.FontName, v.Size, v.DPI, v.XHeight, 229 | ) 230 | } 231 | 232 | fmt.Fprintf(out, "\t}\n}\n") 233 | 234 | err = out.Close() 235 | if err != nil { 236 | log.Fatal(err) 237 | } 238 | } 239 | 240 | const pyXH = ` 241 | import sys 242 | import string 243 | import json 244 | import matplotlib.mathtext as mtex 245 | 246 | dejavu = mtex.DejaVuSansFonts(mtex.FontProperties(), mtex.MathtextBackendPdf()) 247 | 248 | dpi = 72 249 | sizes = [10, 12] 250 | fonts = ["default", "regular", "rm", "it"] 251 | 252 | db = [] 253 | for font in fonts: 254 | for size in sizes: 255 | xh = dejavu.get_xheight(font, size, dpi) 256 | db.append({ 257 | 'font_name': font, 'size': size, 'dpi': dpi, 'xheight': xh, 258 | }) 259 | 260 | json.dump(db, sys.stdout) 261 | sys.stdout.flush() 262 | ` 263 | 264 | func genKerns() { 265 | buf := new(bytes.Buffer) 266 | cmd := exec.Command("python", "-c", pyKerns) 267 | cmd.Stdout = buf 268 | 269 | err := cmd.Run() 270 | if err != nil { 271 | log.Fatalf("could not run python script: %+v", err) 272 | } 273 | 274 | var db []struct { 275 | FontName string `json:"font_name"` 276 | FontType string `json:"font_type"` 277 | Size float64 `json:"size"` 278 | Symbol1 string `json:"sym1"` 279 | Symbol2 string `json:"sym2"` 280 | Kern float64 `json:"kern"` 281 | } 282 | 283 | err = json.NewDecoder(buf).Decode(&db) 284 | if err != nil { 285 | log.Fatalf("could not decode json: %+v", err) 286 | } 287 | 288 | out, err := os.Create("fakebackend_kerns_gen.go") 289 | if err != nil { 290 | log.Fatal(err) 291 | } 292 | 293 | fmt.Fprintf(out, `// Autogenerated. DO NOT EDIT. 294 | 295 | package fakebackend 296 | 297 | import "github.com/go-latex/latex/font" 298 | 299 | func init() { 300 | kernsDb = dbKerns{ 301 | `) 302 | 303 | for _, v := range db { 304 | fmt.Fprintf( 305 | out, 306 | "\t\tkernKey{font: font.Font{Name:%q, Size:%v, Type:%q}, s1: %q, s2: %q}: %g,\n", 307 | v.FontName, v.Size, v.FontType, v.Symbol1, v.Symbol2, v.Kern, 308 | ) 309 | } 310 | 311 | fmt.Fprintf(out, "\t}\n}\n") 312 | 313 | err = out.Close() 314 | if err != nil { 315 | log.Fatal(err) 316 | } 317 | } 318 | 319 | const pyKerns = ` 320 | import sys 321 | import string 322 | import json 323 | import matplotlib.mathtext as mtex 324 | 325 | dejavu = mtex.DejaVuSansFonts(mtex.FontProperties(), mtex.MathtextBackendPdf()) 326 | 327 | dpi = 72 328 | sizes = [10,12] 329 | fonts = [ 330 | ("default", "regular"), 331 | ("default", "rm"), 332 | ("it", "it") 333 | ] 334 | symbols = [ 335 | ("A", "V"), 336 | ("A", "é"), 337 | ("V", "é"), 338 | ("h", "e"), 339 | ("l", "e"), 340 | ("l", "l"), 341 | ("l", "o"), 342 | ("é", "é"), 343 | ("f", "i"), 344 | (" ", "i"), 345 | ("i", "s"), 346 | (" ", "s"), 347 | ("A", r"\sigma"), 348 | ("a", r"\sigma"), 349 | ("é", r"\sigma"), 350 | (" ", r"\sigma"), 351 | (r"\sum", r"\sigma"), 352 | ("1", "."), 353 | ("2", "."), 354 | ] 355 | 356 | db = [] 357 | for font in fonts: 358 | fontName = font[0] 359 | fontClass = font[1] 360 | for size in sizes: 361 | for sym in symbols: 362 | kern = dejavu.get_kern(fontName, fontClass, sym[0], size, fontName, fontClass, sym[1], size, dpi) 363 | db.append({ 364 | 'font_name': font[0], 'font_type': font[1], 'size': size, 365 | 'sym1': sym[0], 366 | 'sym2': sym[1], 367 | 'kern': kern, 368 | }) 369 | 370 | kern = dejavu.get_kern(fontName, fontClass, sym[1], size, fontName, fontClass, sym[0], size, dpi) 371 | db.append({ 372 | 'font_name': font[0], 'font_type': font[1], 'size': size, 373 | 'sym1': sym[1], 374 | 'sym2': sym[0], 375 | 'kern': kern, 376 | }) 377 | 378 | with open("testdata/kerns-dejavu-sans.json", "w") as f: 379 | json.dump(db, f, indent=' ') 380 | pass 381 | json.dump(db, sys.stdout) 382 | sys.stdout.flush() 383 | ` 384 | -------------------------------------------------------------------------------- /internal/tex2unicode/utf8_test.go: -------------------------------------------------------------------------------- 1 | // Copyright ©2020 The go-latex Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package tex2unicode 6 | 7 | import "testing" 8 | 9 | func TestIndex(t *testing.T) { 10 | for _, tc := range []struct { 11 | v string 12 | want rune 13 | math bool 14 | }{ 15 | {v: `a`, want: 'a'}, 16 | {v: `a`, want: 'a', math: true}, 17 | {v: `t`, want: 't'}, 18 | {v: `t`, want: 't', math: true}, 19 | {v: `A`, want: 'A'}, 20 | {v: `A`, want: 'A', math: true}, 21 | {v: `T`, want: 'T'}, 22 | {v: `T`, want: 'T', math: true}, 23 | {v: `0`, want: '0'}, 24 | {v: `0`, want: '0', math: true}, 25 | {v: `-`, want: '-'}, 26 | {v: `-`, want: '−', math: true}, 27 | {v: `\alpha`, want: 'α', math: true}, 28 | {v: `\t`, want: 865, math: true}, 29 | {v: `\aleph`, want: 'ℵ', math: true}, 30 | {v: `\flat`, want: '♭', math: true}, 31 | {v: `\Join`, want: '⨝', math: true}, 32 | {v: `\perp`, want: '⟂', math: true}, 33 | {v: `\pm`, want: '±', math: true}, 34 | {v: `\mp`, want: '∓', math: true}, 35 | {v: `\neq`, want: '≠', math: true}, 36 | {v: `\__sqrt__`, want: '√', math: true}, 37 | {v: `\partial`, want: '∂', math: true}, 38 | {v: `\hbar`, want: 'ħ', math: true}, 39 | {v: `\hslash`, want: 'ℏ', math: true}, 40 | {v: `\int`, want: '∫', math: true}, 41 | {v: `\oint`, want: '∮', math: true}, 42 | {v: `\oiint`, want: '∯', math: true}, 43 | {v: `\infty`, want: '∞', math: true}, 44 | {v: `\sigma`, want: 'σ', math: true}, 45 | {v: `\varsigma`, want: 'ς', math: true}, 46 | {v: `\Sigma`, want: 'Σ', math: true}, 47 | {v: `\sum`, want: '∑', math: true}, 48 | {v: `\Pi`, want: 'Π', math: true}, 49 | {v: `\pi`, want: 'π', math: true}, 50 | {v: `\nabla`, want: '∇', math: true}, 51 | {v: `\varepsilon`, want: 'ε', math: true}, 52 | {v: `\l`, want: 'ł', math: true}, 53 | {v: `\L`, want: 'Ł', math: true}, 54 | {v: `\ast`, want: '∗', math: true}, 55 | } { 56 | t.Run(tc.v, func(t *testing.T) { 57 | got := Index(tc.v, tc.math) 58 | if got != tc.want { 59 | t.Fatalf("error: got=%q, want=%q", got, tc.want) 60 | } 61 | }) 62 | } 63 | } 64 | 65 | func TestHasSymbol(t *testing.T) { 66 | for _, tc := range []struct { 67 | symbol string 68 | want bool 69 | }{ 70 | {`alpha`, true}, 71 | {`\alpha`, false}, 72 | } { 73 | t.Run(tc.symbol, func(t *testing.T) { 74 | got := HasSymbol(tc.symbol) 75 | if got != tc.want { 76 | t.Fatalf("got=%v, want=%v", got, tc.want) 77 | } 78 | }) 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /latex.go: -------------------------------------------------------------------------------- 1 | // Copyright ©2020 The go-latex Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | // Package latex provides types and functions to work with LaTeX. 6 | package latex // import "github.com/go-latex/latex" 7 | -------------------------------------------------------------------------------- /macros.go: -------------------------------------------------------------------------------- 1 | // Copyright ©2020 The go-latex Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package latex 6 | 7 | import ( 8 | "strings" 9 | 10 | "github.com/go-latex/latex/ast" 11 | "github.com/go-latex/latex/internal/tex2unicode" 12 | ) 13 | 14 | type macroParser interface { 15 | parseMacro(p *parser) ast.Node 16 | } 17 | 18 | func (p *parser) addBuiltinMacros() { 19 | p.macros = map[string]macroParser{ 20 | // binary operators 21 | `\amalg`: builtinMacro(""), 22 | `\ast`: builtinMacro(""), 23 | `\bigcirc`: builtinMacro(""), 24 | `\bigtriangledown`: builtinMacro(""), 25 | `\bigtriangleup`: builtinMacro(""), 26 | `\bullet`: builtinMacro(""), 27 | `\cdot`: builtinMacro(""), 28 | `\circ`: builtinMacro(""), 29 | `\cap`: builtinMacro(""), 30 | `\cup`: builtinMacro(""), 31 | `\dagger`: builtinMacro(""), 32 | `\ddagger`: builtinMacro(""), 33 | `\diamond`: builtinMacro(""), 34 | `\div`: builtinMacro(""), 35 | `\lhd`: builtinMacro(""), 36 | `\mp`: builtinMacro(""), 37 | `\odot`: builtinMacro(""), 38 | `\ominus`: builtinMacro(""), 39 | `\oplus`: builtinMacro(""), 40 | `\oslash`: builtinMacro(""), 41 | `\otimes`: builtinMacro(""), 42 | `\pm`: builtinMacro(""), 43 | `\rhd`: builtinMacro(""), 44 | `\setminus`: builtinMacro(""), 45 | `\sqcap`: builtinMacro(""), 46 | `\sqcup`: builtinMacro(""), 47 | `\star`: builtinMacro(""), 48 | `\times`: builtinMacro(""), 49 | `\triangleleft`: builtinMacro(""), 50 | `\triangleright`: builtinMacro(""), 51 | `\uplus`: builtinMacro(""), 52 | `\unlhd`: builtinMacro(""), 53 | `\unrhd`: builtinMacro(""), 54 | `\vee`: builtinMacro(""), 55 | `\wedge`: builtinMacro(""), 56 | `\wr`: builtinMacro(""), 57 | 58 | // arithmetic operators 59 | `\binom`: builtinMacro("AA"), 60 | `\dfrac`: builtinMacro("AA"), 61 | `\frac`: builtinMacro("AA"), 62 | `\stackrel`: builtinMacro("AA"), 63 | `\tfrac`: builtinMacro("AA"), 64 | `\genfrac`: nil, // FIXME(sbinet) 65 | 66 | // relation symbols 67 | `\approx`: builtinMacro(""), 68 | `\asymp`: builtinMacro(""), 69 | `\bowtie`: builtinMacro(""), 70 | `\cong`: builtinMacro(""), 71 | `\dashv`: builtinMacro(""), 72 | `\doteq`: builtinMacro(""), 73 | `\doteqdot`: builtinMacro(""), 74 | `\dotplus`: builtinMacro(""), 75 | `\dots`: builtinMacro(""), 76 | `\equiv`: builtinMacro(""), 77 | `\frown`: builtinMacro(""), 78 | `\geq`: builtinMacro(""), 79 | `\gg`: builtinMacro(""), 80 | `\in`: builtinMacro(""), 81 | `\leq`: builtinMacro(""), 82 | `\ll`: builtinMacro(""), 83 | `\mid`: builtinMacro(""), 84 | `\models`: builtinMacro(""), 85 | `\neq`: builtinMacro(""), 86 | `\ni`: builtinMacro(""), 87 | `\parallel`: builtinMacro(""), 88 | `\perp`: builtinMacro(""), 89 | `\prec`: builtinMacro(""), 90 | `\preceq`: builtinMacro(""), 91 | `\propto`: builtinMacro(""), 92 | `\sim`: builtinMacro(""), 93 | `\simeq`: builtinMacro(""), 94 | `\smile`: builtinMacro(""), 95 | `\sqsubset`: builtinMacro(""), 96 | `\sqsubseteq`: builtinMacro(""), 97 | `\sqsupset`: builtinMacro(""), 98 | `\sqsupseteq`: builtinMacro(""), 99 | `\subset`: builtinMacro(""), 100 | `\subseteq`: builtinMacro(""), 101 | `\succ`: builtinMacro(""), 102 | `\succeq`: builtinMacro(""), 103 | `\supset`: builtinMacro(""), 104 | `\supseteq`: builtinMacro(""), 105 | `\vdash`: builtinMacro(""), 106 | `\Join`: builtinMacro(""), 107 | 108 | // arrow symbols 109 | `\downarrow`: builtinMacro(""), 110 | `\hookleftarrow`: builtinMacro(""), 111 | `\hookrightarrow`: builtinMacro(""), 112 | `\leadsto`: builtinMacro(""), 113 | `\leftarrow`: builtinMacro(""), 114 | `\leftharpoondown`: builtinMacro(""), 115 | `\leftharpoonup`: builtinMacro(""), 116 | `\leftrightarrow`: builtinMacro(""), 117 | `\longleftarrow`: builtinMacro(""), 118 | `\longleftrightarrow`: builtinMacro(""), 119 | `\longmapsto`: builtinMacro(""), 120 | `\longrightarrow`: builtinMacro(""), 121 | `\rightarrow`: builtinMacro(""), 122 | `\mapsto`: builtinMacro(""), 123 | `\nearrow`: builtinMacro(""), 124 | `\nwarrow`: builtinMacro(""), 125 | `\rightharpoondown`: builtinMacro(""), 126 | `\rightharpoonup`: builtinMacro(""), 127 | `\rightleftharpoons`: builtinMacro(""), 128 | `\searrow`: builtinMacro(""), 129 | `\swarrow`: builtinMacro(""), 130 | `\uparrow`: builtinMacro(""), 131 | `\updownarrow`: builtinMacro(""), 132 | `\Downarrow`: builtinMacro(""), 133 | `\Leftarrow`: builtinMacro(""), 134 | `\Leftrightarrow`: builtinMacro(""), 135 | `\Longleftarrow`: builtinMacro(""), 136 | `\Longleftrightarrow`: builtinMacro(""), 137 | `\Longrightarrow`: builtinMacro(""), 138 | `\Rightarrow`: builtinMacro(""), 139 | `\Uparrow`: builtinMacro(""), 140 | `\Updownarrow`: builtinMacro(""), 141 | 142 | // punctuation symbols 143 | `\ldotp`: builtinMacro(""), 144 | `\cdotp`: builtinMacro(""), 145 | 146 | // over-under symbols 147 | `\bigcap`: builtinMacro(""), 148 | `\bigcup`: builtinMacro(""), 149 | `\bigodot`: builtinMacro(""), 150 | `\bigoplus`: builtinMacro(""), 151 | `\bigotimes`: builtinMacro(""), 152 | `\bigsqcup`: builtinMacro(""), 153 | `\biguplus`: builtinMacro(""), 154 | `\bigvee`: builtinMacro(""), 155 | `\bigwedge`: builtinMacro(""), 156 | `\coprod`: builtinMacro(""), 157 | `\prod`: builtinMacro(""), 158 | `\sum`: builtinMacro(""), 159 | 160 | // over-under functions 161 | `\lim`: builtinMacro(""), 162 | `\liminf`: builtinMacro(""), 163 | `\limsup`: builtinMacro(""), 164 | `\max`: builtinMacro(""), 165 | `\min`: builtinMacro(""), 166 | `\sup`: builtinMacro(""), 167 | 168 | // dropsub symbols 169 | `\int`: builtinMacro(""), 170 | `\oint`: builtinMacro(""), 171 | 172 | // font names 173 | `\rm`: builtinMacro(""), 174 | `\cal`: builtinMacro(""), 175 | `\it`: builtinMacro(""), 176 | `\tt`: builtinMacro(""), 177 | `\sf`: builtinMacro(""), 178 | `\bf`: builtinMacro(""), 179 | `\default`: builtinMacro(""), 180 | `\bb`: builtinMacro(""), 181 | `\frak`: builtinMacro(""), 182 | `\scr`: builtinMacro(""), 183 | `\regular`: builtinMacro(""), 184 | 185 | // function names 186 | `\arccos`: builtinMacro(""), 187 | `\arcsin`: builtinMacro(""), 188 | `\arctan`: builtinMacro(""), 189 | `\arg`: builtinMacro(""), 190 | `\cos`: builtinMacro(""), 191 | `\cosh`: builtinMacro(""), 192 | `\cot`: builtinMacro(""), 193 | `\coth`: builtinMacro(""), 194 | `\csc`: builtinMacro(""), 195 | `\deg`: builtinMacro(""), 196 | `\det`: builtinMacro(""), 197 | `\dim`: builtinMacro(""), 198 | `\exp`: builtinMacro("A"), 199 | `\gcd`: builtinMacro(""), 200 | `\hom`: builtinMacro(""), 201 | `\inf`: builtinMacro(""), 202 | `\ker`: builtinMacro(""), 203 | `\lg`: builtinMacro(""), 204 | `\ln`: builtinMacro(""), 205 | `\log`: builtinMacro(""), 206 | `\sec`: builtinMacro(""), 207 | `\sin`: builtinMacro(""), 208 | `\sinh`: builtinMacro(""), 209 | `\sqrt`: builtinMacro("OA"), 210 | `\tan`: builtinMacro(""), 211 | `\tanh`: builtinMacro(""), 212 | `\Pr`: builtinMacro(""), 213 | 214 | // ambi delim 215 | `\backslash`: builtinMacro(""), 216 | `\vert`: builtinMacro(""), 217 | `\Vert`: builtinMacro(""), 218 | 219 | // left delim 220 | `\{`: builtinMacro(""), 221 | `\(`: builtinMacro(""), 222 | `\langle`: builtinMacro(""), 223 | `\lceil`: builtinMacro(""), 224 | `\lfloor`: builtinMacro(""), 225 | 226 | // right delim 227 | `\}`: builtinMacro(""), 228 | `\)`: builtinMacro(""), 229 | `\rangle`: builtinMacro(""), 230 | `\rceil`: builtinMacro(""), 231 | `\rfloor`: builtinMacro(""), 232 | 233 | // symbols 234 | `\alpha`: builtinMacro(""), 235 | `\beta`: builtinMacro(""), 236 | `\gamma`: builtinMacro(""), 237 | `\delta`: builtinMacro(""), 238 | `\iota`: builtinMacro(""), 239 | `\epsilon`: builtinMacro(""), 240 | `\eta`: builtinMacro(""), 241 | `\kappa`: builtinMacro(""), 242 | `\lambda`: builtinMacro(""), 243 | `\mu`: builtinMacro(""), 244 | `\nu`: builtinMacro(""), 245 | `\omicron`: builtinMacro(""), 246 | `\pi`: builtinMacro(""), 247 | `\theta`: builtinMacro(""), 248 | `\xi`: builtinMacro(""), 249 | `\rho`: builtinMacro(""), 250 | `\sigma`: builtinMacro(""), 251 | `\tau`: builtinMacro(""), 252 | `\upsilon`: builtinMacro(""), 253 | `\phi`: builtinMacro(""), 254 | `\chi`: builtinMacro(""), 255 | `\psi`: builtinMacro(""), 256 | `\omega`: builtinMacro(""), 257 | `\zeta`: builtinMacro(""), 258 | `\Alpha`: builtinMacro(""), 259 | `\Beta`: builtinMacro(""), 260 | `\Gamma`: builtinMacro(""), 261 | `\Delta`: builtinMacro(""), 262 | `\Epsilon`: builtinMacro(""), 263 | `\Zeta`: builtinMacro(""), 264 | `\Eta`: builtinMacro(""), 265 | `\Theta`: builtinMacro(""), 266 | `\Iota`: builtinMacro(""), 267 | `\Kappa`: builtinMacro(""), 268 | `\Lambda`: builtinMacro(""), 269 | `\Mu`: builtinMacro(""), 270 | `\Nu`: builtinMacro(""), 271 | `\Xi`: builtinMacro(""), 272 | `\Omicron`: builtinMacro(""), 273 | `\Pi`: builtinMacro(""), 274 | `\Rho`: builtinMacro(""), 275 | `\Sigma`: builtinMacro(""), 276 | `\Tau`: builtinMacro(""), 277 | `\Upsilon`: builtinMacro(""), 278 | `\Phi`: builtinMacro(""), 279 | `\Chi`: builtinMacro(""), 280 | `\Psi`: builtinMacro(""), 281 | `\Omega`: builtinMacro(""), 282 | `\hbar`: builtinMacro(""), 283 | `\nabla`: builtinMacro(""), 284 | 285 | // math font 286 | `\mathbf`: builtinMacro("A"), 287 | `\mathit`: builtinMacro("A"), 288 | `\mathsf`: builtinMacro("A"), 289 | `\mathtt`: builtinMacro("A"), 290 | `\mathcal`: builtinMacro("A"), 291 | `\mathdefault`: builtinMacro("A"), 292 | `\mathbb`: builtinMacro("A"), 293 | `\mathfrak`: builtinMacro("A"), 294 | `\mathscr`: builtinMacro("A"), 295 | `\mathregular`: builtinMacro("A"), 296 | 297 | // text 298 | `\textbf`: builtinMacro("A"), 299 | `\textit`: builtinMacro("A"), 300 | `\textsf`: builtinMacro("A"), 301 | `\texttt`: builtinMacro("A"), 302 | `\textcal`: builtinMacro("A"), 303 | `\textdefault`: builtinMacro("A"), 304 | `\textbb`: builtinMacro("A"), 305 | `\textfrak`: builtinMacro("A"), 306 | `\textscr`: builtinMacro("A"), 307 | `\textregular`: builtinMacro("A"), 308 | 309 | // space, symbols 310 | `\ `: builtinMacro(""), 311 | `\,`: builtinMacro(""), 312 | `\;`: builtinMacro(""), 313 | `\!`: builtinMacro(""), 314 | `\quad`: builtinMacro(""), 315 | `\qquad`: builtinMacro(""), 316 | `\:`: builtinMacro(""), 317 | `\cdots`: builtinMacro(""), 318 | `\ddots`: builtinMacro(""), 319 | `\ldots`: builtinMacro(""), 320 | `\vdots`: builtinMacro(""), 321 | `\hspace`: builtinMacro("A"), 322 | 323 | // catch-all 324 | // 325 | `\overline`: builtinMacro("A"), 326 | `\operatorname`: builtinMacro("A"), 327 | } 328 | 329 | // add all known UTF-8 symbols 330 | for _, k := range tex2unicode.Symbols() { 331 | _, ok := p.macros[`\`+k] 332 | if ok { 333 | continue 334 | } 335 | p.macros[`\`+k] = builtinMacro("") 336 | } 337 | } 338 | 339 | type builtinMacro string 340 | 341 | func (m builtinMacro) parseMacro(p *parser) ast.Node { 342 | node := &ast.Macro{ 343 | Name: &ast.Ident{ 344 | NamePos: p.s.tok.Pos, 345 | Name: p.s.tok.Text, 346 | }, 347 | } 348 | 349 | for _, typ := range strings.ToLower(string(m)) { 350 | switch typ { 351 | case 'a': 352 | p.parseMacroArg(node) 353 | case 'o': 354 | p.parseOptMacroArg(node) 355 | case 'v': 356 | p.parseVerbatimMacroArg(node) 357 | } 358 | } 359 | 360 | return node 361 | } 362 | -------------------------------------------------------------------------------- /mtex/README.md: -------------------------------------------------------------------------------- 1 | # mtex 2 | 3 | `mtex` provides a Go implementation of a naive LaTeX-like math expression parser and renderer. 4 | 5 | ## Example 6 | 7 | ``` 8 | $> mtex-render -font-size=48 -dpi=100 "$\sum\sqrt{\frac{a+b}{2\pi}}\cos\omega\binom{a+b}{\beta}\prod \alpha x\int\frac{\partial x}{x}\hbar$" 9 | ``` 10 | 11 | ![mtex-example](https://github.com/go-latex/latex/raw/master/mtex/testdata/mtex-example.png) 12 | -------------------------------------------------------------------------------- /mtex/macros.go: -------------------------------------------------------------------------------- 1 | // Copyright ©2020 The go-latex Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package mtex 6 | 7 | import ( 8 | "strings" 9 | 10 | "github.com/go-latex/latex/ast" 11 | "github.com/go-latex/latex/tex" 12 | ) 13 | 14 | type handlerFunc func(p *parser, node ast.Node, state tex.State, math bool) tex.Node 15 | 16 | func (h handlerFunc) Handle(p *parser, node ast.Node, state tex.State, math bool) tex.Node { 17 | return h(p, node, state, math) 18 | } 19 | 20 | type handler interface { 21 | Handle(p *parser, node ast.Node, state tex.State, math bool) tex.Node 22 | } 23 | 24 | var ( 25 | builtinMacros = map[string]handler{ 26 | // binary operators 27 | `\amalg`: builtinMacro(""), 28 | `\ast`: builtinMacro(""), 29 | `\bigcirc`: builtinMacro(""), 30 | `\bigtriangledown`: builtinMacro(""), 31 | `\bigtriangleup`: builtinMacro(""), 32 | `\bullet`: builtinMacro(""), 33 | `\cdot`: builtinMacro(""), 34 | `\circ`: builtinMacro(""), 35 | `\cap`: builtinMacro(""), 36 | `\cup`: builtinMacro(""), 37 | `\dagger`: builtinMacro(""), 38 | `\ddagger`: builtinMacro(""), 39 | `\diamond`: builtinMacro(""), 40 | `\div`: builtinMacro(""), 41 | `\lhd`: builtinMacro(""), 42 | `\mp`: builtinMacro(""), 43 | `\odot`: builtinMacro(""), 44 | `\ominus`: builtinMacro(""), 45 | `\oplus`: builtinMacro(""), 46 | `\oslash`: builtinMacro(""), 47 | `\otimes`: builtinMacro(""), 48 | `\pm`: builtinMacro(""), 49 | `\rhd`: builtinMacro(""), 50 | `\setminus`: builtinMacro(""), 51 | `\sqcap`: builtinMacro(""), 52 | `\sqcup`: builtinMacro(""), 53 | `\star`: builtinMacro(""), 54 | `\times`: builtinMacro(""), 55 | `\triangleleft`: builtinMacro(""), 56 | `\triangleright`: builtinMacro(""), 57 | `\uplus`: builtinMacro(""), 58 | `\unlhd`: builtinMacro(""), 59 | `\unrhd`: builtinMacro(""), 60 | `\vee`: builtinMacro(""), 61 | `\wedge`: builtinMacro(""), 62 | `\wr`: builtinMacro(""), 63 | 64 | // arithmetic operators 65 | `\binom`: builtinMacro("AA"), 66 | `\dfrac`: builtinMacro("AA"), 67 | `\frac`: builtinMacro("AA"), 68 | `\stackrel`: builtinMacro("AA"), 69 | `\tfrac`: builtinMacro("AA"), 70 | `\genfrac`: nil, // FIXME(sbinet) 71 | 72 | // relation symbols 73 | `\approx`: builtinMacro(""), 74 | `\asymp`: builtinMacro(""), 75 | `\bowtie`: builtinMacro(""), 76 | `\cong`: builtinMacro(""), 77 | `\dashv`: builtinMacro(""), 78 | `\doteq`: builtinMacro(""), 79 | `\doteqdot`: builtinMacro(""), 80 | `\dotplus`: builtinMacro(""), 81 | `\dots`: builtinMacro(""), 82 | `\equiv`: builtinMacro(""), 83 | `\frown`: builtinMacro(""), 84 | `\geq`: builtinMacro(""), 85 | `\gg`: builtinMacro(""), 86 | `\in`: builtinMacro(""), 87 | `\leq`: builtinMacro(""), 88 | `\ll`: builtinMacro(""), 89 | `\mid`: builtinMacro(""), 90 | `\models`: builtinMacro(""), 91 | `\neq`: builtinMacro(""), 92 | `\ni`: builtinMacro(""), 93 | `\parallel`: builtinMacro(""), 94 | `\perp`: builtinMacro(""), 95 | `\prec`: builtinMacro(""), 96 | `\preceq`: builtinMacro(""), 97 | `\propto`: builtinMacro(""), 98 | `\sim`: builtinMacro(""), 99 | `\simeq`: builtinMacro(""), 100 | `\smile`: builtinMacro(""), 101 | `\sqsubset`: builtinMacro(""), 102 | `\sqsubseteq`: builtinMacro(""), 103 | `\sqsupset`: builtinMacro(""), 104 | `\sqsupseteq`: builtinMacro(""), 105 | `\subset`: builtinMacro(""), 106 | `\subseteq`: builtinMacro(""), 107 | `\succ`: builtinMacro(""), 108 | `\succeq`: builtinMacro(""), 109 | `\supset`: builtinMacro(""), 110 | `\supseteq`: builtinMacro(""), 111 | `\vdash`: builtinMacro(""), 112 | `\Join`: builtinMacro(""), 113 | 114 | // arrow symbols 115 | `\downarrow`: builtinMacro(""), 116 | `\hookleftarrow`: builtinMacro(""), 117 | `\hookrightarrow`: builtinMacro(""), 118 | `\leadsto`: builtinMacro(""), 119 | `\leftarrow`: builtinMacro(""), 120 | `\leftharpoondown`: builtinMacro(""), 121 | `\leftharpoonup`: builtinMacro(""), 122 | `\leftrightarrow`: builtinMacro(""), 123 | `\longleftarrow`: builtinMacro(""), 124 | `\longleftrightarrow`: builtinMacro(""), 125 | `\longmapsto`: builtinMacro(""), 126 | `\longrightarrow`: builtinMacro(""), 127 | `\rightarrow`: builtinMacro(""), 128 | `\mapsto`: builtinMacro(""), 129 | `\nearrow`: builtinMacro(""), 130 | `\nwarrow`: builtinMacro(""), 131 | `\rightharpoondown`: builtinMacro(""), 132 | `\rightharpoonup`: builtinMacro(""), 133 | `\rightleftharpoons`: builtinMacro(""), 134 | `\searrow`: builtinMacro(""), 135 | `\swarrow`: builtinMacro(""), 136 | `\uparrow`: builtinMacro(""), 137 | `\updownarrow`: builtinMacro(""), 138 | `\Downarrow`: builtinMacro(""), 139 | `\Leftarrow`: builtinMacro(""), 140 | `\Leftrightarrow`: builtinMacro(""), 141 | `\Longleftarrow`: builtinMacro(""), 142 | `\Longleftrightarrow`: builtinMacro(""), 143 | `\Longrightarrow`: builtinMacro(""), 144 | `\Rightarrow`: builtinMacro(""), 145 | `\Uparrow`: builtinMacro(""), 146 | `\Updownarrow`: builtinMacro(""), 147 | 148 | // punctuation symbols 149 | `\ldotp`: builtinMacro(""), 150 | `\cdotp`: builtinMacro(""), 151 | 152 | // over-under symbols 153 | `\bigcap`: builtinMacro(""), 154 | `\bigcup`: builtinMacro(""), 155 | `\bigodot`: builtinMacro(""), 156 | `\bigoplus`: builtinMacro(""), 157 | `\bigotimes`: builtinMacro(""), 158 | `\bigsqcup`: builtinMacro(""), 159 | `\biguplus`: builtinMacro(""), 160 | `\bigvee`: builtinMacro(""), 161 | `\bigwedge`: builtinMacro(""), 162 | `\coprod`: builtinMacro(""), 163 | `\prod`: builtinMacro(""), 164 | `\sum`: builtinMacro(""), 165 | 166 | // over-under functions 167 | `\lim`: builtinMacro(""), 168 | `\liminf`: builtinMacro(""), 169 | `\limsup`: builtinMacro(""), 170 | `\max`: builtinMacro(""), 171 | `\min`: builtinMacro(""), 172 | `\sup`: builtinMacro(""), 173 | 174 | // dropsub symbols 175 | `\int`: builtinMacro(""), 176 | `\oint`: builtinMacro(""), 177 | 178 | // font names 179 | `\rm`: builtinMacro(""), 180 | `\cal`: builtinMacro(""), 181 | `\it`: builtinMacro(""), 182 | `\tt`: builtinMacro(""), 183 | `\sf`: builtinMacro(""), 184 | `\bf`: builtinMacro(""), 185 | `\default`: builtinMacro(""), 186 | `\bb`: builtinMacro(""), 187 | `\frak`: builtinMacro(""), 188 | `\scr`: builtinMacro(""), 189 | `\regular`: builtinMacro(""), 190 | 191 | // function names 192 | `\arccos`: builtinMacro(""), 193 | `\arcsin`: builtinMacro(""), 194 | `\arctan`: builtinMacro(""), 195 | `\arg`: builtinMacro(""), 196 | `\cos`: builtinMacro(""), 197 | `\cosh`: builtinMacro(""), 198 | `\cot`: builtinMacro(""), 199 | `\coth`: builtinMacro(""), 200 | `\csc`: builtinMacro(""), 201 | `\deg`: builtinMacro(""), 202 | `\det`: builtinMacro(""), 203 | `\dim`: builtinMacro(""), 204 | `\exp`: builtinMacro("A"), 205 | `\gcd`: builtinMacro(""), 206 | `\hom`: builtinMacro(""), 207 | `\inf`: builtinMacro(""), 208 | `\ker`: builtinMacro(""), 209 | `\lg`: builtinMacro(""), 210 | `\ln`: builtinMacro(""), 211 | `\log`: builtinMacro(""), 212 | `\sec`: builtinMacro(""), 213 | `\sin`: builtinMacro(""), 214 | `\sinh`: builtinMacro(""), 215 | `\sqrt`: builtinMacro("OA"), 216 | `\tan`: builtinMacro(""), 217 | `\tanh`: builtinMacro(""), 218 | `\Pr`: builtinMacro(""), 219 | 220 | // ambi delim 221 | `\backslash`: builtinMacro(""), 222 | `\vert`: builtinMacro(""), 223 | `\Vert`: builtinMacro(""), 224 | 225 | // left delim 226 | `\{`: builtinMacro(""), 227 | `\(`: builtinMacro(""), 228 | `(`: builtinMacro(""), 229 | `\langle`: builtinMacro(""), 230 | `\lceil`: builtinMacro(""), 231 | `\lfloor`: builtinMacro(""), 232 | 233 | // right delim 234 | `\}`: builtinMacro(""), 235 | `\)`: builtinMacro(""), 236 | `)`: builtinMacro(""), 237 | `\rangle`: builtinMacro(""), 238 | `\rceil`: builtinMacro(""), 239 | `\rfloor`: builtinMacro(""), 240 | 241 | // symbols 242 | `\alpha`: builtinMacro(""), 243 | `\beta`: builtinMacro(""), 244 | `\gamma`: builtinMacro(""), 245 | `\delta`: builtinMacro(""), 246 | `\iota`: builtinMacro(""), 247 | `\epsilon`: builtinMacro(""), 248 | `\eta`: builtinMacro(""), 249 | `\kappa`: builtinMacro(""), 250 | `\lambda`: builtinMacro(""), 251 | `\mu`: builtinMacro(""), 252 | `\nu`: builtinMacro(""), 253 | `\omicron`: builtinMacro(""), 254 | `\pi`: builtinMacro(""), 255 | `\theta`: builtinMacro(""), 256 | `\xi`: builtinMacro(""), 257 | `\rho`: builtinMacro(""), 258 | `\sigma`: builtinMacro(""), 259 | `\tau`: builtinMacro(""), 260 | `\upsilon`: builtinMacro(""), 261 | `\phi`: builtinMacro(""), 262 | `\chi`: builtinMacro(""), 263 | `\psi`: builtinMacro(""), 264 | `\omega`: builtinMacro(""), 265 | `\zeta`: builtinMacro(""), 266 | `\Alpha`: builtinMacro(""), 267 | `\Beta`: builtinMacro(""), 268 | `\Gamma`: builtinMacro(""), 269 | `\Delta`: builtinMacro(""), 270 | `\Epsilon`: builtinMacro(""), 271 | `\Zeta`: builtinMacro(""), 272 | `\Eta`: builtinMacro(""), 273 | `\Theta`: builtinMacro(""), 274 | `\Iota`: builtinMacro(""), 275 | `\Kappa`: builtinMacro(""), 276 | `\Lambda`: builtinMacro(""), 277 | `\Mu`: builtinMacro(""), 278 | `\Nu`: builtinMacro(""), 279 | `\Xi`: builtinMacro(""), 280 | `\Omicron`: builtinMacro(""), 281 | `\Pi`: builtinMacro(""), 282 | `\Rho`: builtinMacro(""), 283 | `\Sigma`: builtinMacro(""), 284 | `\Tau`: builtinMacro(""), 285 | `\Upsilon`: builtinMacro(""), 286 | `\Phi`: builtinMacro(""), 287 | `\Chi`: builtinMacro(""), 288 | `\Psi`: builtinMacro(""), 289 | `\Omega`: builtinMacro(""), 290 | `\hbar`: builtinMacro(""), 291 | `\nabla`: builtinMacro(""), 292 | 293 | // math font 294 | `\mathbf`: builtinMacro("A"), 295 | `\mathit`: builtinMacro("A"), 296 | `\mathsf`: builtinMacro("A"), 297 | `\mathtt`: builtinMacro("A"), 298 | `\mathcal`: builtinMacro("A"), 299 | `\mathdefault`: builtinMacro("A"), 300 | `\mathbb`: builtinMacro("A"), 301 | `\mathfrak`: builtinMacro("A"), 302 | `\mathscr`: builtinMacro("A"), 303 | `\mathregular`: builtinMacro("A"), 304 | 305 | // text 306 | `\textbf`: builtinMacro("A"), 307 | `\textit`: builtinMacro("A"), 308 | `\textsf`: builtinMacro("A"), 309 | `\texttt`: builtinMacro("A"), 310 | `\textcal`: builtinMacro("A"), 311 | `\textdefault`: builtinMacro("A"), 312 | `\textbb`: builtinMacro("A"), 313 | `\textfrak`: builtinMacro("A"), 314 | `\textscr`: builtinMacro("A"), 315 | `\textregular`: builtinMacro("A"), 316 | 317 | // space, symbols 318 | `\ `: builtinMacro(""), 319 | `\,`: builtinMacro(""), 320 | `\;`: builtinMacro(""), 321 | `\!`: builtinMacro(""), 322 | `\quad`: builtinMacro(""), 323 | `\qquad`: builtinMacro(""), 324 | `\:`: builtinMacro(""), 325 | `\cdots`: builtinMacro(""), 326 | `\ddots`: builtinMacro(""), 327 | `\ldots`: builtinMacro(""), 328 | `\vdots`: builtinMacro(""), 329 | `\hspace`: builtinMacro("A"), 330 | 331 | // catch-all 332 | // 333 | `\overline`: builtinMacro("A"), 334 | `\operatorname`: builtinMacro("A"), 335 | } 336 | ) 337 | 338 | type builtinMacro string 339 | 340 | func (m builtinMacro) Handle(p *parser, n ast.Node, state tex.State, math bool) tex.Node { 341 | node := n.(*ast.Macro) 342 | if m == "" { 343 | return tex.NewChar(node.Name.Name, state, math) 344 | } 345 | 346 | for _, typ := range strings.ToLower(string(m)) { 347 | switch typ { 348 | case 'a': 349 | panic("not implemented") 350 | case 'o': 351 | panic("not implemented") 352 | case 'v': 353 | panic("not implemented") 354 | } 355 | } 356 | 357 | return nil 358 | } 359 | -------------------------------------------------------------------------------- /mtex/mtex.go: -------------------------------------------------------------------------------- 1 | // Copyright ©2020 The go-latex Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | // Package mtex provides tools to render LaTeX math expressions. 6 | package mtex 7 | -------------------------------------------------------------------------------- /mtex/parser.go: -------------------------------------------------------------------------------- 1 | // Copyright ©2020 The go-latex Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package mtex 6 | 7 | import ( 8 | "fmt" 9 | "math" 10 | "strconv" 11 | "strings" 12 | 13 | "github.com/go-latex/latex" 14 | "github.com/go-latex/latex/ast" 15 | "github.com/go-latex/latex/font" 16 | "github.com/go-latex/latex/internal/tex2unicode" 17 | "github.com/go-latex/latex/mtex/symbols" 18 | "github.com/go-latex/latex/tex" 19 | ) 20 | 21 | // Parse parses a LaTeX math expression and returns the TeX-like box model 22 | // and an error if any. 23 | func Parse(expr string, fontSize, DPI float64, backend font.Backend) (tex.Node, error) { 24 | p := newParser(backend) 25 | return p.parse(expr, fontSize, DPI) 26 | } 27 | 28 | type parser struct { 29 | be font.Backend 30 | 31 | expr string 32 | macros map[string]handler 33 | } 34 | 35 | func newParser(be font.Backend) *parser { 36 | p := &parser{ 37 | be: be, 38 | macros: make(map[string]handler), 39 | } 40 | p.init() 41 | 42 | return p 43 | } 44 | 45 | func (p *parser) parse(x string, size, dpi float64) (tex.Node, error) { 46 | p.expr = x 47 | node, err := latex.ParseExpr(x) 48 | if err != nil { 49 | return nil, fmt.Errorf("could not parse latex expression %q: %w", x, err) 50 | } 51 | 52 | state := tex.NewState(p.be, font.Font{ 53 | Name: "default", 54 | Size: size, 55 | Type: "rm", 56 | }, dpi) 57 | 58 | v := visitor{p: p, state: state} 59 | ast.Walk(&v, node) 60 | nodes := tex.HListOf(v.nodes, true) 61 | 62 | return nodes, nil 63 | } 64 | 65 | type visitor struct { 66 | p *parser 67 | nodes []tex.Node 68 | state tex.State 69 | math bool 70 | } 71 | 72 | func (v *visitor) Visit(n ast.Node) ast.Visitor { 73 | switch n := n.(type) { 74 | case ast.List: 75 | case *ast.Symbol: 76 | switch { 77 | case v.math: 78 | h := v.p.handler(n.Text) 79 | if h == nil { 80 | panic("no handler for symbol [" + n.Text + "]") 81 | } 82 | v.nodes = append(v.nodes, h.Handle(v.p, n, v.state, v.math)) 83 | default: 84 | v.nodes = append(v.nodes, tex.NewChar(string(n.Text), v.state, v.math)) 85 | } 86 | case *ast.Word: 87 | var nodes []tex.Node 88 | for _, x := range n.Text { 89 | nodes = append(nodes, tex.NewChar(string(x), v.state, v.math)) 90 | } 91 | v.nodes = append(v.nodes, tex.HListOf(nodes, true)) 92 | case *ast.Literal: 93 | h := handlerFunc(handleSymbol) 94 | for _, c := range n.Text { 95 | n := &ast.Literal{Text: string(c)} 96 | v.nodes = append(v.nodes, h.Handle(v.p, n, v.state, v.math)) 97 | } 98 | 99 | case *ast.MathExpr: 100 | oldm := v.math 101 | oldt := v.state.Font.Type 102 | v.math = true 103 | v.state.Font.Type = rcparams("mathtext.default").(string) 104 | 105 | for _, x := range n.List { 106 | v.Visit(x) 107 | } 108 | v.math = oldm 109 | v.state.Font.Type = oldt 110 | return nil 111 | 112 | case *ast.Macro: 113 | if n.Name == nil { 114 | panic("macro with nil identifier") 115 | } 116 | macro := n.Name.Name 117 | h := v.p.handler(macro) 118 | if h == nil { 119 | panic(fmt.Errorf("unknown macro %q", macro)) 120 | } 121 | v.nodes = append(v.nodes, h.Handle(v.p, n, v.state, v.math)) 122 | return nil 123 | 124 | case nil: 125 | return v 126 | 127 | default: 128 | panic(fmt.Errorf("unknown ast node %T", n)) 129 | } 130 | return v 131 | } 132 | 133 | func (p *parser) handleNode(node ast.Node, state tex.State, math bool) tex.Node { 134 | v := visitor{p: p, state: state, math: math} 135 | ast.Walk(&v, node) 136 | return tex.HListOf(v.nodes, true) 137 | } 138 | 139 | func (p *parser) handler(name string) handler { 140 | if _, ok := spaceWidth[name]; ok { 141 | return handlerFunc(handleSpace) 142 | } 143 | if symbols.IsSpaced(name) || symbols.PunctuationSymbols.Has(name) { 144 | return handlerFunc(handleSymbol) 145 | } 146 | if name == `\hspace` { 147 | return handlerFunc(handleCustomSpace) 148 | } 149 | if symbols.FunctionNames.Has(name[1:]) { // drop leading `\` 150 | return handlerFunc(handleFunction) 151 | } 152 | switch name { 153 | case `\frac`: 154 | return handlerFunc(handleFrac) 155 | case `\dfrac`: 156 | return handlerFunc(handleDFrac) 157 | case `\tfrac`: 158 | return handlerFunc(handleTFrac) 159 | case `\binom`: 160 | return handlerFunc(handleBinom) 161 | // case `\genfrac`: 162 | // return handlerFunc(handleGenFrac) 163 | case `\sqrt`: 164 | return handlerFunc(handleSqrt) 165 | case `\overline`: 166 | return handlerFunc(handleOverline) 167 | } 168 | _, ok := p.macros[name] 169 | if ok { 170 | return handlerFunc(handleSymbol) 171 | } 172 | return nil 173 | } 174 | 175 | func (p *parser) init() { 176 | for _, k := range tex2unicode.Symbols() { 177 | p.macros[`\`+k] = builtinMacro("") 178 | } 179 | for k, v := range builtinMacros { 180 | p.macros[k] = v 181 | } 182 | } 183 | 184 | func handleSymbol(p *parser, node ast.Node, state tex.State, math bool) tex.Node { 185 | pos := int(node.Pos()) 186 | sym := "" 187 | switch node := node.(type) { 188 | case *ast.Macro: 189 | sym = node.Name.Name 190 | case *ast.Symbol: 191 | sym = node.Text 192 | case *ast.Word: 193 | sym = node.Text 194 | case *ast.Literal: 195 | sym = node.Text 196 | default: 197 | panic("invalid ast Node") 198 | } 199 | ch := tex.NewChar(sym, state, math) 200 | switch { 201 | case symbols.IsSpaced(sym): 202 | i := strings.LastIndexFunc(p.expr[:pos], func(r rune) bool { 203 | return r != ' ' 204 | }) 205 | prev := "" 206 | if i >= 0 { 207 | prev = string(p.expr[i]) 208 | } 209 | switch { 210 | case symbols.BinaryOperators.Has(sym) && (len(strings.Split(p.expr[:pos], " ")) == 0 || 211 | prev == "{" || 212 | symbols.LeftDelim.Has(prev)): 213 | // binary operators at start of string should not be spaced 214 | return ch 215 | default: 216 | return tex.HListOf([]tex.Node{ 217 | p.makeSpace(state, 0.2), 218 | ch, 219 | p.makeSpace(state, 0.2), 220 | }, true) 221 | } 222 | 223 | case symbols.PunctuationSymbols.Has(sym): 224 | switch sym { 225 | case ".": 226 | pos := strings.Index(p.expr[pos:], sym) 227 | if (pos > 0 && isdigit(p.expr[pos-1])) && 228 | (pos < len(p.expr)-1 && isdigit(p.expr[pos+1])) { 229 | // do not space dots as decimal separators. 230 | return ch 231 | } 232 | return tex.HListOf([]tex.Node{ 233 | ch, 234 | p.makeSpace(state, 0.2), 235 | }, true) 236 | } 237 | panic("not implemented") 238 | } 239 | return ch 240 | } 241 | 242 | var spaceWidth = map[string]float64{ 243 | `\,`: 0.16667, // 3/18 em = 3 mu 244 | `\thinspace`: 0.16667, // 3/18 em = 3 mu 245 | `\/`: 0.16667, // 3/18 em = 3 mu 246 | `\>`: 0.22222, // 4/18 em = 4 mu 247 | `\:`: 0.22222, // 4/18 em = 4 mu 248 | `\;`: 0.27778, // 5/18 em = 5 mu 249 | `\ `: 0.33333, // 6/18 em = 6 mu 250 | `~`: 0.33333, // 6/18 em = 6 mu, nonbreakable 251 | `\enspace`: 0.5, // 9/18 em = 9 mu 252 | `\quad`: 1, // 1 em = 18 mu 253 | `\qquad`: 2, // 2 em = 36 mu 254 | `\!`: -0.16667, // -3/18 em = -3 mu 255 | 256 | } 257 | 258 | func handleSpace(p *parser, node ast.Node, state tex.State, math bool) tex.Node { 259 | var ( 260 | width float64 261 | ok bool 262 | ) 263 | switch node := node.(type) { 264 | case *ast.Symbol: 265 | width, ok = spaceWidth[node.Text] 266 | case *ast.Macro: 267 | width, ok = spaceWidth[node.Name.Name] 268 | default: 269 | panic(fmt.Errorf("invalid ast node %#v (%T)", node, node)) 270 | } 271 | if !ok { 272 | panic(fmt.Errorf("could not find a width for %#v (%T)", node, node)) 273 | } 274 | 275 | return p.makeSpace(state, width) 276 | } 277 | 278 | func handleCustomSpace(p *parser, node ast.Node, state tex.State, math bool) tex.Node { 279 | macro := node.(*ast.Macro) 280 | arg := macro.Args[0].(*ast.Arg).List[0].(*ast.Literal).Text 281 | val, err := strconv.ParseFloat(arg, 64) 282 | if err != nil { 283 | panic(fmt.Errorf("could not parse customspace: %+v", err)) 284 | } 285 | return p.makeSpace(state, val) 286 | } 287 | 288 | func handleFunction(p *parser, node ast.Node, state tex.State, math bool) tex.Node { 289 | macro := node.(*ast.Macro) 290 | state.Font.Type = "rm" 291 | fun := macro.Name.Name[1:] // drop leading `\` 292 | nodes := make([]tex.Node, 0, len(fun)) 293 | for _, c := range fun { 294 | nodes = append(nodes, tex.NewChar(string(c), state, math)) 295 | } 296 | return tex.HListOf(nodes, true) 297 | } 298 | 299 | func handleFrac(p *parser, node ast.Node, state tex.State, math bool) tex.Node { 300 | var ( 301 | macro = node.(*ast.Macro) 302 | thickness = state.Backend().UnderlineThickness(state.Font, state.DPI) 303 | numNode = ast.List(macro.Args[0].(*ast.Arg).List) 304 | denNode = ast.List(macro.Args[1].(*ast.Arg).List) 305 | ) 306 | 307 | num := p.handleNode(numNode, state, math) 308 | den := p.handleNode(denNode, state, math) 309 | 310 | // FIXME(sbinet): this should be infered from the context. 311 | // ie: textStyle when in $ $ environment. 312 | // displayStyle when in \[\] environment. 313 | sty := textStyle 314 | 315 | return p.genfrac("", "", thickness, sty, num, den, state) 316 | } 317 | 318 | func handleDFrac(p *parser, node ast.Node, state tex.State, math bool) tex.Node { 319 | var ( 320 | macro = node.(*ast.Macro) 321 | thickness = state.Backend().UnderlineThickness(state.Font, state.DPI) 322 | numNode = ast.List(macro.Args[0].(*ast.Arg).List) 323 | denNode = ast.List(macro.Args[1].(*ast.Arg).List) 324 | ) 325 | 326 | num := p.handleNode(numNode, state, math) 327 | den := p.handleNode(denNode, state, math) 328 | 329 | return p.genfrac("", "", thickness, displayStyle, num, den, state) 330 | } 331 | 332 | func handleTFrac(p *parser, node ast.Node, state tex.State, math bool) tex.Node { 333 | var ( 334 | macro = node.(*ast.Macro) 335 | thickness = state.Backend().UnderlineThickness(state.Font, state.DPI) 336 | numNode = ast.List(macro.Args[0].(*ast.Arg).List) 337 | denNode = ast.List(macro.Args[1].(*ast.Arg).List) 338 | ) 339 | 340 | num := p.handleNode(numNode, state, math) 341 | den := p.handleNode(denNode, state, math) 342 | 343 | return p.genfrac("", "", thickness, textStyle, num, den, state) 344 | } 345 | 346 | func handleBinom(p *parser, node ast.Node, state tex.State, math bool) tex.Node { 347 | var ( 348 | macro = node.(*ast.Macro) 349 | numNode = ast.List(macro.Args[0].(*ast.Arg).List) 350 | denNode = ast.List(macro.Args[1].(*ast.Arg).List) 351 | ) 352 | 353 | num := p.handleNode(numNode, state, math) 354 | den := p.handleNode(denNode, state, math) 355 | 356 | return p.genfrac("(", ")", 0, textStyle, num, den, state) 357 | } 358 | 359 | func (p *parser) genfrac(ldelim, rdelim string, rule float64, style mathStyleKind, num, den tex.Node, state tex.State) tex.Node { 360 | thickness := state.Backend().UnderlineThickness(state.Font, state.DPI) 361 | 362 | if style != displayStyle { 363 | num.Shrink() 364 | den.Shrink() 365 | } 366 | 367 | cnum := tex.HCentered([]tex.Node{num}) 368 | cden := tex.HCentered([]tex.Node{den}) 369 | width := math.Max(num.Width(), den.Width()) 370 | 371 | const additional = false // i.e.: exactly 372 | cnum.HPack(width, additional) 373 | cden.HPack(width, additional) 374 | 375 | vlist := tex.VListOf([]tex.Node{ 376 | cnum, // numerator 377 | tex.VBox(0, thickness*2), // space 378 | tex.HRule(state, rule), // rule 379 | tex.VBox(0, thickness*2), // space 380 | cden, // denominator 381 | }) 382 | 383 | // shift so the fraction line sits in the middle of the '=' sign 384 | fnt := state.Font 385 | fnt.Type = rcparams("mathtext.default").(string) 386 | metrics := state.Backend().Metrics("=", fnt, state.DPI, true) 387 | shift := cden.Height() - ((metrics.YMax+metrics.YMin)/2 - 3*thickness) 388 | vlist.SetShift(shift) 389 | 390 | box := tex.HListOf([]tex.Node{vlist, tex.HBox(2 * thickness)}, true) 391 | if ldelim != "" || rdelim != "" { 392 | if ldelim == "" { 393 | ldelim = "." 394 | } 395 | if rdelim == "" { 396 | rdelim = "." 397 | } 398 | return p.autoSizedDelimiter(ldelim, []tex.Node{box}, rdelim, state) 399 | } 400 | 401 | return box 402 | } 403 | 404 | func handleSqrt(p *parser, node ast.Node, state tex.State, math bool) tex.Node { 405 | var ( 406 | macro = node.(*ast.Macro) 407 | root tex.Node 408 | body *tex.HList 409 | ) 410 | switch len(macro.Args) { 411 | case 2: 412 | root = p.handleNode( 413 | ast.List(macro.Args[0].(*ast.OptArg).List), 414 | state, math, 415 | ) 416 | body = p.handleNode( 417 | ast.List(macro.Args[1].(*ast.Arg).List), 418 | state, math, 419 | ).(*tex.HList) 420 | case 1: 421 | // ok 422 | body = p.handleNode( 423 | ast.List(macro.Args[0].(*ast.Arg).List), 424 | state, math, 425 | ).(*tex.HList) 426 | default: 427 | panic("invalid sqrt") 428 | } 429 | 430 | thickness := state.Backend().UnderlineThickness(state.Font, state.DPI) 431 | 432 | // determine the height of the body, add a little extra to it so 433 | // it doesn't seem too cramped. 434 | height := body.Height() - body.Shift() + 5*thickness 435 | depth := body.Depth() + body.Shift() 436 | check := tex.AutoHeightChar(`\__sqrt__`, height, depth, state, 0) 437 | height = check.Height() - check.Shift() 438 | depth = check.Depth() + check.Shift() 439 | 440 | // put a little extra space to the left and right of the body 441 | padded := tex.HListOf([]tex.Node{ 442 | tex.HBox(2 * thickness), 443 | body, 444 | tex.HBox(2 * thickness), 445 | }, true) 446 | rhs := tex.VListOf([]tex.Node{ 447 | tex.HRule(state, -1), 448 | tex.NewGlue("fill"), 449 | padded, 450 | }) 451 | 452 | // stretch the glue between the HRule and the body 453 | const additional = false 454 | rhs.VPack(height+(state.Font.Size*state.DPI)/(100*12), additional, depth) 455 | 456 | // add the root and shift it upward so it is above the tick. 457 | switch root { 458 | case nil: 459 | root = tex.HBox(check.Width() * 0.5) 460 | default: 461 | root.Shrink() 462 | root.Shrink() 463 | } 464 | 465 | vl := tex.VListOf([]tex.Node{ 466 | tex.HListOf([]tex.Node{ 467 | root, 468 | }, true), 469 | }) 470 | vl.SetShift(-height * 0.6) 471 | 472 | hl := tex.HListOf([]tex.Node{ 473 | vl, // root 474 | // negative kerning to put root over tick 475 | tex.NewKern(-check.Width() * 0.5), 476 | check, 477 | rhs, 478 | }, true) 479 | 480 | return hl 481 | } 482 | 483 | func handleOverline(p *parser, node ast.Node, state tex.State, math bool) tex.Node { 484 | macro := node.(*ast.Macro) 485 | body := p.handleNode( 486 | ast.List(macro.Args[0].(*ast.Arg).List), 487 | state, math, 488 | ).(*tex.HList) 489 | 490 | thickness := state.Backend().UnderlineThickness(state.Font, state.DPI) 491 | 492 | height := body.Height() - body.Shift() + 3*thickness 493 | depth := body.Depth() + body.Shift() 494 | 495 | // place overline above body 496 | rhs := tex.VListOf([]tex.Node{ 497 | tex.HRule(state, -1), 498 | tex.NewGlue("fill"), 499 | tex.HListOf([]tex.Node{body}, true), 500 | }) 501 | 502 | // stretch the glue between the HRule and the body 503 | const additional = false 504 | rhs.VPack(height+(state.Font.Size*state.DPI)/(100*12), additional, depth) 505 | 506 | hl := tex.HListOf([]tex.Node{rhs}, true) 507 | return hl 508 | } 509 | 510 | func (p *parser) makeSpace(state tex.State, percentage float64) *tex.Kern { 511 | const math = true 512 | fnt := state.Font 513 | fnt.Name = "it" 514 | fnt.Type = rcparams("mathtext.default").(string) 515 | width := p.be.Metrics("m", fnt, state.DPI, math).Advance 516 | return tex.NewKern(width * percentage) 517 | } 518 | 519 | func (p *parser) autoSizedDelimiter(left string, middle []tex.Node, right string, state tex.State) tex.Node { 520 | var ( 521 | height float64 522 | depth float64 523 | factor float64 = 1 524 | ) 525 | 526 | if len(middle) > 0 { 527 | for _, node := range middle { 528 | height = math.Max(height, node.Height()) 529 | depth = math.Max(depth, node.Depth()) 530 | } 531 | factor = 0 532 | } 533 | 534 | var parts []tex.Node 535 | if left != "." { 536 | // \left. isn't supposed to produce any symbol 537 | ahc := tex.AutoHeightChar(left, height, depth, state, factor) 538 | parts = append(parts, ahc) 539 | } 540 | parts = append(parts, middle...) 541 | if right != "." { 542 | // \right. isn't supposed to produce any symbol 543 | ahc := tex.AutoHeightChar(right, height, depth, state, factor) 544 | parts = append(parts, ahc) 545 | } 546 | return tex.HListOf(parts, true) 547 | } 548 | 549 | type mathStyleKind int 550 | 551 | const ( 552 | displayStyle mathStyleKind = iota 553 | textStyle 554 | //scriptStyle // FIXME 555 | //scriptScriptStyle // FIXME 556 | ) 557 | 558 | func rcparams(k string) interface{} { 559 | switch k { 560 | case "mathtext.default": 561 | return "it" 562 | default: 563 | panic("unknown rc.params key [" + k + "]") 564 | } 565 | } 566 | 567 | func isdigit(v byte) bool { 568 | switch v { 569 | case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9': 570 | return true 571 | } 572 | return false 573 | } 574 | -------------------------------------------------------------------------------- /mtex/parser_test.go: -------------------------------------------------------------------------------- 1 | // Copyright ©2020 The go-latex Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package mtex 6 | 7 | import ( 8 | "math" 9 | "testing" 10 | 11 | "github.com/go-latex/latex/internal/fakebackend" 12 | ) 13 | 14 | func TestParse(t *testing.T) { 15 | const ( 16 | dpi = 72 17 | ftsize = 10 18 | ) 19 | var ( 20 | be = fakebackend.New() 21 | ) 22 | for _, tc := range []struct { 23 | expr string 24 | w, h, d float64 25 | }{ 26 | { 27 | expr: "hello", 28 | w: 24.1650390625, 29 | h: 7.59375, 30 | d: 0.140625, 31 | }, 32 | { 33 | expr: "$hello$", 34 | w: 24.1650390625, 35 | h: 7.59375, 36 | d: 0.140625, 37 | }, 38 | { 39 | expr: `$\sigma$`, 40 | w: 6.337890625, 41 | h: 5.46875, 42 | d: 0.140625, 43 | }, 44 | { 45 | expr: `$\sigma$ is $12$`, 46 | w: 33.408203125, 47 | h: 7.59375, 48 | d: 0.140625, 49 | }, 50 | { 51 | expr: `$1.1$`, 52 | w: 15.9033203125, 53 | h: 7.296875, 54 | d: 0.0, 55 | }, 56 | { 57 | expr: `$1.$`, 58 | w: 11.4892578125, 59 | h: 7.296875, 60 | d: 0.0, 61 | }, 62 | { 63 | expr: `$.2$`, 64 | w: 11.4892578125, 65 | h: 7.421875, 66 | d: 0.0, 67 | }, 68 | { 69 | expr: `$.$`, 70 | w: 5.126953125, 71 | h: 1.234375, 72 | d: 0.0, 73 | }, 74 | { 75 | expr: `$x.x$`, 76 | w: 16.962890625, 77 | h: 5.46875, 78 | d: 0.0, 79 | }, 80 | //{ // FIXME(sbinet): handler for '(' 81 | // expr: `$\sigma = f(x)$`, 82 | // w: 35.8544921875, 83 | // h: 7.59375, 84 | // d: 1.3125, 85 | //}, 86 | { 87 | expr: `$\sigma \rightarrow \infty$`, 88 | w: 26.943359375, 89 | h: 5.46875, 90 | d: 0.140625, 91 | }, 92 | { 93 | expr: `$\sigma\,=\infty$`, 94 | w: 28.566927001953125, 95 | h: 5.46875, 96 | d: 0.140625, 97 | }, 98 | { 99 | expr: `$\sigma\hspace{2}=\infty$`, 100 | w: 46.42578125, 101 | h: 5.46875, 102 | d: 0.140625, 103 | }, 104 | { 105 | expr: `$\cos\theta$`, 106 | w: 22.9443359375, 107 | h: 7.671875, 108 | d: 0.140625, 109 | }, 110 | { 111 | expr: `$\pi$`, 112 | w: 6.0205078125, 113 | h: 5.46875, 114 | d: 0.1875, 115 | }, 116 | { 117 | expr: `$\frac{x}{y}$`, 118 | w: 5.392578125, 119 | h: 8.2109375, 120 | d: 4.0249999999999995, 121 | }, 122 | { 123 | expr: `$\frac{1}{2}$`, 124 | w: 5.70361328125, 125 | h: 9.490625, 126 | d: 3.9375, 127 | }, 128 | { 129 | expr: `$\frac{1}{2\pi}$`, 130 | w: 9.91796875, 131 | h: 9.490625, 132 | d: 4.06875, 133 | }, 134 | { 135 | expr: `$\dfrac{1}{2}$`, 136 | w: 7.6123046875, 137 | h: 11.6796875, 138 | d: 6.1640625, 139 | }, 140 | { 141 | expr: `$\tfrac{1}{2}$`, 142 | w: 5.70361328125, 143 | h: 9.490625, 144 | d: 3.9375, 145 | }, 146 | { 147 | expr: `$\binom{1}{x}$`, 148 | w: 15.713043212890625, // w/o cm-fallback 149 | h: 8.865625, 150 | d: 2.5703124999999996, 151 | }, 152 | { 153 | expr: `$\sqrt{2x}$`, 154 | w: 22.864837646484375, 155 | h: 11.146875, 156 | d: 0, 157 | }, 158 | { 159 | // FIXME(sbinet): check values somehow. 160 | // but... matplotlib.mathtex doesn't handle `$\sqrt[3]{x}$` 161 | expr: `$\sqrt[3]{2x}$`, 162 | w: 21.940084838867186, 163 | h: 11.146875, 164 | d: 0, 165 | }, 166 | { 167 | expr: `$\overline{ab}$`, 168 | w: 12.4755859375, 169 | h: 10.06875, 170 | d: 0.140625, 171 | }, 172 | } { 173 | t.Run("", func(t *testing.T) { 174 | defer func() { 175 | err := recover() 176 | if err != nil { 177 | t.Errorf("%q: panic: %+v", tc.expr, err) 178 | panic(err) 179 | } 180 | }() 181 | got, err := Parse(tc.expr, ftsize, dpi, be) 182 | if err != nil { 183 | t.Fatalf("could not parse %q: %+v", tc.expr, err) 184 | } 185 | 186 | var ( 187 | w = got.Width() 188 | h = got.Height() 189 | d = got.Depth() 190 | ) 191 | 192 | if got, want := w, tc.w; got != want { 193 | t.Fatalf("%q: invalid width: got=%g, want=%g", tc.expr, got, want) 194 | } 195 | 196 | if got, want := h, tc.h; !cmpEq(got, want) { 197 | t.Fatalf("%q: invalid height: got=%g, want=%g", tc.expr, got, want) 198 | } 199 | 200 | if got, want := d, tc.d; !cmpEq(got, want) { 201 | t.Fatalf("%q: invalid depth: got=%g, want=%g", tc.expr, got, want) 202 | } 203 | }) 204 | } 205 | } 206 | 207 | func cmpEq(a, b float64) bool { 208 | switch { 209 | case math.IsInf(a, -1): 210 | return math.IsInf(b, -1) 211 | case math.IsInf(a, +1): 212 | return math.IsInf(b, +1) 213 | default: 214 | return a == b 215 | } 216 | } 217 | -------------------------------------------------------------------------------- /mtex/render.go: -------------------------------------------------------------------------------- 1 | // Copyright ©2020 The go-latex Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package mtex 6 | 7 | import ( 8 | "fmt" 9 | "math" 10 | 11 | "github.com/go-latex/latex/drawtex" 12 | "github.com/go-latex/latex/font/ttf" 13 | "github.com/go-latex/latex/tex" 14 | ) 15 | 16 | type Renderer interface { 17 | Render(w, h, dpi float64, cnv *drawtex.Canvas) error 18 | } 19 | 20 | func Render(dst Renderer, expr string, size, dpi float64, fonts *ttf.Fonts) error { 21 | var ( 22 | canvas = drawtex.New() 23 | backend *ttf.Backend 24 | ) 25 | switch fonts { 26 | case nil: 27 | backend = ttf.New(canvas) 28 | default: 29 | backend = ttf.NewFrom(canvas, fonts) 30 | } 31 | 32 | box, err := Parse(expr, size, 72, backend) 33 | if err != nil { 34 | return fmt.Errorf("could not parse math expression: %w", err) 35 | } 36 | 37 | var sh tex.Ship 38 | sh.Call(0, 0, box.(tex.Tree)) 39 | 40 | w := box.Width() 41 | h := box.Height() 42 | d := box.Depth() 43 | 44 | err = dst.Render(w/72, math.Ceil(h+math.Max(d, 0))/72, dpi, canvas) 45 | if err != nil { 46 | return fmt.Errorf("could not render math expression: %w", err) 47 | } 48 | 49 | return nil 50 | } 51 | -------------------------------------------------------------------------------- /mtex/render_test.go: -------------------------------------------------------------------------------- 1 | // Copyright ©2020 The go-latex Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package mtex 6 | 7 | import ( 8 | "fmt" 9 | "testing" 10 | 11 | "github.com/go-latex/latex/drawtex" 12 | ) 13 | 14 | type dummyRenderer struct{} 15 | 16 | func (dummyRenderer) Render(width, height, dpi float64, c *drawtex.Canvas) error { 17 | for _, op := range c.Ops() { 18 | switch op.(type) { 19 | case drawtex.GlyphOp: 20 | case drawtex.RectOp: 21 | default: 22 | panic(fmt.Errorf("unknown drawtex operation %T", op)) 23 | } 24 | } 25 | return nil 26 | } 27 | 28 | func TestRender(t *testing.T) { 29 | const ( 30 | dpi = 72 31 | ftsize = 10 32 | ) 33 | for _, tc := range []struct { 34 | expr string 35 | want error 36 | }{ 37 | { 38 | expr: `math $x= 42$`, 39 | }, 40 | { 41 | expr: `math $\sum\sqrt{\frac{a+b}{2\pi}}\cos\Phi$`, 42 | }, 43 | { 44 | expr: `math: $\sum\sqrt{\frac{a+b}{2\pi}}\cos\omega\binom{a+b}{\beta}\prod \alpha x$`, 45 | }, 46 | { 47 | expr: `$\int\frac{\partial x}{x}$`, 48 | }, 49 | } { 50 | t.Run(tc.expr, func(t *testing.T) { 51 | err := Render(dummyRenderer{}, tc.expr, ftsize, dpi, nil) 52 | 53 | switch { 54 | case err != nil && tc.want != nil: 55 | if got, want := err.Error(), tc.want.Error(); got != want { 56 | t.Fatalf("invalid error:\ngot= %v\nwant=%v", got, want) 57 | } 58 | case err == nil && tc.want != nil: 59 | t.Fatalf("expected an error: %v", tc.want) 60 | case err != nil && tc.want == nil: 61 | t.Fatalf("could not render: %v", err) 62 | case err == nil && tc.want == nil: 63 | // ok. 64 | } 65 | }) 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /mtex/symbols/gen-symbols.go: -------------------------------------------------------------------------------- 1 | // Copyright ©2020 The go-latex Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | //go:build ignore 6 | // +build ignore 7 | 8 | // 9 | package main 10 | 11 | import ( 12 | "bytes" 13 | "encoding/json" 14 | "fmt" 15 | "io" 16 | "log" 17 | "os" 18 | "os/exec" 19 | "sort" 20 | ) 21 | 22 | func main() { 23 | f, err := os.Create("symbols_gen.go") 24 | if err != nil { 25 | log.Fatalf("could not create symbols file: %+v", err) 26 | } 27 | defer f.Close() 28 | 29 | gen(f, 30 | sym{"_binary_operators", "BinaryOperators"}, 31 | sym{"_relation_symbols", "RelationSymbols"}, 32 | sym{"_arrow_symbols", "ArrowSymbols"}, 33 | sym{"_punctuation_symbols", "PunctuationSymbols"}, 34 | sym{"_overunder_symbols", "OverUnderSymbols"}, 35 | sym{"_overunder_functions", "OverUnderFunctions"}, 36 | sym{"_dropsub_symbols", "DropSubSymbols"}, 37 | sym{"_fontnames", "FontNames"}, 38 | sym{"_function_names", "FunctionNames"}, 39 | sym{"_ambi_delim", "AmbiDelim"}, 40 | sym{"_left_delim", "LeftDelim"}, 41 | sym{"_right_delim", "RightDelim"}, 42 | ) 43 | 44 | err = f.Close() 45 | if err != nil { 46 | log.Fatalf("could not close symbols file: %+v", err) 47 | } 48 | } 49 | 50 | type sym struct { 51 | Py string `json:"py"` 52 | Go string `json:"go"` 53 | } 54 | 55 | func gen(o io.Writer, syms ...sym) error { 56 | r := new(bytes.Buffer) 57 | err := json.NewEncoder(r).Encode(syms) 58 | if err != nil { 59 | return fmt.Errorf("could not encode input JSON: %+v", err) 60 | } 61 | 62 | stdout := new(bytes.Buffer) 63 | cmd := exec.Command("python", "-c", py, r.String()) 64 | cmd.Stdout = stdout 65 | cmd.Stderr = os.Stderr 66 | 67 | err = cmd.Run() 68 | if err != nil { 69 | return fmt.Errorf("could not run python script: %+v", err) 70 | } 71 | 72 | var out map[string][]string 73 | err = json.NewDecoder(stdout).Decode(&out) 74 | if err != nil { 75 | return fmt.Errorf("could not decode output JSON: %+v", err) 76 | } 77 | 78 | fmt.Fprintf(o, `// Autogenerated. DO NOT EDIT. 79 | 80 | package symbols 81 | 82 | var ( 83 | `) 84 | 85 | keys := make([]string, 0, len(out)) 86 | for k := range out { 87 | keys = append(keys, k) 88 | } 89 | sort.Strings(keys) 90 | 91 | for i := range keys { 92 | k := keys[i] 93 | v := out[k] 94 | if i > 0 { 95 | fmt.Fprintf(o, "\n") 96 | } 97 | fmt.Fprintf(o, "\t%s = NewSet(\n", k) 98 | for _, sym := range v { 99 | fmt.Fprintf(o, "\t\t%q,\n", sym) 100 | } 101 | fmt.Fprintf(o, "\t)\n") 102 | } 103 | 104 | fmt.Fprintf(o, ")\n") 105 | return nil 106 | } 107 | 108 | const py = ` 109 | import sys 110 | import string 111 | import json 112 | import matplotlib.mathtext as mtex 113 | 114 | input = json.loads(sys.argv[1]) 115 | data = {} 116 | for v in input: 117 | symbols = getattr(mtex.Parser, v["py"]) 118 | data[v["go"]] = list(symbols) 119 | 120 | json.dump(data, sys.stdout) 121 | sys.stdout.flush() 122 | ` 123 | -------------------------------------------------------------------------------- /mtex/symbols/set.go: -------------------------------------------------------------------------------- 1 | // Copyright ©2020 The go-latex Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package symbols 6 | 7 | import ( 8 | "sort" 9 | ) 10 | 11 | type Set map[string]struct{} 12 | 13 | func NewSet(vs ...string) Set { 14 | o := make(Set, len(vs)) 15 | for _, k := range vs { 16 | o[k] = struct{}{} 17 | } 18 | return o 19 | } 20 | 21 | func (set Set) Has(k string) bool { 22 | _, ok := set[k] 23 | return ok 24 | } 25 | 26 | func (set Set) Keys() []string { 27 | keys := make([]string, 0, len(set)) 28 | for k := range set { 29 | keys = append(keys, k) 30 | } 31 | sort.Strings(keys) 32 | return keys 33 | } 34 | 35 | func UnionOf(sets ...Set) Set { 36 | o := make(Set, len(sets)) 37 | for _, set := range sets { 38 | for k := range set { 39 | o[k] = struct{}{} 40 | } 41 | } 42 | return o 43 | } 44 | -------------------------------------------------------------------------------- /mtex/symbols/symbols.go: -------------------------------------------------------------------------------- 1 | // Copyright ©2020 The go-latex Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | // Package symbols contains logic about TeX symbols. 6 | package symbols // import "github.com/go-latex/latex/mtex/symbols" 7 | 8 | //go:generate go run ./gen-symbols.go 9 | 10 | var ( 11 | SpacedSymbols = UnionOf(BinaryOperators, RelationSymbols, ArrowSymbols) 12 | ) 13 | 14 | func IsSpaced(s string) bool { 15 | return SpacedSymbols.Has(s) 16 | } 17 | -------------------------------------------------------------------------------- /mtex/symbols/symbols_gen.go: -------------------------------------------------------------------------------- 1 | // Autogenerated. DO NOT EDIT. 2 | 3 | package symbols 4 | 5 | var ( 6 | AmbiDelim = NewSet( 7 | "\\downarrow", 8 | "\\Uparrow", 9 | "\\|", 10 | "\\updownarrow", 11 | "\\vert", 12 | "\\Vert", 13 | "\\backslash", 14 | ".", 15 | "\\Updownarrow", 16 | "/", 17 | "\\Downarrow", 18 | "|", 19 | "\\\\|", 20 | "\\uparrow", 21 | ) 22 | 23 | ArrowSymbols = NewSet( 24 | "\\Uparrow", 25 | "\\searrow", 26 | "\\hookleftarrow", 27 | "\\longleftrightarrow", 28 | "\\longrightarrow", 29 | "\\rightarrow", 30 | "\\leadsto", 31 | "\\nearrow", 32 | "\\Updownarrow", 33 | "\\rightharpoonup", 34 | "\\Longrightarrow", 35 | "\\leftrightarrow", 36 | "\\downarrow", 37 | "\\nwarrow", 38 | "\\leftarrow", 39 | "\\leftharpoondown", 40 | "\\swarrow", 41 | "\\Longleftarrow", 42 | "\\Leftarrow", 43 | "\\Longleftrightarrow", 44 | "\\uparrow", 45 | "\\hookrightarrow", 46 | "\\rightleftharpoons", 47 | "\\mapsto", 48 | "\\Leftrightarrow", 49 | "\\leftharpoonup", 50 | "\\rightharpoondown", 51 | "\\updownarrow", 52 | "\\Rightarrow", 53 | "\\longleftarrow", 54 | "\\Downarrow", 55 | "\\longmapsto", 56 | ) 57 | 58 | BinaryOperators = NewSet( 59 | "\\triangleleft", 60 | "\\cup", 61 | "+", 62 | "\\oplus", 63 | "*", 64 | "\\bullet", 65 | "\\star", 66 | "\\diamond", 67 | "\\div", 68 | "\\bigtriangledown", 69 | "\\unrhd", 70 | "\\wr", 71 | "\\bigtriangleup", 72 | "\\sqcup", 73 | "\\vee", 74 | "\\sqcap", 75 | "\\dagger", 76 | "\\cdot", 77 | "\\unlhd", 78 | "\\triangleright", 79 | "\\ddagger", 80 | "\\amalg", 81 | "\\circ", 82 | "\\odot", 83 | "\\cap", 84 | "\\bigcirc", 85 | "\\lhd", 86 | "\\times", 87 | "-", 88 | "\\wedge", 89 | "\\mp", 90 | "\\otimes", 91 | "\\ominus", 92 | "\\ast", 93 | "\\pm", 94 | "\\oslash", 95 | "\\rhd", 96 | "\\setminus", 97 | "\\uplus", 98 | ) 99 | 100 | DropSubSymbols = NewSet( 101 | "\\oint", 102 | "\\int", 103 | ) 104 | 105 | FontNames = NewSet( 106 | "circled", 107 | "default", 108 | "cal", 109 | "bf", 110 | "regular", 111 | "tt", 112 | "scr", 113 | "sf", 114 | "frak", 115 | "rm", 116 | "it", 117 | "bb", 118 | ) 119 | 120 | FunctionNames = NewSet( 121 | "lim", 122 | "arccos", 123 | "min", 124 | "arcsin", 125 | "gcd", 126 | "arctan", 127 | "sup", 128 | "sec", 129 | "max", 130 | "cos", 131 | "deg", 132 | "arg", 133 | "sin", 134 | "log", 135 | "sinh", 136 | "ker", 137 | "liminf", 138 | "coth", 139 | "exp", 140 | "det", 141 | "ln", 142 | "lg", 143 | "Pr", 144 | "tan", 145 | "tanh", 146 | "csc", 147 | "hom", 148 | "cosh", 149 | "cot", 150 | "dim", 151 | "limsup", 152 | "inf", 153 | ) 154 | 155 | LeftDelim = NewSet( 156 | "\\lfloor", 157 | "<", 158 | "\\{", 159 | "\\langle", 160 | "[", 161 | "(", 162 | "\\lceil", 163 | ) 164 | 165 | OverUnderFunctions = NewSet( 166 | "sup", 167 | "max", 168 | "lim", 169 | "limsup", 170 | "min", 171 | "liminf", 172 | ) 173 | 174 | OverUnderSymbols = NewSet( 175 | "\\biguplus", 176 | "\\bigoplus", 177 | "\\prod", 178 | "\\bigcap", 179 | "\\bigsqcup", 180 | "\\bigodot", 181 | "\\bigvee", 182 | "\\bigwedge", 183 | "\\sum", 184 | "\\bigcup", 185 | "\\coprod", 186 | "\\bigotimes", 187 | ) 188 | 189 | PunctuationSymbols = NewSet( 190 | "!", 191 | ";", 192 | "\\cdotp", 193 | ",", 194 | ".", 195 | "\\ldotp", 196 | ) 197 | 198 | RelationSymbols = NewSet( 199 | "\\ni", 200 | "\\leq", 201 | "\\ll", 202 | "\\supseteq", 203 | "\\succ", 204 | "=", 205 | "\\neq", 206 | "\\parallel", 207 | "\\geq", 208 | "\\prec", 209 | "\\frown", 210 | "\\in", 211 | "\\Join", 212 | "\\sqsubset", 213 | "\\dashv", 214 | "\\vdash", 215 | "\\dots", 216 | "\\asymp", 217 | "\\subset", 218 | "\\subseteq", 219 | "\\sqsupseteq", 220 | "<", 221 | "\\models", 222 | "\\bowtie", 223 | "\\equiv", 224 | ":", 225 | "\\sqsupset", 226 | "\\smile", 227 | "\\propto", 228 | "\\dotplus", 229 | "\\preceq", 230 | "\\cong", 231 | "\\simeq", 232 | ">", 233 | "\\mid", 234 | "\\approx", 235 | "\\supset", 236 | "\\gg", 237 | "\\doteq", 238 | "\\sqsubseteq", 239 | "\\doteqdot", 240 | "\\succeq", 241 | "\\perp", 242 | "\\sim", 243 | ) 244 | 245 | RightDelim = NewSet( 246 | "\\rceil", 247 | "]", 248 | "\\rangle", 249 | ">", 250 | "\\}", 251 | "\\rfloor", 252 | ")", 253 | ) 254 | ) 255 | -------------------------------------------------------------------------------- /mtex/symbols/symbols_test.go: -------------------------------------------------------------------------------- 1 | // Copyright ©2020 The go-latex Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package symbols 6 | 7 | import "testing" 8 | 9 | func TestIsSpaced(t *testing.T) { 10 | for _, tc := range []struct { 11 | symbol string 12 | want bool 13 | }{ 14 | {`\leftarrow`, true}, 15 | {`\dashv`, true}, 16 | {`=`, true}, 17 | {`<`, true}, 18 | {`+`, true}, 19 | {`\pm`, true}, 20 | {`\sum`, false}, 21 | {`\alpha`, false}, 22 | {` `, false}, 23 | } { 24 | t.Run(tc.symbol, func(t *testing.T) { 25 | got := IsSpaced(tc.symbol) 26 | if got != tc.want { 27 | t.Fatalf("got: %v, want: %v", got, tc.want) 28 | } 29 | }) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /mtex/testdata/mtex-example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/go-latex/latex/2790903426af0a03fad34348f8d9dd2a92617ad7/mtex/testdata/mtex-example.png -------------------------------------------------------------------------------- /parser.go: -------------------------------------------------------------------------------- 1 | // Copyright ©2020 The go-latex Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package latex // import "github.com/go-latex/latex" 6 | 7 | import ( 8 | "fmt" 9 | "strings" 10 | 11 | "github.com/go-latex/latex/ast" 12 | "github.com/go-latex/latex/token" 13 | ) 14 | 15 | // ParseExpr parses a simple LaTeX expression. 16 | func ParseExpr(x string) (ast.Node, error) { 17 | p := newParser(x) 18 | return p.parse() 19 | } 20 | 21 | type state int 22 | 23 | const ( 24 | normalState state = iota 25 | mathState 26 | ) 27 | 28 | type parser struct { 29 | s *texScanner 30 | state state 31 | 32 | macros map[string]macroParser 33 | } 34 | 35 | func newParser(x string) *parser { 36 | p := &parser{ 37 | s: newScanner(strings.NewReader(x)), 38 | state: normalState, 39 | } 40 | p.addBuiltinMacros() 41 | return p 42 | } 43 | 44 | func (p *parser) parse() (ast.Node, error) { 45 | var nodes ast.List 46 | for p.s.Next() { 47 | tok := p.s.Token() 48 | node := p.parseNode(tok) 49 | if node == nil { 50 | continue 51 | } 52 | nodes = append(nodes, node) 53 | } 54 | 55 | return nodes, nil 56 | } 57 | 58 | func (p *parser) next() token.Token { 59 | if !p.s.Next() { 60 | return token.Token{Kind: token.EOF} 61 | } 62 | return p.s.tok 63 | } 64 | 65 | func (p *parser) expect(v rune) { 66 | p.next() 67 | if p.s.tok.Text != string(v) { 68 | panic(fmt.Errorf("expected %q, got %q", v, p.s.tok.Text)) 69 | } 70 | } 71 | 72 | func (p *parser) parseNode(tok token.Token) ast.Node { 73 | switch tok.Kind { 74 | case token.Comment: 75 | return nil 76 | case token.Macro: 77 | return p.parseMacro(tok) 78 | case token.Word: 79 | return p.parseWord(tok) 80 | case token.Number: 81 | return p.parseNumber(tok) 82 | case token.Symbol: 83 | switch tok.Text { 84 | case "$": 85 | return p.parseMathExpr(tok) 86 | case "^": 87 | return p.parseSup(tok) 88 | case "_": 89 | return p.parseSub(tok) 90 | default: 91 | return p.parseSymbol(tok) 92 | } 93 | case token.Lbrace: 94 | switch p.state { 95 | case mathState: 96 | return p.parseMathLbrace(tok) 97 | default: 98 | panic("not implemented") 99 | } 100 | case token.Other: 101 | switch tok.Text { 102 | default: 103 | panic("not implemented: " + tok.String()) 104 | } 105 | case token.Space: 106 | switch p.state { 107 | case mathState: 108 | return nil 109 | default: 110 | return p.parseSymbol(tok) 111 | } 112 | 113 | case token.Lparen, token.Rparen, 114 | token.Lbrack, token.Rbrack: 115 | return p.parseSymbol(tok) 116 | 117 | default: 118 | panic(fmt.Errorf("impossible: %v (%v)", tok, tok.Kind)) 119 | } 120 | } 121 | 122 | func (p *parser) parseMathExpr(tok token.Token) ast.Node { 123 | state := p.state 124 | p.state = mathState 125 | defer func() { 126 | p.state = state 127 | }() 128 | 129 | math := &ast.MathExpr{ 130 | Delim: tok.Text, 131 | Left: tok.Pos, 132 | } 133 | var end string 134 | switch tok.Text { 135 | case "$": 136 | end = "$" 137 | case `\(`: 138 | end = `\)` 139 | case `\[`: 140 | end = `\]` 141 | case `\begin`: 142 | panic("not implemented") 143 | default: 144 | panic(fmt.Errorf("opening math-expression delimiter %q not supported", tok.Text)) 145 | } 146 | 147 | loop: 148 | for p.s.Next() { 149 | switch p.s.tok.Text { 150 | case end: 151 | math.Right = p.s.tok.Pos 152 | break loop 153 | default: 154 | node := p.parseNode(p.s.tok) 155 | if node == nil { 156 | continue 157 | } 158 | math.List = append(math.List, node) 159 | } 160 | } 161 | 162 | return math 163 | } 164 | 165 | func (p *parser) parseMacro(tok token.Token) ast.Node { 166 | name := tok.Text 167 | macro, ok := p.macros[name] 168 | if !ok { 169 | panic("unknown macro " + name) 170 | //return nil 171 | } 172 | return macro.parseMacro(p) 173 | } 174 | 175 | func (p *parser) parseWord(tok token.Token) ast.Node { 176 | return &ast.Word{ 177 | WordPos: tok.Pos, 178 | Text: tok.Text, 179 | } 180 | } 181 | 182 | func (p *parser) parseNumber(tok token.Token) ast.Node { 183 | return &ast.Literal{ 184 | LitPos: tok.Pos, 185 | Text: tok.Text, 186 | } 187 | } 188 | 189 | func (p *parser) parseMacroArg(macro *ast.Macro) { 190 | var arg ast.Arg 191 | p.expect('{') 192 | arg.Lbrace = p.s.tok.Pos 193 | 194 | loop: 195 | for p.s.Next() { 196 | switch p.s.tok.Kind { 197 | case token.Rbrace: 198 | arg.Rbrace = p.s.tok.Pos 199 | break loop 200 | default: 201 | node := p.parseNode(p.s.tok) 202 | if node == nil { 203 | continue 204 | } 205 | arg.List = append(arg.List, node) 206 | } 207 | } 208 | macro.Args = append(macro.Args, &arg) 209 | } 210 | 211 | func (p *parser) parseOptMacroArg(macro *ast.Macro) { 212 | nxt := p.s.sc.Peek() 213 | if nxt != '[' { 214 | return 215 | } 216 | 217 | var opt ast.OptArg 218 | 219 | p.expect('[') 220 | opt.Lbrack = p.s.tok.Pos 221 | 222 | loop: 223 | for p.s.Next() { 224 | switch p.s.tok.Kind { 225 | case token.Rbrack: 226 | opt.Rbrack = p.s.tok.Pos 227 | break loop 228 | default: 229 | node := p.parseNode(p.s.tok) 230 | if node == nil { 231 | continue 232 | } 233 | opt.List = append(opt.List, node) 234 | } 235 | } 236 | macro.Args = append(macro.Args, &opt) 237 | } 238 | 239 | func (p *parser) parseVerbatimMacroArg(macro *ast.Macro) { 240 | } 241 | 242 | func (p *parser) parseSup(tok token.Token) ast.Node { 243 | hat := &ast.Sup{ 244 | HatPos: tok.Pos, 245 | } 246 | 247 | switch next := p.s.sc.Peek(); next { 248 | case '{': 249 | p.expect('{') 250 | var list ast.List 251 | loop: 252 | for p.s.Next() { 253 | switch p.s.tok.Kind { 254 | case token.Rbrace: 255 | break loop 256 | default: 257 | node := p.parseNode(p.s.tok) 258 | if node == nil { 259 | continue 260 | } 261 | list = append(list, node) 262 | } 263 | } 264 | hat.Node = list 265 | default: 266 | hat.Node = p.parseNode(p.next()) 267 | } 268 | 269 | return hat 270 | } 271 | 272 | func (p *parser) parseSub(tok token.Token) ast.Node { 273 | sub := &ast.Sub{ 274 | UnderPos: tok.Pos, 275 | } 276 | 277 | switch next := p.s.sc.Peek(); next { 278 | case '{': 279 | p.expect('{') 280 | var list ast.List 281 | loop: 282 | for p.s.Next() { 283 | switch p.s.tok.Kind { 284 | case token.Rbrace: 285 | break loop 286 | default: 287 | node := p.parseNode(p.s.tok) 288 | if node == nil { 289 | continue 290 | } 291 | list = append(list, node) 292 | } 293 | } 294 | sub.Node = list 295 | default: 296 | sub.Node = p.parseNode(p.next()) 297 | } 298 | 299 | return sub 300 | } 301 | 302 | func (p *parser) parseSymbol(tok token.Token) ast.Node { 303 | return &ast.Symbol{ 304 | SymPos: tok.Pos, 305 | Text: tok.Text, 306 | } 307 | } 308 | 309 | func (p *parser) parseMathLbrace(tok token.Token) ast.Node { 310 | var ( 311 | lst ast.List 312 | ldelim = tok.Kind 313 | rdelim = map[token.Kind]token.Kind{ 314 | token.Lbrace: token.Rbrace, 315 | token.Lparen: token.Rparen, 316 | }[ldelim] 317 | ) 318 | 319 | if rdelim == token.Invalid { 320 | panic("impossible: no matching right-delim for: " + tok.String()) 321 | } 322 | 323 | loop: 324 | for p.s.Next() { 325 | switch p.s.tok.Kind { 326 | case rdelim: 327 | break loop 328 | default: 329 | node := p.parseNode(p.s.tok) 330 | if node == nil { 331 | continue 332 | } 333 | lst = append(lst, node) 334 | } 335 | } 336 | return lst 337 | } 338 | -------------------------------------------------------------------------------- /parser_test.go: -------------------------------------------------------------------------------- 1 | // Copyright ©2020 The go-latex Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | // Package latex provides types and functions to work with LaTeX. 6 | package latex // import "github.com/go-latex/latex" 7 | 8 | import ( 9 | "reflect" 10 | "strings" 11 | "testing" 12 | 13 | "github.com/go-latex/latex/ast" 14 | ) 15 | 16 | func TestParser(t *testing.T) { 17 | for _, tc := range []struct { 18 | input string 19 | want ast.Node 20 | }{ 21 | { 22 | input: `hello`, 23 | want: ast.List{&ast.Word{Text: "hello"}}, 24 | }, 25 | { 26 | input: `hello world`, 27 | want: ast.List{ 28 | &ast.Word{Text: "hello"}, 29 | &ast.Symbol{Text: " "}, 30 | &ast.Word{Text: "world"}, 31 | }, 32 | }, 33 | { 34 | input: `empty equation $$`, 35 | want: ast.List{ 36 | &ast.Word{Text: "empty"}, 37 | &ast.Symbol{Text: " "}, 38 | &ast.Word{Text: "equation"}, 39 | &ast.Symbol{Text: " "}, 40 | &ast.MathExpr{ 41 | Delim: "$", 42 | }, 43 | }, 44 | }, 45 | { 46 | input: `$+10x$`, 47 | want: ast.List{ 48 | &ast.MathExpr{ 49 | Delim: "$", 50 | List: ast.List{ 51 | &ast.Symbol{Text: "+"}, 52 | &ast.Literal{Text: "10"}, 53 | &ast.Word{Text: "x"}, 54 | }, 55 | }, 56 | }, 57 | }, 58 | { 59 | input: `${}+10x$`, 60 | want: ast.List{ 61 | &ast.MathExpr{ 62 | Delim: "$", 63 | List: ast.List{ 64 | ast.List{}, // FIXME(sbinet): shouldn't this be a "group"? 65 | &ast.Symbol{Text: "+"}, 66 | &ast.Literal{Text: "10"}, 67 | &ast.Word{Text: "x"}, 68 | }, 69 | }, 70 | }, 71 | }, 72 | { 73 | input: `$\cos$`, 74 | want: ast.List{ 75 | &ast.MathExpr{ 76 | Delim: "$", 77 | List: ast.List{ 78 | &ast.Macro{ 79 | Name: &ast.Ident{Name: `\cos`}, 80 | }, 81 | }, 82 | }, 83 | }, 84 | }, 85 | { 86 | input: `$\sqrt{2x\pi}$`, 87 | want: ast.List{ 88 | &ast.MathExpr{ 89 | Delim: "$", 90 | List: ast.List{ 91 | &ast.Macro{ 92 | Name: &ast.Ident{Name: `\sqrt`}, 93 | Args: ast.List{ 94 | &ast.Arg{ 95 | List: ast.List{ 96 | &ast.Literal{ 97 | Text: "2", 98 | }, 99 | &ast.Word{ 100 | Text: "x", 101 | }, 102 | &ast.Macro{Name: &ast.Ident{Name: `\pi`}}, 103 | }, 104 | }, 105 | }, 106 | }, 107 | }, 108 | }, 109 | }, 110 | }, 111 | { 112 | input: `$\sqrt[3]{2x\pi}$`, 113 | want: ast.List{ 114 | &ast.MathExpr{ 115 | Delim: "$", 116 | List: ast.List{ 117 | &ast.Macro{ 118 | Name: &ast.Ident{Name: `\sqrt`}, 119 | Args: ast.List{ 120 | &ast.OptArg{ 121 | List: ast.List{ 122 | &ast.Literal{ 123 | Text: "3", 124 | }, 125 | }, 126 | }, 127 | &ast.Arg{ 128 | List: ast.List{ 129 | &ast.Literal{ 130 | Text: "2", 131 | }, 132 | &ast.Word{ 133 | Text: "x", 134 | }, 135 | &ast.Macro{Name: &ast.Ident{Name: `\pi`}}, 136 | }, 137 | }, 138 | }, 139 | }, 140 | }, 141 | }, 142 | }, 143 | }, 144 | { 145 | input: `$\sqrt[n]{2x\pi}$`, 146 | want: ast.List{ 147 | &ast.MathExpr{ 148 | Delim: "$", 149 | List: ast.List{ 150 | &ast.Macro{ 151 | Name: &ast.Ident{Name: `\sqrt`}, 152 | Args: ast.List{ 153 | &ast.OptArg{ 154 | List: ast.List{ 155 | &ast.Word{ 156 | Text: "n", 157 | }, 158 | }, 159 | }, 160 | &ast.Arg{ 161 | List: ast.List{ 162 | &ast.Literal{ 163 | Text: "2", 164 | }, 165 | &ast.Word{ 166 | Text: "x", 167 | }, 168 | &ast.Macro{Name: &ast.Ident{Name: `\pi`}}, 169 | }, 170 | }, 171 | }, 172 | }, 173 | }, 174 | }, 175 | }, 176 | }, 177 | { 178 | input: `$\exp{2x\pi}$`, 179 | want: ast.List{ 180 | &ast.MathExpr{ 181 | Delim: "$", 182 | List: ast.List{ 183 | &ast.Macro{ 184 | Name: &ast.Ident{Name: `\exp`}, 185 | Args: ast.List{ 186 | &ast.Arg{ 187 | List: ast.List{ 188 | &ast.Literal{ 189 | Text: "2", 190 | }, 191 | &ast.Word{ 192 | Text: "x", 193 | }, 194 | &ast.Macro{Name: &ast.Ident{Name: `\pi`}}, 195 | }, 196 | }, 197 | }, 198 | }, 199 | }, 200 | }, 201 | }, 202 | }, 203 | { 204 | input: `$e^\pi$`, 205 | want: ast.List{ 206 | &ast.MathExpr{ 207 | Delim: "$", 208 | List: ast.List{ 209 | &ast.Word{Text: "e"}, 210 | &ast.Sup{Node: &ast.Macro{ 211 | Name: &ast.Ident{Name: `\pi`}, 212 | }}, 213 | }, 214 | }, 215 | }, 216 | }, 217 | { 218 | input: `$\mathcal{L}$`, 219 | want: ast.List{ 220 | &ast.MathExpr{ 221 | Delim: "$", 222 | List: ast.List{ 223 | &ast.Macro{ 224 | Name: &ast.Ident{Name: `\mathcal`}, 225 | Args: ast.List{ 226 | &ast.Arg{ 227 | List: ast.List{ 228 | &ast.Word{Text: "L"}, // FIXME: or Ident? 229 | }, 230 | }, 231 | }, 232 | }, 233 | }, 234 | }, 235 | }, 236 | }, 237 | { 238 | input: `$\frac{num}{den}$`, 239 | want: ast.List{ 240 | &ast.MathExpr{ 241 | Delim: "$", 242 | List: ast.List{ 243 | &ast.Macro{ 244 | Name: &ast.Ident{Name: `\frac`}, 245 | Args: ast.List{ 246 | &ast.Arg{ 247 | List: ast.List{ 248 | &ast.Word{Text: "num"}, 249 | }, 250 | }, 251 | &ast.Arg{ 252 | List: ast.List{ 253 | &ast.Word{Text: "den"}, 254 | }, 255 | }, 256 | }, 257 | }, 258 | }, 259 | }, 260 | }, 261 | }, 262 | { 263 | input: `$\sqrt{\frac{e^{3i\pi}}{2\cos 3\pi}}$`, 264 | want: ast.List{ 265 | &ast.MathExpr{ 266 | List: ast.List{ 267 | &ast.Macro{ 268 | Name: &ast.Ident{Name: `\sqrt`}, 269 | Args: ast.List{ 270 | &ast.Arg{ 271 | List: ast.List{ 272 | &ast.Macro{ 273 | Name: &ast.Ident{Name: `\frac`}, 274 | Args: ast.List{ 275 | &ast.Arg{ 276 | List: ast.List{ 277 | &ast.Word{Text: "e"}, 278 | &ast.Sup{Node: ast.List{ 279 | &ast.Literal{Text: "3"}, 280 | &ast.Word{Text: "i"}, 281 | &ast.Macro{Name: &ast.Ident{Name: "\\pi"}}, 282 | }}, 283 | }, 284 | }, 285 | &ast.Arg{ 286 | List: ast.List{ 287 | &ast.Literal{Text: "2"}, 288 | &ast.Macro{Name: &ast.Ident{Name: `\cos`}}, 289 | &ast.Literal{Text: "3"}, 290 | &ast.Macro{Name: &ast.Ident{Name: `\pi`}}, 291 | }, 292 | }, 293 | }, 294 | }, 295 | }, 296 | }, 297 | }, 298 | }, 299 | }, 300 | }, 301 | }, 302 | }, 303 | { 304 | input: `$\sqrt{\frac{e^{3i\pi}}{2\cos 3\pi}}$ \textbf{APLAS} Dummy -- $\sqrt{s}=13\,$TeV $\mathcal{L}\,=\,3\,ab^{-1}$`, 305 | want: ast.List{ 306 | &ast.MathExpr{ 307 | List: ast.List{ 308 | &ast.Macro{ 309 | Name: &ast.Ident{Name: `\sqrt`}, 310 | Args: ast.List{ 311 | &ast.Arg{ 312 | List: ast.List{ 313 | &ast.Macro{ 314 | Name: &ast.Ident{Name: `\frac`}, 315 | Args: ast.List{ 316 | &ast.Arg{ 317 | List: ast.List{ 318 | &ast.Word{Text: "e"}, 319 | &ast.Sup{Node: ast.List{ 320 | &ast.Literal{Text: "3"}, 321 | &ast.Word{Text: "i"}, 322 | &ast.Macro{Name: &ast.Ident{Name: "\\pi"}}, 323 | }}, 324 | }, 325 | }, 326 | &ast.Arg{ 327 | List: ast.List{ 328 | &ast.Literal{Text: "2"}, 329 | &ast.Macro{Name: &ast.Ident{Name: `\cos`}}, 330 | &ast.Literal{Text: "3"}, 331 | &ast.Macro{Name: &ast.Ident{Name: `\pi`}}, 332 | }, 333 | }, 334 | }, 335 | }, 336 | }, 337 | }, 338 | }, 339 | }, 340 | }, 341 | }, 342 | &ast.Symbol{Text: " "}, 343 | &ast.Macro{ 344 | Name: &ast.Ident{Name: `\textbf`}, 345 | Args: ast.List{ 346 | &ast.Arg{ 347 | List: ast.List{ 348 | &ast.Word{Text: "APLAS"}, 349 | }, 350 | }, 351 | }, 352 | }, 353 | &ast.Symbol{Text: " "}, 354 | &ast.Word{Text: "Dummy"}, 355 | &ast.Symbol{Text: " "}, 356 | &ast.Symbol{Text: "-"}, 357 | &ast.Symbol{Text: "-"}, 358 | &ast.Symbol{Text: " "}, 359 | &ast.MathExpr{ 360 | List: ast.List{ 361 | &ast.Macro{ 362 | Name: &ast.Ident{Name: "\\sqrt"}, 363 | Args: ast.List{ 364 | &ast.Arg{ 365 | List: ast.List{ 366 | &ast.Word{Text: "s"}, 367 | }, 368 | }, 369 | }, 370 | }, 371 | &ast.Symbol{Text: "="}, 372 | &ast.Literal{Text: "13"}, 373 | &ast.Macro{Name: &ast.Ident{Name: "\\,"}}, 374 | }, 375 | }, 376 | &ast.Word{Text: "TeV"}, 377 | &ast.Symbol{Text: " "}, 378 | &ast.MathExpr{ 379 | List: ast.List{ 380 | &ast.Macro{ 381 | Name: &ast.Ident{Name: "\\mathcal"}, 382 | Args: ast.List{ 383 | &ast.Arg{ 384 | List: ast.List{ 385 | &ast.Word{Text: "L"}, 386 | }, 387 | }, 388 | }, 389 | }, 390 | &ast.Macro{Name: &ast.Ident{Name: "\\,"}}, 391 | &ast.Symbol{Text: "="}, 392 | &ast.Macro{Name: &ast.Ident{Name: "\\,"}}, 393 | &ast.Literal{Text: "3"}, 394 | &ast.Macro{Name: &ast.Ident{Name: "\\,"}}, 395 | &ast.Word{Text: "ab"}, 396 | &ast.Sup{ 397 | Node: ast.List{ 398 | &ast.Symbol{Text: "-"}, 399 | &ast.Literal{Text: "1"}, 400 | }, 401 | }, 402 | }, 403 | }, 404 | }, 405 | }, 406 | // { // FIXME(sbinet): not ready 407 | // input: `\[x =3\]`, 408 | // want: nil, 409 | // }, 410 | // { // FIXME(sbinet): not ready 411 | // input: `\(x =3\)`, 412 | // want: nil, 413 | // }, 414 | // { // FIXME(sbinet): not ready 415 | // input: `\begin{equation}x=3\end{equation}`, 416 | // want: nil, 417 | // }, 418 | { 419 | input: `$x_i$`, 420 | want: ast.List{ 421 | &ast.MathExpr{ 422 | List: ast.List{ 423 | &ast.Word{Text: "x"}, 424 | &ast.Sub{ 425 | Node: &ast.Word{Text: "i"}, 426 | }, 427 | }, 428 | }, 429 | }, 430 | }, 431 | { 432 | input: `$x^n$`, 433 | want: ast.List{ 434 | &ast.MathExpr{ 435 | List: ast.List{ 436 | &ast.Word{Text: "x"}, 437 | &ast.Sup{ 438 | Node: &ast.Word{Text: "n"}, 439 | }, 440 | }, 441 | }, 442 | }, 443 | }, 444 | { 445 | input: `$\sum_{i=0}^{n}$`, 446 | want: ast.List{ 447 | &ast.MathExpr{ 448 | List: ast.List{ 449 | &ast.Macro{ 450 | Name: &ast.Ident{Name: `\sum`}, 451 | }, 452 | &ast.Sub{ 453 | Node: ast.List{ 454 | &ast.Word{Text: "i"}, 455 | &ast.Symbol{Text: "="}, 456 | &ast.Literal{Text: "0"}, 457 | }, 458 | }, 459 | &ast.Sup{ 460 | Node: ast.List{ 461 | &ast.Word{Text: "n"}, 462 | }, 463 | }, 464 | }, 465 | }, 466 | }, 467 | }, 468 | } { 469 | t.Run("", func(t *testing.T) { 470 | node, err := ParseExpr(tc.input) 471 | if err != nil { 472 | t.Fatal(err) 473 | } 474 | got := new(strings.Builder) 475 | ast.Print(got, node) 476 | 477 | want := new(strings.Builder) 478 | ast.Print(want, tc.want) 479 | 480 | if got.String() != want.String() { 481 | t.Fatalf("invalid ast:\ngot: %v\nwant:%v", got, want) 482 | } 483 | }) 484 | } 485 | 486 | } 487 | 488 | func TestTokenPos(t *testing.T) { 489 | for _, tc := range []struct { 490 | input string 491 | want ast.Node 492 | }{ 493 | { 494 | input: `hello`, 495 | want: ast.List{&ast.Word{WordPos: 0, Text: "hello"}}, 496 | }, 497 | { 498 | input: `hello world`, 499 | want: ast.List{ 500 | &ast.Word{Text: "hello"}, 501 | &ast.Symbol{Text: " ", SymPos: 5}, 502 | &ast.Word{Text: "world", WordPos: 6}, 503 | }, 504 | }, 505 | { 506 | input: `empty equation $$`, 507 | want: ast.List{ 508 | &ast.Word{Text: "empty"}, 509 | &ast.Symbol{Text: " ", SymPos: 5}, 510 | &ast.Word{Text: "equation", WordPos: 6}, 511 | &ast.Symbol{Text: " ", SymPos: 14}, 512 | &ast.MathExpr{ 513 | Delim: "$", 514 | Left: 15, 515 | Right: 16, 516 | }, 517 | }, 518 | }, 519 | { 520 | input: `$+10x$`, 521 | want: ast.List{ 522 | &ast.MathExpr{ 523 | Delim: "$", 524 | Left: 0, 525 | List: ast.List{ 526 | &ast.Symbol{Text: "+", SymPos: 1}, 527 | &ast.Literal{Text: "10", LitPos: 2}, 528 | &ast.Word{Text: "x", WordPos: 4}, 529 | }, 530 | Right: 5, 531 | }, 532 | }, 533 | }, 534 | { 535 | input: `$\sqrt{2x\pi}$`, 536 | want: ast.List{ 537 | &ast.MathExpr{ 538 | Delim: "$", 539 | Left: 0, 540 | List: ast.List{ 541 | &ast.Macro{ 542 | Name: &ast.Ident{Name: `\sqrt`, NamePos: 1}, 543 | Args: ast.List{ 544 | &ast.Arg{ 545 | Lbrace: 6, 546 | List: ast.List{ 547 | &ast.Literal{ 548 | Text: "2", 549 | LitPos: 7, 550 | }, 551 | &ast.Word{ 552 | Text: "x", 553 | WordPos: 8, 554 | }, 555 | &ast.Macro{Name: &ast.Ident{ 556 | Name: `\pi`, 557 | NamePos: 9, 558 | }}, 559 | }, 560 | Rbrace: 12, 561 | }, 562 | }, 563 | }, 564 | }, 565 | Right: 13, 566 | }, 567 | }, 568 | }, 569 | { 570 | input: `$e^\pi$`, 571 | want: ast.List{ 572 | &ast.MathExpr{ 573 | Delim: "$", 574 | Left: 0, 575 | List: ast.List{ 576 | &ast.Word{Text: "e", WordPos: 1}, 577 | &ast.Sup{ 578 | HatPos: 2, 579 | Node: &ast.Macro{ 580 | Name: &ast.Ident{Name: `\pi`, NamePos: 3}, 581 | }, 582 | }, 583 | }, 584 | Right: 6, 585 | }, 586 | }, 587 | }, 588 | // { // FIXME(sbinet): not ready 589 | // input: `\[x =3\]`, 590 | // want: nil, 591 | // }, 592 | // { // FIXME(sbinet): not ready 593 | // input: `\(x =3\)`, 594 | // want: nil, 595 | // }, 596 | // { // FIXME(sbinet): not ready 597 | // input: `\begin{equation}x=3\end{equation}`, 598 | // want: nil, 599 | // }, 600 | { 601 | input: `$x_i$`, 602 | want: ast.List{ 603 | &ast.MathExpr{ 604 | Delim: "$", 605 | Left: 0, 606 | List: ast.List{ 607 | &ast.Word{Text: "x", WordPos: 1}, 608 | &ast.Sub{ 609 | UnderPos: 2, 610 | Node: &ast.Word{Text: "i", WordPos: 3}, 611 | }, 612 | }, 613 | Right: 4, 614 | }, 615 | }, 616 | }, 617 | } { 618 | t.Run("", func(t *testing.T) { 619 | node, err := ParseExpr(tc.input) 620 | if err != nil { 621 | t.Fatal(err) 622 | } 623 | 624 | if got, want := node, tc.want; !reflect.DeepEqual(got, want) { 625 | t.Fatalf("invalid positions:\ngot= %v\nwant=%v", got, want) 626 | } 627 | }) 628 | } 629 | 630 | } 631 | -------------------------------------------------------------------------------- /scanner.go: -------------------------------------------------------------------------------- 1 | // Copyright ©2020 The go-latex Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package latex 6 | 7 | import ( 8 | "fmt" 9 | "io" 10 | "strings" 11 | "text/scanner" 12 | "unicode" 13 | 14 | "github.com/go-latex/latex/token" 15 | ) 16 | 17 | type texScanner struct { 18 | sc scanner.Scanner 19 | 20 | r rune 21 | tok token.Token 22 | } 23 | 24 | func newScanner(r io.Reader) *texScanner { 25 | sc := &texScanner{} 26 | sc.sc.Init(r) 27 | sc.sc.Mode = (scanner.ScanIdents | scanner.ScanInts | scanner.ScanFloats) 28 | sc.sc.Mode |= scanner.ScanStrings 29 | //scanner.ScanRawStrings) 30 | // sc.sc.Error = func(s *scanner.Scanner, msg string) {} 31 | sc.sc.IsIdentRune = func(ch rune, i int) bool { 32 | return unicode.IsLetter(ch) //|| unicode.IsDigit(ch) && i > 0 33 | } 34 | sc.sc.Whitespace = 1<<'\t' | 1<<'\n' | 1<<'\r' 35 | return sc 36 | } 37 | 38 | // Token returns the most recently parsed token 39 | func (s *texScanner) Token() token.Token { 40 | return s.tok 41 | } 42 | 43 | // Next iterates over all tokens. 44 | // Next retrieves the most recent token with Token(). 45 | // It returns false once it reaches token.EOF. 46 | func (s *texScanner) Next() bool { 47 | s.tok = s.scan() 48 | return s.tok.Kind != token.EOF 49 | } 50 | 51 | func (s *texScanner) scan() token.Token { 52 | s.next() 53 | pos := s.pos() 54 | switch s.r { 55 | case scanner.Ident: 56 | return token.Token{ 57 | Kind: token.Word, 58 | Pos: pos, 59 | Text: s.sc.TokenText(), 60 | } 61 | case '\\': 62 | nxt := s.sc.Peek() 63 | switch nxt { 64 | case ' ': 65 | s.next() 66 | return token.Token{ 67 | Kind: token.Space, 68 | Pos: pos, 69 | Text: `\ `, 70 | } 71 | default: 72 | return s.scanMacro() 73 | } 74 | case ' ': 75 | return token.Token{ 76 | Kind: token.Space, 77 | Pos: pos, 78 | Text: ` `, 79 | } 80 | 81 | case '%': 82 | line := s.scanComment() 83 | return token.Token{ 84 | Kind: token.Comment, 85 | Pos: pos, 86 | Text: line, 87 | } 88 | 89 | case '$', '_', '=', '<', '>', '^', '/', '*', '-', '+', 90 | '!', '?', '\'', ':', ',', ';', '.': 91 | return token.Token{ 92 | Kind: token.Symbol, 93 | Pos: pos, 94 | Text: s.sc.TokenText(), 95 | } 96 | 97 | case '[': 98 | return token.Token{ 99 | Kind: token.Lbrack, 100 | Pos: pos, 101 | Text: s.sc.TokenText(), 102 | } 103 | case ']': 104 | return token.Token{ 105 | Kind: token.Rbrack, 106 | Pos: pos, 107 | Text: s.sc.TokenText(), 108 | } 109 | case '{': 110 | return token.Token{ 111 | Kind: token.Lbrace, 112 | Pos: pos, 113 | Text: s.sc.TokenText(), 114 | } 115 | case '}': 116 | return token.Token{ 117 | Kind: token.Rbrace, 118 | Pos: pos, 119 | Text: s.sc.TokenText(), 120 | } 121 | case '(': 122 | return token.Token{ 123 | Kind: token.Lparen, 124 | Pos: pos, 125 | Text: s.sc.TokenText(), 126 | } 127 | case ')': 128 | return token.Token{ 129 | Kind: token.Rparen, 130 | Pos: pos, 131 | Text: s.sc.TokenText(), 132 | } 133 | case scanner.Int, scanner.Float: 134 | return token.Token{ 135 | Kind: token.Number, 136 | Pos: pos, 137 | Text: s.sc.TokenText(), 138 | } 139 | case scanner.String, scanner.Char: 140 | return token.Token{ 141 | Kind: token.Other, 142 | Pos: pos, 143 | Text: s.sc.TokenText(), 144 | } 145 | case scanner.EOF: 146 | return token.Token{ 147 | Kind: token.EOF, 148 | Pos: pos, 149 | } 150 | default: 151 | panic(fmt.Errorf("unhandled token: %v %v", scanner.TokenString(s.r), s.r)) 152 | } 153 | } 154 | 155 | func (s *texScanner) next() { 156 | s.r = s.sc.Scan() 157 | } 158 | 159 | func (s *texScanner) scanMacro() token.Token { 160 | var ( 161 | macro = new(strings.Builder) 162 | pos = s.pos() 163 | ) 164 | s.next() 165 | macro.WriteString(`\` + s.sc.TokenText()) 166 | 167 | return token.Token{ 168 | Kind: token.Macro, 169 | Pos: pos, 170 | Text: macro.String(), 171 | } 172 | } 173 | 174 | func (s *texScanner) scanComment() string { 175 | comment := new(strings.Builder) 176 | comment.WriteString("%") 177 | wsp := s.sc.Whitespace 178 | defer func() { 179 | s.sc.Whitespace = wsp 180 | }() 181 | s.sc.Whitespace = 0 182 | 183 | for { 184 | s.next() 185 | if s.r == '\r' { 186 | continue 187 | } 188 | if s.r == '\n' || s.r == scanner.EOF { 189 | break 190 | } 191 | comment.WriteString(s.sc.TokenText()) 192 | } 193 | return comment.String() 194 | } 195 | 196 | // func (s *texScanner) expect(want rune) { 197 | // s.next() 198 | // if s.r != want { 199 | // panic(fmt.Errorf("invalid rune: got=%q, want=%q", s.r, want)) 200 | // } 201 | // } 202 | 203 | func (s *texScanner) pos() token.Pos { 204 | return token.Pos(s.sc.Position.Offset) 205 | } 206 | -------------------------------------------------------------------------------- /scanner_test.go: -------------------------------------------------------------------------------- 1 | // Copyright ©2020 The go-latex Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package latex 6 | 7 | import ( 8 | "log" 9 | "strings" 10 | "testing" 11 | ) 12 | 13 | func TestScanner(t *testing.T) { 14 | for _, tc := range []struct { 15 | name string 16 | input string 17 | }{ 18 | { 19 | name: "math", 20 | input: `$\sigma_1 = 22x$ ? ok`, 21 | }, 22 | { 23 | name: "", 24 | input: `$\sqrt{\frac{e^{3i\pi}}{2\cos 3\pi}}$`, 25 | }, 26 | { 27 | name: "", 28 | input: `\textbf{APLAS} Dummy -- $\sqrt{s}=13\,$TeV $\mathcal{L}\,=\,3\,ab^{-1}$`, 29 | }, 30 | { 31 | name: "comment", 32 | input: "% boo is 42\r\n%% bar\tis not boo", 33 | }, 34 | { 35 | name: "", 36 | input: "hello\n\\\\world!\\ boo", 37 | }, 38 | { 39 | name: "numbers", 40 | input: "x=23.4\ny=42.\nz=43.x\nw=0x32\nu='c'\nv=\"hello\"", 41 | }, 42 | { 43 | name: "chars", 44 | input: `x='cos'`, 45 | }, 46 | } { 47 | t.Run(tc.name, func(t *testing.T) { 48 | sc := newScanner(strings.NewReader(tc.input)) 49 | for sc.Next() { 50 | tok := sc.Token() 51 | log.Printf("tok: %#v", tok) 52 | } 53 | }) 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /tex/box_test.go: -------------------------------------------------------------------------------- 1 | // Copyright ©2020 The go-latex Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package tex 6 | 7 | import ( 8 | "math" 9 | "testing" 10 | 11 | "github.com/go-latex/latex/font" 12 | "github.com/go-latex/latex/internal/fakebackend" 13 | ) 14 | 15 | func TestBox(t *testing.T) { 16 | const dpi = 72 17 | be := fakebackend.New() 18 | state := NewState(be, font.Font{ 19 | Name: "default", 20 | Size: 12, 21 | Type: "rm", 22 | }, dpi) 23 | for _, tc := range []struct { 24 | node Node 25 | w, h, d float64 26 | }{ 27 | { 28 | node: HBox(10), 29 | w: 10, 30 | h: 0, 31 | d: 0, 32 | }, 33 | { 34 | node: VBox(10, 20), 35 | w: 0, 36 | h: 10, 37 | d: 20, 38 | }, 39 | { 40 | node: HListOf([]Node{VBox(10, 20), HBox(30)}, false), 41 | w: 30, 42 | h: 10, 43 | d: 20, 44 | }, 45 | { 46 | node: HListOf([]Node{VBox(10, 20), HBox(30)}, true), 47 | w: 30, 48 | h: 10, 49 | d: 20, 50 | }, 51 | { 52 | node: VListOf([]Node{VBox(10, 20), HBox(30)}), 53 | w: 30, 54 | h: 30, 55 | d: 0, 56 | }, 57 | { 58 | node: HListOf([]Node{ 59 | VBox(10, 20), HBox(30), 60 | HListOf([]Node{HBox(11), HBox(22)}, false), 61 | }, false), 62 | w: 63, 63 | h: 10, 64 | d: 20, 65 | }, 66 | { 67 | node: HListOf([]Node{ 68 | VBox(10, 20), HBox(30), 69 | HListOf([]Node{HBox(11), HBox(22)}, false), 70 | VListOf([]Node{HBox(15), VBox(11, 22)}), 71 | }, false), 72 | w: 78, 73 | h: 11, 74 | d: 22, 75 | }, 76 | { 77 | node: HListOf([]Node{VBox(10, 20), NewKern(15), HBox(30)}, true), 78 | w: 45, 79 | h: 10, 80 | d: 20, 81 | }, 82 | { 83 | node: VListOf([]Node{ 84 | VBox(10, 20), 85 | VListOf([]Node{ 86 | VBox(11, 22), 87 | NewKern(10), 88 | HBox(40), 89 | }), 90 | HListOf([]Node{VBox(10, 20), NewKern(15), HBox(30)}, true), 91 | HBox(30), 92 | }), 93 | w: 45, 94 | h: 103, 95 | d: 0, 96 | }, 97 | { 98 | node: NewKern(10), 99 | w: 10, 100 | h: 0, 101 | d: 0, 102 | }, 103 | { 104 | node: NewGlue("fil"), 105 | }, 106 | { 107 | node: NewGlue("fill"), 108 | }, 109 | { 110 | node: NewGlue("filll"), 111 | }, 112 | { 113 | node: NewGlue("neg_fil"), 114 | }, 115 | { 116 | node: NewGlue("neg_fill"), 117 | }, 118 | { 119 | node: NewGlue("neg_filll"), 120 | }, 121 | { 122 | node: NewGlue("empty"), 123 | }, 124 | { 125 | node: NewGlue("ss"), 126 | }, 127 | { 128 | node: VListOf([]Node{ 129 | NewKern(10), 130 | VBox(10, 20), 131 | NewKern(10), 132 | VListOf([]Node{ 133 | NewKern(10), 134 | VBox(11, 22), 135 | NewKern(10), 136 | HBox(40), 137 | NewKern(10), 138 | }), 139 | NewKern(10), 140 | HListOf([]Node{ 141 | NewKern(10), VBox(10, 20), 142 | NewKern(15), HBox(30), 143 | NewKern(10), 144 | }, true), 145 | NewKern(10), 146 | HBox(30), 147 | NewKern(10), 148 | }), 149 | w: 65, 150 | h: 173, 151 | d: 0, 152 | }, 153 | { 154 | node: VListOf([]Node{ 155 | NewKern(10), 156 | VBox(10, 20), 157 | NewGlue("fill"), 158 | NewKern(10), 159 | VListOf([]Node{ 160 | NewKern(10), 161 | VBox(11, 22), 162 | NewKern(10), 163 | NewGlue("neg_fill"), 164 | HBox(40), 165 | NewKern(10), 166 | }), 167 | NewKern(10), 168 | HListOf([]Node{ 169 | NewKern(10), VBox(10, 20), 170 | NewGlue("empty"), 171 | NewKern(15), HBox(30), 172 | NewKern(10), 173 | }, true), 174 | NewKern(10), 175 | NewGlue("ss"), 176 | HBox(30), 177 | NewKern(10), 178 | }), 179 | w: 65, 180 | h: 173, 181 | d: 0, 182 | }, 183 | { 184 | node: HListOf([]Node{ 185 | NewKern(10), 186 | NewGlue("fil"), 187 | VBox(10, 20), HBox(30), 188 | NewGlue("fil"), 189 | HListOf([]Node{HBox(11), NewGlue("filll"), HBox(22)}, true), 190 | VListOf([]Node{HBox(15), NewGlue("neg_filll"), VBox(11, 22)}), 191 | }, true), 192 | w: 88, 193 | h: 11, 194 | d: 22, 195 | }, 196 | { 197 | node: HCentered([]Node{ 198 | VBox(10, 20), 199 | HBox(30), 200 | NewKern(15), 201 | HBox(40), 202 | VBox(20, 10), 203 | }), 204 | w: 85, 205 | h: 20, 206 | d: 20, 207 | }, 208 | { 209 | node: VCentered([]Node{ 210 | VBox(10, 20), 211 | HBox(30), 212 | NewKern(15), 213 | HBox(40), 214 | VBox(20, 10), 215 | }), 216 | w: 40, 217 | h: 75, 218 | d: 0, 219 | }, 220 | { 221 | node: NewChar("a", state, false), 222 | w: 5.53125, 223 | h: 6.71875, 224 | d: 0.171875, 225 | }, 226 | { 227 | node: NewChar("a", state, true), 228 | w: 5.53125, 229 | h: 6.71875, 230 | d: 0.171875, 231 | }, 232 | { 233 | node: NewChar(" ", state, false), 234 | w: 3.814453125, 235 | h: 0, 236 | d: 0, 237 | }, 238 | { 239 | node: NewChar(" ", state, true), 240 | w: 3.814453125, 241 | h: 0, 242 | d: 0, 243 | }, 244 | { 245 | node: NewChar(`\sigma`, state, true), 246 | w: 6.578125, 247 | h: 6.5625, 248 | d: 0.171875, 249 | }, 250 | { 251 | node: NewChar(`\sum`, state, true), 252 | w: 10.890625, 253 | h: 12.203125, 254 | d: 3.265625, 255 | }, 256 | { 257 | node: NewChar(`\oint`, state, true), 258 | w: 6.90625, 259 | h: 12.84375, 260 | d: 3.609375, 261 | }, 262 | { 263 | node: NewAccent(`é`, state, false), 264 | w: 6.09375, 265 | h: 9.765625, 266 | d: 0, 267 | }, 268 | { 269 | node: HRule(state, -1), 270 | w: math.Inf(+1), 271 | h: 0.375, 272 | d: 0.375, 273 | }, 274 | { 275 | node: HRule(state, 10), 276 | w: math.Inf(+1), 277 | h: 5, 278 | d: 5, 279 | }, 280 | { 281 | node: VRule(state), 282 | w: 0.75, 283 | h: math.Inf(+1), 284 | d: math.Inf(+1), 285 | }, 286 | { 287 | node: HListOf([]Node{ 288 | NewKern(10), 289 | NewChar("A", state, false), NewChar("V", state, false), 290 | NewChar("A", state, false), NewChar("V", state, false), 291 | NewAccent("é", state, false), NewAccent("é", state, false), 292 | NewChar("A", state, false), NewChar(`\sigma`, state, true), 293 | NewChar(`\sum`, state, true), NewChar(`\sigma`, state, true), 294 | NewKern(10), 295 | }, true), 296 | w: 102.453125, 297 | h: 12.203125, 298 | d: 3.265625, 299 | }, 300 | { 301 | node: HListOf([]Node{ 302 | NewKern(10), 303 | NewChar("A", state, false), NewChar("V", state, false), 304 | NewChar("A", state, false), NewChar("V", state, false), 305 | NewAccent("é", state, false), NewAccent("é", state, false), 306 | NewChar("A", state, false), NewChar(`\sigma`, state, true), 307 | NewChar(`\sum`, state, true), NewChar(`\sigma`, state, true), 308 | NewKern(10), 309 | }, false), 310 | w: 96.3125, 311 | h: 12.203125, 312 | d: 3.265625, 313 | }, 314 | { 315 | node: HListOf([]Node{ 316 | NewKern(10), 317 | NewChar(`\sigma`, state, true), 318 | HRule(state, -1), 319 | NewChar(`\sum`, state, true), 320 | NewKern(10), 321 | }, true), 322 | w: math.Inf(+1), 323 | h: 12.203125, 324 | d: 3.265625, 325 | }, 326 | { 327 | node: HListOf([]Node{ 328 | NewKern(10), 329 | NewChar(`\sigma`, state, true), 330 | HRule(state, -1), 331 | NewChar(`\sum`, state, true), 332 | NewKern(10), 333 | }, false), 334 | w: math.Inf(+1), 335 | h: 12.203125, 336 | d: 3.265625, 337 | }, 338 | { 339 | node: HListOf([]Node{ 340 | NewKern(10), 341 | NewChar(`\sigma`, state, true), 342 | VRule(state), 343 | NewChar(`\sum`, state, true), 344 | NewKern(10), 345 | }, true), 346 | w: 39.787109375, 347 | h: 12.203125, 348 | d: 3.265625, 349 | }, 350 | { 351 | node: HListOf([]Node{ 352 | NewKern(10), 353 | NewChar(`\sigma`, state, true), 354 | VRule(state), 355 | NewChar(`\sum`, state, true), 356 | NewKern(10), 357 | HListOf(nil, true), 358 | }, false), 359 | w: 38.21875, 360 | h: 12.203125, 361 | d: 3.265625, 362 | }, 363 | { 364 | node: HListOf([]Node{ 365 | NewKern(10), 366 | NewAccent(`é`, state, false), 367 | VRule(state), 368 | NewChar(`\sum`, state, true), 369 | NewKern(10), 370 | HListOf(nil, true), 371 | VListOf(nil), 372 | }, false), 373 | w: 37.734375, 374 | h: 12.203125, 375 | d: 3.265625, 376 | }, 377 | { 378 | node: VListOf([]Node{ 379 | NewKern(10), 380 | VRule(state), 381 | HRule(state, 10), 382 | NewKern(10), 383 | HListOf(nil, true), 384 | VListOf(nil), 385 | }), 386 | w: 0.75, 387 | h: math.Inf(+1), 388 | d: 0, 389 | }, 390 | { 391 | node: AutoHeightChar(`(`, 8.865625, 2.5703124999999996, state, 0), 392 | w: 5.0047149658203125, 393 | h: 9.734375, 394 | d: 1.6875, 395 | }, 396 | } { 397 | t.Run("", func(t *testing.T) { 398 | var ( 399 | w = tc.node.Width() 400 | h = tc.node.Height() 401 | d = tc.node.Depth() 402 | ) 403 | 404 | if got, want := w, tc.w; !cmpEq(got, want) { 405 | t.Fatalf("invalid width: got=%g, want=%g", got, want) 406 | } 407 | 408 | if got, want := h, tc.h; !cmpEq(got, want) { 409 | t.Fatalf("invalid height: got=%g, want=%g", got, want) 410 | } 411 | 412 | if got, want := d, tc.d; !cmpEq(got, want) { 413 | t.Fatalf("invalid depth: got=%g, want=%g", got, want) 414 | } 415 | }) 416 | } 417 | } 418 | 419 | func TestShip(t *testing.T) { 420 | const dpi = 72 421 | be := fakebackend.New() 422 | state := NewState(be, font.Font{ 423 | Name: "default", 424 | Size: 12, 425 | Type: "rm", 426 | }, dpi) 427 | for _, tc := range []struct { 428 | node Tree 429 | s int 430 | v, h float64 431 | }{ 432 | { 433 | node: HListOf([]Node{ 434 | NewKern(10), 435 | VRule(state), 436 | HListOf(nil, true), 437 | VListOf(nil), 438 | }, false), 439 | s: 0, 440 | v: 0, 441 | h: 10.75, 442 | }, 443 | { 444 | node: HListOf([]Node{ 445 | NewKern(10), 446 | NewAccent(`é`, state, false), 447 | VRule(state), 448 | NewChar(`\sum`, state, true), 449 | NewKern(10), 450 | HListOf(nil, true), 451 | VListOf(nil), 452 | }, false), 453 | s: 0, 454 | v: 0, 455 | h: 37.734375, 456 | }, 457 | { 458 | node: VListOf([]Node{ 459 | HRule(state, 10), 460 | }), 461 | s: 0, 462 | v: 0, 463 | h: math.Inf(+1), 464 | }, 465 | { 466 | node: VListOf([]Node{ 467 | NewKern(10), 468 | VRule(state), 469 | NewKern(10), 470 | HListOf(nil, true), 471 | VListOf(nil), 472 | NewGlue("filll"), 473 | }), 474 | s: 0, 475 | v: 0, 476 | h: 20.75, 477 | }, 478 | { 479 | node: VListOf([]Node{ 480 | NewKern(10), 481 | VRule(state), 482 | NewKern(10), 483 | HListOf(nil, true), 484 | NewGlue("fil"), 485 | HCentered([]Node{ 486 | NewChar("a", state, false), 487 | NewChar("a", state, false), 488 | }), 489 | NewGlue("fill"), 490 | NewGlue("filll"), 491 | VListOf([]Node{ 492 | NewKern(10), 493 | NewGlue("neg_fil"), 494 | NewGlue("neg_fill"), 495 | NewGlue("neg_filll"), 496 | HListOf(nil, true), 497 | HListOf([]Node{ 498 | NewKern(10), 499 | NewKern(10), 500 | NewAccent("é", state, false), 501 | NewChar(`\sigma`, state, true), 502 | NewChar(`a`, state, false), 503 | HRule(state, 10), 504 | VRule(state), 505 | HListOf([]Node{NewChar("a", state, false)}, true), 506 | VListOf([]Node{ 507 | VBox(1, 2), 508 | VBox(2, 3), 509 | HBox(10), 510 | VListOf([]Node{ 511 | VBox(1, 2), 512 | VBox(1, 2), 513 | HBox(10), 514 | VListOf(nil), 515 | HRule(state, 10), 516 | VRule(state), 517 | }), 518 | }), 519 | }, true), 520 | NewGlue("filll"), 521 | }), 522 | }), 523 | s: 0, 524 | v: 0, 525 | h: 31.8125, 526 | }, 527 | } { 528 | t.Run("", func(t *testing.T) { 529 | var ship Ship 530 | ship.Call(0, 0, tc.node) 531 | var ( 532 | s = ship.cur.s 533 | v = ship.cur.v 534 | h = ship.cur.h 535 | ) 536 | 537 | if got, want := s, tc.s; got != want { 538 | t.Fatalf("invalid shift: got=%d, want=%d", got, want) 539 | } 540 | 541 | if got, want := v, tc.v; !cmpEq(got, want) { 542 | t.Fatalf("invalid v: got=%g, want=%g", got, want) 543 | } 544 | 545 | if got, want := h, tc.h; !cmpEq(got, want) { 546 | t.Fatalf("invalid h: got=%g, want=%g", got, want) 547 | } 548 | }) 549 | } 550 | } 551 | 552 | func cmpEq(a, b float64) bool { 553 | switch { 554 | case math.IsInf(a, -1): 555 | return math.IsInf(b, -1) 556 | case math.IsInf(a, +1): 557 | return math.IsInf(b, +1) 558 | default: 559 | return a == b 560 | } 561 | } 562 | -------------------------------------------------------------------------------- /tex/state.go: -------------------------------------------------------------------------------- 1 | // Copyright ©2020 The go-latex Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package tex 6 | 7 | import ( 8 | "github.com/go-latex/latex/font" 9 | ) 10 | 11 | type State struct { 12 | be font.Backend 13 | Font font.Font 14 | DPI float64 15 | } 16 | 17 | func NewState(be font.Backend, font font.Font, dpi float64) State { 18 | return State{ 19 | be: be, 20 | Font: font, 21 | DPI: dpi, 22 | } 23 | } 24 | 25 | func (state State) Backend() font.Backend { return state.be } 26 | -------------------------------------------------------------------------------- /tex/tex.go: -------------------------------------------------------------------------------- 1 | // Copyright ©2020 The go-latex Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | // Package tex provides a TeX-like box model. 6 | // 7 | // The following is based directly on the document 'woven' from the 8 | // TeX82 source code. This information is also available in printed 9 | // form: 10 | // 11 | // Knuth, Donald E.. 1986. Computers and Typesetting, Volume B: 12 | // TeX: The Program. Addison-Wesley Professional. 13 | // 14 | // An electronic version is also available from: 15 | // 16 | // http://brokestream.com/tex.pdf 17 | // 18 | // The most relevant "chapters" are: 19 | // Data structures for boxes and their friends 20 | // Shipping pages out (Ship class) 21 | // Packaging (hpack and vpack) 22 | // Data structures for math mode 23 | // Subroutines for math mode 24 | // Typesetting math formulas 25 | // 26 | // Many of the docstrings below refer to a numbered "node" in that 27 | // book, e.g., node123 28 | // 29 | // Note that (as TeX) y increases downward. 30 | package tex // import "github.com/go-latex/latex/tex" 31 | -------------------------------------------------------------------------------- /tex/tex_test.go: -------------------------------------------------------------------------------- 1 | // Copyright ©2020 The go-latex Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package tex 6 | 7 | import "testing" 8 | 9 | func TestDetermineOrder(t *testing.T) { 10 | for _, tc := range []struct { 11 | totals []float64 12 | want int 13 | }{ 14 | { 15 | totals: []float64{1, 2, 3, 0}, 16 | want: 2, 17 | }, 18 | { 19 | totals: []float64{1, 2, 3, 4}, 20 | want: 3, 21 | }, 22 | { 23 | totals: []float64{0, 2, 3, 0}, 24 | want: 2, 25 | }, 26 | { 27 | totals: []float64{0, 1, 0, 0}, 28 | want: 1, 29 | }, 30 | } { 31 | t.Run("", func(t *testing.T) { 32 | got := determineOrder(tc.totals) 33 | if got != tc.want { 34 | t.Fatalf("%v: got=%v, want=%v", tc.totals, got, tc.want) 35 | } 36 | }) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /tex/utils.go: -------------------------------------------------------------------------------- 1 | // Copyright ©2020 The go-latex Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package tex 6 | 7 | func maxInt(a, b int) int { 8 | if a > b { 9 | return a 10 | } 11 | return b 12 | } 13 | 14 | func clamp(v float64) float64 { 15 | const ( 16 | min = -1000000000. 17 | max = +1000000000. 18 | ) 19 | switch { 20 | case v < min: 21 | return min 22 | case v > max: 23 | return max 24 | } 25 | return v 26 | } 27 | -------------------------------------------------------------------------------- /token/kind_string.go: -------------------------------------------------------------------------------- 1 | // Code generated by "stringer -type Kind"; DO NOT EDIT. 2 | 3 | // Copyright ©2020 The go-latex Authors. All rights reserved. 4 | // Use of this source code is governed by a BSD-style 5 | // license that can be found in the LICENSE file. 6 | 7 | package token 8 | 9 | import "strconv" 10 | 11 | func _() { 12 | // An "invalid array index" compiler error signifies that the constant values have changed. 13 | // Re-run the stringer command to generate them again. 14 | var x [1]struct{} 15 | _ = x[Invalid-0] 16 | _ = x[Macro-1] 17 | _ = x[EmptyLine-2] 18 | _ = x[Comment-3] 19 | _ = x[Space-4] 20 | _ = x[Word-5] 21 | _ = x[Number-6] 22 | _ = x[Symbol-7] 23 | _ = x[Lbrace-8] 24 | _ = x[Rbrace-9] 25 | _ = x[Lbrack-10] 26 | _ = x[Rbrack-11] 27 | _ = x[Lparen-12] 28 | _ = x[Rparen-13] 29 | _ = x[Other-14] 30 | _ = x[Verbatim-15] 31 | _ = x[EOF-16] 32 | } 33 | 34 | const _Kind_name = "InvalidMacroEmptyLineCommentSpaceWordNumberSymbolLbraceRbraceLbrackRbrackLparenRparenOtherVerbatimEOF" 35 | 36 | var _Kind_index = [...]uint8{0, 7, 12, 21, 28, 33, 37, 43, 49, 55, 61, 67, 73, 79, 85, 90, 98, 101} 37 | 38 | func (i Kind) String() string { 39 | if i < 0 || i >= Kind(len(_Kind_index)-1) { 40 | return "Kind(" + strconv.FormatInt(int64(i), 10) + ")" 41 | } 42 | return _Kind_name[_Kind_index[i]:_Kind_index[i+1]] 43 | } 44 | -------------------------------------------------------------------------------- /token/token.go: -------------------------------------------------------------------------------- 1 | // Copyright ©2020 The go-latex Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | // Package token defines constants representing the lexical tokens of 6 | // LaTeX documents. 7 | package token // import "github.com/go-latex/latex/token" 8 | 9 | //go:generate stringer -type Kind 10 | 11 | import ( 12 | "go/token" 13 | ) 14 | 15 | // Kind is a kind of LaTeX token. 16 | type Kind int 17 | 18 | const ( 19 | Invalid Kind = iota 20 | Macro 21 | EmptyLine 22 | Comment 23 | Space 24 | Word 25 | Number 26 | Symbol // +,-,?,>,>=,... 27 | Lbrace 28 | Rbrace 29 | Lbrack 30 | Rbrack 31 | Lparen 32 | Rparen 33 | Other 34 | Verbatim 35 | EOF 36 | ) 37 | 38 | // Token holds informations about a token. 39 | type Token struct { 40 | Kind Kind // Kind is the kind of token. 41 | Pos Pos // Pos is the position of a token. 42 | Text string 43 | } 44 | 45 | func (t Token) String() string { return t.Text } 46 | 47 | // Pos is a compact encoding of a source position within a file set. 48 | // 49 | // Aliased from go/token.Pos 50 | type Pos = token.Pos 51 | -------------------------------------------------------------------------------- /token/token_test.go: -------------------------------------------------------------------------------- 1 | // Copyright ©2020 The go-latex Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package token 6 | 7 | import "testing" 8 | 9 | func TestToken(t *testing.T) { 10 | want := "token text" 11 | tok := Token{Text: want} 12 | 13 | got := tok.String() 14 | if got != want { 15 | t.Fatalf("invalid stringer: got=%q, want=%q", got, want) 16 | } 17 | } 18 | 19 | func TestKind(t *testing.T) { 20 | want := "Macro" 21 | kind := Macro 22 | got := kind.String() 23 | if got != want { 24 | t.Fatalf("invalid stringer: got=%q, want=%q", got, want) 25 | } 26 | } 27 | --------------------------------------------------------------------------------