Skip to content
This repository has been archived by the owner on Apr 18, 2023. It is now read-only.

Commit

Permalink
feat: Add attributes to strings (#126)
Browse files Browse the repository at this point in the history
* adding attributes dictionary

* update

* added highlighted text element

* use attributes

* tests

* pr fix

* Update RichTextViewUITests.swift
  • Loading branch information
BisuGit authored Dec 3, 2019
1 parent a54cda1 commit b40e030
Show file tree
Hide file tree
Showing 10 changed files with 110 additions and 8 deletions.
9 changes: 8 additions & 1 deletion Example/Source/InputOutputModuleView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ import UIKit

class InputOutputModuleView: UIView, RichTextViewDelegate {

// MARK: - Constants

let attributes = ["456": [NSAttributedString.Key.backgroundColor: UIColor.lightGray]]

// MARK: - IBOutlets

@IBOutlet var contentView: UIView!
Expand Down Expand Up @@ -42,7 +46,10 @@ class InputOutputModuleView: UIView, RichTextViewDelegate {
private func updateText(_ text: String) {
self.inputLabel.text = text
self.outputRichTextView.textViewDelegate = self
self.outputRichTextView.update(input: text)
self.outputRichTextView.update(
input: text,
attributes: self.attributes
)
}

// MARK: - RichTextViewDelegate
Expand Down
3 changes: 2 additions & 1 deletion Example/Source/ViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,8 @@ class ViewController: UIViewController {
InputOutputModuleView(text: "<a href='https://www.google.com'>jump to page</a>"),
InputOutputModuleView(text: Text.tableHTML),
InputOutputModuleView(text: "<blockquote>Here is a blockquote</blockquote>"),
InputOutputModuleView(text: "Look [interactive-element id=123]This is an interactive element[/interactive-element] Wow")
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")
]
}

Expand Down
2 changes: 1 addition & 1 deletion Podfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -79,4 +79,4 @@ SPEC CHECKSUMS:

PODFILE CHECKSUM: f6fe77da859ca9643b5846ec01310846dcbfa657

COCOAPODS: 1.8.3
COCOAPODS: 1.8.4
1 change: 1 addition & 0 deletions Source/Extensions/NSMutableAttributedStringExtension.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

extension NSAttributedString.Key {
static let customLink = NSAttributedString.Key(rawValue: "customLink")
static let highlight = NSAttributedString.Key(rawValue: "highlight")
}

extension NSMutableAttributedString {
Expand Down
8 changes: 6 additions & 2 deletions Source/RichTextView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ public class RichTextView: UIView {
latexTextBaselineOffset: CGFloat = 0,
interactiveTextColor: UIColor = UIColor.blue,
textViewDelegate: RichTextViewDelegate? = nil,
customAdditionalAttributes: [String: [NSAttributedString.Key: Any]]? = nil,
frame: CGRect,
completion: (([ParsingError]?) -> Void)? = nil) {
self.input = input
Expand All @@ -49,7 +50,8 @@ public class RichTextView: UIView {
font: font,
textColor: textColor,
latexTextBaselineOffset: latexTextBaselineOffset,
interactiveTextColor: interactiveTextColor
interactiveTextColor: interactiveTextColor,
attributes: customAdditionalAttributes
)
self.textColor = textColor
self.textViewDelegate = textViewDelegate
Expand All @@ -76,14 +78,16 @@ public class RichTextView: UIView {
textColor: UIColor? = nil,
latexTextBaselineOffset: CGFloat? = nil,
interactiveTextColor: UIColor? = nil,
attributes: [String: [NSAttributedString.Key: Any]]? = nil,
completion: (([ParsingError]?) -> Void)? = nil) {
self.input = input ?? self.input
self.richTextParser = RichTextParser(
latexParser: latexParser ?? self.richTextParser.latexParser,
font: font ?? self.richTextParser.font,
textColor: textColor ?? self.textColor,
latexTextBaselineOffset: latexTextBaselineOffset ?? self.richTextParser.latexTextBaselineOffset,
interactiveTextColor: interactiveTextColor ?? self.richTextParser.interactiveTextColor
interactiveTextColor: interactiveTextColor ?? self.richTextParser.interactiveTextColor,
attributes: attributes
)
self.textColor = textColor ?? self.textColor
self.subviews.forEach { $0.removeFromSuperview() }
Expand Down
35 changes: 33 additions & 2 deletions Source/Text Parsing/RichTextParser.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,15 @@ class RichTextParser {
enum ParserConstants {
static let mathTagName = "math"
static let interactiveElementTagName = "interactive-element"
static let highlightedElementTagName = "highlighted-element"
static let latexRegex = "\\[\(ParserConstants.mathTagName)\\](.*?)\\[\\/\(ParserConstants.mathTagName)\\]"
static let latexRegexCaptureGroupIndex = 0
static let interactiveElementRegex = """
\\[\(ParserConstants.interactiveElementTagName)\\sid=.+?\\].*?\\[\\/\(ParserConstants.interactiveElementTagName)\\]
"""
static let highlightedElementRegex = """
\\[\(ParserConstants.highlightedElementTagName)\\sid=.+?\\].*?\\[\\/\(ParserConstants.highlightedElementTagName)\\]
"""
typealias RichTextWithErrors = (output: NSAttributedString, errors: [ParsingError]?)
}

Expand All @@ -29,19 +33,22 @@ class RichTextParser {
let textColor: UIColor
let latexTextBaselineOffset: CGFloat
let interactiveTextColor: UIColor
let attributes: [String: [NSAttributedString.Key: Any]]?

// MARK: - Init

init(latexParser: LatexParserProtocol = LatexParser(),
font: UIFont = UIFont.systemFont(ofSize: UIFont.systemFontSize),
textColor: UIColor = UIColor.black,
latexTextBaselineOffset: CGFloat = 0,
interactiveTextColor: UIColor = UIColor.blue) {
interactiveTextColor: UIColor = UIColor.blue,
attributes: [String: [NSAttributedString.Key: Any]]? = nil) {
self.latexParser = latexParser
self.font = font
self.textColor = textColor
self.latexTextBaselineOffset = latexTextBaselineOffset
self.interactiveTextColor = interactiveTextColor
self.attributes = attributes
}

// MARK: - Utility Functions
Expand Down Expand Up @@ -125,8 +132,11 @@ class RichTextParser {
let interactiveElementPositions = self.extractPositions(
fromRanges: mutableAttributedString.string.ranges(of: ParserConstants.interactiveElementRegex, options: .regularExpression)
)
let highlightedElementPositions = self.extractPositions(
fromRanges: mutableAttributedString.string.ranges(of: ParserConstants.highlightedElementRegex, options: .regularExpression)
)
let latexPositions = self.extractPositions(fromRanges: self.getLatexRanges(inText: mutableAttributedString.string))
let splitPositions = interactiveElementPositions + latexPositions
let splitPositions = interactiveElementPositions + latexPositions + highlightedElementPositions
if splitPositions.isEmpty {
return (mutableAttributedString.trimmingTrailingNewlinesAndWhitespaces(), nil)
}
Expand All @@ -150,6 +160,8 @@ class RichTextParser {
for attributedString in attributedStringComponents {
if self.isTextInteractiveElement(attributedString.string) {
output.append(self.extractInteractiveElement(from: attributedString))
} else if self.isTextHighlightedElement(attributedString.string) {
output.append(self.extractHighlightedElement(from: attributedString))
} else if self.isTextLatex(attributedString.string) {
if let attributedLatexString = self.extractLatex(from: attributedString.string) {
output.append(attributedLatexString)
Expand Down Expand Up @@ -189,6 +201,21 @@ class RichTextParser {
return mutableAttributedInput
}

func extractHighlightedElement(from input: NSAttributedString) -> NSMutableAttributedString {
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 else {
return NSMutableAttributedString(string: highlightedElementText)
}
let attributes: [NSAttributedString.Key: Any] = [
.highlight: highlightedElementID,
.backgroundColor: richTextAttributes[highlightedElementID]?[.backgroundColor]
].merging(input.attributes(at: 0, effectiveRange: nil)) { (current, _) in current }
let mutableAttributedInput = NSMutableAttributedString(string: " " + highlightedElementText + " ", attributes: attributes)
return mutableAttributedInput
}

func isTextLatex(_ text: String) -> Bool {
return !self.getLatexRanges(inText: text).isEmpty
}
Expand All @@ -197,6 +224,10 @@ class RichTextParser {
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>]) -> [String.Index] {
return ranges.flatMap { range in
return [range.lowerBound, range.upperBound]
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added ...extViewUITests/[email protected]
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
37 changes: 37 additions & 0 deletions UITests/RichTextViewUITests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,24 @@ class RichTextViewUITests: QuickSpec {
})
}
}
it("Renders a string with highlights") {
let richTextView = RichTextView(
input: "[highlighted-element id=123]Test[/highlighted-element]",
customAdditionalAttributes: ["123": [NSAttributedString.Key.backgroundColor: UIColor.lightGray]],
frame: CGRect(origin: .zero, size: Defaults.size
))
richTextView.backgroundColor = UIColor.white
self.richTextView = richTextView
self.viewController = UIViewController()
self.viewController?.view.addSubview(richTextView)
self.window?.rootViewController = self.viewController
waitUntil(timeout: Defaults.timeOut) { done in
DispatchQueue.main.asyncAfter(deadline: .now() + Defaults.delay, execute: {
expect(self.window).to(haveValidSnapshot())
done()
})
}
}
}
context("Update") {
it("Updates Input Properly") {
Expand Down Expand Up @@ -151,6 +169,25 @@ class RichTextViewUITests: QuickSpec {
})
}
}
it("Updates highlight Color Properly") {
let richTextView = RichTextView(frame: CGRect(origin: .zero, size: Defaults.size))
richTextView.backgroundColor = UIColor.white
richTextView.update(
input: "[highlighted-element id=123]* Heading[/highlighted-element]",
attributes: ["123": [NSMutableAttributedString.Key.backgroundColor: UIColor.lightGray]]
)
self.richTextView = richTextView
self.viewController = UIViewController()
self.viewController?.view.addSubview(richTextView)
self.window?.rootViewController = self.viewController
waitUntil(timeout: Defaults.timeOut) { done in
DispatchQueue.main.asyncAfter(deadline: .now() + Defaults.delay, execute: {
expect(self.window).to(haveValidSnapshot())
done()
})
}
}

}
afterEach {
UIView.setAnimationsEnabled(false)
Expand Down
23 changes: 22 additions & 1 deletion UnitTests/Text Parsing/RichTextParserSpec.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,16 @@ class RichTextParserSpec: QuickSpec {
static let codeText = "[code]print('Hello World')[/code]"
static let basicInteractiveElement = "[interactive-element id=123]This is an interactive element[/interactive-element]"
static let complexInteractiveElement = "Look! An interactive element: [interactive-element id=123]element[/interactive-element]"
static let highlightedElement = "[highlighted-element id=123]This is an highlighted element[/highlighted-element]"
static let complexHighlightedElement = "Look! An highlighted element: [highlighted-element id=123]element[/highlighted-element]"
}

var richTextParser: RichTextParser!

override func spec() {
describe("RichTextParser") {
beforeEach {
self.richTextParser = RichTextParser()
self.richTextParser = RichTextParser(attributes: ["123": [NSAttributedString.Key.backgroundColor: UIColor.lightGray]])
}
context("Latex Parsing") {
it("succesfully returns an NSAttributedString with an image") {
Expand Down Expand Up @@ -58,6 +60,25 @@ class RichTextParserSpec: QuickSpec {
expect(output).to(equal(expectedAttributedString))
}
}
context("highlighted Element") {
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] = [
NSAttributedString.Key(rawValue: "highlight"): "123",
.backgroundColor: UIColor.lightGray
]
let expectedAttributedString = NSAttributedString(string: " This is an highlighted element ", attributes: attributes)
expect(output).to(equal(expectedAttributedString))
}
it("succesfully returns an NSAttributedString with the highlighted property from a complex highlighted element") {
let output = self.richTextParser.extractHighlightedElement(from: NSAttributedString(string: MarkDownText.complexHighlightedElement))
let attributes: [NSAttributedString.Key: Any] = [
NSAttributedString.Key(rawValue: "highlight"): "123",
.backgroundColor: UIColor.lightGray
]
let expectedAttributedString = NSAttributedString(string: " element ", attributes: attributes)
expect(output).to(equal(expectedAttributedString))
}
}
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
Expand Down

0 comments on commit b40e030

Please sign in to comment.