element-ios/Riot/Modules/MatrixKit/Utils/EventFormatter/HTMLFormatter.swift

108 lines
4.9 KiB
Swift
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//
// Copyright 2021-2024 New Vector Ltd.
//
// SPDX-License-Identifier: AGPL-3.0-only
// Please see LICENSE in the repository root for full details.
//
import Foundation
import DTCoreText
import UIKit
@objcMembers
class HTMLFormatter: NSObject {
/// Builds an attributed string from a string containing html.
///
/// - Parameters:
/// - htmlString: The html string to use.
/// - allowedTags: The html tags that should be allowed.
/// - font: The default font to use.
/// - imageHandler: The image handler for the formatted string
/// - extraOptions: Extra (or override) options to apply for the format. See DTCoreText's documentation for available options.
/// - postFormatOperations: Optional block to provide operations to apply
/// - Returns: The built `NSAttributedString`.
/// - Note: It is recommended to include "p" and "body" tags in `allowedTags` as these are often added when parsing.
static func formatHTML(_ htmlString: String,
withAllowedTags allowedTags: [String],
font: UIFont,
andImageHandler imageHandler: DTHTMLElement.ImageHandler? = nil,
extraOptions: [AnyHashable: Any] = [:],
postFormatOperations: ((NSMutableAttributedString) -> Void)? = nil) -> NSAttributedString {
guard let data = htmlString.data(using: .utf8) else {
return NSAttributedString(string: htmlString)
}
let sanitizeCallback: DTHTMLAttributedStringBuilderWillFlushCallback = { [allowedTags, font, imageHandler] (element: DTHTMLElement?) in
element?.sanitize(with: allowedTags, bodyFont: font, imageHandler: imageHandler)
}
var options: [AnyHashable: Any] = [
DTUseiOS6Attributes: true,
DTDefaultFontFamily: font.familyName,
DTDefaultFontName: font.fontName,
DTDefaultFontSize: font.pointSize,
DTDefaultLinkDecoration: false,
DTDefaultLinkColor: ThemeService.shared().theme.colors.links,
DTWillFlushBlockCallBack: sanitizeCallback
]
options.merge(extraOptions) { (_, new) in new }
guard let string = self.formatHTML(data, options: options) else {
return NSAttributedString(string: htmlString)
}
let mutableString = NSMutableAttributedString(attributedString: string)
MXKTools.removeDTCoreTextArtifacts(mutableString)
postFormatOperations?(mutableString)
// Remove CTForegroundColorFromContext attribute to fix the iOS 16 black link color issue
// REF: https://github.com/Cocoanetics/DTCoreText/issues/792
mutableString.removeAttribute(NSAttributedString.Key("CTForegroundColorFromContext"), range: NSRange(location: 0, length: mutableString.length))
return mutableString
}
/// Builds an attributed string by replacing a `%@` placeholder with the supplied link text and URL.
/// - Parameters:
/// - string: The string to be formatted.
/// - link: The link text to be inserted.
/// - url: The URL to be linked to.
/// - Returns: An attributed string.
static func format(_ string: String, with link: String, using url: URL) -> NSAttributedString {
let baseString = NSMutableAttributedString(string: string)
let attributedLink = NSAttributedString(string: link, attributes: [.link: url])
let linkRange = (baseString.string as NSString).range(of: "%@")
baseString.replaceCharacters(in: linkRange, with: attributedLink)
return baseString
}
}
extension HTMLFormatter {
/// This replicates DTCoreText's NSAttributedString `initWithHTMLData`.
/// It sets the sanitize callback on the builder from Swift to avoid EXC_BAD_ACCESS crashes.
///
/// - Parameters:
/// - data: The data in HTML format from which to create the attributed string.
/// - options: Specifies how the document should be loaded.
/// - Returns: Returns an initialized object, or `nil` if the data cant be decoded.
@objc static func formatHTML(_ data: Data,
options: [AnyHashable: Any]) -> NSAttributedString? {
guard !data.isEmpty else {
return nil
}
let stringBuilder = DTHTMLAttributedStringBuilder(html: data,
options: options,
// DTCoreText doesn't use document attributes anyway
documentAttributes: nil)
if let willFlushCallback = options[DTWillFlushBlockCallBack] as? DTHTMLAttributedStringBuilderWillFlushCallback {
stringBuilder?.willFlushCallback = willFlushCallback
}
return stringBuilder?.generatedAttributedString()
}
}