Skip to content

Commit

Permalink
Merge pull request #17 from colinc86/develop
Browse files Browse the repository at this point in the history
Develop
  • Loading branch information
colinc86 authored Jun 21, 2023
2 parents 3d1888a + 0d6a913 commit 3af34ba
Show file tree
Hide file tree
Showing 11 changed files with 581 additions and 502 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ It won't
Add the dependency to your package manifest file.

```swift
.package(url: "https://github.com/colinc86/LaTeXSwiftUI", from: "1.2.2")
.package(url: "https://github.com/colinc86/LaTeXSwiftUI", from: "1.2.3")
```

## ⌨️ Usage
Expand Down
8 changes: 8 additions & 0 deletions Sources/LaTeXSwiftUI/Extensions/Font+Extensions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ import Cocoa
#endif

internal extension Font {

/// The font's text style.
func textStyle() -> _Font.TextStyle? {
switch self {
case .largeTitle, .largeTitle.bold(), .largeTitle.italic(), .largeTitle.monospaced(): return .largeTitle
Expand All @@ -48,6 +50,12 @@ internal extension Font {
default: return nil
}
}

/// The font's x-height.
var xHeight: CGFloat {
_Font.preferredFont(from: self).xHeight
}

}

internal extension _Font {
Expand Down
41 changes: 41 additions & 0 deletions Sources/LaTeXSwiftUI/Extensions/MathJax+Extensions.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
//
// MathJax+Extensions.swift
// LaTeXSwiftUI
//
// Copyright (c) 2023 Colin Campbell
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to
// deal in the Software without restriction, including without limitation the
// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
// sell copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
// IN THE SOFTWARE.
//

import Foundation
import MathJaxSwift

internal extension MathJax {

static var svgRenderer: MathJax? = {
do {
return try MathJax(preferredOutputFormat: .svg)
}
catch {
NSLog("Error creating MathJax instance: \(error)")
return nil
}
}()

}
112 changes: 71 additions & 41 deletions Sources/LaTeXSwiftUI/LaTeX.swift
Original file line number Diff line number Diff line change
Expand Up @@ -104,18 +104,18 @@ public struct LaTeX: View {

/// The package's shared data cache.
public static var dataCache: NSCache<NSString, NSData> {
Renderer.shared.dataCache
Cache.shared.dataCache
}

#if os(macOS)
/// The package's shared image cache.
public static var imageCache: NSCache<NSString, NSImage> {
Renderer.shared.imageCache
Cache.shared.imageCache
}
#else
/// The package's shared image cache.
public static var imageCache: NSCache<NSString, UIImage> {
Renderer.shared.imageCache
Cache.shared.imageCache
}
#endif

Expand Down Expand Up @@ -156,24 +156,8 @@ public struct LaTeX: View {

// MARK: Private properties

/// The view's render state.
@StateObject private var renderState: LaTeXRenderState

/// Renders the blocks synchronously.
///
/// This will block whatever thread you call it on.
private var syncBlocks: [ComponentBlock] {
Renderer.shared.render(
blocks: Parser.parse(unencodeHTML ? latex.htmlUnescape() : latex, mode: parsingMode),
font: font ?? .body,
displayScale: displayScale,
texOptions: texOptions)
}

/// The TeX options to use when submitting requests to the renderer.
private var texOptions: TeXInputProcessorOptions {
TeXInputProcessorOptions(processEscapes: processEscapes, errorMode: errorMode)
}
/// The view's renderer.
@StateObject private var renderer: Renderer

// MARK: Initializers

Expand All @@ -182,33 +166,35 @@ public struct LaTeX: View {
/// - Parameter latex: The LaTeX input.
public init(_ latex: String) {
self.latex = latex
_renderState = StateObject(wrappedValue: LaTeXRenderState(latex: latex))
_renderer = StateObject(wrappedValue: Renderer(latex: latex))
}

// MARK: View body

public var body: some View {
VStack(spacing: 0) {
if renderState.rendered {
bodyWithBlocks(renderState.blocks)
if renderer.rendered {
// If our blocks have been rendered, display them
bodyWithBlocks(renderer.blocks)
}
else if isCached() {
// If our blocks are cached, display them
bodyWithBlocks(renderSync())
}
else {
// The view is not rendered nor cached
switch renderingStyle {
case .empty:
Text("")
.task(render)
case .original:
Text(latex)
.task(render)
case .progress:
ProgressView()
.task(render)
case .empty, .original, .progress:
// Render the components asynchronously
loadingView().task(renderAsync)
case .wait:
bodyWithBlocks(syncBlocks)
// Render the components synchronously
bodyWithBlocks(renderSync())
}
}
}
.animation(renderingAnimation, value: renderState.rendered)
.animation(renderingAnimation, value: renderer.rendered)
.environmentObject(renderer)
}

}
Expand All @@ -220,7 +206,7 @@ extension LaTeX {
/// Preloads the view's SVG and image data.
public func preload() {
Task {
await render()
await renderAsync()
}
}
}
Expand All @@ -229,14 +215,45 @@ extension LaTeX {

extension LaTeX {

/// Checks the renderer's caches for the current view.
///
/// If this method returns `true`, then there is no need to do an async
/// render.
///
/// - Returns: A boolean indicating whether the components to the view are
/// cached.
private func isCached() -> Bool {
renderer.isCached(
unencodeHTML: unencodeHTML,
parsingMode: parsingMode,
processEscapes: processEscapes,
errorMode: errorMode,
font: font ?? .body,
displayScale: displayScale)
}

/// Renders the view's components.
@Sendable private func render() async {
await renderState.render(
@Sendable private func renderAsync() async {
await renderer.render(
unencodeHTML: unencodeHTML,
parsingMode: parsingMode,
processEscapes: processEscapes,
errorMode: errorMode,
font: font ?? .body,
displayScale: displayScale)
}

/// Renders the view's components synchronously.
///
/// - Returns: The rendered components.
private func renderSync() -> [ComponentBlock] {
renderer.renderSync(
unencodeHTML: unencodeHTML,
parsingMode: parsingMode,
font: font,
displayScale: displayScale,
texOptions: texOptions)
processEscapes: processEscapes,
errorMode: errorMode,
font: font ?? .body,
displayScale: displayScale)
}

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

@MainActor @ViewBuilder private func loadingView() -> some View {
switch renderingStyle {
case .empty:
Text("")
case .original:
Text(latex)
case .progress:
ProgressView()
default:
EmptyView()
}
}

}

@available(iOS 16.1, *)
Expand Down
146 changes: 146 additions & 0 deletions Sources/LaTeXSwiftUI/Models/Cache.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
//
// Cache.swift
// LaTeXSwiftUI
//
// Copyright (c) 2023 Colin Campbell
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to
// deal in the Software without restriction, including without limitation the
// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
// sell copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
// IN THE SOFTWARE.
//

import CryptoKit
import Foundation
import MathJaxSwift

fileprivate protocol CacheKey: Codable {

/// The key type used to identify the cache key in storage.
static var keyType: String { get }

/// A key to use if encoding fails.
var fallbackKey: String { get }

}

extension CacheKey {

/// The key to use in the cache.
func key() -> String {
do {
let data = try JSONEncoder().encode(self)
let hashedData = SHA256.hash(data: data)
return hashedData.compactMap { String(format: "%02x", $0) }.joined() + "-" + Self.keyType
}
catch {
return fallbackKey + "-" + Self.keyType
}
}

}

internal class Cache {

// MARK: Types

/// An SVG cache key.
struct SVGCacheKey: CacheKey {
static let keyType: String = "svg"
let componentText: String
let conversionOptions: ConversionOptions
let texOptions: TeXInputProcessorOptions
internal var fallbackKey: String { componentText }
}

/// An image cache key.
struct ImageCacheKey: CacheKey {
static let keyType: String = "image"
let svg: SVG
let xHeight: CGFloat
internal var fallbackKey: String { String(data: svg.data, encoding: .utf8) ?? "" }
}

// MARK: Static properties

/// The shared cache.
static let shared = Cache()

// MARK: Public properties

/// The renderer's data cache.
let dataCache: NSCache<NSString, NSData> = NSCache()

/// The renderer's image cache.
let imageCache: NSCache<NSString, _Image> = NSCache()

// MARK: Private properties

/// Semaphore for thread-safe access to `dataCache`.
let dataCacheSemaphore = DispatchSemaphore(value: 1)

/// Semaphore for thread-safe access to `imageCache`.
let imageCacheSemaphore = DispatchSemaphore(value: 1)

}

// MARK: Public methods

extension Cache {

/// Safely access the cache value for the given key.
///
/// - Parameter key: The key of the value to get.
/// - Returns: A value.
func dataCacheValue(for key: SVGCacheKey) -> Data? {
dataCacheSemaphore.wait()
defer { dataCacheSemaphore.signal() }
return dataCache.object(forKey: key.key() as NSString) as Data?
}

/// Safely sets the cache value.
///
/// - Parameters:
/// - value: The value to set.
/// - key: The value's key.
func setDataCacheValue(_ value: Data, for key: SVGCacheKey) {
dataCacheSemaphore.wait()
dataCache.setObject(value as NSData, forKey: key.key() as NSString)
dataCacheSemaphore.signal()
}

/// Safely access the cache value for the given key.
///
/// - Parameter key: The key of the value to get.
/// - Returns: A value.
func imageCacheValue(for key: ImageCacheKey) -> _Image? {
imageCacheSemaphore.wait()
defer { imageCacheSemaphore.signal() }
return imageCache.object(forKey: key.key() as NSString)
}

/// Safely sets the cache value.
///
/// - Parameters:
/// - value: The value to set.
/// - key: The value's key.
func setImageCacheValue(_ value: _Image, for key: ImageCacheKey) {
imageCacheSemaphore.wait()
imageCache.setObject(value, forKey: key.key() as NSString)
imageCacheSemaphore.signal()
}

}
Loading

0 comments on commit 3af34ba

Please sign in to comment.