mirror of
https://github.com/kean/Pulse.git
synced 2026-05-30 21:07:33 +00:00
132 lines
5.1 KiB
Swift
132 lines
5.1 KiB
Swift
// The MIT License (MIT)
|
||
//
|
||
// Copyright (c) 2020–2023 Alexander Grebenyuk (github.com/kean).
|
||
|
||
import Foundation
|
||
|
||
#if os(macOS)
|
||
import AppKit
|
||
#endif
|
||
|
||
#if os(iOS)
|
||
import PDFKit
|
||
#endif
|
||
|
||
#if os(iOS) || os(tvOS)
|
||
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)
|
||
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: NSMakeRange(0, renderer.numberOfPages))
|
||
|
||
let bounds = UIGraphicsGetPDFContextBounds()
|
||
for i in 0 ..< renderer.numberOfPages {
|
||
UIGraphicsBeginPDFPage()
|
||
renderer.drawPage(at: i, in: bounds)
|
||
}
|
||
UIGraphicsEndPDFContext()
|
||
|
||
return data as Data
|
||
}
|
||
#endif
|
||
}
|
||
|
||
private var isDarkMode: Bool {
|
||
#if os(iOS) || os(tvOS)
|
||
return UIViewController().traitCollection.userInterfaceStyle == .dark
|
||
#elseif os(watchOS)
|
||
return true
|
||
#elseif os(macOS)
|
||
return NSAppearance.currentDrawing().name != .aqua
|
||
#endif
|
||
}
|