From 23b8a47cad088e69be12e38790b7c6878ae76b13 Mon Sep 17 00:00:00 2001 From: Rajdeep Kwatra Date: Fri, 19 Jul 2024 22:14:02 +1000 Subject: [PATCH] Added helper function to enumerate over continuous ranges of an attribute ignoring difference in value --- .../Helpers/NSAttributedString+Content.swift | 34 +++++++++++++++++++ .../NSAttributedStringExtensionTests.swift | 12 +++++++ 2 files changed, 46 insertions(+) diff --git a/Proton/Sources/Swift/Helpers/NSAttributedString+Content.swift b/Proton/Sources/Swift/Helpers/NSAttributedString+Content.swift index 80877c6a..87e3b980 100644 --- a/Proton/Sources/Swift/Helpers/NSAttributedString+Content.swift +++ b/Proton/Sources/Swift/Helpers/NSAttributedString+Content.swift @@ -54,6 +54,40 @@ public extension NSAttributedString { } return string.makeNSRange(from: range) } + + /// Enumerates over continuous ranges of text based on the presence or absence of a specified attribute. + /// - Parameters: + /// - attributeName: The name of the attribute to check for presence or absence. + /// - range: The range within the attributed string to enumerate. Defaults to the entire string. + /// - using: The block to apply to continuous ranges, indicating whether the attribute was present or absent for the range. + func enumerateContinuousRangesByAttribute(_ attributeName: NSAttributedString.Key, in range: NSRange? = nil, using block: (_ isPresent: Bool, _ range: NSRange) -> Void) { + let enumerationRange = range ?? NSRange(location: 0, length: self.length) + var lastRange: NSRange? = nil + var attributeWasPresentInLastRange = false + + self.enumerateAttributes(in: enumerationRange, options: []) { attributes, currentRange, _ in + let attributeIsPresent = attributes[attributeName] != nil + if let lastRangeUnwrapped = lastRange { + if attributeWasPresentInLastRange != attributeIsPresent { + // Process the last range if the attribute presence state changes + block(attributeWasPresentInLastRange, lastRangeUnwrapped) + lastRange = currentRange + } else { + // Extend the last range efficiently if the state hasn't changed + lastRange = NSRange(location: lastRangeUnwrapped.location, length: NSMaxRange(currentRange) - lastRangeUnwrapped.location) + } + } else { + // Initialize with the first range + lastRange = currentRange + } + attributeWasPresentInLastRange = attributeIsPresent + } + + // Process the final range after enumeration + if let lastRangeUnwrapped = lastRange { + block(attributeWasPresentInLastRange, lastRangeUnwrapped) + } + } } extension NSAttributedString { diff --git a/Proton/Tests/ExtensionTests/NSAttributedStringExtensionTests.swift b/Proton/Tests/ExtensionTests/NSAttributedStringExtensionTests.swift index 5b1668ab..801a32b3 100644 --- a/Proton/Tests/ExtensionTests/NSAttributedStringExtensionTests.swift +++ b/Proton/Tests/ExtensionTests/NSAttributedStringExtensionTests.swift @@ -218,4 +218,16 @@ class NSAttributedStringExtensionTests: XCTestCase { let range = text.reverseRange(of: "isx", startingLocation: text.length - 1) XCTAssertNil(range) } + + func testEnumeratesIgnoringValue() { + let text = NSMutableAttributedString(string: "This is a test string", attributes: [.inlineContentType: "a"]) + text.append(NSAttributedString(string: " Some more text", attributes: [.inlineContentType: "b"])) + text.append(NSAttributedString(string: " And more text", attributes: [.inlineContentType: "c"])) + text.append(NSAttributedString(string: "Not with attribute", attributes: [:])) + text.append(NSAttributedString(string: "Again with attribute", attributes: [.inlineContentType: "c"])) + + text.enumerateContinuousRangesByAttribute(.inlineContentType) { isPresent, range in + print("\(text.substring(from: range)) - \(isPresent)") + } + } }