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)
}
@@ -14,7 +14,9 @@ struct AttributedStringBuilderComponent: Hashable {
}
protocol AttributedStringBuilderProtocol {
func fromPlain(_ string: String?) -> AttributedString?
func fromHTML(_ htmlString: String?) -> AttributedString?
func fromMarkdown(_ string: String?) -> AttributedString?
func blockquoteCoalescedComponentsFrom(_ attributedString: AttributedString?) -> [AttributedStringBuilderComponent]?
}
@@ -1,27 +0,0 @@
//
// AttributedStringBuilderUtils.h
// ElementX
//
// Created by Stefan Ceriu on 23/03/2022.
// Copyright © 2022 Element. All rights reserved.
//
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
FOUNDATION_EXPORT NSString *const kMXKToolsBlockquoteMarkAttribute;
@interface AttributedStringBuilderUtils : NSObject
+ (NSString*)cssToMarkBlockquotes;
+ (NSAttributedString *)removeDTCoreTextArtifacts:(NSAttributedString *)attributedString;
+ (NSAttributedString*)removeMarkedBlockquotesArtifacts:(NSAttributedString*)attributedString;
+ (NSAttributedString*)createLinks:(NSAttributedString*)attributedString;
@end
NS_ASSUME_NONNULL_END
@@ -1,340 +0,0 @@
//
// AttributedStringBuilderUtils.m
// ElementX
//
// Created by Stefan Ceriu on 23/03/2022.
// Copyright © 2022 Element. All rights reserved.
//
#import "AttributedStringBuilderUtils.h"
@import DTCoreText;
// Temporary background color used to identify blockquote blocks with DTCoreText.
#define kMXKToolsBlockquoteMarkColor [UIColor magentaColor]
// Attribute in an NSAttributeString that marks a blockquote block that was in the original HTML string.
NSString *const kMXKToolsBlockquoteMarkAttribute = @"kMXKToolsBlockquoteMarkAttribute";
static NSRegularExpression *userIdRegex;
static NSRegularExpression *roomIdRegex;
static NSRegularExpression *roomAliasRegex;
static NSRegularExpression *eventIdRegex;
static NSRegularExpression *groupIdRegex;
static NSRegularExpression *httpLinksRegex;
static NSRegularExpression *htmlTagsRegex;
#define MATRIX_HOMESERVER_DOMAIN_REGEX @"[A-Z0-9]+((\\.|\\-)[A-Z0-9]+){0,}(:[0-9]{2,5})?"
NSString *const kMXToolsRegexStringForMatrixUserIdentifier = @"@[\\x21-\\x39\\x3B-\\x7F]+:" MATRIX_HOMESERVER_DOMAIN_REGEX;
NSString *const kMXToolsRegexStringForMatrixRoomAlias = @"#[A-Z0-9._%#@=+-]+:" MATRIX_HOMESERVER_DOMAIN_REGEX;
NSString *const kMXToolsRegexStringForMatrixRoomIdentifier = @"![A-Z0-9]+:" MATRIX_HOMESERVER_DOMAIN_REGEX;
NSString *const kMXToolsRegexStringForMatrixEventIdentifier = @"\\$[A-Z0-9]+:" MATRIX_HOMESERVER_DOMAIN_REGEX;
NSString *const kMXToolsRegexStringForMatrixEventIdentifierV3 = @"\\$[A-Z0-9\\/+]+";
NSString *const kMXToolsRegexStringForMatrixGroupIdentifier = @"\\+[A-Z0-9=_\\-./]+:" MATRIX_HOMESERVER_DOMAIN_REGEX;
@implementation AttributedStringBuilderUtils
+ (void)initialize
{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
userIdRegex = [NSRegularExpression regularExpressionWithPattern:kMXToolsRegexStringForMatrixUserIdentifier options:NSRegularExpressionCaseInsensitive error:nil];
roomIdRegex = [NSRegularExpression regularExpressionWithPattern:kMXToolsRegexStringForMatrixRoomIdentifier options:NSRegularExpressionCaseInsensitive error:nil];
roomAliasRegex = [NSRegularExpression regularExpressionWithPattern:kMXToolsRegexStringForMatrixRoomAlias options:NSRegularExpressionCaseInsensitive error:nil];
eventIdRegex = [NSRegularExpression regularExpressionWithPattern:kMXToolsRegexStringForMatrixEventIdentifier options:NSRegularExpressionCaseInsensitive error:nil];
groupIdRegex = [NSRegularExpression regularExpressionWithPattern:kMXToolsRegexStringForMatrixGroupIdentifier options:NSRegularExpressionCaseInsensitive error:nil];
httpLinksRegex = [NSRegularExpression regularExpressionWithPattern:@"(?i)\\b(https?://\\S*)\\b" options:NSRegularExpressionCaseInsensitive error:nil];
htmlTagsRegex = [NSRegularExpression regularExpressionWithPattern:@"<(\\w+)[^>]*>" options:NSRegularExpressionCaseInsensitive error:nil];
});
}
+ (NSString*)cssToMarkBlockquotes
{
return [NSString stringWithFormat:@"blockquote {background: #%lX; display: block;}", (unsigned long)[[self class] rgbValueWithColor:kMXKToolsBlockquoteMarkColor]];
}
+ (NSUInteger)rgbValueWithColor:(UIColor*)color
{
CGFloat red, green, blue, alpha;
[color getRed:&red green:&green blue:&blue alpha:&alpha];
NSUInteger rgbValue = ((int)(red * 255) << 16) + ((int)(green * 255) << 8) + (blue * 255);
return rgbValue;
}
+ (NSAttributedString *)removeDTCoreTextArtifacts:(NSAttributedString *)attributedString
{
NSMutableAttributedString *mutableAttributedString = [[NSMutableAttributedString alloc] initWithAttributedString:attributedString];
// 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 ([mutableAttributedString.string hasSuffixCharacterFromSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]])
{
[mutableAttributedString deleteCharactersInRange:NSMakeRange(mutableAttributedString.length - 1, 1)];
}
// New lines may have also been introduced by the paragraph style
// Make sure the last paragraph style has no spacing
[mutableAttributedString enumerateAttributesInRange:NSMakeRange(0, mutableAttributedString.length) options:(NSAttributedStringEnumerationReverse) usingBlock:^(NSDictionary *attrs, NSRange range, BOOL *stop) {
if (attrs[NSParagraphStyleAttributeName])
{
NSString *subString = [mutableAttributedString.string substringWithRange:range];
NSArray *components = [subString componentsSeparatedByCharactersInSet:[NSCharacterSet newlineCharacterSet]];
NSMutableDictionary *updatedAttrs = [NSMutableDictionary dictionaryWithDictionary:attrs];
NSMutableParagraphStyle *paragraphStyle = [updatedAttrs[NSParagraphStyleAttributeName] mutableCopy];
paragraphStyle.paragraphSpacing = 0;
updatedAttrs[NSParagraphStyleAttributeName] = paragraphStyle;
if (components.count > 1)
{
NSString *lastComponent = components.lastObject;
NSRange range2 = NSMakeRange(range.location, range.length - lastComponent.length);
[mutableAttributedString setAttributes:attrs range:range2];
range2 = NSMakeRange(range2.location + range2.length, lastComponent.length);
[mutableAttributedString setAttributes:updatedAttrs range:range2];
}
else
{
[mutableAttributedString setAttributes:updatedAttrs range:range];
}
}
// Check only the last paragraph
*stop = YES;
}];
// Image rendering failed on an exception until we replace the DTImageTextAttachments with a simple NSTextAttachment subclass
// (thanks to https://github.com/Cocoanetics/DTCoreText/issues/863).
[mutableAttributedString enumerateAttribute:NSAttachmentAttributeName
inRange:NSMakeRange(0, mutableAttributedString.length)
options:0
usingBlock:^(id value, NSRange range, BOOL *stop) {
if ([value isKindOfClass:DTImageTextAttachment.class])
{
DTImageTextAttachment *attachment = (DTImageTextAttachment*)value;
NSTextAttachment *textAttachment = [[NSTextAttachment alloc] init];
if (attachment.image)
{
textAttachment.image = attachment.image;
CGRect frame = textAttachment.bounds;
frame.size = attachment.displaySize;
textAttachment.bounds = frame;
}
// Note we remove here attachment without image.
NSAttributedString *attrStringWithImage = [NSAttributedString attributedStringWithAttachment:textAttachment];
[mutableAttributedString replaceCharactersInRange:range withAttributedString:attrStringWithImage];
}
}];
return mutableAttributedString;
}
+ (NSAttributedString*)removeMarkedBlockquotesArtifacts:(NSAttributedString*)attributedString
{
NSMutableAttributedString *mutableAttributedString = [[NSMutableAttributedString alloc] initWithAttributedString:attributedString];
// 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
// `DTTextBlocksAttribute` case
[attributedString enumerateAttribute:DTTextBlocksAttribute
inRange:NSMakeRange(0, attributedString.length)
options:0
usingBlock:^(id _Nullable value, NSRange range, BOOL * _Nonnull stop)
{
if ([value isKindOfClass:NSArray.class])
{
NSArray *array = (NSArray*)value;
if (array.count > 0 && [array[0] isKindOfClass:DTTextBlock.class])
{
DTTextBlock *dtTextBlock = (DTTextBlock *)array[0];
if ([dtTextBlock.backgroundColor isEqual:kMXKToolsBlockquoteMarkColor])
{
// Apply our own attribute
[mutableAttributedString addAttribute:kMXKToolsBlockquoteMarkAttribute value:@(YES) range:range];
// Fix a boring behaviour where DTCoreText add a " " string before a string corresponding
// to an HTML blockquote. This " " string has ParagraphStyle.headIndent = 0 which breaks
// the blockquote block indentation
if (range.location > 0)
{
NSRange prevRange = NSMakeRange(range.location - 1, 1);
NSRange effectiveRange;
NSParagraphStyle *paragraphStyle = [attributedString attribute:NSParagraphStyleAttributeName
atIndex:prevRange.location
effectiveRange:&effectiveRange];
// Check if this is the " " string
if (paragraphStyle && effectiveRange.length == 1 && paragraphStyle.firstLineHeadIndent != 25)
{
// Fix its paragraph style
NSMutableParagraphStyle *newParagraphStyle = [paragraphStyle mutableCopy];
newParagraphStyle.firstLineHeadIndent = 25.0;
newParagraphStyle.headIndent = 25.0;
[mutableAttributedString addAttribute:NSParagraphStyleAttributeName value:newParagraphStyle range:prevRange];
}
}
}
}
}
}];
// `NSBackgroundColorAttributeName` case
[mutableAttributedString enumerateAttribute:NSBackgroundColorAttributeName
inRange:NSMakeRange(0, mutableAttributedString.length)
options:0
usingBlock:^(id value, NSRange range, BOOL *stop)
{
if ([value isKindOfClass:UIColor.class] && [(UIColor*)value isEqual:kMXKToolsBlockquoteMarkColor])
{
// Remove the marked background
[mutableAttributedString removeAttribute:NSBackgroundColorAttributeName range:range];
// And apply our own attribute
[mutableAttributedString addAttribute:kMXKToolsBlockquoteMarkAttribute value:@(YES) range:range];
}
}];
return mutableAttributedString;
}
+ (NSAttributedString*)createLinks:(NSAttributedString*)attributedString
{
if (!attributedString)
{
return nil;
}
NSMutableAttributedString *postRenderAttributedString;
[[self class] createHTTPLinksInAttributedString:attributedString withWorkingAttributedString:&postRenderAttributedString];
[[self class] createLinksInAttributedString:attributedString matchingRegex:userIdRegex withWorkingAttributedString:&postRenderAttributedString];
[[self class] createLinksInAttributedString:attributedString matchingRegex:roomIdRegex withWorkingAttributedString:&postRenderAttributedString];
[[self class] createLinksInAttributedString:attributedString matchingRegex:roomAliasRegex withWorkingAttributedString:&postRenderAttributedString];
[[self class] createLinksInAttributedString:attributedString matchingRegex:eventIdRegex withWorkingAttributedString:&postRenderAttributedString];
[[self class] createLinksInAttributedString:attributedString matchingRegex:groupIdRegex withWorkingAttributedString:&postRenderAttributedString];
return postRenderAttributedString ? postRenderAttributedString : attributedString;
}
+ (void)createHTTPLinksInAttributedString:(NSAttributedString*)attributedString
withWorkingAttributedString:(NSMutableAttributedString* __autoreleasing *)mutableAttributedString
{
// Enumerate each string matching the regex
[httpLinksRegex enumerateMatchesInString:attributedString.string options:0 range:NSMakeRange(0, attributedString.length) usingBlock:^(NSTextCheckingResult *match, NSMatchingFlags flags, BOOL *stop) {
// Do not create a link if there is already one on the found match
__block BOOL hasAlreadyLink = NO;
[attributedString enumerateAttributesInRange:match.range options:0 usingBlock:^(NSDictionary<NSString *,id> * _Nonnull attrs, NSRange range, BOOL * _Nonnull stop) {
if (attrs[NSLinkAttributeName])
{
hasAlreadyLink = YES;
*stop = YES;
}
}];
if (!hasAlreadyLink)
{
// Create the output string only if it is necessary because attributed strings cost CPU
if (!*mutableAttributedString)
{
*mutableAttributedString = [[NSMutableAttributedString alloc] initWithAttributedString:attributedString];
}
// Make the link clickable
// Caution: We need here to escape the non-ASCII characters (like '#' in room alias)
// to convert the link into a legal URL string.
NSString *link = [attributedString.string substringWithRange:match.range];
link = [link stringByAddingPercentEncodingWithAllowedCharacters:[NSCharacterSet URLQueryAllowedCharacterSet]];
[*mutableAttributedString addAttribute:NSLinkAttributeName value:link range:match.range];
[*mutableAttributedString addAttribute:NSForegroundColorAttributeName value:UIColor.blueColor range:match.range];
}
}];
}
+ (void)createLinksInAttributedString:(NSAttributedString*)attributedString
matchingRegex:(NSRegularExpression*)regex
withWorkingAttributedString:(NSMutableAttributedString* __autoreleasing *)mutableAttributedString
{
__block NSArray *linkMatches;
// Enumerate each string matching the regex
[regex enumerateMatchesInString:attributedString.string options:0 range:NSMakeRange(0, attributedString.length) usingBlock:^(NSTextCheckingResult *match, NSMatchingFlags flags, BOOL *stop) {
// Do not create a link if there is already one on the found match
__block BOOL hasAlreadyLink = NO;
[attributedString enumerateAttributesInRange:match.range options:0 usingBlock:^(NSDictionary<NSString *,id> * _Nonnull attrs, NSRange range, BOOL * _Nonnull stop) {
if (attrs[NSLinkAttributeName])
{
hasAlreadyLink = YES;
*stop = YES;
}
}];
// Do not create a link if the match is part of an http link.
// The http link will be automatically generated by the UI afterwards.
// So, do not break it now by adding a link on a subset of this http link.
if (!hasAlreadyLink)
{
if (!linkMatches)
{
// Search for the links in the string only once
// Do not use NSDataDetector with NSTextCheckingTypeLink because is not able to
// manage URLs with 2 hashes like "https://matrix.to/#/#matrix:matrix.org"
// Such URL is not valid but web browsers can open them and users C+P them...
// NSDataDetector does not support it but UITextView and UIDataDetectorTypeLink
// detect them when they are displayed. So let the UI create the link at display.
linkMatches = [httpLinksRegex matchesInString:attributedString.string options:0 range:NSMakeRange(0, attributedString.length)];
}
for (NSTextCheckingResult *linkMatch in linkMatches)
{
// If the match is fully in the link, skip it
if (NSIntersectionRange(match.range, linkMatch.range).length == match.range.length)
{
hasAlreadyLink = YES;
break;
}
}
}
if (!hasAlreadyLink)
{
// Create the output string only if it is necessary because attributed strings cost CPU
if (!*mutableAttributedString)
{
*mutableAttributedString = [[NSMutableAttributedString alloc] initWithAttributedString:attributedString];
}
// Make the link clickable
// Caution: We need here to escape the non-ASCII characters (like '#' in room alias)
// to convert the link into a legal URL string.
NSString *link = [attributedString.string substringWithRange:match.range];
link = [link stringByAddingPercentEncodingWithAllowedCharacters:[NSCharacterSet URLQueryAllowedCharacterSet]];
[*mutableAttributedString addAttribute:NSLinkAttributeName value:link range:match.range];
[*mutableAttributedString addAttribute:NSForegroundColorAttributeName value:UIColor.blueColor range:match.range];
}
}];
}
@end
@@ -10,7 +10,7 @@ import Foundation
enum BlockquoteAttribute: AttributedStringKey {
typealias Value = Bool
public static var name = kMXKToolsBlockquoteMarkAttribute
public static var name = "MXBlockquoteAttribute"
}
extension AttributeScopes {
@@ -0,0 +1,35 @@
//
// MatrixEntitityRegex.swift
// ElementX
//
// Created by Stefan Ceriu on 26/03/2022.
// Copyright © 2022 Element. All rights reserved.
//
import Foundation
enum MatrixEntityRegex: String {
case homeserver
case userId
case roomAlias
case roomId
case eventId
case groupId
var rawValue: String {
switch self {
case .homeserver:
return "[A-Z0-9]+((\\.|\\-)[A-Z0-9]+){0,}(:[0-9]{2,5})?"
case .userId:
return "@[\\x21-\\x39\\x3B-\\x7F]+:" + MatrixEntityRegex.homeserver.rawValue
case .roomAlias:
return "#[A-Z0-9._%#@=+-]+:" + MatrixEntityRegex.homeserver.rawValue
case .roomId:
return "![A-Z0-9]+:" + MatrixEntityRegex.homeserver.rawValue
case .eventId:
return "\\$[A-Z0-9]+:" + MatrixEntityRegex.homeserver.rawValue
case .groupId:
return "\\+[A-Z0-9=_\\-./]+:" + MatrixEntityRegex.homeserver.rawValue
}
}
}
@@ -30,7 +30,8 @@ struct RoomTimelineItemFactory {
switch roomMessage {
case let message as TextRoomMessage:
let attributedText = attributedStringBuilder.fromHTML(message.htmlBody ?? message.body)
let attributedText = (message.htmlBody != nil ? attributedStringBuilder.fromHTML(message.htmlBody) : attributedStringBuilder.fromPlain(message.body))
let attributedComponents = attributedStringBuilder.blockquoteCoalescedComponentsFrom(attributedText)
return TextRoomTimelineItem(id: message.id,
@@ -1,5 +1,3 @@
//
// Use this file to import your target's public headers that you would like to expose to Swift.
//
#import "AttributedStringBuilderUtils.h"