From c4a4c10a75f99c0244394a40d303a368c9b9d540 Mon Sep 17 00:00:00 2001 From: Jorge Leandro Perez Date: Wed, 6 Sep 2017 11:08:05 -0300 Subject: [PATCH 1/3] String+EndOfParagraph: New Helpers --- .../Extensions/String+EndOfParagraph.swift | 60 +++++++++++++++++-- 1 file changed, 54 insertions(+), 6 deletions(-) diff --git a/Aztec/Classes/Extensions/String+EndOfParagraph.swift b/Aztec/Classes/Extensions/String+EndOfParagraph.swift index a57e56593..86fa26ff2 100644 --- a/Aztec/Classes/Extensions/String+EndOfParagraph.swift +++ b/Aztec/Classes/Extensions/String+EndOfParagraph.swift @@ -7,22 +7,70 @@ extension String { /// This methods verifies if the receiver string contains an End of Paragraph before the specified index. /// - /// - Parameters: - /// - index: the index to check + /// - Parameter index: the index to check /// /// - Returns: `true` if the receiver contains an end-of-paragraph character before the specified Index. /// func isEndOfParagraph(before index: String.Index) -> Bool { - assert(index != startIndex) + return isEndOfParagraph(at: self.index(before: index)) + } + - let previousIndex = self.index(before: index) - guard previousIndex != endIndex else { + /// This methods verifies if the receiver string contains an End of Paragraph at the specified index. + /// + /// - Parameter index: the index to check + /// + /// - Returns: `true` if the receiver contains an end-of-paragraph character at the specified Index. + /// + func isEndOfParagraph(at index: String.Index) -> Bool { + guard index != endIndex else { return true } - let endingString = substring(with: previousIndex ..< index) + let endingString = substring(with: index ..< self.index(after: index)) let paragraphSeparators = [String(.carriageReturn), String(.lineFeed), String(.paragraphSeparator)] return paragraphSeparators.contains(endingString) } + + + /// This methods verifies if the receiver string contains a new paragraph at the specified index. + /// + /// - Parameter index: the index to check + /// + /// - Returns: `true` if the receiver contains a new paragraph at the specified Index. + /// + func isStartOfParagraph(at index: String.Index) -> Bool { + guard index != startIndex else { + return true + } + + return isEndOfParagraph(before: index) + } + + + /// Checks if the receiver has an empty paragraph at the specified index. + /// + /// - Parameter index: the receiver's index to check + /// + /// - Returns: `true` if the specified index is in an empty paragraph, `false` otherwise. + /// + func isEmptyParagraph(at index: String.Index) -> Bool { + return isStartOfParagraph(at: index) && isEndOfParagraph(at: index) + } + + + /// Checks if the receiver has an empty paragraph at the specified offset. + /// + /// - Parameter offset: the receiver's offset to check + /// + /// - Returns: `true` if the specified offset is in an empty line, `false` otherwise. + /// + func isEmptyParagraph(at offset: Int) -> Bool { + guard let index = self.indexFromLocation(offset) else { + return true + } + + return isEmptyParagraph(at: index) + } } From 602fecf9d5fe706decccc089e35c27f7a79d5390 Mon Sep 17 00:00:00 2001 From: Jorge Leandro Perez Date: Wed, 6 Sep 2017 11:08:23 -0300 Subject: [PATCH 2/3] TextView: Wiring Empty Paragraph Helper --- Aztec/Classes/Extensions/String+EndOfLine.swift | 2 +- Aztec/Classes/TextKit/TextView.swift | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Aztec/Classes/Extensions/String+EndOfLine.swift b/Aztec/Classes/Extensions/String+EndOfLine.swift index b30265d3d..a7f3dbcfc 100644 --- a/Aztec/Classes/Extensions/String+EndOfLine.swift +++ b/Aztec/Classes/Extensions/String+EndOfLine.swift @@ -23,7 +23,7 @@ extension String { func isEmptyLine(at offset: Int) -> Bool { guard let index = self.indexFromLocation(offset) else { return true - } + } return isEmptyLine(at: index) } diff --git a/Aztec/Classes/TextKit/TextView.swift b/Aztec/Classes/TextKit/TextView.swift index c19343e65..c029e3666 100644 --- a/Aztec/Classes/TextKit/TextView.swift +++ b/Aztec/Classes/TextKit/TextView.swift @@ -1323,7 +1323,7 @@ private extension TextView { /// - Returns: `true` if we should nuke the paragraph attributes. /// private func mustRemoveParagraphStylesBeforeRemovingCharacter(at range: NSRange) -> Bool { - return storage.string.isEmptyLine(at: range.location) + return storage.string.isEmptyParagraph(at: range.location) } // MARK: - WORKAROUND: Removing paragraph styles after entering a newline. From 962c7475397477415fc565d5adc7b5901e9f5c0d Mon Sep 17 00:00:00 2001 From: Jorge Leandro Perez Date: Wed, 6 Sep 2017 12:30:27 -0300 Subject: [PATCH 3/3] Implements StringParagraphTests --- Aztec.xcodeproj/project.pbxproj | 16 ++-- ...Paragraph.swift => String+Paragraph.swift} | 25 ++--- .../StringEndOfParagraphTests.swift | 17 ---- .../Extensions/StringParagraphTests.swift | 92 +++++++++++++++++++ 4 files changed, 113 insertions(+), 37 deletions(-) rename Aztec/Classes/Extensions/{String+EndOfParagraph.swift => String+Paragraph.swift} (98%) delete mode 100644 AztecTests/Extensions/StringEndOfParagraphTests.swift create mode 100644 AztecTests/Extensions/StringParagraphTests.swift diff --git a/Aztec.xcodeproj/project.pbxproj b/Aztec.xcodeproj/project.pbxproj index 501a9419a..943c72141 100644 --- a/Aztec.xcodeproj/project.pbxproj +++ b/Aztec.xcodeproj/project.pbxproj @@ -47,8 +47,8 @@ B57D1C3D1E92C38000EA4B16 /* HTMLAttachment.swift in Sources */ = {isa = PBXBuildFile; fileRef = B57D1C3C1E92C38000EA4B16 /* HTMLAttachment.swift */; }; B59C9F9F1DF74BB80073B1D6 /* UIFont+Traits.swift in Sources */ = {isa = PBXBuildFile; fileRef = B59C9F9E1DF74BB80073B1D6 /* UIFont+Traits.swift */; }; B5A99D841EBA073D00DED081 /* HTMLStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5A99D831EBA073D00DED081 /* HTMLStorage.swift */; }; - B5AB79F81F5F3E0B00DF26F5 /* String+EndOfParagraph.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5AB79F71F5F3E0B00DF26F5 /* String+EndOfParagraph.swift */; }; - B5AB79FA1F5F403C00DF26F5 /* StringEndOfParagraphTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5AB79F91F5F403C00DF26F5 /* StringEndOfParagraphTests.swift */; }; + B5AB79F81F5F3E0B00DF26F5 /* String+Paragraph.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5AB79F71F5F3E0B00DF26F5 /* String+Paragraph.swift */; }; + B5AB79FA1F5F403C00DF26F5 /* StringParagraphTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5AB79F91F5F403C00DF26F5 /* StringParagraphTests.swift */; }; B5AF89321E93CFC80051EFDB /* RenderableAttachmentDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5AF89311E93CFC80051EFDB /* RenderableAttachmentDelegate.swift */; }; B5B86D371DA3EC250083DB3F /* NSRange+Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = F18733C41DA096EE005AEB80 /* NSRange+Helpers.swift */; }; B5B96DAB1E01B2F300791315 /* UIPasteboard+Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5B96DAA1E01B2F300791315 /* UIPasteboard+Helpers.swift */; }; @@ -221,8 +221,8 @@ B57D1C3C1E92C38000EA4B16 /* HTMLAttachment.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HTMLAttachment.swift; sourceTree = ""; }; B59C9F9E1DF74BB80073B1D6 /* UIFont+Traits.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIFont+Traits.swift"; sourceTree = ""; }; B5A99D831EBA073D00DED081 /* HTMLStorage.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HTMLStorage.swift; sourceTree = ""; }; - B5AB79F71F5F3E0B00DF26F5 /* String+EndOfParagraph.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "String+EndOfParagraph.swift"; sourceTree = ""; }; - B5AB79F91F5F403C00DF26F5 /* StringEndOfParagraphTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = StringEndOfParagraphTests.swift; path = Extensions/StringEndOfParagraphTests.swift; sourceTree = ""; }; + B5AB79F71F5F3E0B00DF26F5 /* String+Paragraph.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "String+Paragraph.swift"; sourceTree = ""; }; + B5AB79F91F5F403C00DF26F5 /* StringParagraphTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = StringParagraphTests.swift; path = Extensions/StringParagraphTests.swift; sourceTree = ""; }; B5AF89311E93CFC80051EFDB /* RenderableAttachmentDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RenderableAttachmentDelegate.swift; sourceTree = ""; }; B5B96DAA1E01B2F300791315 /* UIPasteboard+Helpers.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIPasteboard+Helpers.swift"; sourceTree = ""; }; B5BC4FED1DA2C17800614582 /* NSAttributedString+Lists.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "NSAttributedString+Lists.swift"; sourceTree = ""; }; @@ -511,7 +511,7 @@ B5B96DAA1E01B2F300791315 /* UIPasteboard+Helpers.swift */, B5C99D3E1E72E2E700335355 /* UIStackView+Helpers.swift */, F1DE83D41EF20493009269E6 /* UIColor+Parsers.swift */, - B5AB79F71F5F3E0B00DF26F5 /* String+EndOfParagraph.swift */, + B5AB79F71F5F3E0B00DF26F5 /* String+Paragraph.swift */, ); path = Extensions; sourceTree = ""; @@ -779,7 +779,7 @@ B5375F481EC2569500F5D7EC /* StringHTMLTests.swift */, FF152D8D1E68552A00FF596C /* StringRangeConversionTests.swift */, FF7DCB4A1E815F9400AB77CB /* UIColorHexParserTests.swift */, - B5AB79F91F5F403C00DF26F5 /* StringEndOfParagraphTests.swift */, + B5AB79F91F5F403C00DF26F5 /* StringParagraphTests.swift */, ); name = Extensions; sourceTree = ""; @@ -970,7 +970,7 @@ F12F586C1EF20394008AE298 /* HTMLParagraphFormatter.swift in Sources */, F17BC86B1F4E466100398E2B /* NSAttributedString+HTMLInitializer.swift in Sources */, F12F58661EF20394008AE298 /* StandardAttributeFormatter.swift in Sources */, - B5AB79F81F5F3E0B00DF26F5 /* String+EndOfParagraph.swift in Sources */, + B5AB79F81F5F3E0B00DF26F5 /* String+Paragraph.swift in Sources */, B5A99D841EBA073D00DED081 /* HTMLStorage.swift in Sources */, F13CE53E1F4DD05E0043368D /* PipelineProcessor.swift in Sources */, F17BC8AA1F4E512800398E2B /* AttributedStringParser.swift in Sources */, @@ -1077,7 +1077,7 @@ B5F84B631E706B720089A76C /* NSAttributedStringAnalyzerTests.swift in Sources */, F10BE61C1EA7B1DB002E4625 /* NSAttributedStringReplaceOcurrencesTests.swift in Sources */, B5375F491EC2569500F5D7EC /* StringHTMLTests.swift in Sources */, - B5AB79FA1F5F403C00DF26F5 /* StringEndOfParagraphTests.swift in Sources */, + B5AB79FA1F5F403C00DF26F5 /* StringParagraphTests.swift in Sources */, B57534521F267D63009D4904 /* ArrayHelperTests.swift in Sources */, F14665451EA7C230008DE2B8 /* NSMutableAttributedStringReplaceOcurrencesTests.swift in Sources */, F17BC8B51F4E517100398E2B /* AttributedStringParserTests.swift in Sources */, diff --git a/Aztec/Classes/Extensions/String+EndOfParagraph.swift b/Aztec/Classes/Extensions/String+Paragraph.swift similarity index 98% rename from Aztec/Classes/Extensions/String+EndOfParagraph.swift rename to Aztec/Classes/Extensions/String+Paragraph.swift index 86fa26ff2..64bb0370a 100644 --- a/Aztec/Classes/Extensions/String+EndOfParagraph.swift +++ b/Aztec/Classes/Extensions/String+Paragraph.swift @@ -5,14 +5,18 @@ import Foundation // extension String { - /// This methods verifies if the receiver string contains an End of Paragraph before the specified index. + /// This methods verifies if the receiver string contains a new paragraph at the specified index. /// /// - Parameter index: the index to check /// - /// - Returns: `true` if the receiver contains an end-of-paragraph character before the specified Index. + /// - Returns: `true` if the receiver contains a new paragraph at the specified Index. /// - func isEndOfParagraph(before index: String.Index) -> Bool { - return isEndOfParagraph(at: self.index(before: index)) + func isStartOfParagraph(at index: String.Index) -> Bool { + guard index != startIndex else { + return true + } + + return isEndOfParagraph(before: index) } @@ -34,18 +38,15 @@ extension String { } - /// This methods verifies if the receiver string contains a new paragraph at the specified index. + /// This methods verifies if the receiver string contains an End of Paragraph before the specified index. /// /// - Parameter index: the index to check /// - /// - Returns: `true` if the receiver contains a new paragraph at the specified Index. + /// - Returns: `true` if the receiver contains an end-of-paragraph character before the specified Index. /// - func isStartOfParagraph(at index: String.Index) -> Bool { - guard index != startIndex else { - return true - } - - return isEndOfParagraph(before: index) + func isEndOfParagraph(before index: String.Index) -> Bool { + assert(index != startIndex) + return isEndOfParagraph(at: self.index(before: index)) } diff --git a/AztecTests/Extensions/StringEndOfParagraphTests.swift b/AztecTests/Extensions/StringEndOfParagraphTests.swift deleted file mode 100644 index e95a83f8c..000000000 --- a/AztecTests/Extensions/StringEndOfParagraphTests.swift +++ /dev/null @@ -1,17 +0,0 @@ -import XCTest -@testable import Aztec - -class StringEndOfParagraphTests: XCTestCase { - - func testIsEndOfParagraphReturnsTrueWheneverTestStringEndsWithCarriageReturn() { - let test = "something" + String(.carriageReturn) - - XCTAssertTrue(test.isEndOfParagraph(before: test.endIndex)) - } - - func testIsEndOfParagraphReturnsFalseWheneverTestStringEndsWithLineSeparator() { - let test = "something" + String(.lineSeparator) - - XCTAssertFalse(test.isEndOfParagraph(before: test.endIndex)) - } -} diff --git a/AztecTests/Extensions/StringParagraphTests.swift b/AztecTests/Extensions/StringParagraphTests.swift new file mode 100644 index 000000000..7dd47a96e --- /dev/null +++ b/AztecTests/Extensions/StringParagraphTests.swift @@ -0,0 +1,92 @@ +import XCTest +@testable import Aztec + +class StringParagraphTests: XCTestCase { + + /// Verifies that isStartOfParagraph(at:) returns true on the first position of an empty string + /// + func testIsStartOfParagraphReturnsTrueOnEmptyStrings() { + let sample = String() + + XCTAssertTrue(sample.isStartOfParagraph(at: sample.startIndex)) + } + + /// Verifies that isStartOfParagraph(at:) returns true when checked against the first position + /// + func testIsStartOfParagraphReturnsTrueAtTheBeginningOfNewParagraphs() { + let sample = "Sample" + + XCTAssertTrue(sample.isStartOfParagraph(at: sample.startIndex)) + } + + /// Verifies that isStartOfParagraph(at:) returns false when checked against any position that is not the first one + /// + func testIsStartOfParagraphReturnsFalseAtAnyPositionThatIsNotTheFirstOne() { + let sample = "Sample" + + for location in 1 ..< sample.characters.count { + let index = sample.indexFromLocation(location)! + XCTAssertFalse(sample.isStartOfParagraph(at: index)) + } + } + + /// Verifies that isEndOfParagraph(before:) returns false when checked against a Line Separator character + /// (which effectively adds a new paragraph) + /// + func testIsEndOfParagraphReturnsTrueWheneverTestStringEndsWithCarriageReturn() { + let sample = "Sample" + String(.carriageReturn) + + XCTAssertTrue(sample.isEndOfParagraph(before: sample.endIndex)) + } + + /// Verifies that isEndOfParagraph(before:) returns false when checked against a Line Separator character + /// (which effectively adds a newline that belongs to the current paragraph) + /// + func testIsEndOfParagraphReturnsFalseWheneverTestStringEndsWithLineSeparator() { + let sample = "Sample" + String(.lineSeparator) + + XCTAssertFalse(sample.isEndOfParagraph(before: sample.endIndex)) + } + + /// Verifies that isEndOfParagraph(at:) does not crash on empty strings + /// + func testIsEndOfParagraphDoesNotCrashOnEmptyStrings() { + let sample = String() + + XCTAssertNoThrow(sample.isEndOfParagraph(at: sample.endIndex)) + } + + /// Verifies that isEmptyParagraph(at:) does not crash on empty strings + /// + func testIsEmptyParagraphDoesNotCrashOnEmptyStrings() { + let sample = String() + + XCTAssertNoThrow(sample.isEmptyParagraph(at: sample.endIndex)) + } + + /// Verifies that isEmptyParagraph(at:) returns false on any position that does not belong to an empty paragraph. + /// + func testIsEmptyParagraphReturnsFalseOnNonEmptyParagraphs() { + let sample = "Sample" + + for i in 0 ..< sample.characters.count { + XCTAssertFalse(sample.isEmptyParagraph(at: i)) + } + } + + /// Verifies that isEmptyParagraph(at:) returns false on empty lines that DO belong to the previous paragraph. + /// + func testIsEmptyParagraphReturnsFalseOnEmptyLinesThatBelongToABiggerParagraph() { + let sample = "Sample" + String(.lineSeparator) + + XCTAssertFalse(sample.isEmptyParagraph(at: sample.characters.count - 1)) + } + + /// Verifies that isEmptyParagraph(at:) returns true on empty lines, that do not belong to the previous paragraph. + /// + func testIsEmptyParagraphReturnsTrueOnEmptyLinesThatDoNotBelongToABiggerParagraph() { + let sample = "Sample" + String(.lineFeed) + + XCTAssertTrue(sample.isEmptyParagraph(at: sample.characters.count)) + } +}