mirror of
https://gitlab.opencode.de/bwi/bundesmessenger/clients/bundesmessenger-ios.git
synced 2026-04-17 06:58:28 +02:00
Improve HTML formatter
This commit is contained in:
@@ -535,10 +535,13 @@
|
||||
|
||||
// Apply additional treatments
|
||||
NSInteger mxIdsBitMask = (MXKTOOLS_USER_IDENTIFIER_BITWISE | MXKTOOLS_ROOM_IDENTIFIER_BITWISE | MXKTOOLS_ROOM_ALIAS_BITWISE | MXKTOOLS_EVENT_IDENTIFIER_BITWISE | MXKTOOLS_GROUP_IDENTIFIER_BITWISE);
|
||||
attributedString = [MXKTools createLinksInAttributedString:attributedString forEnabledMatrixIds:mxIdsBitMask];
|
||||
|
||||
NSMutableAttributedString *mutableStr = [[NSMutableAttributedString alloc] initWithAttributedString:attributedString];
|
||||
[MXKTools createLinksInMutableAttributedString:mutableStr forEnabledMatrixIds:mxIdsBitMask];
|
||||
[MXKTools removeDTCoreTextArtifacts:mutableStr];
|
||||
|
||||
// Finalize the attributed string by removing DTCoreText artifacts (Trim trailing newlines, replace DTImageTextAttachments...)
|
||||
_groupLongDescription.attributedText = [MXKTools removeDTCoreTextArtifacts:attributedString];
|
||||
_groupLongDescription.attributedText = mutableStr;
|
||||
_groupLongDescription.contentOffset = CGPointZero;
|
||||
}
|
||||
else
|
||||
|
||||
111
Riot/Modules/MatrixKit/Utils/EventFormatter/HTMLFormatter.swift
Normal file
111
Riot/Modules/MatrixKit/Utils/EventFormatter/HTMLFormatter.swift
Normal file
@@ -0,0 +1,111 @@
|
||||
//
|
||||
// Copyright 2021 New Vector Ltd
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
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.
|
||||
/// - imageHandler: The image handler for the formatted string
|
||||
/// - extraOptions: Extra options to apply for the format
|
||||
/// - font: The default font to use.
|
||||
/// - 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.
|
||||
func formatHTML(_ htmlString: String,
|
||||
withAllowedTags allowedTags: [String],
|
||||
andImageHandler imageHandler: DTHTMLElement.ImageHandler? = nil,
|
||||
extraOptions: [AnyHashable: Any] = [:],
|
||||
font: UIFont,
|
||||
postFormatOperations: ((NSMutableAttributedString) -> Void)? = nil) -> NSAttributedString {
|
||||
guard let data = htmlString.data(using: .utf8) else {
|
||||
return NSAttributedString(string: htmlString)
|
||||
}
|
||||
|
||||
let sanitizeCallback: DTHTMLAttributedStringBuilderWillFlushCallback = { [allowedTags, font] (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,
|
||||
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)
|
||||
|
||||
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.
|
||||
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 can’t be decoded.
|
||||
@objc 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()
|
||||
}
|
||||
}
|
||||
@@ -28,6 +28,7 @@
|
||||
#import "MXRoom+Sync.h"
|
||||
|
||||
#import "MXKRoomNameStringLocalizer.h"
|
||||
#import "GeneratedInterface-Swift.h"
|
||||
|
||||
static NSString *const kHTMLATagRegexPattern = @"<a href=(?:'|\")(.*?)(?:'|\")>([^<]*)</a>";
|
||||
|
||||
@@ -1723,7 +1724,8 @@ static NSString *const kHTMLATagRegexPattern = @"<a href=(?:'|\")(.*?)(?:'|\")>(
|
||||
{
|
||||
// body font is the same with the whole string font, no need to change body font
|
||||
// apply additional treatments
|
||||
return [self postRenderAttributedString:str];
|
||||
[self postRenderAttributedString:str];
|
||||
return str;
|
||||
}
|
||||
|
||||
NSString *body;
|
||||
@@ -1740,13 +1742,14 @@ static NSString *const kHTMLATagRegexPattern = @"<a href=(?:'|\")(.*?)(?:'|\")>(
|
||||
{
|
||||
// body not found in the whole string
|
||||
// apply additional treatments
|
||||
return [self postRenderAttributedString:str];
|
||||
[self postRenderAttributedString:str];
|
||||
return str;
|
||||
}
|
||||
|
||||
NSMutableAttributedString *mutableStr = [str mutableCopy];
|
||||
[mutableStr addAttribute:NSFontAttributeName value:fontForBody range:bodyRange];
|
||||
[str addAttribute:NSFontAttributeName value:fontForBody range:bodyRange];
|
||||
// apply additional treatments
|
||||
return [self postRenderAttributedString:mutableStr];
|
||||
[self postRenderAttributedString:str];
|
||||
return str;
|
||||
}
|
||||
|
||||
- (NSAttributedString*)renderHTMLString:(NSString*)htmlString forEvent:(MXEvent*)event withRoomState:(MXRoomState*)roomState
|
||||
@@ -1769,81 +1772,25 @@ static NSString *const kHTMLATagRegexPattern = @"<a href=(?:'|\")(.*?)(?:'|\")>(
|
||||
|
||||
// Apply the css style that corresponds to the event state
|
||||
UIFont *fontForWholeString = [self fontForEvent:event string:htmlString];
|
||||
|
||||
// Do some sanitisation before finalizing the string
|
||||
|
||||
HTMLFormatter *htmlFormatter = [[HTMLFormatter alloc] init];
|
||||
|
||||
MXWeakify(self);
|
||||
DTHTMLAttributedStringBuilderWillFlushCallback sanitizeCallback = ^(DTHTMLElement *element) {
|
||||
NSAttributedString *str = [htmlFormatter formatHTML:html
|
||||
withAllowedTags:_allowedHTMLTags
|
||||
andImageHandler:_htmlImageHandler
|
||||
extraOptions:@{ DTDefaultTextColor: [self textColorForEvent:event],
|
||||
DTDefaultStyleSheet: dtCSS }
|
||||
font:fontForWholeString
|
||||
postFormatOperations:^(NSMutableAttributedString *mutableStr) {
|
||||
MXStrongifyAndReturnIfNil(self);
|
||||
[element sanitizeWith:self.allowedHTMLTags bodyFont:fontForWholeString imageHandler:self.htmlImageHandler];
|
||||
};
|
||||
[self postFormatMutableAttributedString:mutableStr
|
||||
forEvent:event
|
||||
andRepliedEvent:repliedEvent
|
||||
defaultFont:fontForWholeString];
|
||||
}];
|
||||
|
||||
NSDictionary *options = @{
|
||||
DTUseiOS6Attributes: @(YES), // Enable it to be able to display the attributed string in a UITextView
|
||||
DTDefaultFontFamily: fontForWholeString.familyName,
|
||||
DTDefaultFontName: fontForWholeString.fontName,
|
||||
DTDefaultFontSize: @(fontForWholeString.pointSize),
|
||||
DTDefaultTextColor: [self textColorForEvent:event],
|
||||
DTDefaultLinkDecoration: @(NO),
|
||||
DTDefaultStyleSheet: dtCSS,
|
||||
DTWillFlushBlockCallBack: sanitizeCallback
|
||||
};
|
||||
|
||||
// Do not use the default HTML renderer of NSAttributedString because this method
|
||||
// runs on the UI thread which we want to avoid because renderHTMLString is called
|
||||
// most of the time from a background thread.
|
||||
// Use DTCoreText HTML renderer instead.
|
||||
// Using DTCoreText, which renders static string, helps to avoid code injection attacks
|
||||
// that could happen with the default HTML renderer of NSAttributedString which is a
|
||||
// webview.
|
||||
NSAttributedString *str = [[NSAttributedString alloc] initWithHTMLData:[html dataUsingEncoding:NSUTF8StringEncoding] options:options documentAttributes:NULL];
|
||||
|
||||
// Apply additional treatments
|
||||
str = [self postRenderAttributedString:str];
|
||||
|
||||
// Finalize the attributed string by removing DTCoreText artifacts (Trim trailing newlines).
|
||||
str = [MXKTools removeDTCoreTextArtifacts:str];
|
||||
|
||||
// Finalize HTML blockquote blocks marking
|
||||
str = [MXKTools removeMarkedBlockquotesArtifacts:str];
|
||||
|
||||
if (repliedEvent && repliedEvent.isRedactedEvent)
|
||||
{
|
||||
// Replace the description of an empty replied event
|
||||
NSMutableAttributedString *mutableStr = [[NSMutableAttributedString alloc] initWithAttributedString:str];
|
||||
NSRange nullRange = [mutableStr.string rangeOfString:@"(null)"];
|
||||
if (nullRange.location != NSNotFound)
|
||||
{
|
||||
[mutableStr replaceCharactersInRange:nullRange withAttributedString:[self redactedMessageReplacementAttributedString]];
|
||||
str = mutableStr;
|
||||
}
|
||||
}
|
||||
|
||||
UIFont *fontForBody = [self fontForEvent:event string:nil];
|
||||
if ([fontForWholeString isEqual:fontForBody])
|
||||
{
|
||||
// body font is the same with the whole string font, no need to change body font
|
||||
return str;
|
||||
}
|
||||
|
||||
NSString *body;
|
||||
if (event.content[kMXMessageContentKeyNewContent])
|
||||
{
|
||||
MXJSONModelSetString(body, event.content[kMXMessageContentKeyNewContent][kMXMessageBodyKey]);
|
||||
}
|
||||
else
|
||||
{
|
||||
MXJSONModelSetString(body, event.content[kMXMessageBodyKey]);
|
||||
}
|
||||
NSRange bodyRange = [str.string rangeOfString:body];
|
||||
if (bodyRange.location == NSNotFound)
|
||||
{
|
||||
// body not found in the whole string
|
||||
return str;
|
||||
}
|
||||
|
||||
NSMutableAttributedString *mutableStr = [str mutableCopy];
|
||||
[mutableStr addAttribute:NSFontAttributeName value:fontForBody range:bodyRange];
|
||||
return mutableStr;
|
||||
return str;
|
||||
}
|
||||
|
||||
- (NSAttributedString*)redactedMessageReplacementAttributedString
|
||||
@@ -2017,11 +1964,55 @@ static NSString *const kHTMLATagRegexPattern = @"<a href=(?:'|\")(.*?)(?:'|\")>(
|
||||
return html;
|
||||
}
|
||||
|
||||
- (NSAttributedString*)postRenderAttributedString:(NSAttributedString*)attributedString
|
||||
- (void)postFormatMutableAttributedString:(NSMutableAttributedString*)mutableAttributedString
|
||||
forEvent:(MXEvent*)event
|
||||
andRepliedEvent:(MXEvent*)repliedEvent
|
||||
defaultFont:(UIFont*)defaultFont
|
||||
{
|
||||
if (!attributedString)
|
||||
[self postRenderAttributedString:mutableAttributedString];
|
||||
[MXKTools removeMarkedBlockquotesArtifacts:mutableAttributedString];
|
||||
|
||||
if (repliedEvent && repliedEvent.isRedactedEvent)
|
||||
{
|
||||
return nil;
|
||||
// Replace the description of an empty replied event
|
||||
NSRange nullRange = [mutableAttributedString.string rangeOfString:@"(null)"];
|
||||
if (nullRange.location != NSNotFound)
|
||||
{
|
||||
[mutableAttributedString replaceCharactersInRange:nullRange withAttributedString:[self redactedMessageReplacementAttributedString]];
|
||||
}
|
||||
}
|
||||
|
||||
UIFont *fontForBody = [self fontForEvent:event string:nil];
|
||||
if ([defaultFont isEqual:fontForBody])
|
||||
{
|
||||
// body font is the same with the whole string font, no need to change body font
|
||||
return;
|
||||
}
|
||||
|
||||
NSString *body;
|
||||
if (event.content[kMXMessageContentKeyNewContent])
|
||||
{
|
||||
MXJSONModelSetString(body, event.content[kMXMessageContentKeyNewContent][kMXMessageBodyKey]);
|
||||
}
|
||||
else
|
||||
{
|
||||
MXJSONModelSetString(body, event.content[kMXMessageBodyKey]);
|
||||
}
|
||||
NSRange bodyRange = [mutableAttributedString.string rangeOfString:body];
|
||||
if (bodyRange.location == NSNotFound)
|
||||
{
|
||||
// body not found in the whole string
|
||||
return;
|
||||
}
|
||||
|
||||
[mutableAttributedString addAttribute:NSFontAttributeName value:fontForBody range:bodyRange];
|
||||
}
|
||||
|
||||
- (void)postRenderAttributedString:(NSMutableAttributedString*)mutableAttributedString
|
||||
{
|
||||
if (!mutableAttributedString)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
NSInteger enabledMatrixIdsBitMask= 0;
|
||||
@@ -2056,7 +2047,7 @@ static NSString *const kHTMLATagRegexPattern = @"<a href=(?:'|\")(.*?)(?:'|\")>(
|
||||
enabledMatrixIdsBitMask |= MXKTOOLS_GROUP_IDENTIFIER_BITWISE;
|
||||
}
|
||||
|
||||
return [MXKTools createLinksInAttributedString:attributedString forEnabledMatrixIds:enabledMatrixIdsBitMask];
|
||||
[MXKTools createLinksInMutableAttributedString:mutableAttributedString forEnabledMatrixIds:enabledMatrixIdsBitMask];
|
||||
}
|
||||
|
||||
- (NSAttributedString *)renderString:(NSString *)string withPrefix:(NSString *)prefix forEvent:(MXEvent *)event
|
||||
|
||||
@@ -369,19 +369,17 @@ manualChangeMessageForVideo:(NSString*)manualChangeMessageForVideo
|
||||
- Trim trailing whitespace and newlines in the string content.
|
||||
- Replace DTImageTextAttachments with a simple NSTextAttachment subclass.
|
||||
|
||||
@param attributedString an attributed string.
|
||||
@return the resulting string.
|
||||
@param mutableAttributedString a mutable attributed string.
|
||||
*/
|
||||
+ (NSAttributedString*)removeDTCoreTextArtifacts:(NSAttributedString*)attributedString;
|
||||
+ (void)removeDTCoreTextArtifacts:(NSMutableAttributedString*)mutableAttributedString;
|
||||
|
||||
/**
|
||||
Make some matrix identifiers clickable in the string content.
|
||||
|
||||
@param attributedString an attributed string.
|
||||
@param mutableAttributedString a mutable attributed string.
|
||||
@param enabledMatrixIdsBitMask the bitmask used to list the types of matrix id to process (see MXKTOOLS_XXX__BITWISE).
|
||||
@return the resulting string.
|
||||
*/
|
||||
+ (NSAttributedString*)createLinksInAttributedString:(NSAttributedString*)attributedString forEnabledMatrixIds:(NSInteger)enabledMatrixIdsBitMask;
|
||||
+ (void)createLinksInMutableAttributedString:(NSMutableAttributedString*)mutableAttributedString forEnabledMatrixIds:(NSInteger)enabledMatrixIdsBitMask;
|
||||
|
||||
#pragma mark - HTML processing - blockquote display handling
|
||||
|
||||
@@ -398,10 +396,9 @@ manualChangeMessageForVideo:(NSString*)manualChangeMessageForVideo
|
||||
/**
|
||||
Removing DTCoreText artifacts used to mark blockquote blocks.
|
||||
|
||||
@param attributedString an attributed string.
|
||||
@return the resulting string.
|
||||
@param mutableAttributedString a mutable attributed string.
|
||||
*/
|
||||
+ (NSAttributedString*)removeMarkedBlockquotesArtifacts:(NSAttributedString*)attributedString;
|
||||
+ (void)removeMarkedBlockquotesArtifacts:(NSMutableAttributedString*)mutableAttributedString;
|
||||
|
||||
/**
|
||||
Enumerate all sections of the attributed string that refer to an HTML blockquote block.
|
||||
|
||||
@@ -939,10 +939,8 @@ manualChangeMessageForVideo:(NSString*)manualChangeMessageForVideo
|
||||
|
||||
#pragma mark - HTML processing
|
||||
|
||||
+ (NSAttributedString*)removeDTCoreTextArtifacts:(NSAttributedString*)attributedString
|
||||
+ (void)removeDTCoreTextArtifacts:(NSMutableAttributedString*)mutableAttributedString
|
||||
{
|
||||
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
|
||||
@@ -1009,62 +1007,61 @@ manualChangeMessageForVideo:(NSString*)manualChangeMessageForVideo
|
||||
[mutableAttributedString replaceCharactersInRange:range withAttributedString:attrStringWithImage];
|
||||
}
|
||||
}];
|
||||
|
||||
return mutableAttributedString;
|
||||
}
|
||||
|
||||
+ (NSAttributedString*)createLinksInAttributedString:(NSAttributedString*)attributedString forEnabledMatrixIds:(NSInteger)enabledMatrixIdsBitMask
|
||||
+ (void)createLinksInMutableAttributedString:(NSMutableAttributedString*)mutableAttributedString forEnabledMatrixIds:(NSInteger)enabledMatrixIdsBitMask
|
||||
{
|
||||
if (!attributedString)
|
||||
if (!mutableAttributedString)
|
||||
{
|
||||
return nil;
|
||||
return;
|
||||
}
|
||||
|
||||
NSMutableAttributedString *postRenderAttributedString;
|
||||
|
||||
|
||||
// If enabled, make user id clickable
|
||||
if (enabledMatrixIdsBitMask & MXKTOOLS_USER_IDENTIFIER_BITWISE)
|
||||
{
|
||||
[MXKTools createLinksInAttributedString:attributedString matchingRegex:userIdRegex withWorkingAttributedString:&postRenderAttributedString];
|
||||
[MXKTools createLinksInMutableAttributedString:mutableAttributedString matchingRegex:userIdRegex];
|
||||
}
|
||||
|
||||
// If enabled, make room id clickable
|
||||
if (enabledMatrixIdsBitMask & MXKTOOLS_ROOM_IDENTIFIER_BITWISE)
|
||||
{
|
||||
[MXKTools createLinksInAttributedString:attributedString matchingRegex:roomIdRegex withWorkingAttributedString:&postRenderAttributedString];
|
||||
[MXKTools createLinksInMutableAttributedString:mutableAttributedString matchingRegex:roomIdRegex];
|
||||
}
|
||||
|
||||
// If enabled, make room alias clickable
|
||||
if (enabledMatrixIdsBitMask & MXKTOOLS_ROOM_ALIAS_BITWISE)
|
||||
{
|
||||
[MXKTools createLinksInAttributedString:attributedString matchingRegex:roomAliasRegex withWorkingAttributedString:&postRenderAttributedString];
|
||||
[MXKTools createLinksInMutableAttributedString:mutableAttributedString matchingRegex:roomAliasRegex];
|
||||
}
|
||||
|
||||
// If enabled, make event id clickable
|
||||
if (enabledMatrixIdsBitMask & MXKTOOLS_EVENT_IDENTIFIER_BITWISE)
|
||||
{
|
||||
[MXKTools createLinksInAttributedString:attributedString matchingRegex:eventIdRegex withWorkingAttributedString:&postRenderAttributedString];
|
||||
[MXKTools createLinksInMutableAttributedString:mutableAttributedString matchingRegex:eventIdRegex];
|
||||
}
|
||||
|
||||
// If enabled, make group id clickable
|
||||
if (enabledMatrixIdsBitMask & MXKTOOLS_GROUP_IDENTIFIER_BITWISE)
|
||||
{
|
||||
[MXKTools createLinksInAttributedString:attributedString matchingRegex:groupIdRegex withWorkingAttributedString:&postRenderAttributedString];
|
||||
[MXKTools createLinksInMutableAttributedString:mutableAttributedString matchingRegex:groupIdRegex];
|
||||
}
|
||||
|
||||
return postRenderAttributedString ? postRenderAttributedString : attributedString;
|
||||
}
|
||||
|
||||
+ (void)createLinksInAttributedString:(NSAttributedString*)attributedString matchingRegex:(NSRegularExpression*)regex withWorkingAttributedString:(NSMutableAttributedString* __autoreleasing *)mutableAttributedString
|
||||
+ (void)createLinksInMutableAttributedString:(NSMutableAttributedString*)mutableAttributedString matchingRegex:(NSRegularExpression*)regex
|
||||
{
|
||||
__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) {
|
||||
[regex enumerateMatchesInString:mutableAttributedString.string
|
||||
options:0
|
||||
range:NSMakeRange(0, mutableAttributedString.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) {
|
||||
[mutableAttributedString enumerateAttributesInRange:match.range
|
||||
options:0
|
||||
usingBlock:^(NSDictionary<NSString *,id> * _Nonnull attrs, NSRange range, BOOL * _Nonnull stop) {
|
||||
|
||||
if (attrs[NSLinkAttributeName])
|
||||
{
|
||||
@@ -1086,7 +1083,7 @@ manualChangeMessageForVideo:(NSString*)manualChangeMessageForVideo
|
||||
// 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)];
|
||||
linkMatches = [httpLinksRegex matchesInString:mutableAttributedString.string options:0 range:NSMakeRange(0, mutableAttributedString.length)];
|
||||
}
|
||||
|
||||
for (NSTextCheckingResult *linkMatch in linkMatches)
|
||||
@@ -1102,18 +1099,12 @@ manualChangeMessageForVideo:(NSString*)manualChangeMessageForVideo
|
||||
|
||||
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];
|
||||
NSString *link = [mutableAttributedString.string substringWithRange:match.range];
|
||||
link = [link stringByAddingPercentEncodingWithAllowedCharacters:[NSCharacterSet URLQueryAllowedCharacterSet]];
|
||||
[*mutableAttributedString addAttribute:NSLinkAttributeName value:link range:match.range];
|
||||
[mutableAttributedString addAttribute:NSLinkAttributeName value:link range:match.range];
|
||||
}
|
||||
}];
|
||||
}
|
||||
@@ -1125,10 +1116,8 @@ manualChangeMessageForVideo:(NSString*)manualChangeMessageForVideo
|
||||
return [NSString stringWithFormat:@"blockquote {background: #%lX; display: block;}", (unsigned long)[MXKTools rgbValueWithColor:kMXKToolsBlockquoteMarkColor]];
|
||||
}
|
||||
|
||||
+ (NSAttributedString*)removeMarkedBlockquotesArtifacts:(NSAttributedString*)attributedString
|
||||
+ (void)removeMarkedBlockquotesArtifacts:(NSMutableAttributedString*)mutableAttributedString
|
||||
{
|
||||
NSMutableAttributedString *mutableAttributedString = [[NSMutableAttributedString alloc] initWithAttributedString:attributedString];
|
||||
|
||||
// Enumerate all sections marked thanks to `cssToMarkBlockquotes`
|
||||
// and apply our own attribute instead.
|
||||
|
||||
@@ -1137,10 +1126,10 @@ manualChangeMessageForVideo:(NSString*)manualChangeMessageForVideo
|
||||
// - 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)
|
||||
[mutableAttributedString enumerateAttribute:DTTextBlocksAttribute
|
||||
inRange:NSMakeRange(0, mutableAttributedString.length)
|
||||
options:0
|
||||
usingBlock:^(id _Nullable value, NSRange range, BOOL * _Nonnull stop)
|
||||
{
|
||||
if ([value isKindOfClass:NSArray.class])
|
||||
{
|
||||
@@ -1161,9 +1150,9 @@ manualChangeMessageForVideo:(NSString*)manualChangeMessageForVideo
|
||||
NSRange prevRange = NSMakeRange(range.location - 1, 1);
|
||||
|
||||
NSRange effectiveRange;
|
||||
NSParagraphStyle *paragraphStyle = [attributedString attribute:NSParagraphStyleAttributeName
|
||||
atIndex:prevRange.location
|
||||
effectiveRange:&effectiveRange];
|
||||
NSParagraphStyle *paragraphStyle = [mutableAttributedString attribute:NSParagraphStyleAttributeName
|
||||
atIndex:prevRange.location
|
||||
effectiveRange:&effectiveRange];
|
||||
|
||||
// Check if this is the " " string
|
||||
if (paragraphStyle && effectiveRange.length == 1 && paragraphStyle.firstLineHeadIndent != 25)
|
||||
@@ -1197,8 +1186,6 @@ manualChangeMessageForVideo:(NSString*)manualChangeMessageForVideo
|
||||
[mutableAttributedString addAttribute:kMXKToolsBlockquoteMarkAttribute value:@(YES) range:range];
|
||||
}
|
||||
}];
|
||||
|
||||
return mutableAttributedString;
|
||||
}
|
||||
|
||||
+ (void)enumerateMarkedBlockquotesInAttributedString:(NSAttributedString*)attributedString usingBlock:(void (^)(NSRange range, BOOL *stop))block
|
||||
|
||||
@@ -46,7 +46,6 @@
|
||||
#import "NSArray+Element.h"
|
||||
#import "ShareItemSender.h"
|
||||
#import "Contact.h"
|
||||
#import "HTMLFormatter.h"
|
||||
#import "RoomTimelineCellProvider.h"
|
||||
#import "PlainRoomTimelineCellProvider.h"
|
||||
#import "BubbleRoomTimelineCellProvider.h"
|
||||
|
||||
@@ -1,38 +0,0 @@
|
||||
//
|
||||
// Copyright 2021 New Vector Ltd
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
#import <CoreGraphics/CoreGraphics.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface HTMLFormatter : NSObject
|
||||
|
||||
/** Builds an attributed string from a string containing html.
|
||||
@param htmlString The html string to use.
|
||||
@param allowedTags The html tags that should be allowed.
|
||||
@param fontSize The default font size to use.
|
||||
|
||||
Note: It is recommended to include "p" and "body" tags in
|
||||
`allowedTags` as these are often added when parsing.
|
||||
*/
|
||||
- (NSAttributedString * _Nonnull)formatHTML:(NSString * _Nonnull)htmlString
|
||||
withAllowedTags:(NSArray<NSString *> * _Nonnull)allowedTags
|
||||
fontSize:(CGFloat)fontSize;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
@@ -1,61 +0,0 @@
|
||||
//
|
||||
// Copyright 2021 New Vector Ltd
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
#import "HTMLFormatter.h"
|
||||
#import "GeneratedInterface-Swift.h"
|
||||
|
||||
@implementation HTMLFormatter
|
||||
|
||||
- (NSAttributedString *)formatHTML:(NSString *)htmlString withAllowedTags:(NSArray<NSString *> *)allowedTags fontSize:(CGFloat)fontSize
|
||||
{
|
||||
// TODO: This method should be more general purpose and usable from MXKEventFormatter and GroupHomeViewController
|
||||
// FIXME: The implementation is currently in Objective-C as there is a crash in the callback when implemented in Swift
|
||||
UIFont *font = [UIFont systemFontOfSize:fontSize];
|
||||
|
||||
// Do some sanitisation before finalizing the string
|
||||
DTHTMLAttributedStringBuilderWillFlushCallback sanitizeCallback = ^(DTHTMLElement *element) {
|
||||
[element sanitizeWith:allowedTags bodyFont:font imageHandler:nil];
|
||||
};
|
||||
|
||||
NSDictionary *options = @{
|
||||
DTUseiOS6Attributes: @(YES), // Enable it to be able to display the attributed string in a UITextView
|
||||
DTDefaultFontFamily: font.familyName,
|
||||
DTDefaultFontName: font.fontName,
|
||||
DTDefaultFontSize: @(font.pointSize),
|
||||
DTDefaultLinkDecoration: @(NO),
|
||||
DTWillFlushBlockCallBack: sanitizeCallback
|
||||
};
|
||||
|
||||
// Do not use the default HTML renderer of NSAttributedString because this method
|
||||
// runs on the UI thread which we want to avoid because renderHTMLString is called
|
||||
// most of the time from a background thread.
|
||||
// Use DTCoreText HTML renderer instead.
|
||||
// Using DTCoreText, which renders static string, helps to avoid code injection attacks
|
||||
// that could happen with the default HTML renderer of NSAttributedString which is a
|
||||
// webview.
|
||||
NSAttributedString *string = [[NSAttributedString alloc] initWithHTMLData:[htmlString dataUsingEncoding:NSUTF8StringEncoding] options:options documentAttributes:NULL];
|
||||
|
||||
// Apply additional treatments
|
||||
string = [MXKTools removeDTCoreTextArtifacts:string];
|
||||
|
||||
if (!string) {
|
||||
return [[NSAttributedString alloc] initWithString:htmlString];
|
||||
}
|
||||
|
||||
return string;
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -1,35 +0,0 @@
|
||||
//
|
||||
// Copyright 2021 New Vector Ltd
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
extension HTMLFormatter {
|
||||
/// 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.
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -15,10 +15,15 @@
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import UIKit
|
||||
|
||||
@available(iOS 14.0, *)
|
||||
struct AnalyticsPromptStrings: AnalyticsPromptStringsProtocol {
|
||||
let point1 = HTMLFormatter().formatHTML(VectorL10n.analyticsPromptPoint1, withAllowedTags: ["b", "p"], fontSize: UIFont.systemFontSize)
|
||||
let point2 = HTMLFormatter().formatHTML(VectorL10n.analyticsPromptPoint2, withAllowedTags: ["b", "p"], fontSize: UIFont.systemFontSize)
|
||||
let point1 = HTMLFormatter().formatHTML(VectorL10n.analyticsPromptPoint1,
|
||||
withAllowedTags: ["b", "p"],
|
||||
font: UIFont.systemFont(ofSize: UIFont.systemFontSize))
|
||||
let point2 = HTMLFormatter().formatHTML(VectorL10n.analyticsPromptPoint2,
|
||||
withAllowedTags: ["b", "p"],
|
||||
font: UIFont.systemFont(ofSize: UIFont.systemFontSize))
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user