Files
Pulse/Sources/PulseUI/Helpers/TextUtilities.swift
2024-07-14 15:55:18 -04:00

136 lines
5.3 KiB
Swift

// The MIT License (MIT)
//
// Copyright (c) 2020-2024 Alexander Grebenyuk (github.com/kean).
import Foundation
#if os(macOS)
import AppKit
#endif
#if os(iOS) || os(visionOS)
import PDFKit
#endif
#if os(iOS) || os(tvOS) || os(visionOS)
import UIKit
#endif
enum TextUtilities {
static func plainText(from string: NSAttributedString) -> String {
string.string
}
static func html(from string: NSAttributedString) throws -> Data {
let range = NSRange(location: 0, length: string.length)
let data = try string.data(from: range, documentAttributes: [
.documentType: NSAttributedString.DocumentType.html
])
guard let html = NSMutableString(data: data, encoding: NSUTF8StringEncoding) else {
return data
}
func getRange(of string: String) -> NSRange? {
let range = html.range(of: string)
return range.location == NSNotFound ? nil : range
}
func insert(_ string: String, at index: Int) {
html.insert("\n\(string)", at: index)
}
if let range = getRange(of: "<head>") {
insert(#"<meta name="viewport" content="width=device-width, initial-scale=1">"#, at: range.upperBound)
insert(#"<meta name="generator" content="Pulse">"#, at: range.upperBound)
}
if let range = getRange(of: "<style type=\"text/css\">") {
insert(#"span { line-height: 1.4; }"#, at: range.upperBound)
insert(#"body { word-wrap: break-word; padding: 8px; }"#, at: range.upperBound)
// TODO: Rewrite this without sync
if Thread.isMainThread {
if isDarkMode {
insert(#"body { background-color: #2C2A28; }"#, at: range.upperBound)
}
} else {
DispatchQueue.main.sync {
if isDarkMode {
insert(#"body { background-color: #2C2A28; }"#, at: range.upperBound)
}
}
}
}
if let range = getRange(of: "</style>") {
do {
let regular = #"font-family: 'SF Pro Text', -apple-system, sans-serif"#
let regex = try NSRegularExpression(pattern: "font-family: '.SFUI-\\w*'", options: [])
regex.replaceMatches(in: html, range: NSRange(location: 0, length: range.upperBound), withTemplate: regular)
} catch {
// Should never happen
}
}
if let range = getRange(of: "</style>") {
do {
let mono = #"font-family: 'SF Mono', SFMono-Regular, ui-monospace, Menlo, monospace;"#
let regex = try NSRegularExpression(pattern: "font-family: '.AppleSystemUIFontMonospaced-\\w*'", options: [])
regex.replaceMatches(in: html, range: NSRange(location: 0, length: range.upperBound), withTemplate: mono)
} catch {
// Should never happen
}
}
return html.data(using: NSUTF8StringEncoding) ?? data
}
/// Renders the given attributed string as PDF
#if os(iOS) || os(visionOS)
static func pdf(from string: NSAttributedString) throws -> Data {
let string = NSMutableAttributedString(attributedString: string)
string.enumerateAttribute(.font, in: NSRange(location: 0, length: string.length)) { font, range, _ in
guard let font = font as? UXFont else { return }
let scaledFont = UXFont(descriptor: font.fontDescriptor, size: (font.pointSize * 0.7).rounded())
string.addAttribute(.font, value: scaledFont, range: range)
}
let formatter = UISimpleTextPrintFormatter(attributedText: string)
let renderer = UIPrintPageRenderer()
renderer.addPrintFormatter(formatter, startingAtPageAt: 0)
let pageSize = CGSize(width: 612, height: 792) // US letter size
let pageMargins = UIEdgeInsets(top: 32, left: 32, bottom: 32, right: 32)
// Calculate the printable rect from the above two
let printableRect = CGRect(x: pageMargins.left, y: pageMargins.top, width: pageSize.width - pageMargins.left - pageMargins.right, height: pageSize.height - pageMargins.top - pageMargins.bottom)
let paperRect = CGRect(x: 0, y: 0, width: pageSize.width, height: pageSize.height)
renderer.setValue(NSValue(cgRect: paperRect), forKey: "paperRect")
renderer.setValue(NSValue(cgRect: printableRect), forKey: "printableRect")
let data = NSMutableData()
UIGraphicsBeginPDFContextToData(data, paperRect, nil)
renderer.prepare(forDrawingPages: NSRange(location: 0, length: renderer.numberOfPages))
let bounds = UIGraphicsGetPDFContextBounds()
for i in 0 ..< renderer.numberOfPages {
UIGraphicsBeginPDFPage()
renderer.drawPage(at: i, in: bounds)
}
UIGraphicsEndPDFContext()
return data as Data
}
#endif
static func har(from string: NSAttributedString) -> Data {
string.string.data(using: .utf8) ?? Data()
}
}
private var isDarkMode: Bool {
#if os(iOS) || os(tvOS) || os(visionOS)
return UIViewController().traitCollection.userInterfaceStyle == .dark
#elseif os(watchOS)
return true
#elseif os(macOS)
return NSAppearance.currentDrawing().name != .aqua
#endif
}