Skip to content

Commit 5b47f2d

Browse files
committed
Fix parsing and support non-numbered equations.
1 parent d4be710 commit 5b47f2d

File tree

4 files changed

+199
-101
lines changed

4 files changed

+199
-101
lines changed

Sources/LaTeXSwiftUI/LaTeX.swift

Lines changed: 21 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,7 @@ extension LaTeX {
135135
///
136136
/// - Returns: A stack view.
137137
@MainActor private func asStack() -> some View {
138-
VStack(alignment: .leading, spacing: lineSpacing) {
138+
VStack(alignment: .leading, spacing: lineSpacing + 4) {
139139
ForEach(blocks, id: \.self) { block in
140140
if block.isEquationBlock,
141141
let (image, size) = image(for: block) {
@@ -188,20 +188,28 @@ extension LaTeX {
188188
@available(iOS 16.1, *)
189189
struct LaTeX_Previews: PreviewProvider {
190190
static var previews: some View {
191+
// VStack {
192+
// LaTeX("Hello, $\\LaTeX$!")
193+
// .font(.title)
194+
//
195+
// LaTeX("Hello, $\\LaTeX$!")
196+
// .font(.title2)
197+
// .foregroundColor(.cyan)
198+
//
199+
// LaTeX("Hello, $\\LaTeX$!")
200+
// .font(.title3)
201+
// .foregroundColor(.pink)
202+
// }
203+
// .fontDesign(.serif)
204+
// .previewLayout(.sizeThatFits)
205+
191206
VStack {
192-
LaTeX("Hello, $\\LaTeX$!")
193-
.font(.title)
194-
195-
LaTeX("Hello, $\\LaTeX$!")
196-
.font(.title2)
197-
.foregroundColor(.cyan)
198-
199-
LaTeX("Hello, $\\LaTeX$!")
200-
.font(.title3)
201-
.foregroundColor(.pink)
207+
LaTeX("This is a block equation with some block after it $$\\text{and this is some text on the next line}$$ with some more text that comes after it!")
208+
.lineSpacing(5)
209+
Divider()
210+
Text("This is a block equation with some block afte and it wraps and this is some text on the next line and blah blah with some more text that comes after it!")
211+
.lineSpacing(5)
202212
}
203-
.fontDesign(.serif)
204-
.previewLayout(.sizeThatFits)
205213
}
206214

207215
}

Sources/LaTeXSwiftUI/Models/Component.swift

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,11 @@ internal struct Component: CustomStringConvertible, Equatable, Hashable {
5656
/// - Example: `\begin{equation}x^2\end{equation}`
5757
case namedEquation
5858

59+
/// A named equation component.
60+
///
61+
/// - Example: `\begin{equation*}x^2\end{equation*}`
62+
case namedNoNumberEquation
63+
5964
/// The component's description.
6065
var description: String {
6166
rawValue
@@ -67,8 +72,9 @@ internal struct Component: CustomStringConvertible, Equatable, Hashable {
6772
case .text: return ""
6873
case .inlineEquation: return "$"
6974
case .texEquation: return "$$"
70-
case .blockEquation: return "\\]"
75+
case .blockEquation: return "\\["
7176
case .namedEquation: return "\\begin{equation}"
77+
case .namedNoNumberEquation: return "\\begin{equation*}"
7278
}
7379
}
7480

@@ -80,6 +86,7 @@ internal struct Component: CustomStringConvertible, Equatable, Hashable {
8086
case .texEquation: return "$$"
8187
case .blockEquation: return "\\]"
8288
case .namedEquation: return "\\end{equation}"
89+
case .namedNoNumberEquation: return "\\end{equation*}"
8390
}
8491
}
8592

@@ -139,7 +146,7 @@ internal struct Component: CustomStringConvertible, Equatable, Hashable {
139146
text = String(text[text.index(text.startIndex, offsetBy: type.leftTerminator.count)...])
140147
}
141148
if text.hasSuffix(type.rightTerminator) {
142-
text = String(text[..<text.index(text.startIndex, offsetBy: text.count - type.rightTerminator.count)])
149+
text = String(text[..<text.index(text.endIndex, offsetBy: -type.rightTerminator.count)])
143150
}
144151
self.text = text
145152
}

Sources/LaTeXSwiftUI/Models/Parser.swift

Lines changed: 88 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,9 @@ internal struct Parser {
1313
// MARK: Types
1414

1515
/// An equation component.
16-
private struct EquationComponent {
17-
let regex: String
18-
let terminatingRegex: String
16+
private struct EquationComponent<T, U> {
17+
let regex: Regex<T>
18+
let terminatingRegex: Regex<U>
1919
let equation: Component.ComponentType
2020
let supportsRecursion: Bool
2121
}
@@ -24,38 +24,46 @@ internal struct Parser {
2424

2525
/// An inline equation component.
2626
private static let inline = EquationComponent(
27-
regex: #"\$(.|\s)*?\$"#,
28-
terminatingRegex: #"\$"#,
27+
regex: #/\$(.|\s)*?\$/#,
28+
terminatingRegex: #/\$/#,
2929
equation: .inlineEquation,
3030
supportsRecursion: false)
3131

3232
/// An TeX-style block equation component.
3333
private static let tex = EquationComponent(
34-
regex: #"\$\$(.|\s)*?\$\$"#,
35-
terminatingRegex: #"\$\$"#,
34+
regex: #/\$\$(.|\s)*?\$\$/#,
35+
terminatingRegex: #/\$\$/#,
3636
equation: .texEquation,
3737
supportsRecursion: false)
3838

3939
/// A block equation.
4040
private static let block = EquationComponent(
41-
regex: #"\\\[(.|\s)*\\\]"#,
42-
terminatingRegex: #"\\\]"#,
41+
regex: #/\\\[(.|\s)*?\\\]/#,
42+
terminatingRegex: #/\\\]/#,
4343
equation: .blockEquation,
4444
supportsRecursion: false)
4545

4646
/// A named equation component.
4747
private static let named = EquationComponent(
48-
regex: #"\\begin{equation}(.|\s)*\\end{equation}"#,
49-
terminatingRegex: #"\\end{equation}"#,
48+
regex: #/\\begin{equation}(.|\s)*\\end{equation}/#,
49+
terminatingRegex: #/\\end{equation}/#,
5050
equation: .namedEquation,
5151
supportsRecursion: true)
5252

53+
/// A named no number equation component.
54+
private static let namedNoNumber = EquationComponent(
55+
regex: #/\\begin{equation\*}(.|\s)*\\end{equation\*}/#,
56+
terminatingRegex: #/\\end{equation\*}/#,
57+
equation: .namedNoNumberEquation,
58+
supportsRecursion: true)
59+
5360
// Order matters
5461
private static let allEquations: [EquationComponent] = [
5562
inline,
5663
tex,
5764
block,
58-
named
65+
named,
66+
namedNoNumber
5967
]
6068

6169
}
@@ -95,72 +103,90 @@ extension Parser {
95103
/// - Parameter input: The input string.
96104
/// - Returns: An array of LaTeX components.
97105
static func parse(_ input: String) -> [Component] {
98-
let matches = allEquations.map({ ($0, input.range(of: $0.regex, options: .regularExpression)) }).filter { match in
99-
guard let range = match.1 else { return false }
100-
let firstIndex = range.lowerBound
101-
let lastIndex = range.upperBound
102-
let componentIsEmpty = Component(text: String(input[range]), type: match.0.equation).text.isEmpty
103-
let previousIndexLast = input.index(lastIndex, offsetBy: -1 - match.0.equation.rightTerminator.count)
106+
// Get the first match of each each equation type
107+
let matches = allEquations.map({ ($0, input.firstMatch(of: $0.regex)) })
108+
109+
// Filter the matches
110+
let filteredMatches = matches.filter { match in
111+
// We only want matches with ranges
112+
guard let range = match.1?.range else {
113+
return false
114+
}
115+
116+
print("found match \(match.0.equation)")
104117

105-
if firstIndex == input.startIndex {
106-
return input[previousIndexLast] != "\\" && !componentIsEmpty
118+
// Make sure the inner component is non-empty
119+
let text = Component(text: String(input[range]), type: match.0.equation).text
120+
print(text)
121+
guard !text.isEmpty else {
122+
return false
107123
}
108124

109-
let previousIndexFirst = input.index(before: firstIndex)
110-
return input[previousIndexFirst] != "\\" && input[previousIndexLast] != "\\" && !componentIsEmpty
111-
}
112-
113-
let allStart = matches.map({ $0.1?.lowerBound })
114-
var equationRange: Range<String.Index>?
115-
var equation: Component.ComponentType = .text
116-
117-
for match in matches {
118-
guard isSmallest(match.1?.lowerBound, outOf: allStart) else {
119-
continue
125+
// Make sure the starting terminator isn't escaped
126+
guard range.lowerBound >= input.startIndex else {
127+
return false
120128
}
121-
guard let matchRange = match.1 else {
122-
continue
129+
if range.lowerBound > input.startIndex, input[input.index(before: range.lowerBound)] == "\\" {
130+
return false
123131
}
132+
print("leading not escaped")
124133

125-
if match.0.supportsRecursion {
126-
let terminatingMatches = input.ranges(of: match.0.terminatingRegex).filter { range in
127-
let index = range.lowerBound
128-
if index == input.startIndex { return true }
129-
let previousIndex = input.index(before: index)
130-
return input[previousIndex] != "\\"
131-
}
132-
if let lastMatch = terminatingMatches.last {
133-
equationRange = matchRange.lowerBound ..< lastMatch.upperBound
134-
}
134+
// Make sure the ending terminator isn't escaped
135+
let endingTerminatorStartIndex = input.index(range.upperBound, offsetBy: -match.0.equation.rightTerminator.count)
136+
guard endingTerminatorStartIndex >= input.startIndex else {
137+
return false
135138
}
136-
else {
137-
equationRange = match.1
139+
if endingTerminatorStartIndex > input.startIndex, input[input.index(before: endingTerminatorStartIndex)] == "\\" {
140+
return false
138141
}
142+
print("trailing not escaped")
139143

140-
if equationRange != nil {
141-
equation = match.0.equation
142-
break
143-
}
144+
// The component has content and isn't escaped
145+
return true
144146
}
145147

146-
if let equationRange = equationRange {
147-
let stringBeforeEquation = String(input[..<equationRange.lowerBound])
148-
let equationString = String(input[equationRange])
149-
let remainingString = String(input[equationRange.upperBound...])
150-
var components = [Component]()
151-
if !stringBeforeEquation.isEmpty {
152-
components.append(Component(text: stringBeforeEquation, type: .text))
148+
// Get the first matched equation
149+
guard let smallestMatch = filteredMatches.min(by: { $0.1!.range.lowerBound < $1.1!.range.lowerBound }) else {
150+
return input.isEmpty ? [] : [Component(text: input, type: .text)]
151+
}
152+
153+
// If the equation supports recursion, then we'll need to find the last
154+
// match of its terminating regex component.
155+
let equationRange: Range<String.Index>
156+
if smallestMatch.0.supportsRecursion {
157+
var terminatingMatches = input.matches(of: smallestMatch.0.terminatingRegex).filter { match in
158+
// Make sure the terminator isn't escaped
159+
if match.range.lowerBound == input.startIndex { return true }
160+
let previousIndex = input.index(before: match.range.lowerBound)
161+
return input[previousIndex] != "\\"
153162
}
154-
components.append(Component(text: equationString, type: equation))
155-
if remainingString.isEmpty {
156-
return components
163+
terminatingMatches.sort(by: { $0.range.lowerBound < $1.range.lowerBound })
164+
if let lastMatch = terminatingMatches.last {
165+
equationRange = smallestMatch.1!.range.lowerBound ..< lastMatch.range.upperBound
157166
}
158167
else {
159-
return components + parse(remainingString)
168+
equationRange = smallestMatch.1!.range
160169
}
161170
}
162-
163-
return input.isEmpty ? [] : [Component(text: input, type: .text)]
171+
else {
172+
equationRange = smallestMatch.1!.range
173+
}
174+
175+
// We got our equation range, so lets return the components.
176+
let stringBeforeEquation = String(input[..<equationRange.lowerBound])
177+
let equationString = String(input[equationRange])
178+
let remainingString = String(input[equationRange.upperBound...])
179+
var components = [Component]()
180+
if !stringBeforeEquation.isEmpty {
181+
components.append(Component(text: stringBeforeEquation, type: .text))
182+
}
183+
components.append(Component(text: equationString, type: smallestMatch.0.equation))
184+
if remainingString.isEmpty {
185+
return components
186+
}
187+
else {
188+
return components + parse(remainingString)
189+
}
164190
}
165191

166192
}

0 commit comments

Comments
 (0)