Skip to content

Commit 5c8747a

Browse files
committed
Check for cached values.
1 parent f2b3cc4 commit 5c8747a

File tree

5 files changed

+364
-253
lines changed

5 files changed

+364
-253
lines changed

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 {

Sources/LaTeXSwiftUI/LaTeX.swift

Lines changed: 66 additions & 34 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+
Renderer.shared.cache.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+
Renderer.shared.cache.imageCache
114114
}
115115
#else
116116
/// The package's shared image cache.
117117
public static var imageCache: NSCache<NSString, UIImage> {
118-
Renderer.shared.imageCache
118+
Renderer.shared.cache.imageCache
119119
}
120120
#endif
121121

@@ -159,22 +159,6 @@ public struct LaTeX: View {
159159
/// The view's render state.
160160
@StateObject private var renderState: LaTeXRenderState
161161

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-
}
177-
178162
// MARK: Initializers
179163

180164
/// Initializes a view with a LaTeX input string.
@@ -190,21 +174,22 @@ public struct LaTeX: View {
190174
public var body: some View {
191175
VStack(spacing: 0) {
192176
if renderState.rendered {
177+
// If our blocks have been rendered, display them
193178
bodyWithBlocks(renderState.blocks)
194179
}
180+
else if isCached() {
181+
// If our blocks are cached, display them
182+
bodyWithBlocks(renderSync())
183+
}
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
}
@@ -220,7 +205,7 @@ extension LaTeX {
220205
/// Preloads the view's SVG and image data.
221206
public func preload() {
222207
Task {
223-
await render()
208+
await renderAsync()
224209
}
225210
}
226211
}
@@ -229,14 +214,45 @@ extension LaTeX {
229214

230215
extension LaTeX {
231216

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

242258
/// Creates the view's body based on its block mode.
@@ -254,6 +270,22 @@ extension LaTeX {
254270
}
255271
}
256272

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

259291
@available(iOS 16.1, *)
Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
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 renderer's data cache.
80+
internal let dataCache: NSCache<NSString, NSData> = NSCache()
81+
82+
/// Semaphore for thread-safe access to `dataCache`.
83+
internal let dataCacheSemaphore = DispatchSemaphore(value: 1)
84+
85+
/// The renderer's image cache.
86+
internal let imageCache: NSCache<NSString, _Image> = NSCache()
87+
88+
/// Semaphore for thread-safe access to `imageCache`.
89+
internal let imageCacheSemaphore = DispatchSemaphore(value: 1)
90+
91+
}
92+
93+
// MARK: Public methods
94+
95+
extension Cache {
96+
97+
/// Safely access the cache value for the given key.
98+
///
99+
/// - Parameter key: The key of the value to get.
100+
/// - Returns: A value.
101+
func dataCacheValue(for key: SVGCacheKey) -> Data? {
102+
dataCacheSemaphore.wait()
103+
defer { dataCacheSemaphore.signal() }
104+
return dataCache.object(forKey: key.key() as NSString) as Data?
105+
}
106+
107+
/// Safely sets the cache value.
108+
///
109+
/// - Parameters:
110+
/// - value: The value to set.
111+
/// - key: The value's key.
112+
func setDataCacheValue(_ value: Data, for key: SVGCacheKey) {
113+
dataCacheSemaphore.wait()
114+
dataCache.setObject(value as NSData, forKey: key.key() as NSString)
115+
dataCacheSemaphore.signal()
116+
}
117+
118+
/// Safely access the cache value for the given key.
119+
///
120+
/// - Parameter key: The key of the value to get.
121+
/// - Returns: A value.
122+
func imageCacheValue(for key: ImageCacheKey) -> _Image? {
123+
imageCacheSemaphore.wait()
124+
defer { imageCacheSemaphore.signal() }
125+
return imageCache.object(forKey: key.key() as NSString)
126+
}
127+
128+
/// Safely sets the cache value.
129+
///
130+
/// - Parameters:
131+
/// - value: The value to set.
132+
/// - key: The value's key.
133+
func setImageCacheValue(_ value: _Image, for key: ImageCacheKey) {
134+
imageCacheSemaphore.wait()
135+
imageCache.setObject(value, forKey: key.key() as NSString)
136+
imageCacheSemaphore.signal()
137+
}
138+
139+
}

0 commit comments

Comments
 (0)