Skip to content

Commit

Permalink
Improved API
Browse files Browse the repository at this point in the history
  • Loading branch information
Bruno Muniz Azevedo Filho committed Oct 26, 2023
1 parent 3881cf9 commit f63377d
Show file tree
Hide file tree
Showing 2 changed files with 64 additions and 62 deletions.
62 changes: 25 additions & 37 deletions Sources/MultiAttributedString/MultiAttributedString.swift
Original file line number Diff line number Diff line change
@@ -1,64 +1,52 @@
import UIKit

public extension NSAttributedString {
public extension String {

/// Creates an `NSAttributedString` by applying the specified attributes between given symbol pairs.
/// Creates an `NSAttributedString` by applying specified attributes to portions of the string surrounded by specified delimiters.
///
/// This method allows you to easily stylize parts of a string using symbol delimiters. For example,
/// to make the text between '$' symbols red, you would include an attribute with the '$' symbol and a red text color.
/// This method offers a convenient way to stylize certain parts of a string by using symbol delimiters. For instance,
/// if you want the text between two '$' symbols to appear in red, provide an attribute with the '$' delimiter and a red text color.
///
/// - Parameters:
/// - rawText: The input string containing symbol delimiters to denote areas to be stylized.
/// - pairs: A dictionary where each key is a symbol delimiter and the corresponding value is a set of attributes to apply.
/// - Returns: An `NSAttributedString` with the attributes applied based on the symbol delimiters.
static func with(
_ rawText: String,
attributesBetween pairs: [String: ([NSAttributedString.Key: Any])]
) -> NSAttributedString {
/// - rawText: The string containing delimiters that specify which sections should be stylized.
/// - pairs: A dictionary where the key represents a symbol delimiter and its corresponding value denotes the set of attributes to be applied.
/// - Returns: An `NSAttributedString` with attributes applied as per the specified delimiters.
func applying(attributes: [String: ([NSAttributedString.Key: Any])]) -> NSAttributedString {
let mutableAttributedString = NSMutableAttributedString(string: self)

// Replace any escaped symbols (e.g., \*) with a placeholder to prevent them from being processed.
var preprocessedText = rawText
for pair in pairs {
let escapedSymbol = "\\\(pair.key)"
let placeholder = "{\(pair.key)}" // Placeholder to replace escaped symbols.
preprocessedText = preprocessedText.replacingOccurrences(of: escapedSymbol, with: placeholder)
}

let mutableAttributedString = NSMutableAttributedString(string: preprocessedText)

// Use stacks to track the positions of opening and closing symbols.
// Stacks to track the positions of starting and ending delimiters.
var symbolStack: [String] = []
var startPositionStack: [String.Index] = []

var currentIndex = preprocessedText.startIndex
var currentIndex = self.startIndex

// Iterate through the string's characters.
while currentIndex < preprocessedText.endIndex {
let character = preprocessedText[currentIndex]
// Traverse each character in the string.
while currentIndex < self.endIndex {
let character = self[currentIndex]
let symbol = String(character)

// If the current character matches a symbol from the pairs...
if let pair = pairs.first(where: { $0.key == symbol }) {
// If the current character is a delimiter defined in the attributes...
if let pair = attributes.first(where: { $0.key == symbol }) {
if symbolStack.contains(pair.key) {
// If it's a closing symbol, pop from the stacks and apply the attributes.
// If the symbol is a closing delimiter, retrieve the positions from the stacks and apply the attributes.
if let lastSymbol = symbolStack.last, lastSymbol == pair.key {
let start = startPositionStack.removeLast()
let range = NSRange(start..<currentIndex, in: preprocessedText)
let range = NSRange(start..<currentIndex, in: self)
mutableAttributedString.addAttributes(pair.value, range: range)
symbolStack.removeLast()
}
} else {
// If it's an opening symbol, push its details to the stacks.
// If the symbol is an opening delimiter, store its details onto the stacks.
symbolStack.append(pair.key)
startPositionStack.append(currentIndex)
}
}
currentIndex = preprocessedText.index(after: currentIndex)

currentIndex = self.index(after: currentIndex)
}

// Remove the delimiter symbols from the final string.
for pair in pairs {
// Eliminate the delimiter symbols from the resulting string.
for pair in attributes {
mutableAttributedString.mutableString.replaceOccurrences(
of: pair.key,
with: "",
Expand All @@ -67,8 +55,8 @@ public extension NSAttributedString {
)
}

// Replace placeholders with their original symbols (e.g., replace "{*}" with "*").
for pair in pairs {
// Substitute placeholders with the actual delimiters (e.g., replacing "{*}" with "*").
for pair in attributes {
let placeholder = "{\(pair.key)}"
mutableAttributedString.mutableString.replaceOccurrences(
of: placeholder,
Expand Down
64 changes: 39 additions & 25 deletions Tests/MultiAttributedStringTests/MultiAttributedStringTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@ import XCTest
final class MultiAttributedStringTests: XCTestCase {
// Test attribute application between `$` symbols.
func testSimpleAttribution() {
let result = NSAttributedString.with("This is $red$ text.", attributesBetween: [
"$": [.foregroundColor: UIColor.red]
])
let result = "This is $red$ text.".applying(
attributes: [
"$": [.foregroundColor: UIColor.red]
]
)
let range = (result.string as NSString).range(of: "red")
let attributes = result.attributes(at: range.location, effectiveRange: nil)

Expand All @@ -15,9 +17,11 @@ final class MultiAttributedStringTests: XCTestCase {

// Test escaping of symbols using a backslash.
func testEscapeCharacter() {
let result = NSAttributedString.with("This is \\$not red\\$ text but this is $red$.", attributesBetween: [
"$": [.foregroundColor: UIColor.red]
])
let result = "This is \\$not red\\$ text but this is $red$.".applying(
attributes: [
"$": [.foregroundColor: UIColor.red]
]
)
let rangeOfNotRed = (result.string as NSString).range(of: "not red")
let attributesOfNotRed = result.attributes(at: rangeOfNotRed.location, effectiveRange: nil)
XCTAssertNil(attributesOfNotRed[.foregroundColor])
Expand All @@ -29,10 +33,12 @@ final class MultiAttributedStringTests: XCTestCase {

// Test application of multiple attributes.
func testMultipleAttributions() {
let result = NSAttributedString.with("This is $red$ and this is #blue#.", attributesBetween: [
"$": [.foregroundColor: UIColor.red],
"#": [.foregroundColor: UIColor.blue]
])
let result = "This is $red$ and this is #blue#.".applying(
attributes: [
"$": [.foregroundColor: UIColor.red],
"#": [.foregroundColor: UIColor.blue]
]
)
let rangeOfRed = (result.string as NSString).range(of: "red")
let attributesOfRed = result.attributes(at: rangeOfRed.location, effectiveRange: nil)
XCTAssertEqual(attributesOfRed[.foregroundColor] as! UIColor, UIColor.red)
Expand All @@ -44,21 +50,25 @@ final class MultiAttributedStringTests: XCTestCase {

// Test nested symbols' attribute application.
func testNestedSymbols() {
let result = NSAttributedString.with("This is a $#nested$# test.", attributesBetween: [
"$": [.foregroundColor: UIColor.red],
"#": [.foregroundColor: UIColor.blue]
])
let result = "This is a $#nested$# test.".applying(
attributes: [
"$": [.foregroundColor: UIColor.red],
"#": [.foregroundColor: UIColor.blue]
]
)
let rangeOfNested = (result.string as NSString).range(of: "nested")
let attributesOfNested = result.attributes(at: rangeOfNested.location, effectiveRange: nil)
XCTAssertEqual(attributesOfNested[.foregroundColor] as! UIColor, UIColor.red)
}

// Test consecutive non-overlapping attributes.
func testConsecutiveDifferentAttributes() {
let result = NSAttributedString.with("This is $red$#blue# text.", attributesBetween: [
"$": [.foregroundColor: UIColor.red],
"#": [.foregroundColor: UIColor.blue]
])
let result = "This is $red$#blue# text.".applying(
attributes: [
"$": [.foregroundColor: UIColor.red],
"#": [.foregroundColor: UIColor.blue]
]
)
let rangeOfRed = (result.string as NSString).range(of: "red")
let attributesOfRed = result.attributes(at: rangeOfRed.location, effectiveRange: nil)
XCTAssertEqual(attributesOfRed[.foregroundColor] as! UIColor, UIColor.red)
Expand All @@ -70,20 +80,24 @@ final class MultiAttributedStringTests: XCTestCase {

// Test that unpaired symbols don't apply attributes.
func testUnpairedSymbols() {
let result = NSAttributedString.with("This is $unpaired text.", attributesBetween: [
"$": [.foregroundColor: UIColor.red]
])
let result = "This is $unpaired text.".applying(
attributes: [
"$": [.foregroundColor: UIColor.red]
]
)
let rangeOfUnpaired = (result.string as NSString).range(of: "unpaired")
let attributesOfUnpaired = result.attributes(at: rangeOfUnpaired.location, effectiveRange: nil)
XCTAssertNil(attributesOfUnpaired[.foregroundColor])
}

// Test handling of multiple escaped symbols.
func testMultipleEscapedSymbols() {
let result = NSAttributedString.with("This is \\$not\\$ \\#colored\\# text.", attributesBetween: [
"$": [.foregroundColor: UIColor.red],
"#": [.foregroundColor: UIColor.blue]
])
let result = "This is \\$not\\$ \\#colored\\# text.".applying(
attributes: [
"$": [.foregroundColor: UIColor.red],
"#": [.foregroundColor: UIColor.blue]
]
)
let rangeOfNot = (result.string as NSString).range(of: "not")
let attributesOfNot = result.attributes(at: rangeOfNot.location, effectiveRange: nil)
XCTAssertNil(attributesOfNot[.foregroundColor])
Expand Down

0 comments on commit f63377d

Please sign in to comment.