From 40f5c664cfbe1bd4d9aeb9cf9f2627d6e06c0b6e Mon Sep 17 00:00:00 2001 From: Ahmed Elkady Date: Wed, 20 May 2020 10:09:33 -0400 Subject: [PATCH] fix: multiple latex not rendering (#149) * Good stopping point * Fixed and cleaned up everything * Get tests working * Small fix * Add example * update snapshot tests * Revert "update snapshot tests" This reverts commit e8a689bc7c2bbf0376cdd24f43959842b761a6e8. * added ui * Update RichTextParser.swift * Update RichTextViewUITests.swift * Update RichTextViewUITests.swift * Update RichTextParser.swift * snapshots Co-authored-by: Simon --- .swiftlint.yml | 1 + Example/Source/InputOutputModuleView.swift | 2 +- Example/Source/ViewController.swift | 3 +- .../NSMutableAttributedStringExtension.swift | 1 - Source/RichTextView.swift | 6 +- Source/Text Parsing/LatexParserProtocol.swift | 5 +- Source/Text Parsing/RichTextParser.swift | 402 +++++++++++------- ...ers_a_string_with_bullet_attributes@2x.png | Bin 23170 -> 23059 bytes ...ers_a_string_with_bullet_attributes@3x.png | Bin 0 -> 47751 bytes ...t__Renders_a_string_with_highlights@2x.png | Bin 22510 -> 22544 bytes ...e__Updates_highlight_Color_Properly@2x.png | Bin 23486 -> 23677 bytes ...e__Updates_highlight_Color_Properly@3x.png | Bin 49529 -> 48928 bytes UITests/RichTextViewUITests.swift | 8 +- .../Text Parsing/RichTextParserSpec.swift | 54 ++- 14 files changed, 294 insertions(+), 188 deletions(-) create mode 100644 UITests/ReferenceImages/RichTextViewUITests/RichTextView_UI__Rendering_Various_Strings_of_Rich_Text__Renders_a_string_with_bullet_attributes@3x.png diff --git a/.swiftlint.yml b/.swiftlint.yml index 30f723a..36c2046 100644 --- a/.swiftlint.yml +++ b/.swiftlint.yml @@ -22,5 +22,6 @@ type_body_length: warning: 300 # warning error: 400 # error file_length: + warning: 450 error: 500 function_parameter_count: 6 diff --git a/Example/Source/InputOutputModuleView.swift b/Example/Source/InputOutputModuleView.swift index b377dee..3812dd5 100644 --- a/Example/Source/InputOutputModuleView.swift +++ b/Example/Source/InputOutputModuleView.swift @@ -48,7 +48,7 @@ class InputOutputModuleView: UIView, RichTextViewDelegate { self.outputRichTextView.textViewDelegate = self self.outputRichTextView.update( input: text, - attributes: self.attributes + customAdditionalAttributes: self.attributes ) } diff --git a/Example/Source/ViewController.swift b/Example/Source/ViewController.swift index 8d5c341..60242fa 100644 --- a/Example/Source/ViewController.swift +++ b/Example/Source/ViewController.swift @@ -117,7 +117,8 @@ class ViewController: UIViewController { InputOutputModuleView(text: "
Here is a blockquote
"), InputOutputModuleView(text: "Look [interactive-element id=123]This is an interactive element[/interactive-element] Wow"), InputOutputModuleView(text: "Look [highlighted-element id=456]This is an highlighted element[/highlighted-element] Wow"), - InputOutputModuleView(text: "Here is more LaTeX [math]A^{}=P^{}\\left(1+\\frac{r}{n}\\right)^{nt}[/math]") + InputOutputModuleView(text: "Here is more LaTeX [math]A^{}=P^{}\\left(1+\\frac{r}{n}\\right)^{nt}[/math]"), + InputOutputModuleView(text: "Multiple Latex and Markdwon: *Yes* [math]_2[/math]TEST[math]_3[/math]") ] } diff --git a/Source/Extensions/NSMutableAttributedStringExtension.swift b/Source/Extensions/NSMutableAttributedStringExtension.swift index 1e3988d..6cc8986 100644 --- a/Source/Extensions/NSMutableAttributedStringExtension.swift +++ b/Source/Extensions/NSMutableAttributedStringExtension.swift @@ -9,7 +9,6 @@ extension NSAttributedString.Key { static let customLink = NSAttributedString.Key(rawValue: "customLink") static let highlight = NSAttributedString.Key(rawValue: "highlight") - static let bullet = NSAttributedString.Key(rawValue: "bullets") } extension NSMutableAttributedString { diff --git a/Source/RichTextView.swift b/Source/RichTextView.swift index f26175d..af8f6e3 100644 --- a/Source/RichTextView.swift +++ b/Source/RichTextView.swift @@ -51,7 +51,7 @@ public class RichTextView: UIView { textColor: textColor, latexTextBaselineOffset: latexTextBaselineOffset, interactiveTextColor: interactiveTextColor, - attributes: customAdditionalAttributes + customAdditionalAttributes: customAdditionalAttributes ) self.textColor = textColor self.textViewDelegate = textViewDelegate @@ -78,7 +78,7 @@ public class RichTextView: UIView { textColor: UIColor? = nil, latexTextBaselineOffset: CGFloat? = nil, interactiveTextColor: UIColor? = nil, - attributes: [String: [NSAttributedString.Key: Any]]? = nil, + customAdditionalAttributes: [String: [NSAttributedString.Key: Any]]? = nil, completion: (([ParsingError]?) -> Void)? = nil) { self.input = input ?? self.input self.richTextParser = RichTextParser( @@ -87,7 +87,7 @@ public class RichTextView: UIView { textColor: textColor ?? self.textColor, latexTextBaselineOffset: latexTextBaselineOffset ?? self.richTextParser.latexTextBaselineOffset, interactiveTextColor: interactiveTextColor ?? self.richTextParser.interactiveTextColor, - attributes: attributes + customAdditionalAttributes: customAdditionalAttributes ?? self.richTextParser.customAdditionalAttributes ) self.textColor = textColor ?? self.textColor self.subviews.forEach { $0.removeFromSuperview() } diff --git a/Source/Text Parsing/LatexParserProtocol.swift b/Source/Text Parsing/LatexParserProtocol.swift index e739887..b099797 100644 --- a/Source/Text Parsing/LatexParserProtocol.swift +++ b/Source/Text Parsing/LatexParserProtocol.swift @@ -15,7 +15,6 @@ public protocol LatexParserProtocol: class { extension LatexParserProtocol { public func extractLatex(from input: String, textColor: UIColor, baselineOffset: CGFloat, fontSize: CGFloat, height: CGFloat?) -> NSAttributedString? { - let latexInput = self.extractLatexStringInsideTags(from: input) var mathImage: UIImage? @@ -79,9 +78,9 @@ extension LatexParserProtocol { } private func getLatexOffset(fromInput input: String, image: UIImage, fontSize: CGFloat) -> CGFloat { - let defaultSubScriptOffset: CGFloat = 2.66 + let defaultSubScriptOffset = RichTextParser.ParserConstants.defaultSubScriptOffset let imageOffset = max((image.size.height - fontSize)/2, 0) - let subscriptOffset: CGFloat = input.contains("_") ? defaultSubScriptOffset : 0 + let subscriptOffset: CGFloat = input.contains(RichTextParser.ParserConstants.latexSubscriptCharacter) ? defaultSubScriptOffset : 0 return max(subscriptOffset, imageOffset) } } diff --git a/Source/Text Parsing/RichTextParser.swift b/Source/Text Parsing/RichTextParser.swift index f1a92d1..e2e6253 100644 --- a/Source/Text Parsing/RichTextParser.swift +++ b/Source/Text Parsing/RichTextParser.swift @@ -24,10 +24,20 @@ class RichTextParser { static let highlightedElementRegex = """ \\[\(ParserConstants.highlightedElementTagName)\\sid=.+?\\].*?\\[\\/\(ParserConstants.highlightedElementTagName)\\] """ + private static let tAPlaceholderPrefix = "{RichTextView-TextAttachmentPosition" + private static let tAPlaceholderSuffix = "}" + static let textAttachmentPlaceholderAssigner = "=" + static let textAttachmentPlaceholderRegex = + "\\\(ParserConstants.tAPlaceholderPrefix)\(ParserConstants.textAttachmentPlaceholderAssigner)[0-9]+?\\\(ParserConstants.tAPlaceholderSuffix)" + static let textAttachmentPlaceholder = + "\(ParserConstants.tAPlaceholderPrefix)\(ParserConstants.textAttachmentPlaceholderAssigner)%d\(ParserConstants.tAPlaceholderSuffix)" typealias RichTextWithErrors = (output: NSAttributedString, errors: [ParsingError]?) static let bulletString = "•" static let listOpeningHTMLString = " [RichDataType] { - var errors: [ParsingError]? - if input == "" { + if input.isEmpty { return [RichDataType.text(richText: NSAttributedString(string: ""), font: self.font, errors: nil)] } + var errors: [ParsingError]? return self.splitInputOnVideoPortions(input).compactMap { input -> RichDataType in if self.isStringAVideoTag(input) { return RichDataType.video(tag: input, error: nil) } - let results = self.getAttributedText(from: input) + let results = self.getRichTextWithErrors(from: input) if errors == nil { errors = results.errors } else if let resultErrors = results.errors { @@ -76,107 +86,245 @@ class RichTextParser { } } - func getAttributedText(from input: String) -> ParserConstants.RichTextWithErrors { - let strippedInput = self.stripCodeTagsIfNecessary(from: input) - let strippedInputAsMutableAttributedString = NSMutableAttributedString(string: strippedInput) - let strippedInputWithSpecialDataTypesHandled = self.getAttributedStringWithSpecialDataTypesHandled( - from: strippedInputAsMutableAttributedString + func getRichTextWithErrors(from input: String) -> ParserConstants.RichTextWithErrors { + let input = self.stripCodeTagsIfNecessary(from: input) + let inputAsMutableAttributedString = NSMutableAttributedString(string: input) + let richTextWithSpecialDataTypesHandled = self.getRichTextWithSpecialDataTypesHandled( + fromString: inputAsMutableAttributedString ) - let parsingErrors = strippedInputWithSpecialDataTypesHandled.errors - let outputAttributedStringToReturn = NSMutableAttributedString(attributedString: strippedInputWithSpecialDataTypesHandled.output) - let HTMLAndMarkdownParsedString = self.parseHTMLAndMarkdown(withAttributedString: outputAttributedStringToReturn, parsingErrors: parsingErrors) - let mutableAttributedInput = HTMLAndMarkdownParsedString.0 - let allParsingErrors = HTMLAndMarkdownParsedString.1 - let parsedStringWithAttributes = self.getAttributesIfNecessary( - fromAttributedString: strippedInputWithSpecialDataTypesHandled.output, - parsedString: mutableAttributedInput + let textAttachmentAttributesInRichText = self.extractTextAttachmentAttributesInOrder(fromAttributedString: richTextWithSpecialDataTypesHandled.output) + let richTextWithHTMLAndMarkdownHandled = self.getRichTextWithHTMLAndMarkdownHandled( + fromString: self.replaceTextAttachmentsWithPlaceHolderInfo(inAttributedString: richTextWithSpecialDataTypesHandled.output) ) - parsedStringWithAttributes.replaceFont(with: self.font) - parsedStringWithAttributes.replaceColor(with: self.textColor) - return (parsedStringWithAttributes.trimmingTrailingNewlinesAndWhitespaces(), allParsingErrors) - } - // MARK: - Helpers + let outputRichText = self.mergeSpecialDataAndHTMLMarkdownAttribute( + htmlMarkdownString: NSMutableAttributedString(attributedString: richTextWithHTMLAndMarkdownHandled.output), + specialDataTypesString: richTextWithSpecialDataTypesHandled.output, + textAttachmentAttributes: textAttachmentAttributesInRichText + ).trimmingTrailingNewlinesAndWhitespaces() + + outputRichText.replaceFont(with: self.font) + outputRichText.replaceColor(with: self.textColor) + + if richTextWithSpecialDataTypesHandled.errors == nil, richTextWithHTMLAndMarkdownHandled.errors == nil { + return (outputRichText, nil) + } - private func getAttributesIfNecessary(fromAttributedString attributedString: NSAttributedString, - parsedString: NSMutableAttributedString) -> NSMutableAttributedString { - let rangeOfAttributedString = NSRange(location: 0, length: attributedString.length) - attributedString.enumerateAttributes(in: rangeOfAttributedString) { (attributes, range, _) in - if attributes.isEmpty { + let outputErrors = (richTextWithSpecialDataTypesHandled.errors ?? [ParsingError]()) + (richTextWithHTMLAndMarkdownHandled.errors ?? [ParsingError]()) + return (outputRichText, outputErrors) + } + + private func mergeSpecialDataAndHTMLMarkdownAttribute(htmlMarkdownString: NSMutableAttributedString, + specialDataTypesString: NSAttributedString, + textAttachmentAttributes: [[NSAttributedString.Key: Any]]) -> NSMutableAttributedString { + let outputString = self.mergeTextAttachmentsAndHTMLMarkdownAttributes( + htmlMarkdownString: htmlMarkdownString, + textAttachmentAttributes: textAttachmentAttributes + ) + let rangeOfSpecialDataString = NSRange(location: 0, length: specialDataTypesString.length) + specialDataTypesString.enumerateAttributes(in: rangeOfSpecialDataString) { (attributes, range, _) in + if attributes.isEmpty || attributes[.attachment] != nil { return } - let substring = attributedString.string[ - max(range.lowerBound, 0).. parsedString.length { + let rangeOfSubstringInOutputString = (outputString.string as NSString).range(of: specialDataSubstring) + if rangeOfSubstringInOutputString.location == NSNotFound || + rangeOfSubstringInOutputString.location < 0 || + rangeOfSubstringInOutputString.location + rangeOfSubstringInOutputString.length > outputString.length { return } - let attributedSubstring = NSAttributedString(string: substring, attributes: attributes) - parsedString.replaceCharacters(in: rangeInOutput, with: attributedSubstring) + let newOutuptSubstring = NSMutableAttributedString(attributedString: outputString.attributedSubstring(from: rangeOfSubstringInOutputString)) + newOutuptSubstring.addAttributes(attributes, range: NSRange(location: 0, length: newOutuptSubstring.length)) + newOutuptSubstring.replaceCharacters(in: NSRange(location: 0, length: newOutuptSubstring.length), with: specialDataSubstring) + outputString.replaceCharacters(in: rangeOfSubstringInOutputString, with: newOutuptSubstring) } - return parsedString + return outputString + } + + private func mergeTextAttachmentsAndHTMLMarkdownAttributes(htmlMarkdownString: NSMutableAttributedString, + textAttachmentAttributes: [[NSAttributedString.Key: Any]]) -> NSMutableAttributedString { + let textAttachmentRegex = try? NSRegularExpression(pattern: ParserConstants.textAttachmentPlaceholderRegex, options: []) + let inputRange = NSRange(location: 0, length: htmlMarkdownString.length) + guard let textAttachmentMatches = textAttachmentRegex?.matches(in: htmlMarkdownString.string, options: [], range: inputRange) else { + return htmlMarkdownString + } + + for match in textAttachmentMatches.reversed() { + let matchedSubstring = htmlMarkdownString.attributedSubstring(from: match.range).string + let matchedComponentsSeparatedByAssigner = matchedSubstring.components( + separatedBy: ParserConstants.textAttachmentPlaceholderAssigner + ) + let decimalCharacters = CharacterSet.decimalDigits.inverted + guard let textAttachmentPositionAsSubstring = matchedComponentsSeparatedByAssigner.last?.components(separatedBy: decimalCharacters).joined(), + let textAttachmentPosition = Int(textAttachmentPositionAsSubstring), + textAttachmentAttributes.indices.contains(textAttachmentPosition) else { + continue + } + let textAttachmentAttributes = textAttachmentAttributes[textAttachmentPosition] + guard let textAttachment = textAttachmentAttributes[.attachment] as? NSTextAttachment else { + continue + } + let textAttachmentAttributedString = NSMutableAttributedString(attachment: textAttachment) + textAttachmentAttributedString.addAttributes( + textAttachmentAttributes, + range: NSRange(location: 0, length: textAttachmentAttributedString.length) + ) + htmlMarkdownString.replaceCharacters(in: match.range, with: textAttachmentAttributedString) + } + return htmlMarkdownString + } + + // MARK: - Boolean Checkers + + func isTextLatex(_ text: String) -> Bool { + return !self.getLatexRanges(inText: text).isEmpty } - private func parseHTMLAndMarkdown(withAttributedString mutableAttributedString: NSMutableAttributedString, - parsingErrors: [ParsingError]?) -> (NSMutableAttributedString, [ParsingError]?) { - var allParsingErrors: [ParsingError]? = parsingErrors - let relevantString = mutableAttributedString.string - let cleanRelevantString = relevantString.replaceTrailingWhiteSpaceWithNonBreakingSpace().replaceLeadingWhiteSpaceWithNonBreakingSpace() - guard let inputAsHTMLString = try? Down(markdownString: cleanRelevantString).toHTML([.unsafe, .hardBreaks]), + func isTextInteractiveElement(_ text: String) -> Bool { + return text.ranges(of: ParserConstants.interactiveElementRegex, options: .regularExpression).count != 0 + } + + func isTextHighlightedElement(_ text: String) -> Bool { + return text.ranges(of: ParserConstants.highlightedElementRegex, options: .regularExpression).count != 0 + } + + private func isStringAVideoTag(_ input: String) -> Bool { + return input.range(of: RichTextViewConstants.videoTagRegex, options: .regularExpression, range: nil, locale: nil) != nil + } + + // MARK: - Video Functions + + private func splitInputOnVideoPortions(_ input: String) -> [String] { + return input.getComponents(separatedBy: RichTextViewConstants.videoTagRegex) + } + + // MARK: - HTML/Markdown Helpers + + private func getRichTextWithHTMLAndMarkdownHandled(fromString mutableAttributedString: NSMutableAttributedString) -> ParserConstants.RichTextWithErrors { + let inputString = mutableAttributedString.string + let inputStringWithoutBreakingSpaces = inputString.replaceTrailingWhiteSpaceWithNonBreakingSpace().replaceLeadingWhiteSpaceWithNonBreakingSpace() + guard let inputAsHTMLString = try? Down(markdownString: inputStringWithoutBreakingSpaces).toHTML([.unsafe, .hardBreaks]), let inputAsHTMLWithZeroWidthSpaceRemoved = inputAsHTMLString.replaceAppropiateZeroWidthSpaces(), let htmlData = inputAsHTMLWithZeroWidthSpaceRemoved.data(using: .utf8) else { - if allParsingErrors == nil { - allParsingErrors = [ParsingError]() - } - allParsingErrors?.append(ParsingError.attributedTextGeneration(text: relevantString)) - return (mutableAttributedString.trimmingTrailingNewlinesAndWhitespaces(), allParsingErrors) + return (mutableAttributedString.trimmingTrailingNewlinesAndWhitespaces(), [ParsingError.attributedTextGeneration(text: inputString)]) + } + let parsedAttributedString = self.getParsedHTMLAttributedString(fromData: htmlData) + guard let parsedHTMLAttributedString = parsedAttributedString else { + return (mutableAttributedString.trimmingTrailingNewlinesAndWhitespaces(), [ParsingError.attributedTextGeneration(text: inputString)]) } + let parsedMutableAttributedString = NSMutableAttributedString(attributedString: parsedHTMLAttributedString) + let finalOutputString = self.addCustomStylingToBulletPointsIfNecessary(parsedMutableAttributedString) + return (finalOutputString, nil) + } + + private func getParsedHTMLAttributedString(fromData data: Data) -> NSAttributedString? { var attributedString: NSAttributedString? + let options: [NSAttributedString.DocumentReadingOptionKey: Any] = [ + .documentType: NSAttributedString.DocumentType.html, + .characterEncoding: String.Encoding.utf8.rawValue + ] if Thread.isMainThread { - attributedString = try? NSAttributedString(data: htmlData, options: - [.documentType: NSAttributedString.DocumentType.html, .characterEncoding: String.Encoding.utf8.rawValue], documentAttributes: nil) + attributedString = try? NSAttributedString(data: data, options: options, documentAttributes: nil) } else { DispatchQueue.main.sync { - attributedString = try? NSAttributedString(data: htmlData, options: - [.documentType: NSAttributedString.DocumentType.html, .characterEncoding: String.Encoding.utf8.rawValue], documentAttributes: nil) + attributedString = try? NSAttributedString(data: data, options: options, documentAttributes: nil) } } - guard let attributedStringNonOptional = attributedString else { - if allParsingErrors == nil { - allParsingErrors = [ParsingError]() - } - allParsingErrors?.append(ParsingError.attributedTextGeneration(text: relevantString)) - return (mutableAttributedString.trimmingTrailingNewlinesAndWhitespaces(), allParsingErrors) + return attributedString + } + + private func addCustomStylingToBulletPointsIfNecessary(_ input: NSMutableAttributedString) -> NSMutableAttributedString { + guard let customBulletAttributes = self.customAdditionalAttributes?[ParserConstants.bulletCustomAttributeIdentifier], + let bulletPointRegex = try? NSRegularExpression(pattern: ParserConstants.bulletString, options: []) else { + return input } - let mutableAttributedInput = NSMutableAttributedString(attributedString: attributedStringNonOptional) - guard let attributes = self.attributes?[NSAttributedString.Key.bullet.rawValue], - mutableAttributedString.string.contains(ParserConstants.listOpeningHTMLString), - mutableAttributedString.string.contains(ParserConstants.listClosingHTMLString) else { - return (mutableAttributedInput, allParsingErrors) + let bulletPointMatches = bulletPointRegex.matches( + in: input.string, + options: [], + range: NSRange(location: 0, length: input.string.count) + ) + let output = input + bulletPointMatches.reversed().forEach { match in + output.addAttributes(customBulletAttributes, range: match.range) } - let parsedStringWithBulletAttributes = getParsedStringWithBulletAttributes(mutableAttributedInput: mutableAttributedInput, attributes: attributes) - return(parsedStringWithBulletAttributes, allParsingErrors) + return output + } + + private func stripCodeTagsIfNecessary(from input: String) -> String { + return input.replacingOccurrences(of: "[code]", with: "`").replacingOccurrences(of: "[/code]", with: "`") } - private func getParsedStringWithBulletAttributes(mutableAttributedInput: NSMutableAttributedString, - attributes: [NSAttributedString.Key: Any] ) -> NSMutableAttributedString { - let range = mutableAttributedInput.string.ranges(of: ParserConstants.bulletString, options: .literal) - let positions = self.extractPositions(fromRanges: range) - let splitString = self.split(mutableAttributedString: mutableAttributedInput, onPositions: positions) - let stringWithBulletAttributes = NSMutableAttributedString(attributedString: NSAttributedString.init(string: "")) - for string in splitString { - if string.string == ParserConstants.bulletString { - let bullet = NSAttributedString(string: ParserConstants.bulletString, attributes: attributes) - stringWithBulletAttributes.append(bullet) - } else { - stringWithBulletAttributes.append(string) + // MARK: - String Helpers + + private func split(mutableAttributedString: NSMutableAttributedString, onPositions positions: [String.Index]) -> [NSAttributedString] { + let splitStrings = mutableAttributedString.string.split(atPositions: positions) + var output = [NSAttributedString]() + for string in splitStrings { + let range = (mutableAttributedString.string as NSString).range(of: string) + let attributedString = mutableAttributedString.attributedSubstring(from: range) + output.append(attributedString) + } + return output + } + + private func extractPositions(fromRanges ranges: [Range]) -> [String.Index] { + return ranges.flatMap { [$0.lowerBound, $0.upperBound] }.sorted() + } + + // MARK: - Text Attachment Functions + + private func extractTextAttachmentAttributesInOrder(fromAttributedString input: NSAttributedString) -> [[NSAttributedString.Key: Any]] { + var output = [[NSAttributedString.Key: Any]]() + let range = NSRange(location: 0, length: input.length) + input.enumerateAttributes(in: range, options: [.reverse]) { (attributes, _, _) in + guard attributes.keys.contains(.attachment) else { + return + } + output.append(attributes) + } + return output + } + + private func replaceTextAttachmentsWithPlaceHolderInfo(inAttributedString input: NSAttributedString) -> NSMutableAttributedString { + let output = NSMutableAttributedString(attributedString: input) + let range = NSRange(location: 0, length: input.length) + var position = 0 + input.enumerateAttributes(in: range, options: [.reverse]) { (attributes, range, _) in + guard attributes.keys.contains(.attachment) else { + return } + output.replaceCharacters(in: range, with: String(format: ParserConstants.textAttachmentPlaceholder, position)) + position += 1 } - return stringWithBulletAttributes + return output } - private func getAttributedStringWithSpecialDataTypesHandled(from mutableAttributedString: NSMutableAttributedString) -> ParserConstants.RichTextWithErrors { + // MARK: - Special Data Type Helpers + + private func getLatexRanges(inText text: String) -> [Range] { + guard let regex = try? NSRegularExpression(pattern: ParserConstants.latexRegex, options: [.caseInsensitive, .dotMatchesLineSeparators]) else { + return [] + } + let range = NSRange(location: 0, length: text.count) + let matches = regex.matches(in: text, range: range) + return matches.compactMap { match in + return Range(match.range(at: ParserConstants.latexRegexCaptureGroupIndex), in: text) + } + } + + private func calculateContentHeight() -> CGFloat { + let frame = NSString(string: "").boundingRect( + with: CGSize(width: 0, height: .max), + options: [.usesFontLeading, .usesLineFragmentOrigin], + attributes: [.font: self.font], + context: nil + ) + return frame.size.height + } + + private func getRichTextWithSpecialDataTypesHandled(fromString mutableAttributedString: NSMutableAttributedString) -> ParserConstants.RichTextWithErrors { let interactiveElementPositions = self.extractPositions( fromRanges: mutableAttributedString.string.ranges(of: ParserConstants.interactiveElementRegex, options: .regularExpression) ) @@ -188,41 +336,36 @@ class RichTextParser { if splitPositions.isEmpty { return (mutableAttributedString.trimmingTrailingNewlinesAndWhitespaces(), nil) } - return self.getRichTextWithErrors(fromComponents: self.split(mutableAttributedString: mutableAttributedString, onPositions: splitPositions)) - } - - private func split(mutableAttributedString: NSMutableAttributedString, onPositions positions: [String.Index]) -> [NSAttributedString] { - let splitStrings = mutableAttributedString.string.split(atPositions: positions) - var output = [NSAttributedString]() - for string in splitStrings { - let range = (mutableAttributedString.string as NSString).range(of: string) - let attributedString = mutableAttributedString.attributedSubstring(from: range) - output.append(attributedString) - } - return output + return self.mergeSpecialDataComponentsAndReturnRichText( + self.split(mutableAttributedString: mutableAttributedString, onPositions: splitPositions) + ) } - private func getRichTextWithErrors(fromComponents attributedStringComponents: [NSAttributedString]) -> ParserConstants.RichTextWithErrors { + private func mergeSpecialDataComponentsAndReturnRichText(_ components: [NSAttributedString]) -> ParserConstants.RichTextWithErrors { let output = NSMutableAttributedString() var parsingErrors: [ParsingError]? - for attributedString in attributedStringComponents { + components.forEach { attributedString in if self.isTextInteractiveElement(attributedString.string) { output.append(self.extractInteractiveElement(from: attributedString)) - } else if self.isTextHighlightedElement(attributedString.string) { + return + } + if self.isTextHighlightedElement(attributedString.string) { output.append(self.extractHighlightedElement(from: attributedString)) - } else if self.isTextLatex(attributedString.string) { + return + } + if self.isTextLatex(attributedString.string) { if let attributedLatexString = self.extractLatex(from: attributedString.string) { output.append(attributedLatexString) - } else { - if parsingErrors == nil { - parsingErrors = [ParsingError]() - } - output.append(attributedString) - parsingErrors?.append(ParsingError.latexGeneration(text: attributedString.string)) + return + } + if parsingErrors == nil { + parsingErrors = [ParsingError]() } - } else { output.append(attributedString) + parsingErrors?.append(ParsingError.latexGeneration(text: attributedString.string)) + return } + output.append(attributedString) } return (output.trimmingTrailingNewlinesAndWhitespaces(), parsingErrors) } @@ -233,7 +376,7 @@ class RichTextParser { textColor: self.textColor, baselineOffset: self.latexTextBaselineOffset, fontSize: self.font.pointSize, - height: calculateContentHeight() + height: self.calculateContentHeight() ) } @@ -254,7 +397,7 @@ class RichTextParser { let highlightedElementTagName = ParserConstants.highlightedElementTagName let highlightedElementID = input.string.getSubstring(inBetween: "[\(highlightedElementTagName) id=", and: "]") ?? input.string let highlightedElementText = input.string.getSubstring(inBetween: "]", and: "[/\(highlightedElementTagName)]") ?? input.string - guard let richTextAttributes = self.attributes?[highlightedElementID] else { + guard let richTextAttributes = self.customAdditionalAttributes?[highlightedElementID] else { return NSMutableAttributedString(string: highlightedElementText) } let attributes: [NSAttributedString.Key: Any] = [.highlight: highlightedElementID] @@ -263,55 +406,4 @@ class RichTextParser { let mutableAttributedInput = NSMutableAttributedString(string: highlightedElementText, attributes: attributes) return mutableAttributedInput } - - func isTextLatex(_ text: String) -> Bool { - return !self.getLatexRanges(inText: text).isEmpty - } - - func isTextInteractiveElement(_ text: String) -> Bool { - return text.ranges(of: ParserConstants.interactiveElementRegex, options: .regularExpression).count != 0 - } - - func isTextHighlightedElement(_ text: String) -> Bool { - return text.ranges(of: ParserConstants.highlightedElementRegex, options: .regularExpression).count != 0 - } - - private func extractPositions(fromRanges ranges: [Range]) -> [String.Index] { - return ranges.flatMap { range in - return [range.lowerBound, range.upperBound] - }.sorted() - } - - private func splitInputOnVideoPortions(_ input: String) -> [String] { - return input.getComponents(separatedBy: RichTextViewConstants.videoTagRegex) - } - - private func isStringAVideoTag(_ input: String) -> Bool { - return input.range(of: RichTextViewConstants.videoTagRegex, options: .regularExpression, range: nil, locale: nil) != nil - } - - private func stripCodeTagsIfNecessary(from input: String) -> String { - return input.replacingOccurrences(of: "[code]", with: "`").replacingOccurrences(of: "[/code]", with: "`") - } - - private func getLatexRanges(inText text: String) -> [Range] { - guard let regex = try? NSRegularExpression(pattern: ParserConstants.latexRegex, options: [.caseInsensitive, .dotMatchesLineSeparators]) else { - return [] - } - let range = NSRange(location: 0, length: text.count) - let matches = regex.matches(in: text, range: range) - return matches.compactMap { match in - return Range(match.range(at: ParserConstants.latexRegexCaptureGroupIndex), in: text) - } - } - - private func calculateContentHeight() -> CGFloat { - let frame = NSString(string: "").boundingRect( - with: CGSize(width: 0, height: .max), - options: [.usesFontLeading, .usesLineFragmentOrigin], - attributes: [.font: self.font], - context: nil) - - return frame.size.height - } } diff --git a/UITests/ReferenceImages/RichTextViewUITests/RichTextView_UI__Rendering_Various_Strings_of_Rich_Text__Renders_a_string_with_bullet_attributes@2x.png b/UITests/ReferenceImages/RichTextViewUITests/RichTextView_UI__Rendering_Various_Strings_of_Rich_Text__Renders_a_string_with_bullet_attributes@2x.png index 4ffb64b454529f54be6728d6920bd70e881b1045..dbec122e8d95af695007268aad503da40d11c8e8 100644 GIT binary patch literal 23059 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 R_`f#CYgMFH{sXKTW@!Kb literal 23170 zcmeHOYgAKL77hEzCaTcEmAG1PCL|E60FOzED}-_5(r74QUQy^ z>S!t`SX~tz6vS78K!VZ|m6sMVNM6RMC?PilgqwHf2J>)!&fl5yBV2NG);Z^X_w4=c z{hfV^KTn98{C?p3C=_aP{H6`tP$*9bg_^Je{T_1XzId7a;<0aA+?N8o(;q=kgF&1v0uuOKOeol+W-ICyu~S+2Po9i(fAGPzMy*y z%61gp$drk8Zk{XIG%tQ*(8l>&V;5YzP=vdCPI_wE){Dg(r$7AU^8@Ixkh6b;#{N-+ zn~yp4{yI<0;F*PUG`K12KW_2*{e!-;@Bxd!HX(KC$lkp#CGGHQjJSEI{nB=!L8iW? zJ-|_wEst9yNBiMo@=zY0-e^C4Wx%QdwiP`KH~#hQ{hz6{Defk}#>DW6)T6;6?iNZY z(A5KONq!!+@@d%W4fg)t-o7JS?i>IYlkDlC#NUq3D02PmUC@x~7$0fh?Cp7n`Qx$Y ze!i|T(KPYf>Djx>_w_C?c}LuvGu1VwA5`O>jHf>u4f`e>jSX{+iIXOIxkn54^Mm|P zg%xgcjTyslc+WXkZ}b)%P8zW){)1m!W9Ct5lU7TnOrP?+Q`0-?>KB@2dr zid@K(X0#1I`a4lG)Bss2Sbek4iF2h<-`5Y_naG>0pedNu5|Nl?k|-#qGNQ{9zYOq$ zqSq9PNeoSRNn9{NVw#z9s?I!+DC|?xVN<$sg0kZN$xz4G z?Tc_y8r_;T6;h`!ub98A;?og`CS)+XD=Z77aLHdQLmUma%(XYXmE{j%%};$2mH3LN zeNQF&-~WOe``5M+*~V@!;dO&q&d?^wQl#34aafrYkJqQSSZW%eukpHK;=**MdIwj< zA+T1Dv@hdgn6F~nIBc^jjnNtbE7-P|t4MFo23(%qok|)h>qx`e^dd~z4_;2TY_=j? zIqMb_E>(=(lBu`k=geKQWfTxD>24C<3X}@m|JZssq7=Xc@V2EPqdv! zrrJ7kI6ZgQ%&vz7Y2@S5Gi|=3gks{$Y+v#OeQ%jc#%RdJsCz??W|XoMxWRm*|dM;h8>IY zSZhDQR$pQ(nxU;_YElzn!%!iivA&}%fpQ_&_1@J2pqyIFG!oYGgI~2yp8Mh|{EbL-J`?$z(6vIOfYyaKRxDct$+)s#% zWa)lRR9fnh-;an!kJskz9v#*B8+6l7dMO(J)t(=Mk8ljUf5LnvtLMwaG}hS$>qYHS zQSh}~WI;ptKHImFD5+S)=Ciw2_n(SAsLdJu1r6&8e{0nVj6Jm^>u4?eGG@0#NW>eS zXAE4ds)2Z;RggN`7rs%zc1A&QpCl_e8! z)aJfez-tnkUqq5y*+rMIVm;k_ryp7I;sYmwi>X+)E;Cs2f=gyS(Cuwfp2J#R4X{+) zZ!x+4kk29gDMzsL;KN>+YI{_Lzv0`L*-Np&h}93`pHY!Vz`#)ukzjclks=jYFDQxnax=jV9c~z&bMpf{H7EN0O!Eg_80xuS&ZZXJTMoqxSM|tfwNf`B^|UW>wNyC9Bko{9yA^6qx=4{IRvt+WMDeDTiCNOf5qe7fg|S#^-#Po~sH<w0X#YsHP*yYL+Lg#gBv7XdeYKoTYN_${auLyrLv!ez- z9cp-%k>ZGin=2*`GWeWO!RX^1T(02F@8vUQZ8hE!H&IT$&TOUyLF)eF%rwJ2BJ1j!vEE$_{gN@;4o$dJMC=wT*OIVIwM4IqQ>ChGD}v{r zwOL_dWG(&A1uI4vI)7MKW>ED|TjfC%{!X=WFMc*dRChSW_7_)hy`E{g9j!2Q_LvcC zZxGxQx;S7XVtWDDZo#Fmk@w}bMQD;~Kghufj~GvDb5php-#jaN-klJgW)Kmqbs9S5 z*6Z`1{gWeHZ?toY08fgu1PJwi4xeAyv?sf#nslSSi&S9ximLlFpjOaNzj$AV^;fF` zYjgIAlW2>yIpT(Ykg@r;a)tRj$x;rn726fa*<*e-(6Lg{h?oI;^(#EZl_T^L?0g`} z?Wu$UAxKT=q6UH#ras`oIquxye?5`ts+?V%&hGaSOuZY zqV8#Z&5g&o#9h zMQByKXJFtJ)+OC^xuH%pyP;U=Ph9iEa?{fTn8k?0#;)XPzvOdv%@loHK_FC|zs4G9 zFB+Pn3n0;zc6T)!I+Dm^+e`{_=PFf6$L6Hcsvf$NaQt2&ZoG744zHw2B(<|Cj8N3e zBk+^jAln#ExJZbs8?FL*!EFdr2&$K>Y=LVK=i7$Npsso~&Bd8_CloL!DMwv-Pt0dA zeDQ@vPP+2L9YHu=+8O4g4^)0x9<_Gsjt$QB7lT76yZ&^%JM~-cp(LHVS>*0=F;2QK z`j~6Qcz^GCr|FZlDef-6iQ)HWg*(mB8~w<;fN{7l#NB0m9<@ue#hv|K@I}LcH#WMv z%spyl8H?DRU`=;xky*7BCcyJmeR94AeEG<}L|u>c?(*Z?2^5WqC- zo&)#@hl>V403ZN;jt4X#j&Ov%00;mC2(j%M2*@=YsU83V00GkQ@$3=k5e{Jm00Dpi zQk`AN1e&EoR|7x*Ab^}do;`wn4M(*A00IC3w*2fWGuZod=xP8600bxtuxF3?pg!Vl zA;n5X6Xs@N_^7n$i**0zAZcay(y8^~rKC#_Y z;OQ=b!Cg7nFx6FS>4!$ofbt`XT?MQ$%~Y|gz{bPV!=q*GP}mfAiOmcs%Hdv+K5)2X zzE4HiJI6a(l6>5?Gv3~l;O1i++|@P7e6_2(2bG_fr<_ec`p(&oNwit6vKpM9-x8@8 zaq2rKE0i_woXqg_Jji$&AL4$lCDKesdW7^5gFFUiP5e9ON)9%B03I5=X|TM3O}}*$ zz>xv(J?? zE%y{8Dh?n+&y>|BS?&o>l z;d}SDFaBkptJ-HepP^7FwcTI4>_?##F({OBn~D;$=DL@~b>vGfa=+_#R9WkqVdS61 zr@r=zM4{H%N6J=Zj5vor9~74NbcLqO<`yo$E?h_pe_~ zaPd%Fe=sFP^SQ~y@?F>)Pt9(xwAj{CI(69HQ8oCQl~rMn7uJc>%&Jw&Cd?K@){^Xq zF(QIZa1d9kdYOT95=u^9QAJ~Zp|q+_ZzQouVe!W`^9xEI4LcWZt#QrS8Baj<|0uPG z58Hb*Y=MnG8WyY}4KuI?SOaw9ljeeoeiTZ{0{9eZZwl4`Ye*s~aGKyW!D;@VI?bj< zn=0O%W4t-P)R&JH&+^Pk?S@$oXc6_wy36PDrMYY;S?AJB-CyN~XX=7zjt zR8f*i`3j4!xQ;8k2o^L}4Ub0l3eJB(E1bE<>D2M=j3r@|S1KXGP_(i(K7B54KxDePOI6*JHAltPn zX1uIw>=MsAmW&f|3NJKX&csbTQO==ZM#Non)?T$V<^=!u9ZS_nv!fBI#Dvk4c#K2Xd3A@n-3i;lyvP)>h(~hM-^vpQUa`FDpt<=N|C!YlvCuB@A2EXCP=8QN)LRckr8Lx1M?JA(yrt>~^>>=RsZ zU?w`MV)S8e(Rq^7w&q*R&(ti9>e7zhtG5mpa9dkZxt7hpwMr9iH1i z=t-J6^wn7fnaT!>Ca>#Ai66A5YtuLdyd~z*PgGNi(W4<`iMekR<141$=3>W*eTLLT z*P`aLUB_%#RpYg)+XnCl_IY(gTJ0jRH8rW*oA{zTWOd!L%C|pbsGMJO!*zUx1De!+ zY-HF4(?>g1G{X!^yozoUlbkiz0_ zHX|Ih_eq%QAluWYTN$S_eBfQ0ZrK(OJc(wns8E`gaO@g<29NE%o5+J-x#%m zv2-e%k*cppn;pZN_u;BL%c>_S(PFleN6;ZX8yStNvsSb|f!8K&OJu0}+49sT2Mo&j z%=AruKtoIEbyOGP`U!9@|xw;$~iRrXJ^q7$lX`Y9C=c`@TJu zndy``#=D(S#`IhMSTr+Y_!2p0q3C_F=2Hs4Dsqe@7V)dIX{V@Yo|!|`89QEIBqA0J zaZ$7Ug{3lwf8(JNT455|gUM-0IFPr!d0;xLQ1~}j_A49lj_h3o4%G?Q;{Kg*t!Z(7 z)a$${s{(cgqfjgyyku{E03Rb3&`A^34Ng;NrQ*@)M+hr7IfxR*+S8F^ubmNSQ#dW#uc&;8CeY)J1`cLC8~ot zoV$TiJ<~z-jdo;mUI!N@=uMHBSM0+H^`QooW}|9jY1K$}HN3+pYUUd~B}2tH|IACDXA&Og^n@ZHqHHKOb0{=M5ASX&Xlm6-v$S`6sV9B1tO9vm zONwxsiqmWG3~lzUx%*nM!rkFCPe?&emSPX%r-xjfwg%6V^+l%$!viK9?juq{&uGX5 z@{7c9?97UG_X+=45eN4afun0s4F~3q{S|eAFaVB#}@yMY! zi>~}!U)5zuiRV?(^X)klQ{p#;H1(F9gtY@4f0>Kp%IjKFGW-a+;g3V2>rH(vcxqg} znh4Pi`*>EMzOG*U#FPFR)e0-*aDqT}d)A#r%r@?gRK25bXMXOg4q`Z_>ctM->uvWJ zM*cCf7Sk{i#3qbKH`>1}ezb#g%j0b>UHDdgRQ9ME-8G8VXYhoC4P(fGZae*ERT~== zzp8Ohn|~C&e|WYv&9adH%=oQ*gGJ+-^3NFq^ma?7Oh3_NjwvmIJ)!Kxf6_O`K0U(T zLtsZnx98Hn!g9BC`IPeyr0e4l=<50kUUzL0>XO}JbqkvY_?#YUqIG{qo{ZwEn62nu z=@HZu9CqiBKO*LQ&$3oDua9Ao8Pn`@gg>B3swf(hcwj6^!c{Ht_XlFD{>b)z>zizq zt~s&_gK*VJEi-`?sOdO3=*X)i+j@_VO72&uXF!W%7n`{M_G;8PqS+T_nSqA|ZhK4e zN35lduhETO{AHV&V>6A& zEv|1wv0oKCo*lC=PZ)N4RC_##8+d}5xxHWb+%Np`n{%oul6Y$*!S8*MW(zu~2DvB< zdh~w#+g8UnNeU_ExalS`a*5>=hj4F_l~YEDq(m*8-NZ}3O|zBMgm6aU@Og!$4yVR4 zBxHUhLc6|rA#1a2)iledz1A%!Lhyw8tF9c;bHmIv4W86eZdBV&&6Y)JJ$8um;hlQE zE_I1SPy0rhB?l6bc!%23X+`Nn{3ZErqnZ4iZR7X=<;>>U{ao62b zy{?XvKd#bu68-e9jrsZ{@mK{>R_v1Mjw1o*-G=po=wKJZdG)Q|Y6Uj2IyRLoa}Z9D z$$u>_*vhILEHF(;^P142XX0)T^GM>^i7NVR_*m_THl|_FQk2$VH<)L|deG$?S=cy0 zJXt z{%gfPtp=3`wojWK^>BG3%~sSk;Wbm8Jv%yuB9UO6K4a*8bM#w>Ub|YqQO!lfX%yB- zM&OR^N21LyNTnm3U&fFCgrP~|*PnXvMa~jWpVRddBYxu8y~GNyLh7c%?+5d->VZde zaE+~!aHTJ*H;l{|oQmq~l|;pT(ag(az9)IXC{Hqyt1Q%P#1A-oIO8`A9NjA=R&(Lg z*YZW*{A3{`!IJeW5p;iE)*+j05JE!;4I#AjNIs|{s3NE$ zs3NE$sNyFKz|cGl&BM?<49&yPyiB*%zuC}yF{e!qg)(|3nF|2e3Ty?o{yVc37zzvp zh5|!BVF3D~p=d_%>>5z`3u{?P&ahi#Mldh}7-23h0VE(|0!9EMd~iyD z1V92HVZL+&!YCQ!08IcRfF^(@AS?hQfDsl{FTgv1cL47I!U)0$!U)x7D7i~m0OJl| zgbxExfCNASAmL-;40HfG03Co12n)anV1xzDIp7_@JAiipVFY0WVT5K*=x9}ewqB$u z8jJu&fG+os2_w(}=m2y8I!He*1eySv0Ga@r0GjZrLv^kr3xpAb5rh$h5rh$h@t^wC z{Jwi%Ad>>DS|fS434jg2_D=;H01N;I0F#G_P>>u4i~vUX&`SnL03-ksK0bs6bohTv zhl(iHHANIEDc~Be>uQ_vXG#H{i__s2y(XBy&wwJ(7?nmTxGCXCCyUd@owpmU9y)X6Q;sPsQ%AF@-}C@ z_V0U_e`*c>a2YPze-8GR$QL#L;|br8yDqz9f8@NB&Sc51M9$NrL0P)dZ0=a+lT56x z=NiqMZ~jccv~$n2foTD?l874w0R#aA;eYGK(rYBu%9_=G%%acCFKpOOcNMs}>~L_zE^xE~FY}=k{8=%5G`}a+H1jAda6B0nylGb^R^Bsw zYhcV3ry3!<7q{3tts^*vUbxuuY1r(~{(!cFAI;HI+TAdp^2jNm(~06 zUo|c%-zsb9tNvPir7NLGBU7_+H2qHv6R(L(gV%b)6$1-{wXwrHy9MP2-4;YP9U^X+ zBwDXv^1j731=a?3%{G5-zVRP+4w{miF54_8!0P+eRIW#jhriV#s3=zw^YzbLP=oXK z4x0XnZ#|~X@y`FAzt=&dSMr&4lKiFN!@^tDjZ2X9^XrTJE?OHx-7{x(zMe3WkdRQJ zmbQ3s419tjA^`a{O%$lvQQ$=E^$}GiioTj|HLzN5b6S$~{HjIQMD)L_`vev22F2$61(KDGo84-%HBA>UP0m;Mvrod{^^Nt zHW_G6<7GxhzP`$!A6oIdgD`of*#w30&sJyPo$=1vkbFIjKjjl>)JnwJ|?GmG4E_g4@<_*R@90Iiet$|#Unh>(4}lWI9p@v zM&gxS_u>(g;~CkRSQfFSFGoL$V518|t^N+8hU8sx(s-3?lzn{qsb_aWGdwxj{g2PNm;$A~`iYS!a(W1s_h^VwUmnZpt<3T1&X9NFED2=> zbb6lPNxt5|R>rfB52k_UDzwtG{))Yh8sw__B;DveXu=Sxqxb*?6&*3zNlQ3wv8K*v zrbMpt1ivU8jaBDY)Pqe^*9u%C9Y>F6az4lkh&h?91Y&HVA4{bcQS|bAi)kbs$d<(8 z&k4v_DqZ%{rGCQN3rrq0@6tRCKwh#}x6uC~5A=dLB#h@Uei^|Nx6tYG$_jQQkEzcj zYLEZQIo7^UENLpJ2@s_?8KYMCSsx=>0j_c44Xwsu_!%il=g&6+{FX0{ z$vl7*U2V=K(pJ^#bou&~^%EF0cPtH1$EHm_Syi3*$M({Ki6O&eTL`fSp*Rjo$T17~ zJsg+rbMMi#wkroM_PXrk8o(vAOQR3u7RCTHJV{vI;psD1xk9wnt;h;dO z{t~xe`KE^EceMZUsHclClnMS*F_Ax1Uh3l@N9L6dcHD_Q^5_rZ3QkTPU7>ZXr16mE zb^Pf5^uFAo_9$9(0Fy^C5VWOz|NOkxeP*=8|CGjMiIQ2xEoYR;AWlg1E@8q8SuBeK z($|tSO({fC8DDTLN3Q+xCDMu5SB@!*Y6_S2bkSa23UNyo@6)Gwwz%@Z?1@BzI9sDAbIq3DIZU6$aP2N-y3%mIF>Ab?)H<2h zJ7H9KGI;Eyj8!Z-BWf3M)I?It4;1$P^YR(58`HR=>zXvCYoBiXG^z0ldGh`BAuS?p zoj$hF}*^{)r%adPYOj^17<7fy{B9bH0QM@d{k}g$NzP^UGti>ij`)KxE)B!K?1jYK@ z{%G9*@$~fNebkdkP^>PO_k>v^?(9kBhIDuYSkWAGI#<&B;=VsxtbPQ=>iy(*Xt6pp ze{FYENxBDWa6eb#8qHCmP4Way+Adb0!&%{?wcXc;7qYFM_}5lJ3hnUgGSr|A7^E>C zqJ6jz`r!FR{ROLmTcjX`RTYtmdSMZ^so}=qj0M)H2hSxa+SIcntbO`zIh9+pZX_Hv zI0Pzo9a)0*2|58p4>|!lVS35|kpPhZkpQa$ERk@YfXRcCrCjI)^VAw50Xkutvw=u} zNPtLyd;%m@AhYD`2I)007D6XLCrne|5D5?o5D9Se1@1t>Z8$qT+=~ReuFwh43DcYL z5D5?o5D8GJ0_8eTvxLcm!Y#0?_J84ow-lhvH<>OXJ82kY(oeeq^0-be!{eq8V<3RP7=W%LA)h$Trm|j*}q$yai zHLr4?kBT(F-F}u2EHXkjH!2s$c3(bY&=ftRH^$om(WBtPM+$2yci5^f1QE#v3dYRy zg{UCW&LP9asx1V9aObt%wh;@?p$55S30pSgE8jK5YAcLc~rP{!f1?#kVUI8a3a7JdCLBeS$oO_$s3UI+; z2Ul6<6(n3*!u7O;^nfG+9@0giG(Zx;1`>m2(i4&hkVJqaf`y`oBmyK6%&7oL1V|!S zw%s6!07(Q(DnJqek_hHhfFuGW5iCj(NFspkCP*S+A&FpFIYANuk_i7hi6Fqn8Tajf U-F8A~{>k33t>M8{o2e)M1Mk**A^-pY literal 22510 zcmeHNZB!Fi8cr~l_*L-RE$Bq0vTb#v$E|D;Nd#O(SF%g>^mHM{b&u> zie;&T$*h7_n z$je#3RmOhSd*{+XeA8|FfsKi&MTQ@;ZXNm6-XiYI=~=>Vb~W$N)b%;XCQW_i*tC5@ z&Rl10_Mu;Is9qL6Gf2$s_%d|cx<)&0@*ilM{paq1jvia4W$2dqe7^5EjH8gQY{Yy= zz~LtHgq19I9H;(V^YOA3-71?NX6|u|(%5!Iuc<#%68EB}vaCR8u$_^tLEd9-4KL$a z^*!%cotq<1qM8?rrdk6&JzNpQR4{%X&LF()8Z`5&f4aP1=`H5$ds- zMleb6Cv`qVJjvB>f4wBbA#;$jrbeW4=t98!z?s9KnW{{7(eF&;K4>Yc?+-qd7FXQg zv%Mr3_9JlNF>*u-uWcF@yT6m;gKv7(y)zwZX;y{5(N^Izw3<~~ZTV=PU7^!|tzxh@ zrcC~;0U4`xXPAA3eWRb1)IpXi^4V@dqx>R1S1-HuJ{BGl!9hHLCKbYg7D#jABsnZ0 z z%PN5;PoTLX&?phS)UK!vF9;S*iL&06a`Gn8saN>kvI;`~vS>^cB6ut0?=7`WtP%u& zRJE-NAK{nVad)lOxA(%>KE}x56wmnqOlI+CCC_)cW-ax!xqo&Mumex&lcbN|O3@CP!NGDc#ug{9Qgf;K+hNGc&k)UF3 zpV{`E+_)bly(ogbIe=X~i`HxrgfW%2Z~WVa+U?_4)nY=f4R+}2`^5&^&}Jvf_(@mf zG1%U~pm;~7|GGtdWPI>l-~7-R6!si#4^hyR7?G#j^yv%TKG)Cu!`|En&P`EdJ#KDY zF=EWVhbe~1*E`_wm{xnD>jvabFENgwG>UyN-zD&F`QyOncV5O!Q_zRevJ;@wHgXNGPhZ- zLRX09NeN%r`mtG0%xM=FkwoB69SI80M>;+{(W;{d;*>aL)%UO~sEw}@C`=jGVEp&y zrq;C&iW>N*Eovr22w(Q2|9qm#3_j_+)}Jeb1ScAl#Y@1$B(*si7jIrQx-t^6AmIxR^#OReoyhfF(;?pmof zD8B5aW0b^^l_xPIOb@mFUeo1MfC4}vIwt@w04@M7 zfb$4&DnkpZ00n?T^k@@s0dN6u0i4`}`~^A=02BZU(P#*`0Js3S0CL>^FEjlDsZ`!Q zNjBZ=w6a*KVLI7<kz+ z(X!F=oJB5oflHZMz465*PuN`C6_r$|8fRS9-K5Psro#FvM$8mc$yg zh~uNq#o=-|)BVk7a$@2(EIoPZTsWNHA*P_Xu<_X=Rc!2!E0dIt%%mS8^}5j}v20OTTC-U1pK5C$W< zBoGmRhyX-{h${dQ0f-2+D*zDzhzOBmHy|Pa5h3CVKtuo{0__SwL;xZ}BvS;42tY)L qxB|#7qWuAg2tY*m-$jIG_Nwdc(r=8Gaw;W|mA4^3r#@@Pp??G0#zx2h diff --git a/UITests/ReferenceImages/RichTextViewUITests/RichTextView_UI__Update__Updates_highlight_Color_Properly@2x.png b/UITests/ReferenceImages/RichTextViewUITests/RichTextView_UI__Update__Updates_highlight_Color_Properly@2x.png index 65ae98bc87ea43bdd29ed88153a370ce297b3ace..b3824230db5626cd8d4d1e8e36dc2a2db7aeb67c 100644 GIT binary patch literal 23677 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 literal 23486 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 diff --git a/UITests/ReferenceImages/RichTextViewUITests/RichTextView_UI__Update__Updates_highlight_Color_Properly@3x.png b/UITests/ReferenceImages/RichTextViewUITests/RichTextView_UI__Update__Updates_highlight_Color_Properly@3x.png index 667636a6ce3f61181d6fcec7d2c16ca8b425b4d5..9e6fca40fe1568e6640e63e3f5e4107c77d6128b 100644 GIT binary patch literal 48928 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 XEFtgo1?b0UU|!pOx0P)@c=~?;>914u literal 49529 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 xNuX*cmv}w^5tJ_-NsKLAa;dFKEC diff --git a/UITests/RichTextViewUITests.swift b/UITests/RichTextViewUITests.swift index bd0e53e..27a6170 100644 --- a/UITests/RichTextViewUITests.swift +++ b/UITests/RichTextViewUITests.swift @@ -134,9 +134,9 @@ class RichTextViewUITests: QuickSpec { """ let richTextView = RichTextView( input: listHTMLString, - customAdditionalAttributes: ["bullet": [NSAttributedString.Key.font: UIFont.systemFont(ofSize: 15)]], - frame: CGRect(origin: .zero, size: Defaults.size - )) + customAdditionalAttributes: ["bullets": [NSAttributedString.Key.font: UIFont.systemFont(ofSize: 15)]], + frame: CGRect(origin: .zero, size: Defaults.size) + ) richTextView.backgroundColor = UIColor.white self.richTextView = richTextView self.viewController = UIViewController() @@ -201,7 +201,7 @@ class RichTextViewUITests: QuickSpec { richTextView.backgroundColor = UIColor.white richTextView.update( input: "[highlighted-element id=123]Heading[/highlighted-element]", - attributes: ["123": [ + customAdditionalAttributes: ["123": [ NSAttributedString.Key.backgroundColor: UIColor.lightGray, NSAttributedString.Key.underlineStyle: 1] ] diff --git a/UnitTests/Text Parsing/RichTextParserSpec.swift b/UnitTests/Text Parsing/RichTextParserSpec.swift index fcf09ec..63487d5 100644 --- a/UnitTests/Text Parsing/RichTextParserSpec.swift +++ b/UnitTests/Text Parsing/RichTextParserSpec.swift @@ -30,10 +30,7 @@ class RichTextParserSpec: QuickSpec { override func spec() { describe("RichTextParser") { beforeEach { - self.richTextParser = RichTextParser(attributes: ["123": [ - NSAttributedString.Key.backgroundColor: UIColor.lightGray, - NSAttributedString.Key.underlineStyle: 1] - ]) + self.richTextParser = RichTextParser() } context("Latex Parsing") { it("succesfully returns an NSAttributedString with an image") { @@ -64,6 +61,15 @@ class RichTextParserSpec: QuickSpec { } } context("highlighted Element") { + beforeEach { + self.richTextParser = RichTextParser( + customAdditionalAttributes: ["123": [ + NSAttributedString.Key.backgroundColor: UIColor.lightGray, + NSAttributedString.Key.underlineStyle: 1 + ]] + ) + + } it("succesfully returns an NSAttributedString with the highlighted property from a basic highlighted element") { let output = self.richTextParser.extractHighlightedElement(from: NSAttributedString(string: MarkDownText.highlightedElement)) let attributes: [NSAttributedString.Key: Any] = [ @@ -87,25 +93,25 @@ class RichTextParserSpec: QuickSpec { } context("Rich text to attributed string") { it("generates a single attributed string with multiple rich text types") { - let regularText = self.richTextParser.getAttributedText(from: MarkDownText.regularText).output + let regularText = self.richTextParser.getRichTextWithErrors(from: MarkDownText.regularText).output expect(regularText.string.range(of: "Some Text")).toNot(beNil()) - let complexHTML = self.richTextParser.getAttributedText(from: MarkDownText.complexHTML).output + let complexHTML = self.richTextParser.getRichTextWithErrors(from: MarkDownText.complexHTML).output expect(complexHTML.string.range(of: "Message")).toNot(beNil()) - let complexLatex = self.richTextParser.getAttributedText(from: MarkDownText.complexLatex).output + let complexLatex = self.richTextParser.getRichTextWithErrors(from: MarkDownText.complexLatex).output expect(complexLatex.string.range(of: "More Text")).toNot(beNil()) } it("generates a single attributed string with multiple rich text types on a background thread") { waitUntil { done in DispatchQueue.global().async { - let regularText = self.richTextParser.getAttributedText(from: MarkDownText.regularText).output + let regularText = self.richTextParser.getRichTextWithErrors(from: MarkDownText.regularText).output expect(regularText.string.range(of: "Some Text")).toNot(beNil()) - let complexHTML = self.richTextParser.getAttributedText(from: MarkDownText.complexHTML).output + let complexHTML = self.richTextParser.getRichTextWithErrors(from: MarkDownText.complexHTML).output expect(complexHTML.string.range(of: "Message")).toNot(beNil()) - let complexLatex = self.richTextParser.getAttributedText(from: MarkDownText.complexLatex).output + let complexLatex = self.richTextParser.getRichTextWithErrors(from: MarkDownText.complexLatex).output expect(complexLatex.string.range(of: "More Text")).toNot(beNil()) done() } @@ -117,7 +123,7 @@ class RichTextParserSpec: QuickSpec { let output = self.richTextParser.getRichDataTypes(from: "Look at this video: youtube[12345]") expect(output.count).to(equal(2)) expect(output[0]).to(equal(RichDataType.text( - richText: self.richTextParser.getAttributedText(from: "Look at this video: ").output, + richText: self.richTextParser.getRichTextWithErrors(from: "Look at this video: ").output, font: self.richTextParser.font, errors: [ParsingError]() ))) @@ -127,7 +133,7 @@ class RichTextParserSpec: QuickSpec { let output = self.richTextParser.getRichDataTypes(from: "Look at this!") expect(output.count).to(equal(1)) expect(output[0]).to(equal(RichDataType.text( - richText: self.richTextParser.getAttributedText(from: "Look at this!").output, + richText: self.richTextParser.getRichTextWithErrors(from: "Look at this!").output, font: self.richTextParser.font, errors: [ParsingError]() ))) @@ -141,7 +147,7 @@ class RichTextParserSpec: QuickSpec { } context("Strip Code Tags") { it("Successfully strips code tags from input") { - let output = self.richTextParser.getAttributedText(from: MarkDownText.codeText).output + let output = self.richTextParser.getRichTextWithErrors(from: MarkDownText.codeText).output expect(output.string).to(equal("print('Hello World')")) } } @@ -186,13 +192,21 @@ class RichTextParserSpec: QuickSpec { extension RichDataType: Equatable { public static func == (lhs: RichDataType, rhs: RichDataType) -> Bool { - switch (lhs, rhs) { - case let (.text(left), .text(right)): - return left.richText == right.richText - case let (.video(left), .video(right)): - return left.tag == right.tag - default: - return false + switch lhs { + case .video(tag: let lhsTag, error: _): + switch rhs { + case .video(tag: let rhsTag, error: _): + return lhsTag == rhsTag + default: + return false + } + case .text(richText: let lhsRichText, font: _, errors: _): + switch rhs { + case .text(richText: let rhsRichText, font: _, errors: _): + return lhsRichText.string == rhsRichText.string + default: + return false + } } } }