Skip to content

Commit b980256

Browse files
committed
Add format equation number and fix parsing issue.
1 parent 916c330 commit b980256

File tree

10 files changed

+220
-42
lines changed

10 files changed

+220
-42
lines changed

README.md

Lines changed: 43 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
A SwiftUI view that renders LaTeX equations.
44

5-
![Swift Version](https://img.shields.io/badge/Swift-5.7-orange?logo=swift) ![iOS Version](https://img.shields.io/badge/iOS-16-informational) ![macOS Version](https://img.shields.io/badge/macOS-13-informational)
5+
[![](https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2Fcolinc86%2FLaTeXSwiftUI%2Fbadge%3Ftype%3Dswift-versions)](https://swiftpackageindex.com/colinc86/LaTeXSwiftUI) [![](https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2Fcolinc86%2FLaTeXSwiftUI%2Fbadge%3Ftype%3Dplatforms)](https://swiftpackageindex.com/colinc86/LaTeXSwiftUI)
66

77
<center><img src="./assets/images/device.png" width="362"></center>
88

@@ -20,7 +20,10 @@ A SwiftUI view that renders LaTeX equations.
2020
- [Equation Number Mode](#equation-number-mode)
2121
- [Equation Number Start](#equation-number-start)
2222
- [Equation Number Offset](#equation-number-offset)
23+
- [Format Equation Number](#format-equation-number)
2324
- [Unencode HTML](#🔗-unencode-html)
25+
- [Rendering Style](#🕶️-rendering-style)
26+
- [Animated](#🪩-animated)
2427
- [Caching](#🗄️-caching)
2528
- [Preloading](#🏃‍♀️-preloading)
2629

@@ -44,7 +47,7 @@ It won't
4447
Add the dependency to your package manifest file.
4548

4649
```swift
47-
.package(url: "https://github.com/colinc86/LaTeXSwiftUI", from: "1.1.0")
50+
.package(url: "https://github.com/colinc86/LaTeXSwiftUI", from: "1.2.0")
4851
```
4952

5053
## ⌨️ Usage
@@ -186,24 +189,26 @@ The default starting number is `1`, but if you need to start at a different valu
186189

187190
To change the left or right offset of the equation number, use the `equationNumberOffset` modifier.
188191

189-
```swift
190-
// Don't number block equations (default)
191-
LaTeX("$$a + b = c$$")
192-
.equationNumberMode(.none)
192+
##### Format Equation Number
193+
194+
You can set a closure on the view to do your own custom formatting. The `formatEquationNumber` modifier takes a closure that is passed the equation number and returns a string.
193195

194-
// Add left numbers and a leading offset
195-
LaTeX("$$d + e = f$$")
196-
.equationNumberMode(.left)
196+
```swift
197+
LaTeX("$$E = mc^2$$")
198+
.equationNumberMode(.right)
197199
.equationNumberOffset(10)
200+
.padding([.bottom])
198201

199-
// Add right numbers, a leading offset, and start at 2
200-
LaTeX("$$h + i = j$$ $$k + l = m$$")
202+
LaTeX("$$E = mc^2$$ $$E = mc^2$$")
201203
.equationNumberMode(.right)
204+
.equationNumberOffset(10)
202205
.equationNumberStart(2)
203-
.equationNumberOffset(20)
206+
.formatEquationNumber { n in
207+
return "~[\(n)]~"
208+
}
204209
```
205210

206-
> <img src="./assets/images/numbers.png" width="428" height="152">
211+
> <img src="./assets/images/numbers.png" width="258" height="145">
207212
208213
#### 🔗 Unencode HTML
209214

@@ -220,6 +225,31 @@ LaTeX("$x^2&lt;1$")
220225

221226
> <img src="./assets/images/unencoded.png" width="72.5" height="34">
222227
228+
#### 🕶️ Rendering Style
229+
230+
The view has four rendering styles.
231+
232+
| Style | Asynchronous | Description |
233+
|:-----------|:-------------|:-------------------------------------------------------------------------|
234+
| `empty` | Yes | The view remains empty until its finished rendering. |
235+
| `original` | Yes | The view displays the input text until its finished rendering. |
236+
| `progress` | Yes | The view displays a progress view until its finished rendering. |
237+
| `wait` | No | *(default)* The view blocks the main queue until its finished rendering. |
238+
239+
The `wait` style is the default style, and loads the view synchronously on the main queue. To get better performance and move SVG rendering off of the main queue, use any of the other three styles.
240+
241+
#### 🪩 Animated
242+
243+
The `animated` modifier applies to the view when using the asynchronous rendering styles `empty`, `original`, or `progress`.
244+
245+
```swift
246+
LaTeX(input)
247+
.renderingStyle(.original)
248+
.animated()
249+
```
250+
251+
> In the above example, the input text will be displayed until the SVGs have been rendered at which point the rendered views will animate in to view.
252+
223253
### 🗄️ Caching
224254

225255
`LaTeXSwiftUI` caches its SVG responses from MathJax and the images rendered as a result of the view's environment. If you want to control the cache, then you can access the static `dataCache` and `imageCache` properties.

Sources/LaTeXSwiftUI/Extensions/EnvironmentValues+Extensions.swift

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,10 @@ private struct EquationNumberOffsetKey: EnvironmentKey {
6262
static let defaultValue: CGFloat = 0.0
6363
}
6464

65+
private struct FormatEquationNumberKey: EnvironmentKey {
66+
static let defaultValue: LaTeX.FormatEquationNumber = { "(\($0))" }
67+
}
68+
6569
private struct RenderingStyleKey: EnvironmentKey {
6670
static let defaultValue: LaTeX.RenderingStyle = .wait
6771
}
@@ -126,6 +130,12 @@ extension EnvironmentValues {
126130
set { self[EquationNumberOffsetKey.self] = newValue }
127131
}
128132

133+
/// The closure used to format equation number before displaying them.
134+
var formatEquationNumber: LaTeX.FormatEquationNumber {
135+
get { self[FormatEquationNumberKey.self] }
136+
set { self[FormatEquationNumberKey.self] = newValue }
137+
}
138+
129139
/// The rendering style of this environment.
130140
var renderingStyle: LaTeX.RenderingStyle {
131141
get { self[RenderingStyleKey.self] }
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
//
2+
// Range+Extensions.swift
3+
// LaTeXSwiftUI
4+
//
5+
// Copyright (c) 2023 Colin Campbell
6+
//
7+
// Permission is hereby granted, free of charge, to any person obtaining a copy
8+
// of this software and associated documentation files (the "Software"), to
9+
// deal in the Software without restriction, including without limitation the
10+
// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
11+
// sell copies of the Software, and to permit persons to whom the Software is
12+
// furnished to do so, subject to the following conditions:
13+
//
14+
// The above copyright notice and this permission notice shall be included in
15+
// all copies or substantial portions of the Software.
16+
//
17+
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18+
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19+
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20+
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21+
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
22+
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
23+
// IN THE SOFTWARE.
24+
//
25+
26+
import Foundation
27+
28+
extension Range<String.Index> {
29+
30+
/// Evaluates whether or not this range is a subrange of the given range.
31+
///
32+
/// - Parameter range: The range this range is a subset of.
33+
/// - Returns: Whether or not the receiver is a subset.
34+
func isSubrange(of range: Self) -> Bool {
35+
guard range.lowerBound != lowerBound || range.upperBound != upperBound else {
36+
return false
37+
}
38+
return range.lowerBound <= lowerBound && upperBound <= range.upperBound
39+
}
40+
41+
}

Sources/LaTeXSwiftUI/Extensions/View+Extensions.swift

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,31 @@ import SwiftUI
2828

2929
public extension View {
3030

31+
/// Preloads a `LaTeX` view's SVG and image data.
32+
///
33+
/// This method should be called last in the view's modifier chain (if at
34+
/// all).
35+
///
36+
/// - Example:
37+
///
38+
/// ```
39+
/// LaTeX("Hello, $\\LaTeX$!")
40+
/// .font(.title)
41+
/// .processEscapes()
42+
/// .preload()
43+
/// ```
44+
///
45+
/// - Note: If the receiver isn't a `LaTeX` view, then this method does
46+
/// nothing.
47+
///
48+
/// - Returns: A preloaded view.
49+
func preload() -> some View {
50+
if let latex = self as? LaTeX {
51+
latex.preload()
52+
}
53+
return self
54+
}
55+
3156
/// Sets the image rendering mode for images rendered by MathJax.
3257
///
3358
/// - Parameter mode: The template rendering mode.
@@ -112,6 +137,15 @@ public extension View {
112137
environment(\.equationNumberOffset, offset)
113138
}
114139

140+
/// Sets a block that lets you format the equation number that will be
141+
/// displayed for named block equations.
142+
///
143+
/// - Parameter perform: The block that will format the equation number.
144+
/// - Returns: A view that formats its equation numbers.
145+
func formatEquationNumber(_ perform: @escaping LaTeX.FormatEquationNumber) -> some View {
146+
environment(\.formatEquationNumber, perform)
147+
}
148+
115149
/// Sets the view rendering style.
116150
///
117151
/// - Parameter style: The rendering style to use.

Sources/LaTeXSwiftUI/LaTeX.swift

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,10 @@ public struct LaTeX: View {
3434

3535
// MARK: Types
3636

37+
/// A closure that takes an equation number and returns a string to display in
38+
/// the view.
39+
public typealias FormatEquationNumber = (_ n: Int) -> String
40+
3741
/// The view's block rendering mode.
3842
public enum BlockMode {
3943

@@ -199,6 +203,18 @@ public struct LaTeX: View {
199203

200204
}
201205

206+
// MARK: Public methods
207+
208+
extension LaTeX {
209+
210+
/// Preloads the view's SVG and image data.
211+
public func preload() {
212+
Task {
213+
await render()
214+
}
215+
}
216+
}
217+
202218
// MARK: Private methods
203219

204220
extension LaTeX {
@@ -271,6 +287,25 @@ struct LaTeX_Previews: PreviewProvider {
271287
}
272288
.fontDesign(.serif)
273289
.previewLayout(.sizeThatFits)
290+
.previewDisplayName("Hello, LaTeX!")
291+
292+
VStack {
293+
LaTeX("$$E = mc^2$$")
294+
.equationNumberMode(.right)
295+
.equationNumberOffset(10)
296+
.padding([.bottom])
297+
298+
LaTeX("\\begin{equation} E = mc^2 \\end{equation} \\begin{equation} E = mc^2 \\end{equation}")
299+
.equationNumberMode(.right)
300+
.equationNumberOffset(10)
301+
.equationNumberStart(2)
302+
}
303+
.fontDesign(.serif)
304+
.previewLayout(.sizeThatFits)
305+
.previewDisplayName("Equation Numbers")
306+
.formatEquationNumber { n in
307+
return "~[\(n)]~"
308+
}
274309
}
275310

276311
}

Sources/LaTeXSwiftUI/Models/LaTeXRenderState.swift

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,9 @@ extension LaTeXRenderState {
6666
/// - displayScale: The `displayScale` environment variable.
6767
/// - texOptions: The `texOptions` environment variable.
6868
func render(animated: Bool, unencodeHTML: Bool, parsingMode: LaTeX.ParsingMode, font: Font?, displayScale: CGFloat, texOptions: TeXInputProcessorOptions) async {
69-
guard !(await isRendering) else {
69+
let isRen = await isRendering
70+
let ren = await rendered
71+
guard !isRen && !ren else {
7072
return
7173
}
7274
await MainActor.run {

Sources/LaTeXSwiftUI/Models/Parser.swift

Lines changed: 26 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -121,15 +121,25 @@ extension Parser {
121121
/// - Parameter input: The input string.
122122
/// - Returns: An array of LaTeX components.
123123
static func parse(_ input: String) -> [Component] {
124+
print("parsing input \(input)")
124125
// Get the first match of each each equation type
125-
let matches = allEquations.map({ ($0, input.firstMatch(of: $0.regex)) })
126+
// let matches = allEquations.map({ ($0, input.firstMatch(of: $0.regex)) })
127+
let matchArrays = allEquations.map { equationComponent in
128+
let regexMatches = input.matches(of: equationComponent.regex)
129+
return regexMatches.map({ (equationComponent, $0) })
130+
}
131+
132+
let matches = matchArrays.reduce([], +)
133+
134+
for match in matches {
135+
let substring = input[match.1.range]
136+
print("match: \(substring)")
137+
}
126138

127139
// Filter the matches
128140
let filteredMatches = matches.filter { match in
129141
// We only want matches with ranges
130-
guard let range = match.1?.range else {
131-
return false
132-
}
142+
let range = match.1.range
133143

134144
// Make sure the inner component is non-empty
135145
let text = Component(text: String(input[range]), type: match.0.equation).text
@@ -154,36 +164,28 @@ extension Parser {
154164
return false
155165
}
156166

167+
// Make sure the range isn't in any other range.
168+
// I.e. we only use top-level matches.
169+
for subMatch in matches {
170+
let subRange = subMatch.1.range
171+
172+
if range.isSubrange(of: subRange) {
173+
return false
174+
}
175+
}
176+
157177
// The component has content and isn't escaped
158178
return true
159179
}
160180

161181
// Get the first matched equation
162-
guard let smallestMatch = filteredMatches.min(by: { $0.1!.range.lowerBound < $1.1!.range.lowerBound }) else {
182+
guard let smallestMatch = filteredMatches.min(by: { $0.1.range.lowerBound < $1.1.range.lowerBound }) else {
163183
return input.isEmpty ? [] : [Component(text: input, type: .text)]
164184
}
165185

166186
// If the equation supports recursion, then we'll need to find the last
167187
// match of its terminating regex component.
168-
let equationRange: Range<String.Index>
169-
if smallestMatch.0.supportsRecursion {
170-
var terminatingMatches = input.matches(of: smallestMatch.0.terminatingRegex).filter { match in
171-
// Make sure the terminator isn't escaped
172-
if match.range.lowerBound == input.startIndex { return true }
173-
let previousIndex = input.index(before: match.range.lowerBound)
174-
return input[previousIndex] != "\\"
175-
}
176-
terminatingMatches.sort(by: { $0.range.lowerBound < $1.range.lowerBound })
177-
if let lastMatch = terminatingMatches.last {
178-
equationRange = smallestMatch.1!.range.lowerBound ..< lastMatch.range.upperBound
179-
}
180-
else {
181-
equationRange = smallestMatch.1!.range
182-
}
183-
}
184-
else {
185-
equationRange = smallestMatch.1!.range
186-
}
188+
let equationRange: Range<String.Index> = smallestMatch.1.range
187189

188190
// We got our equation range, so lets return the components.
189191
let stringBeforeEquation = String(input[..<equationRange.lowerBound])

0 commit comments

Comments
 (0)