mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-03 06:31:27 +00:00
Reimplemented ObjC attributed string handling in swift.
This commit is contained in:
@@ -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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user