Skip to content

Commit 3af34ba

Browse files
authored
Merge pull request #17 from colinc86/develop
Develop
2 parents 3d1888a + 0d6a913 commit 3af34ba

File tree

11 files changed

+581
-502
lines changed

11 files changed

+581
-502
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ It won't
4747
Add the dependency to your package manifest file.
4848

4949
```swift
50-
.package(url: "https://github.com/colinc86/LaTeXSwiftUI", from: "1.2.2")
50+
.package(url: "https://github.com/colinc86/LaTeXSwiftUI", from: "1.2.3")
5151
```
5252

5353
## ⌨️ Usage

Sources/LaTeXSwiftUI/Extensions/Font+Extensions.swift

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@ import Cocoa
3333
#endif
3434

3535
internal extension Font {
36+
37+
/// The font's text style.
3638
func textStyle() -> _Font.TextStyle? {
3739
switch self {
3840
case .largeTitle, .largeTitle.bold(), .largeTitle.italic(), .largeTitle.monospaced(): return .largeTitle
@@ -48,6 +50,12 @@ internal extension Font {
4850
default: return nil
4951
}
5052
}
53+
54+
/// The font's x-height.
55+
var xHeight: CGFloat {
56+
_Font.preferredFont(from: self).xHeight
57+
}
58+
5159
}
5260

5361
internal extension _Font {
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
//
2+
// MathJax+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+
import MathJaxSwift
28+
29+
internal extension MathJax {
30+
31+
static var svgRenderer: MathJax? = {
32+
do {
33+
return try MathJax(preferredOutputFormat: .svg)
34+
}
35+
catch {
36+
NSLog("Error creating MathJax instance: \(error)")
37+
return nil
38+
}
39+
}()
40+
41+
}

Sources/LaTeXSwiftUI/LaTeX.swift

Lines changed: 71 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -104,18 +104,18 @@ public struct LaTeX: View {
104104

105105
/// The package's shared data cache.
106106
public static var dataCache: NSCache<NSString, NSData> {
107-
Renderer.shared.dataCache
107+
Cache.shared.dataCache
108108
}
109109

110110
#if os(macOS)
111111
/// The package's shared image cache.
112112
public static var imageCache: NSCache<NSString, NSImage> {
113-
Renderer.shared.imageCache
113+
Cache.shared.imageCache
114114
}
115115
#else
116116
/// The package's shared image cache.
117117
public static var imageCache: NSCache<NSString, UIImage> {
118-
Renderer.shared.imageCache
118+
Cache.shared.imageCache
119119
}
120120
#endif
121121

@@ -156,24 +156,8 @@ public struct LaTeX: View {
156156

157157
// MARK: Private properties
158158

159-
/// The view's render state.
160-
@StateObject private var renderState: LaTeXRenderState
161-
162-
/// Renders the blocks synchronously.
163-
///
164-
/// This will block whatever thread you call it on.
165-
private var syncBlocks: [ComponentBlock] {
166-
Renderer.shared.render(
167-
blocks: Parser.parse(unencodeHTML ? latex.htmlUnescape() : latex, mode: parsingMode),
168-
font: font ?? .body,
169-
displayScale: displayScale,
170-
texOptions: texOptions)
171-
}
172-
173-
/// The TeX options to use when submitting requests to the renderer.
174-
private var texOptions: TeXInputProcessorOptions {
175-
TeXInputProcessorOptions(processEscapes: processEscapes, errorMode: errorMode)
176-
}
159+
/// The view's renderer.
160+
@StateObject private var renderer: Renderer
177161

178162
// MARK: Initializers
179163

@@ -182,33 +166,35 @@ public struct LaTeX: View {
182166
/// - Parameter latex: The LaTeX input.
183167
public init(_ latex: String) {
184168
self.latex = latex
185-
_renderState = StateObject(wrappedValue: LaTeXRenderState(latex: latex))
169+
_renderer = StateObject(wrappedValue: Renderer(latex: latex))
186170
}
187171

188172
// MARK: View body
189173

190174
public var body: some View {
191175
VStack(spacing: 0) {
192-
if renderState.rendered {
193-
bodyWithBlocks(renderState.blocks)
176+
if renderer.rendered {
177+
// If our blocks have been rendered, display them
178+
bodyWithBlocks(renderer.blocks)
179+
}
180+
else if isCached() {
181+
// If our blocks are cached, display them
182+
bodyWithBlocks(renderSync())
194183
}
195184
else {
185+
// The view is not rendered nor cached
196186
switch renderingStyle {
197-
case .empty:
198-
Text("")
199-
.task(render)
200-
case .original:
201-
Text(latex)
202-
.task(render)
203-
case .progress:
204-
ProgressView()
205-
.task(render)
187+
case .empty, .original, .progress:
188+
// Render the components asynchronously
189+
loadingView().task(renderAsync)
206190
case .wait:
207-
bodyWithBlocks(syncBlocks)
191+
// Render the components synchronously
192+
bodyWithBlocks(renderSync())
208193
}
209194
}
210195
}
211-
.animation(renderingAnimation, value: renderState.rendered)
196+
.animation(renderingAnimation, value: renderer.rendered)
197+
.environmentObject(renderer)
212198
}
213199

214200
}
@@ -220,7 +206,7 @@ extension LaTeX {
220206
/// Preloads the view's SVG and image data.
221207
public func preload() {
222208
Task {
223-
await render()
209+
await renderAsync()
224210
}
225211
}
226212
}
@@ -229,14 +215,45 @@ extension LaTeX {
229215

230216
extension LaTeX {
231217

218+
/// Checks the renderer's caches for the current view.
219+
///
220+
/// If this method returns `true`, then there is no need to do an async
221+
/// render.
222+
///
223+
/// - Returns: A boolean indicating whether the components to the view are
224+
/// cached.
225+
private func isCached() -> Bool {
226+
renderer.isCached(
227+
unencodeHTML: unencodeHTML,
228+
parsingMode: parsingMode,
229+
processEscapes: processEscapes,
230+
errorMode: errorMode,
231+
font: font ?? .body,
232+
displayScale: displayScale)
233+
}
234+
232235
/// Renders the view's components.
233-
@Sendable private func render() async {
234-
await renderState.render(
236+
@Sendable private func renderAsync() async {
237+
await renderer.render(
238+
unencodeHTML: unencodeHTML,
239+
parsingMode: parsingMode,
240+
processEscapes: processEscapes,
241+
errorMode: errorMode,
242+
font: font ?? .body,
243+
displayScale: displayScale)
244+
}
245+
246+
/// Renders the view's components synchronously.
247+
///
248+
/// - Returns: The rendered components.
249+
private func renderSync() -> [ComponentBlock] {
250+
renderer.renderSync(
235251
unencodeHTML: unencodeHTML,
236252
parsingMode: parsingMode,
237-
font: font,
238-
displayScale: displayScale,
239-
texOptions: texOptions)
253+
processEscapes: processEscapes,
254+
errorMode: errorMode,
255+
font: font ?? .body,
256+
displayScale: displayScale)
240257
}
241258

242259
/// Creates the view's body based on its block mode.
@@ -254,6 +271,19 @@ extension LaTeX {
254271
}
255272
}
256273

274+
@MainActor @ViewBuilder private func loadingView() -> some View {
275+
switch renderingStyle {
276+
case .empty:
277+
Text("")
278+
case .original:
279+
Text(latex)
280+
case .progress:
281+
ProgressView()
282+
default:
283+
EmptyView()
284+
}
285+
}
286+
257287
}
258288

259289
@available(iOS 16.1, *)
Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
//
2+
// Cache.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 CryptoKit
27+
import Foundation
28+
import MathJaxSwift
29+
30+
fileprivate protocol CacheKey: Codable {
31+
32+
/// The key type used to identify the cache key in storage.
33+
static var keyType: String { get }
34+
35+
/// A key to use if encoding fails.
36+
var fallbackKey: String { get }
37+
38+
}
39+
40+
extension CacheKey {
41+
42+
/// The key to use in the cache.
43+
func key() -> String {
44+
do {
45+
let data = try JSONEncoder().encode(self)
46+
let hashedData = SHA256.hash(data: data)
47+
return hashedData.compactMap { String(format: "%02x", $0) }.joined() + "-" + Self.keyType
48+
}
49+
catch {
50+
return fallbackKey + "-" + Self.keyType
51+
}
52+
}
53+
54+
}
55+
56+
internal class Cache {
57+
58+
// MARK: Types
59+
60+
/// An SVG cache key.
61+
struct SVGCacheKey: CacheKey {
62+
static let keyType: String = "svg"
63+
let componentText: String
64+
let conversionOptions: ConversionOptions
65+
let texOptions: TeXInputProcessorOptions
66+
internal var fallbackKey: String { componentText }
67+
}
68+
69+
/// An image cache key.
70+
struct ImageCacheKey: CacheKey {
71+
static let keyType: String = "image"
72+
let svg: SVG
73+
let xHeight: CGFloat
74+
internal var fallbackKey: String { String(data: svg.data, encoding: .utf8) ?? "" }
75+
}
76+
77+
// MARK: Static properties
78+
79+
/// The shared cache.
80+
static let shared = Cache()
81+
82+
// MARK: Public properties
83+
84+
/// The renderer's data cache.
85+
let dataCache: NSCache<NSString, NSData> = NSCache()
86+
87+
/// The renderer's image cache.
88+
let imageCache: NSCache<NSString, _Image> = NSCache()
89+
90+
// MARK: Private properties
91+
92+
/// Semaphore for thread-safe access to `dataCache`.
93+
let dataCacheSemaphore = DispatchSemaphore(value: 1)
94+
95+
/// Semaphore for thread-safe access to `imageCache`.
96+
let imageCacheSemaphore = DispatchSemaphore(value: 1)
97+
98+
}
99+
100+
// MARK: Public methods
101+
102+
extension Cache {
103+
104+
/// Safely access the cache value for the given key.
105+
///
106+
/// - Parameter key: The key of the value to get.
107+
/// - Returns: A value.
108+
func dataCacheValue(for key: SVGCacheKey) -> Data? {
109+
dataCacheSemaphore.wait()
110+
defer { dataCacheSemaphore.signal() }
111+
return dataCache.object(forKey: key.key() as NSString) as Data?
112+
}
113+
114+
/// Safely sets the cache value.
115+
///
116+
/// - Parameters:
117+
/// - value: The value to set.
118+
/// - key: The value's key.
119+
func setDataCacheValue(_ value: Data, for key: SVGCacheKey) {
120+
dataCacheSemaphore.wait()
121+
dataCache.setObject(value as NSData, forKey: key.key() as NSString)
122+
dataCacheSemaphore.signal()
123+
}
124+
125+
/// Safely access the cache value for the given key.
126+
///
127+
/// - Parameter key: The key of the value to get.
128+
/// - Returns: A value.
129+
func imageCacheValue(for key: ImageCacheKey) -> _Image? {
130+
imageCacheSemaphore.wait()
131+
defer { imageCacheSemaphore.signal() }
132+
return imageCache.object(forKey: key.key() as NSString)
133+
}
134+
135+
/// Safely sets the cache value.
136+
///
137+
/// - Parameters:
138+
/// - value: The value to set.
139+
/// - key: The value's key.
140+
func setImageCacheValue(_ value: _Image, for key: ImageCacheKey) {
141+
imageCacheSemaphore.wait()
142+
imageCache.setObject(value, forKey: key.key() as NSString)
143+
imageCacheSemaphore.signal()
144+
}
145+
146+
}

0 commit comments

Comments
 (0)