Reimplemented ObjC attributed string handling in swift.

This commit is contained in:
Stefan Ceriu
2022-03-28 15:37:07 +03:00
parent e069aa48dd
commit 2fcbd33158
10 changed files with 234 additions and 394 deletions
@@ -11,20 +11,53 @@ import DTCoreText
struct AttributedStringBuilder: AttributedStringBuilderProtocol {
private var defaultCSS: String {
AttributedStringBuilderUtils.cssToMarkBlockquotes() +
"""
pre,code {
background-color: #F5F7FA;
display: inline;
font-family: monospace;
white-space: pre;
-coretext-fontname: Menlo-Regular;
private let temporaryBlockquoteMarkingColor = UIColor.magenta
private let linkColor = UIColor.blue
private let userIdDetector: NSRegularExpression
private let roomIdDetector: NSRegularExpression
private let eventIdDetector: NSRegularExpression
private let groupIdDetector: NSRegularExpression
private let roomAliasDetector: NSRegularExpression
private let linkDetector: NSDataDetector
init() {
do {
userIdDetector = try NSRegularExpression(pattern: MatrixEntityRegex.userId.rawValue, options: .caseInsensitive)
roomIdDetector = try NSRegularExpression(pattern: MatrixEntityRegex.roomId.rawValue, options: .caseInsensitive)
eventIdDetector = try NSRegularExpression(pattern: MatrixEntityRegex.eventId.rawValue, options: .caseInsensitive)
groupIdDetector = try NSRegularExpression(pattern: MatrixEntityRegex.groupId.rawValue, options: .caseInsensitive)
roomAliasDetector = try NSRegularExpression(pattern: MatrixEntityRegex.roomAlias.rawValue, options: .caseInsensitive)
linkDetector = try NSDataDetector(types: NSTextCheckingResult.CheckingType.link.rawValue)
} catch {
fatalError()
}
h1,h2,h3 {
font-size: 1.2em;
}
func fromPlain(_ string: String?) -> AttributedString? {
guard let string = string else {
return nil
}
"""
let mutableAttributedString = NSMutableAttributedString(string: string)
addLinkAttributesToAttributedString(mutableAttributedString)
return try? AttributedString(mutableAttributedString, including: \.elementX)
}
func fromMarkdown(_ string: String?) -> AttributedString? {
guard let string = string else {
return nil
}
guard let mutableAttributedString = try? NSMutableAttributedString(markdown: string) else {
return nil
}
addLinkAttributesToAttributedString(mutableAttributedString)
return try? AttributedString(mutableAttributedString, including: \.elementX)
}
// Do not use the default HTML renderer of NSAttributedString because this method
@@ -49,6 +82,7 @@ struct AttributedStringBuilder: AttributedStringBuilderProtocol {
DTDefaultFontName: defaultFont.fontName,
DTDefaultFontSize: defaultFont.pointSize,
DTDefaultTextColor: defaultColor,
DTDefaultLinkColor: linkColor,
DTDefaultStyleSheet: DTCSSStylesheet(styleBlock: self.defaultCSS) as Any
]
@@ -60,17 +94,19 @@ struct AttributedStringBuilder: AttributedStringBuilderProtocol {
element?.sanitize(font: defaultFont)
}
guard var nsAttributedString = builder.generatedAttributedString() else {
guard let attributedString = builder.generatedAttributedString() else {
return nil
}
nsAttributedString = AttributedStringBuilderUtils.removeDTCoreTextArtifacts(nsAttributedString)
let mutableAttributedString = NSMutableAttributedString(attributedString: attributedString)
nsAttributedString = AttributedStringBuilderUtils.removeMarkedBlockquotesArtifacts(nsAttributedString)
removeDTCoreTextArtifactsFromAttributedString(mutableAttributedString)
nsAttributedString = AttributedStringBuilderUtils.createLinks(nsAttributedString)
replaceMarkedBlockquotes(mutableAttributedString)
return try? AttributedString(nsAttributedString, including: \.elementX)
addLinkAttributesToAttributedString(mutableAttributedString)
return try? AttributedString(mutableAttributedString, including: \.elementX)
}
func blockquoteCoalescedComponentsFrom(_ attributedString: AttributedString?) -> [AttributedStringBuilderComponent]? {
@@ -83,4 +119,116 @@ struct AttributedStringBuilder: AttributedStringBuilderProtocol {
isBlockquote: value != nil)
}
}
// MARK: - Private
private func replaceMarkedBlockquotes(_ attributedString: NSMutableAttributedString) {
// Enumerate all sections marked thanks to `cssToMarkBlockquotes`
// and apply our own attribute instead.
// According to blockquotes in the string, DTCoreText can apply 2 policies:
// - define a `DTTextBlocksAttribute` attribute on a <blockquote> block
// - or, just define a `NSBackgroundColorAttributeName` attribute
attributedString.enumerateAttribute(.DTTextBlocks, in: .init(location: 0, length: attributedString.length), options: []) { value, range, _ in
guard let value = value as? NSArray,
let dtTextBlock = value.firstObject as? DTTextBlock,
dtTextBlock.backgroundColor == temporaryBlockquoteMarkingColor else {
return
}
attributedString.addAttribute(.MXBlockquote, value: true, range: range)
}
attributedString.enumerateAttribute(.backgroundColor, in: .init(location: 0, length: attributedString.length), options: []) { value, range, _ in
guard let value = value as? UIColor,
value == temporaryBlockquoteMarkingColor else {
return
}
attributedString.removeAttribute(.backgroundColor, range: range)
attributedString.addAttribute(.MXBlockquote, value: true, range: range)
}
}
private func removeDTCoreTextArtifactsFromAttributedString(_ attributedString: NSMutableAttributedString) {
guard attributedString.length > 0 else {
return
}
// DTCoreText adds a newline at the end of plain text ( https://github.com/Cocoanetics/DTCoreText/issues/779 )
// or after a blockquote section.
// Trim trailing whitespace and newlines in the string content
while (attributedString.string as NSString).hasSuffixCharacter(from: .whitespacesAndNewlines) {
attributedString.deleteCharacters(in: .init(location: attributedString.length - 1, length: 1))
}
}
private func addLinkAttributesToAttributedString(_ attributedString: NSMutableAttributedString) {
let string = attributedString.string
let range = NSRange(location: 0, length: attributedString.string.count)
var matches = userIdDetector.matches(in: string, options: [], range: range)
matches.append(contentsOf: roomIdDetector.matches(in: string, options: [], range: range))
matches.append(contentsOf: eventIdDetector.matches(in: string, options: [], range: range))
matches.append(contentsOf: groupIdDetector.matches(in: string, options: [], range: range))
matches.append(contentsOf: roomAliasDetector.matches(in: string, options: [], range: range))
matches.append(contentsOf: linkDetector.matches(in: string, options: [], range: range))
guard matches.count > 0 else {
return
}
matches.forEach { match in
guard let matchRange = Range(match.range, in: string) else {
return
}
let link = string[matchRange].addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed)
attributedString.addAttributes([NSAttributedString.Key.link: link as Any,
NSAttributedString.Key.foregroundColor: linkColor],
range: match.range)
}
}
private var cssToMarkBlockquotes: String {
return "blockquote {background: \(temporaryBlockquoteMarkingColor.toHexString()); display: block;}"
}
private var defaultCSS: String {
cssToMarkBlockquotes +
"""
pre,code {
background-color: #F5F7FA;
display: inline;
font-family: monospace;
white-space: pre;
-coretext-fontname: Menlo-Regular;
}
h1,h2,h3 {
font-size: 1.2em;
}
"""
}
}
extension UIColor {
func toHexString() -> String {
var red: CGFloat = 0.0
var green: CGFloat = 0.0
var blue: CGFloat = 0.0
var alpha: CGFloat = 0.0
getRed(&red, green: &green, blue: &blue, alpha: &alpha)
let rgb: Int = (Int)(red*255)<<16 | (Int)(green*255)<<8 | (Int)(blue*255)<<0
return NSString(format: "#%06x", rgb) as String
}
}
extension NSAttributedString.Key {
static let DTTextBlocks: NSAttributedString.Key = .init(rawValue: DTTextBlocksAttribute)
static let MXBlockquote: NSAttributedString.Key = .init(rawValue: BlockquoteAttribute.name)
}