diff --git a/Example/Podfile.lock b/Example/Podfile.lock index 0998c57..7aa4d17 100644 --- a/Example/Podfile.lock +++ b/Example/Podfile.lock @@ -1,5 +1,5 @@ PODS: - - Marker (1.0.0) + - Marker (1.1.0) DEPENDENCIES: - Marker (from `../`) @@ -9,7 +9,7 @@ EXTERNAL SOURCES: :path: ../ SPEC CHECKSUMS: - Marker: a298cccb34f84d81beb9206ad618cd8f114f524b + Marker: ef3ead037c66a1340eec2fd9f79b27cb36dcbec3 PODFILE CHECKSUM: 2d22b0ce73bebf9f2dee7cbf15b441cc137adc5e diff --git a/Marker.podspec b/Marker.podspec index fa8b65c..d12c1b6 100644 --- a/Marker.podspec +++ b/Marker.podspec @@ -8,7 +8,7 @@ Pod::Spec.new do |s| s.name = "Marker" - s.version = "1.0.0" + s.version = "1.1.0" s.summary = "A light wrapper around NSAttributedString." s.description = <<-DESC TODO: Add long description of the pod here. diff --git a/Marker/Marker.xcodeproj/project.pbxproj b/Marker/Marker.xcodeproj/project.pbxproj index f45312d..e83de0b 100644 --- a/Marker/Marker.xcodeproj/project.pbxproj +++ b/Marker/Marker.xcodeproj/project.pbxproj @@ -21,7 +21,7 @@ 271C85C31ED4E2B700F8BBBB /* TagParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 271C85B21ED4E2B700F8BBBB /* TagParser.swift */; }; 271C85C51ED4E2B700F8BBBB /* TextStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 271C85B41ED4E2B700F8BBBB /* TextStyle.swift */; }; 271C85C61ED4E2B700F8BBBB /* TextTransform.swift in Sources */ = {isa = PBXBuildFile; fileRef = 271C85B51ED4E2B700F8BBBB /* TextTransform.swift */; }; - 271C85C91ED4E37700F8BBBB /* ParserTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 271C85C81ED4E37700F8BBBB /* ParserTests.swift */; }; + 271C85C91ED4E37700F8BBBB /* MarkdownParserTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 271C85C81ED4E37700F8BBBB /* MarkdownParserTests.swift */; }; 27C4E5D01ED5ED2400DDE387 /* Marker.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 27C4E5C71ED5ED2400DDE387 /* Marker.framework */; }; 27C4E5EC1ED5ED3100DDE387 /* Marker.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 27C4E5E31ED5ED3000DDE387 /* Marker.framework */; }; 27C4E5FA1ED5ED6400DDE387 /* Marker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 271C85A41ED4E2B700F8BBBB /* Marker.swift */; }; @@ -50,8 +50,23 @@ 27C4E61B1ED5ED6500DDE387 /* TextTransform.swift in Sources */ = {isa = PBXBuildFile; fileRef = 271C85B51ED4E2B700F8BBBB /* TextTransform.swift */; }; 27C4E61C1ED5ED6900DDE387 /* Marker.h in Headers */ = {isa = PBXBuildFile; fileRef = 271C858D1ED4E2A000F8BBBB /* Marker.h */; settings = {ATTRIBUTES = (Public, ); }; }; 27C4E61D1ED5ED6C00DDE387 /* Marker.h in Headers */ = {isa = PBXBuildFile; fileRef = 271C858D1ED4E2A000F8BBBB /* Marker.h */; settings = {ATTRIBUTES = (Public, ); }; }; - 480C7A5F1F15832A0094E4EA /* ParserTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 271C85C81ED4E37700F8BBBB /* ParserTests.swift */; }; - 480C7A601F15832A0094E4EA /* ParserTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 271C85C81ED4E37700F8BBBB /* ParserTests.swift */; }; + 480104771FA1355F00F20FF8 /* Rule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 480104741FA1355F00F20FF8 /* Rule.swift */; }; + 480104781FA1355F00F20FF8 /* Rule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 480104741FA1355F00F20FF8 /* Rule.swift */; }; + 480104791FA1355F00F20FF8 /* Rule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 480104741FA1355F00F20FF8 /* Rule.swift */; }; + 4801047A1FA1355F00F20FF8 /* TokenParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 480104751FA1355F00F20FF8 /* TokenParser.swift */; }; + 4801047B1FA1355F00F20FF8 /* TokenParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 480104751FA1355F00F20FF8 /* TokenParser.swift */; }; + 4801047C1FA1355F00F20FF8 /* TokenParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 480104751FA1355F00F20FF8 /* TokenParser.swift */; }; + 4801047D1FA1355F00F20FF8 /* Token.swift in Sources */ = {isa = PBXBuildFile; fileRef = 480104761FA1355F00F20FF8 /* Token.swift */; }; + 4801047E1FA1355F00F20FF8 /* Token.swift in Sources */ = {isa = PBXBuildFile; fileRef = 480104761FA1355F00F20FF8 /* Token.swift */; }; + 4801047F1FA1355F00F20FF8 /* Token.swift in Sources */ = {isa = PBXBuildFile; fileRef = 480104761FA1355F00F20FF8 /* Token.swift */; }; + 480C7A5F1F15832A0094E4EA /* MarkdownParserTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 271C85C81ED4E37700F8BBBB /* MarkdownParserTests.swift */; }; + 480C7A601F15832A0094E4EA /* MarkdownParserTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 271C85C81ED4E37700F8BBBB /* MarkdownParserTests.swift */; }; + 481303A01FA3BA7C001F1DF1 /* String+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4813039F1FA3BA7C001F1DF1 /* String+Extensions.swift */; }; + 481303A11FA3BC64001F1DF1 /* String+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4813039F1FA3BA7C001F1DF1 /* String+Extensions.swift */; }; + 481303A21FA3BC65001F1DF1 /* String+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4813039F1FA3BA7C001F1DF1 /* String+Extensions.swift */; }; + 481B0A771FA9143800CB651C /* ElementParserTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 481B0A751FA9142D00CB651C /* ElementParserTests.swift */; }; + 481B0A781FA9143800CB651C /* ElementParserTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 481B0A751FA9142D00CB651C /* ElementParserTests.swift */; }; + 481B0A791FA9143900CB651C /* ElementParserTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 481B0A751FA9142D00CB651C /* ElementParserTests.swift */; }; 484053B21F72EA6000626C55 /* UIButtonExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 484053A21F72EA4F00626C55 /* UIButtonExtension.swift */; }; 484053B31F72EA6000626C55 /* Color.swift in Sources */ = {isa = PBXBuildFile; fileRef = 484053A41F72EA4F00626C55 /* Color.swift */; }; 484053B41F72EA6000626C55 /* Font.swift in Sources */ = {isa = PBXBuildFile; fileRef = 484053A61F72EA4F00626C55 /* Font.swift */; }; @@ -135,11 +150,16 @@ 271C85B41ED4E2B700F8BBBB /* TextStyle.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TextStyle.swift; sourceTree = ""; }; 271C85B51ED4E2B700F8BBBB /* TextTransform.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TextTransform.swift; sourceTree = ""; }; 271C85C71ED4E37600F8BBBB /* MarkerTests-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "MarkerTests-Bridging-Header.h"; sourceTree = ""; }; - 271C85C81ED4E37700F8BBBB /* ParserTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ParserTests.swift; sourceTree = ""; }; + 271C85C81ED4E37700F8BBBB /* MarkdownParserTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MarkdownParserTests.swift; sourceTree = ""; }; 27C4E5C71ED5ED2400DDE387 /* Marker.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Marker.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 27C4E5CF1ED5ED2400DDE387 /* Marker-tvOSTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "Marker-tvOSTests.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; 27C4E5E31ED5ED3000DDE387 /* Marker.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Marker.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 27C4E5EB1ED5ED3100DDE387 /* Marker-macOSTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "Marker-macOSTests.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; + 480104741FA1355F00F20FF8 /* Rule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Rule.swift; sourceTree = ""; }; + 480104751FA1355F00F20FF8 /* TokenParser.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TokenParser.swift; sourceTree = ""; }; + 480104761FA1355F00F20FF8 /* Token.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Token.swift; sourceTree = ""; }; + 4813039F1FA3BA7C001F1DF1 /* String+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+Extensions.swift"; sourceTree = ""; }; + 481B0A751FA9142D00CB651C /* ElementParserTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ElementParserTests.swift; sourceTree = ""; }; 484053A11F72EA4F00626C55 /* NSButtonExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSButtonExtension.swift; sourceTree = ""; }; 484053A21F72EA4F00626C55 /* UIButtonExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIButtonExtension.swift; sourceTree = ""; }; 484053A41F72EA4F00626C55 /* Color.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Color.swift; sourceTree = ""; }; @@ -251,7 +271,8 @@ 271C85971ED4E2A000F8BBBB /* MarkerTests */ = { isa = PBXGroup; children = ( - 271C85C81ED4E37700F8BBBB /* ParserTests.swift */, + 481B0A751FA9142D00CB651C /* ElementParserTests.swift */, + 271C85C81ED4E37700F8BBBB /* MarkdownParserTests.swift */, 48F232831F1FBD1300E86D5D /* TextStyleEquatableTests.swift */, 48F232871F1FC2F900E86D5D /* TextStyleFactoryFunctionTests.swift */, 48F232781F1FBA2200E86D5D /* TextTransformEquatableTests.swift */, @@ -268,9 +289,13 @@ 271C85AD1ED4E2B700F8BBBB /* ElementParser.swift */, 271C85AE1ED4E2B700F8BBBB /* MarkdownElement.swift */, 271C85AF1ED4E2B700F8BBBB /* MarkdownParser.swift */, + 480104741FA1355F00F20FF8 /* Rule.swift */, + 4813039F1FA3BA7C001F1DF1 /* String+Extensions.swift */, 271C85B01ED4E2B700F8BBBB /* Symbol.swift */, 271C85B11ED4E2B700F8BBBB /* Tag.swift */, 271C85B21ED4E2B700F8BBBB /* TagParser.swift */, + 480104761FA1355F00F20FF8 /* Token.swift */, + 480104751FA1355F00F20FF8 /* TokenParser.swift */, ); path = Parser; sourceTree = ""; @@ -613,14 +638,18 @@ buildActionMask = 2147483647; files = ( 271C85C51ED4E2B700F8BBBB /* TextStyle.swift in Sources */, + 4801047D1FA1355F00F20FF8 /* Token.swift in Sources */, 48F232751F1FB7B600E86D5D /* TextTransform+Extensions.swift in Sources */, 271C85C01ED4E2B700F8BBBB /* MarkdownParser.swift in Sources */, 484053B81F72EA6000626C55 /* UITextFieldExtension.swift in Sources */, 484053B31F72EA6000626C55 /* Color.swift in Sources */, + 480104771FA1355F00F20FF8 /* Rule.swift in Sources */, 271C85BB1ED4E2B700F8BBBB /* Markup.swift in Sources */, 271C85BD1ED4E2B700F8BBBB /* Element.swift in Sources */, 271C85BF1ED4E2B700F8BBBB /* MarkdownElement.swift in Sources */, 271C85C31ED4E2B700F8BBBB /* TagParser.swift in Sources */, + 4801047A1FA1355F00F20FF8 /* TokenParser.swift in Sources */, + 481303A01FA3BA7C001F1DF1 /* String+Extensions.swift in Sources */, 48F232801F1FBA7800E86D5D /* TextStyle+Extensions.swift in Sources */, 484053B21F72EA6000626C55 /* UIButtonExtension.swift in Sources */, 484053BA1F72EA6000626C55 /* UITextViewExtension.swift in Sources */, @@ -642,9 +671,10 @@ buildActionMask = 2147483647; files = ( 48F2327C1F1FBA4400E86D5D /* TextTransformEquatableTests.swift in Sources */, + 481B0A771FA9143800CB651C /* ElementParserTests.swift in Sources */, 48F232881F1FC2F900E86D5D /* TextStyleFactoryFunctionTests.swift in Sources */, 48F232841F1FBD1300E86D5D /* TextStyleEquatableTests.swift in Sources */, - 271C85C91ED4E37700F8BBBB /* ParserTests.swift in Sources */, + 271C85C91ED4E37700F8BBBB /* MarkdownParserTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -653,16 +683,20 @@ buildActionMask = 2147483647; files = ( 27C4E6091ED5ED6400DDE387 /* TextStyle.swift in Sources */, + 4801047E1FA1355F00F20FF8 /* Token.swift in Sources */, 48F232761F1FB7B600E86D5D /* TextTransform+Extensions.swift in Sources */, 27C4E6041ED5ED6400DDE387 /* MarkdownParser.swift in Sources */, 484053C21F72EA6000626C55 /* UITextFieldExtension.swift in Sources */, 484053BD1F72EA6000626C55 /* Color.swift in Sources */, + 480104781FA1355F00F20FF8 /* Rule.swift in Sources */, 27C4E5FF1ED5ED6400DDE387 /* Markup.swift in Sources */, 484053CF1F72EA7A00626C55 /* UIButtonExtension.swift in Sources */, 27C4E6011ED5ED6400DDE387 /* Element.swift in Sources */, 27C4E6031ED5ED6400DDE387 /* MarkdownElement.swift in Sources */, + 4801047B1FA1355F00F20FF8 /* TokenParser.swift in Sources */, 27C4E6071ED5ED6400DDE387 /* TagParser.swift in Sources */, 48F232811F1FBA7800E86D5D /* TextStyle+Extensions.swift in Sources */, + 481303A11FA3BC64001F1DF1 /* String+Extensions.swift in Sources */, 484053C41F72EA6000626C55 /* UITextViewExtension.swift in Sources */, 484053C01F72EA6000626C55 /* LineBreakMode.swift in Sources */, 27C4E6051ED5ED6400DDE387 /* Symbol.swift in Sources */, @@ -682,9 +716,10 @@ buildActionMask = 2147483647; files = ( 48F2327D1F1FBA4400E86D5D /* TextTransformEquatableTests.swift in Sources */, + 481B0A781FA9143800CB651C /* ElementParserTests.swift in Sources */, 48F232891F1FC2F900E86D5D /* TextStyleFactoryFunctionTests.swift in Sources */, 48F232851F1FBD1300E86D5D /* TextStyleEquatableTests.swift in Sources */, - 480C7A601F15832A0094E4EA /* ParserTests.swift in Sources */, + 480C7A601F15832A0094E4EA /* MarkdownParserTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -695,6 +730,8 @@ 27C4E61A1ED5ED6500DDE387 /* TextStyle.swift in Sources */, 484053C51F72EA6100626C55 /* NSButtonExtension.swift in Sources */, 48F232821F1FBA7800E86D5D /* TextStyle+Extensions.swift in Sources */, + 4801047C1FA1355F00F20FF8 /* TokenParser.swift in Sources */, + 480104791FA1355F00F20FF8 /* Rule.swift in Sources */, 27C4E6151ED5ED6500DDE387 /* MarkdownParser.swift in Sources */, 484053C71F72EA6100626C55 /* Color.swift in Sources */, 48F232771F1FB7B600E86D5D /* TextTransform+Extensions.swift in Sources */, @@ -705,6 +742,8 @@ 27C4E6141ED5ED6500DDE387 /* MarkdownElement.swift in Sources */, 27C4E6181ED5ED6500DDE387 /* TagParser.swift in Sources */, 484053CA1F72EA6100626C55 /* LineBreakMode.swift in Sources */, + 481303A21FA3BC65001F1DF1 /* String+Extensions.swift in Sources */, + 4801047F1FA1355F00F20FF8 /* Token.swift in Sources */, 27C4E6161ED5ED6500DDE387 /* Symbol.swift in Sources */, 27C4E6131ED5ED6500DDE387 /* ElementParser.swift in Sources */, 27C4E60B1ED5ED6500DDE387 /* Marker.swift in Sources */, @@ -721,9 +760,10 @@ buildActionMask = 2147483647; files = ( 48F2327E1F1FBA4500E86D5D /* TextTransformEquatableTests.swift in Sources */, + 481B0A791FA9143900CB651C /* ElementParserTests.swift in Sources */, 48F2328A1F1FC2F900E86D5D /* TextStyleFactoryFunctionTests.swift in Sources */, 48F232861F1FBD1300E86D5D /* TextStyleEquatableTests.swift in Sources */, - 480C7A5F1F15832A0094E4EA /* ParserTests.swift in Sources */, + 480C7A5F1F15832A0094E4EA /* MarkdownParserTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/Marker/Marker/Marker.swift b/Marker/Marker/Marker.swift index 6e110c3..c2622c5 100644 --- a/Marker/Marker/Marker.swift +++ b/Marker/Marker/Marker.swift @@ -64,41 +64,46 @@ public func parsedMarkdownString(from markdownText: String, elements.forEach { (element) in var font: Font? = nil - var strikethroughStyle: NSUnderlineStyle? = nil - var underlineStyle: NSUnderlineStyle? = nil switch element { - case .em(_): + case .em: font = textStyle.emFont - case .strong(_): + case .strong: font = textStyle.strongFont - case .strikethrough(_): - strikethroughStyle = textStyle.strikethroughStyle - case .underline(_): - underlineStyle = textStyle.underlineStyle - } - - if let font = font { - attributedString.addAttributes([AttributedStringKey.font: font], range: NSRange(element.range, in: parsedString)) - } - - if let strikethroughStyle = strikethroughStyle { - attributedString.addAttributes([AttributedStringKey.strikethroughStyle: strikethroughStyle.rawValue], - range: NSRange(element.range, in: parsedString)) - + case .strikethrough(let range): + if let strikethroughStyle = textStyle.strikethroughStyle { + attributedString.addAttributes([AttributedStringKey.strikethroughStyle: strikethroughStyle.rawValue], + range: NSRange(range, in: parsedString)) + } if let strikethroughColor = textStyle.strikethroughColor { attributedString.addAttributes([AttributedStringKey.strikethroughColor: strikethroughColor], - range: NSRange(element.range, in: parsedString)) + range: NSRange(range, in: parsedString)) + } + case .underline(let range): + if let underlineStyle = textStyle.underlineStyle { + attributedString.addAttributes([AttributedStringKey.underlineStyle: underlineStyle.rawValue], + range: NSRange(range, in: parsedString)) } - } - - if let underlineStyle = underlineStyle { - attributedString.addAttributes([AttributedStringKey.underlineStyle: underlineStyle.rawValue], range: NSRange(element.range, in: parsedString)) - if let underlineColor = textStyle.underlineColor { - attributedString.addAttributes([AttributedStringKey.underlineColor: underlineColor], range: NSRange(element.range, in: parsedString)) + attributedString.addAttributes([AttributedStringKey.underlineColor: underlineColor], + range: NSRange(range, in: parsedString)) + } + case .link(let range, let urlString): + attributedString.addAttribute(AttributedStringKey.link, + value: urlString, + range: NSRange(range, in: parsedString)) + + if let linkFont = textStyle.linkFont { + attributedString.addAttribute(AttributedStringKey.font, + value: linkFont, + range: NSRange(range, in: parsedString)) } } + + if let font = font { + attributedString.addAttributes([AttributedStringKey.font: font], + range: NSRange(element.range, in: parsedString)) + } } return attributedString @@ -119,17 +124,23 @@ public func parsedMarkupString(from text: String, return NSAttributedString(string: text, textStyle: textStyle) } - let (parsedString, elements) = try ElementParser.parse(text, for: markups.map { Symbol(character: $0.0) }) - + let markupRules = Dictionary( + uniqueKeysWithValues: markups.map { (key, value) in + return (Rule(symbol: Symbol(character: key)), value) + } + ) + + let (parsedString, elements) = try ElementParser.parse(text, using: Array(markupRules.keys)) + let attributedString = NSMutableAttributedString(string: textStyle.textTransform.applied(to: parsedString)) attributedString.addAttributes(textStyle.attributes, range: NSRange(location: 0, length: parsedString.count)) - + elements.forEach { (element) in - if let markup = markups[Character(element.symbol.rawValue)] { + if let markup = markupRules[element.rule] { attributedString.addAttributes(markup.attributes, range: NSRange(element.range, in: parsedString)) } } - + return attributedString } diff --git a/Marker/Marker/NSAttributedString+Extensions.swift b/Marker/Marker/NSAttributedString+Extensions.swift index 3bb11bb..396066a 100644 --- a/Marker/Marker/NSAttributedString+Extensions.swift +++ b/Marker/Marker/NSAttributedString+Extensions.swift @@ -6,7 +6,7 @@ // Copyright © 2017 Prolific Interactive. All rights reserved. // -internal extension NSAttributedString { +extension NSAttributedString { /// Initializes `NSAttributedString` instance with given string and text style. /// diff --git a/Marker/Marker/Parser/Element.swift b/Marker/Marker/Parser/Element.swift index c2b1d43..c3a88fe 100644 --- a/Marker/Marker/Parser/Element.swift +++ b/Marker/Marker/Parser/Element.swift @@ -8,13 +8,11 @@ import Foundation -internal typealias Index = String.CharacterView.Index - /// Markup element. Contains a symbol and the range it applies to. -internal struct Element { +struct Element { - /// Markup symbol. - let symbol: Symbol + /// Markup rule. + let rule: Rule /// Range that the receiver applies to. let range: Range diff --git a/Marker/Marker/Parser/ElementParser.swift b/Marker/Marker/Parser/ElementParser.swift index 95ed50e..39525f5 100644 --- a/Marker/Marker/Parser/ElementParser.swift +++ b/Marker/Marker/Parser/ElementParser.swift @@ -8,79 +8,36 @@ import Foundation -/// Markup element parser. -internal struct ElementParser { - - /// Parser error. - /// - /// - tagMismatch: Opening tag doesn't match closing tag. - /// - unclosedTags: A tag was left unclosed. - enum ParserError: LocalizedError { - - case tagMismatch - case unclosedTags - - var errorDescription: String? { - switch self { - case .tagMismatch: - return "Opening tag doesn't match closing tag." - case .unclosedTags: - return "A tag was left unclosed." - } - } - - } - +/// Bare bones parser that strips `string` for symbols defined in `rules`. +struct ElementParser { + // MARK: - Static functions - /// Parses specified string and returns a tuple containing string stripped of tag characters and an array of markup elements. + /// Parses specified string of symbols defined in `rules` and returns a tuple containing string stripped of matching characters and a list of matched elements. /// /// - Parameters: /// - string: String to be parsed. - /// - symbols: Symbols to parse for. - /// - Returns: Tuple containing string stripped of tag characters and an array of markup elements. + /// - rules: Rules with symbols to parse for. + /// - Returns: Tuple containing string stripped of matching characters and a list of matched elements. /// - Throws: Parser error. - static func parse(_ string: String, for symbols: [Symbol]) throws -> (strippedString: String, elements: [Element]) { - let parser = TagParser(symbols: symbols) - let tags = parser.parse(string) - - guard tags.count > 0 else { + static func parse(_ string: String, using rules: [Rule]) throws -> (strippedString: String, elements: [Element]) { + let tokens = try TokenParser.parse(string, using: rules) + + guard tokens.count > 0 else { return (string, []) } - guard tags.count % 2 == 0 else { - throw ParserError.unclosedTags - } - var strippedString: String = "" + var strippedString = "" var elements: [Element] = [] - var startIndex: Index = string.startIndex - for i in stride(from: 0, to: tags.count, by: 2) { - let openingTag = tags[i] - let closingTag = tags[i + 1] + for token in tokens { + let range = strippedString.append(contentOf: token) - if openingTag.symbol != closingTag.symbol { - throw ParserError.tagMismatch + if let rule = token.rule { + elements.append(Element(rule: rule, range: range)) } - - // Add the text from the last closing tag to the current opening tag. - strippedString += string[startIndex..) - case strong(Range) - case strikethrough(Range) - case underline(Range) + case em(range: Range) + case strong(range: Range) + case strikethrough(range: Range) + case underline(range: Range) + case link(range: Range, urlString: String) /// Range of characters that the elements apply to. var range: Range { switch self { - case .em(let range): - return range - case .strong(let range): - return range - case .strikethrough(let range): - return range - case .underline(let range): + case .em(let range), .strong(let range), .strikethrough(let range), .underline(let range), .link(let range, _): return range } } diff --git a/Marker/Marker/Parser/MarkdownParser.swift b/Marker/Marker/Parser/MarkdownParser.swift index 75aeca5..b7f7989 100644 --- a/Marker/Marker/Parser/MarkdownParser.swift +++ b/Marker/Marker/Parser/MarkdownParser.swift @@ -6,8 +6,8 @@ // Copyright © 2016 Prolific Interactive. All rights reserved. // -/// Simple bare-bones markdown parser. -internal struct MarkdownParser { +/// Markdown parser. +struct MarkdownParser { /// Parser error. /// @@ -22,54 +22,97 @@ internal struct MarkdownParser { // MARK: - Private properties - private static let underscoreEmSymbol = Symbol(rawValue: "_") - private static let asteriskEmSymbol = Symbol(rawValue: "*") + private static let underscoreEmSymbol = Symbol(character: "_") + private static let asteriskEmSymbol = Symbol(character: "*") private static let underscoreStrongSymbol = Symbol(rawValue: "__") private static let asteriskStrongSymbol = Symbol(rawValue: "**") private static let tildeStrikethroughSymbol = Symbol(rawValue: "~~") private static let equalUnderlineSymbol = Symbol(rawValue: "==") + private static let linkTextOpeningSymbol = Symbol(character: "[") + private static let linkTextClosingSymbol = Symbol(character: "]") + private static let linkURLOpeningSymbol = Symbol(character: "(") + private static let linkURLClosingSymbol = Symbol(character: ")") // MARK: - Static functions - /// Parses specified string and returns a tuple containing string stripped of tag characters and an array of Markdown elements. + /// Parses specified string and returns a tuple containing string stripped of symbols and an array of Markdown elements. /// /// - Parameter string: String to be parsed. /// - Returns: Tuple containing string stripped of tag characters and an array of Markdown elements. /// - Throws: Parser error. static func parse(_ string: String) throws -> (strippedString: String, elements: [MarkdownElement]) { guard - let underscoreEmSymbol = underscoreEmSymbol, - let asteriskEmSymbol = asteriskEmSymbol, let underscoreStrongSymbol = underscoreStrongSymbol, let asteriskStrongSymbol = asteriskStrongSymbol, let tildeStrikethroughSymbol = tildeStrikethroughSymbol, let equalUnderlineSymbol = equalUnderlineSymbol else { return (string, []) } - - func transformToMarkdownElement(_ element: Element) throws -> MarkdownElement { - switch element.symbol { - case underscoreEmSymbol, asteriskEmSymbol: - return .em(element.range) - case underscoreStrongSymbol, asteriskStrongSymbol: - return .strong(element.range) - case tildeStrikethroughSymbol: - return .strikethrough(element.range) - case equalUnderlineSymbol: - return .underline(element.range) + + let underscoreEmRule = Rule(symbol: underscoreEmSymbol) + let asteriskEmRule = Rule(symbol: asteriskEmSymbol) + let underscoreStrongRule = Rule(symbol: underscoreStrongSymbol) + let asteriskStrongRule = Rule(symbol: asteriskStrongSymbol) + let tildeStrikethroughRule = Rule(symbol: tildeStrikethroughSymbol) + let equalUnderlineRule = Rule(symbol: equalUnderlineSymbol) + let linkTextRule = Rule(openingSymbol: linkTextOpeningSymbol, closingSymbol: linkTextClosingSymbol) + let linkURLRule = Rule(openingSymbol: linkURLOpeningSymbol, closingSymbol: linkURLClosingSymbol) + + let tokens = try TokenParser.parse(string, + using: [underscoreEmRule, + asteriskEmRule, + underscoreStrongRule, + asteriskStrongRule, + tildeStrikethroughRule, + equalUnderlineRule, + linkTextRule, + linkURLRule]) + + guard tokens.count > 0 else { + return (string, []) + } + + var strippedString = "" + var elements: [MarkdownElement] = [] + + var i = 0 + while i < tokens.count { + let token = tokens[i] + + // For `em`, `strong`, and other single token rules, + // it's just a matter of appending the content of matched token and storing the new range. + // But, for links, look for the square brackets and make sure that it's followed by parentheses directly. + // For everything else including parentheses by themseleves should be ignored. + switch token.rule { + case .some(underscoreEmRule), .some(asteriskEmRule): + let range = strippedString.append(contentOf: token) + elements.append(.em(range: range)) + case .some(underscoreStrongRule), .some(asteriskStrongRule): + let range = strippedString.append(contentOf: token) + elements.append(.strong(range: range)) + case .some(tildeStrikethroughRule): + let range = strippedString.append(contentOf: token) + elements.append(.strikethrough(range: range)) + case .some(equalUnderlineRule): + let range = strippedString.append(contentOf: token) + elements.append(.underline(range: range)) + case .some(linkTextRule): + guard i + 1 < tokens.count, tokens[i + 1].rule == linkURLRule else { + fallthrough + } + + let range = strippedString.append(contentOf: token) + elements.append(.link(range: range,urlString: tokens[i + 1].string)) + + i += 1 default: - throw ParserError.invalidTagSymbol + strippedString += token.stringWithRuleSymbols } + + i += 1 } - - let (strippedString, elements) = try ElementParser.parse(string, - for: [underscoreEmSymbol, - asteriskEmSymbol, - underscoreStrongSymbol, - asteriskStrongSymbol, - tildeStrikethroughSymbol, - equalUnderlineSymbol]) - return try (strippedString, elements.map(transformToMarkdownElement)) + + return (strippedString, elements) } } diff --git a/Marker/Marker/Parser/Rule.swift b/Marker/Marker/Parser/Rule.swift new file mode 100644 index 0000000..8c74b60 --- /dev/null +++ b/Marker/Marker/Parser/Rule.swift @@ -0,0 +1,59 @@ +// +// Rule.swift +// Marker +// +// Created by Htin Linn on 10/25/17. +// Copyright © 2017 Prolific Interactive. All rights reserved. +// + +import Foundation + +/// A parser "rule". Contains opening and closing symbols for matching. +struct Rule { + + /// A set of character(s) that an element starts with. + let openingSymbol: Symbol + + /// A set of character(s) that an element ends with. + let closingSymbol: Symbol + + /// Initializes a `Rule` with given symbols. + /// + /// - Parameters: + /// - openingSymbol: Opening symbol. + /// - closingSymbol: Closing symbol. + init(openingSymbol: Symbol, closingSymbol: Symbol) { + self.openingSymbol = openingSymbol + self.closingSymbol = closingSymbol + } + + /// Initialize a `Rule` with given symbol as both opening and ending symbols. + /// + /// - Parameter symbol: Symbol. + init(symbol: Symbol) { + self.init(openingSymbol: symbol, closingSymbol: symbol) + } + +} + +// MARK: - Protocol conformance + +// MARK: Equatable + +extension Rule: Equatable { + + static func ==(lhs: Rule, rhs: Rule) -> Bool { + return lhs.openingSymbol == rhs.openingSymbol && lhs.closingSymbol == rhs.closingSymbol + } + +} + +// MARK: Hashable + +extension Rule: Hashable { + + var hashValue: Int { + return openingSymbol.hashValue ^ closingSymbol.hashValue + } + +} diff --git a/Marker/Marker/Parser/String+Extensions.swift b/Marker/Marker/Parser/String+Extensions.swift new file mode 100644 index 0000000..6acdee5 --- /dev/null +++ b/Marker/Marker/Parser/String+Extensions.swift @@ -0,0 +1,29 @@ +// +// String+Extensions.swift +// Marker +// +// Created by Htin Linn on 10/27/17. +// Copyright © 2017 Prolific Interactive. All rights reserved. +// + +import Foundation + +/// Shorthand for `String.Index` used in parsing. +typealias Index = String.Index + +extension String { + + /// Appends the characters (without the rule symbols) from the given token and returns the range of appended `String`. + /// + /// - Parameter token: Token whose content should be appended. + /// - Returns: Range of appended `String`. + mutating func append(contentOf token: Token) -> Range { + let startIndex = self.endIndex + + self += token.string + let endIndex = self.endIndex + + return startIndex..) { // It's important that longer character symbols come before shorter character symbols // because of the way that parser matches characters. self.symbols = symbols.sorted { (lhs, rhs) in diff --git a/Marker/Marker/Parser/Token.swift b/Marker/Marker/Parser/Token.swift new file mode 100644 index 0000000..40da446 --- /dev/null +++ b/Marker/Marker/Parser/Token.swift @@ -0,0 +1,26 @@ +// +// Token.swift +// Marker +// +// Created by Htin Linn on 10/25/17. +// Copyright © 2017 Prolific Interactive. All rights reserved. +// + +import Foundation + +/// An entity that the token parser outputs that contains a `String` and a matching rule. +/// A token with an empty `rule` denotes subparts of the `String` that matches no rule. +struct Token { + + /// Content of the token. + let string: String + + /// Matching rule. + let rule: Rule? + + /// Content of the token with opening and closing symbols. + var stringWithRuleSymbols: String { + return (rule?.openingSymbol.rawValue ?? "") + string + (rule?.closingSymbol.rawValue ?? "") + } + +} diff --git a/Marker/Marker/Parser/TokenParser.swift b/Marker/Marker/Parser/TokenParser.swift new file mode 100644 index 0000000..7292e78 --- /dev/null +++ b/Marker/Marker/Parser/TokenParser.swift @@ -0,0 +1,142 @@ +// +// TokenParser.swift +// Marker +// +// Created by Htin Linn on 10/25/17. +// Copyright © 2017 Prolific Interactive. All rights reserved. +// + +import Foundation + +/// Tokenizer that breaks down a given string into tokens based on prescribed rules. +struct TokenParser { + + /// Parser error. + /// + /// - unclosedTags: A tag was left unclosed. + enum Error: LocalizedError { + + case unclosedTags + + var errorDescription: String? { + switch self { + case .unclosedTags: + return "A tag was left unclosed." + } + } + + } + + /// Parses given string based on specified rules and returns a list of tokens that match the rules. + /// + /// - Parameters: + /// - string: String to be parsed. + /// - rules: Rules + /// - Returns: Rules with symbols to parse for. + /// - Throws: Parser error. + static func parse(_ string: String, using rules: [Rule]) throws -> [Token] { + let parser = TagParser(symbols: Set(rules.flatMap{ [$0.openingSymbol, $0.closingSymbol] })) + let tags = parser.parse(string) + + guard tags.count > 0 else { + return [] + } + + var tokens: [Token] = [] + var lastTokenEndIndex: Index = string.startIndex + var currentTokenRule: Rule? = nil + var currentTokenOpeningTag: Tag? = nil + var tagsToEscape: [Tag] = [] + + // Checks if there is currently open token that hasn't been closed yet. + // i.e. if the opening and closing symbols are "(" and ")" respectively, only "(" has been matched. + func hasNoCurrentOpenToken() -> Bool { + return currentTokenRule == nil && currentTokenOpeningTag == nil + } + + // Add a text token (token with no rule) based on the end index of the last token. + func addTextTokenIfNeeded(_ index: Index) { + guard index > lastTokenEndIndex else { + return + } + + tokens.append(Token(string: escapedString(from: lastTokenEndIndex, to: index), rule: nil)) + } + + // Create a string that's clear of escaped characters based on the "tagsToEscape" that has been accumulated. + func escapedString(from startIndex: Index, to endIndex: Index) -> String { + var escapedString = "" + + var fromIndex = startIndex + for tag in tagsToEscape { + // Skip the index right before the tag's index here and exclude the "\". + escapedString += string[fromIndex.. string.startIndex && string[string.index(before: tag.index)] == "\\" { + tagsToEscape.append(tag) + continue + } + + if hasNoCurrentOpenToken() { + openNewToken() + } else { + closeCurrentToken() + } + } + + guard hasNoCurrentOpenToken() else { + throw Error.unclosedTags + } + + // Add the text between the last token and end of the string as a text token. + addTextTokenIfNeeded(string.endIndex) + + return tokens + } + +} + diff --git a/Marker/Marker/TextAttributes.swift b/Marker/Marker/TextAttributes.swift index d0e1e29..4977aed 100644 --- a/Marker/Marker/TextAttributes.swift +++ b/Marker/Marker/TextAttributes.swift @@ -16,11 +16,12 @@ /// Text attributes. public typealias TextAttributes = [NSAttributedStringKey: Any] - internal struct AttributedStringKey { + struct AttributedStringKey { static let font = NSAttributedStringKey.font static let foregroundColor = NSAttributedStringKey.foregroundColor static let kern = NSAttributedStringKey.kern + static let link = NSAttributedStringKey.link static let paragraphStyle = NSAttributedStringKey.paragraphStyle static let strikethroughStyle = NSAttributedStringKey.strikethroughStyle static let strikethroughColor = NSAttributedStringKey.strikethroughColor @@ -37,6 +38,7 @@ static let font = NSFontAttributeName static let foregroundColor = NSForegroundColorAttributeName static let kern = NSKernAttributeName + static let link = NSLinkAttributeName static let paragraphStyle = NSParagraphStyleAttributeName static let strikethroughStyle = NSStrikethroughStyleAttributeName static let strikethroughColor = NSStrikethroughColorAttributeName diff --git a/Marker/Marker/TextStyle+Extensions.swift b/Marker/Marker/TextStyle+Extensions.swift index 6c8f981..ecd29c4 100644 --- a/Marker/Marker/TextStyle+Extensions.swift +++ b/Marker/Marker/TextStyle+Extensions.swift @@ -58,6 +58,8 @@ public extension TextStyle { newStrikethroughColor: Color? = nil, newUnderlineStyle: NSUnderlineStyle? = nil, newUnderlineColor: Color? = nil, + newLinkFont: Font? = nil, + newLinkColor: Color? = nil, newTextTransform: TextTransform? = nil) -> TextStyle { let fontToUse = newFont ?? font let emFontToUse = newEmFont ?? emFont @@ -78,6 +80,8 @@ public extension TextStyle { let strikethroughColorToUse = newStrikethroughColor ?? strikethroughColor let underlineStyleToUse = newUnderlineStyle ?? underlineStyle let underlineColorToUse = newUnderlineColor ?? underlineColor + let linkFontToUse = newLinkFont ?? linkFont + let linkColorToUse = newLinkColor ?? linkColor let textTransformToUse = newTextTransform ?? textTransform return TextStyle( @@ -100,6 +104,8 @@ public extension TextStyle { strikethroughColor: strikethroughColorToUse, underlineStyle: underlineStyleToUse, underlineColor: underlineColorToUse, + linkFont: linkFontToUse, + linkColor: linkColorToUse, textTransform: textTransformToUse ) } @@ -157,6 +163,8 @@ public func ==(lhs: TextStyle, rhs: TextStyle) -> Bool { lhs.strikethroughColor == rhs.strikethroughColor, lhs.underlineStyle == rhs.underlineStyle, lhs.underlineColor == rhs.underlineColor, + lhs.linkFont == rhs.linkFont, + lhs.linkColor == rhs.linkColor, lhs.textTransform == rhs.textTransform else { return false } diff --git a/Marker/Marker/TextStyle.swift b/Marker/Marker/TextStyle.swift index 4dae316..fa9d97e 100644 --- a/Marker/Marker/TextStyle.swift +++ b/Marker/Marker/TextStyle.swift @@ -73,6 +73,12 @@ public struct TextStyle { /// Stroke color for underlined text. public var underlineColor: Color? + + /// Font for displaying links. + public var linkFont: Font? + + /// Text color for links. + public var linkColor: Color? /// Text transform. public var textTransform: TextTransform @@ -133,31 +139,32 @@ public struct TextStyle { } // MARK: - Init/Deinit - - /** - Initializes a text style object with given parameters. - - - parameter font: Regualar font. - - parameter emFont: Emphasis font. - - parameter strongFont: Strong font. - - parameter textColor: Text color. - - parameter characterSpacing: Character spacing (kerning). - - parameter lineSpacing: Line spacing. - - parameter lineHeightMultiple: Line height mulitple. - - parameter minimumLineHeight: Minimum line height. - - parameter maximumLineHeight: Maximum line height. - - parameter paragraphSpacing: Paragraph spacing. - - parameter paragraphSpacingBefore: Paragraph spacing before. - - parameter textAlignment: Text alignment. - - parameter lineBreakMode: Line break node. - - parameter strikethroughStyle: Strikethrough style. - - parameter strikethroughColor: Strikethrough color. - - parameter underlineStyle: Underline style. - - parameter underlineColor: Underline color. - - parameter textTransform: Text transform option. - - - returns: An initialized text style object. - */ + + /// Initializes and returns a text style object with given parameters. + /// + /// - Parameters: + /// - font: Regualar font. + /// - emFont: Emphasis font. + /// - strongFont: Strong font. + /// - textColor: Text color. + /// - characterSpacing: Character spacing (kerning). + /// - lineSpacing: Line spacing. + /// - lineHeightMultiple: Line height mulitple. + /// - minimumLineHeight: Minimum line height. + /// - maximumLineHeight: Maximum line height. + /// - firstLineHeadIndent: First line head indent. + /// - headIndent: Head indent. + /// - paragraphSpacing: Paragraph spacing. + /// - paragraphSpacingBefore: Paragraph spacing before. + /// - textAlignment: Text alignment. + /// - lineBreakMode: Line break mode. + /// - strikethroughStyle: Strikethrough style. + /// - strikethroughColor: Strikethrough color. + /// - underlineStyle: Underline style. + /// - underlineColor: Underline color. + /// - linkFont: Link style. + /// - linkColor: Link color. + /// - textTransform: Text transform option. public init(font: Font, emFont: Font? = nil, strongFont: Font? = nil, @@ -177,6 +184,8 @@ public struct TextStyle { strikethroughColor: Color? = nil, underlineStyle: NSUnderlineStyle? = nil, underlineColor: Color? = nil, + linkFont: Font? = nil, + linkColor: Color? = nil, textTransform: TextTransform = .none) { self.font = font self.emFont = (emFont == nil) ? font : emFont! @@ -197,6 +206,8 @@ public struct TextStyle { self.strikethroughColor = strikethroughColor self.underlineStyle = underlineStyle self.underlineColor = underlineColor + self.linkFont = linkFont + self.linkColor = linkColor self.textTransform = textTransform } } diff --git a/Marker/Marker/TextTransform.swift b/Marker/Marker/TextTransform.swift index 15751b1..1a441d8 100644 --- a/Marker/Marker/TextTransform.swift +++ b/Marker/Marker/TextTransform.swift @@ -27,7 +27,7 @@ public enum TextTransform { /// /// - Parameter string: String to be transformed. /// - Returns: Transformed string. - internal func applied(to string: String) -> String { + func applied(to string: String) -> String { switch self { case .none: return string diff --git a/Marker/Marker/Utility/Extensions/TextView/NSTextViewExtension.swift b/Marker/Marker/Utility/Extensions/TextView/NSTextViewExtension.swift index 484b4cd..8260e93 100644 --- a/Marker/Marker/Utility/Extensions/TextView/NSTextViewExtension.swift +++ b/Marker/Marker/Utility/Extensions/TextView/NSTextViewExtension.swift @@ -29,6 +29,10 @@ public extension NSTextView { /// - markdownText: The Markdown text to be displayed in the text view. /// - textStyle: Text style object containing style information. func setMarkdownText(_ markdownText: String, using textStyle: TextStyle) { + if let linkColor = textStyle.linkColor { + linkTextAttributes = [AttributedStringKey.foregroundColor: linkColor] + } + let attributedText = attributedMarkdownString(from: markdownText, using: textStyle) textStorage?.setAttributedString(attributedText) } diff --git a/Marker/Marker/Utility/Extensions/TextView/UITextViewExtension.swift b/Marker/Marker/Utility/Extensions/TextView/UITextViewExtension.swift index 77416b7..e996a53 100644 --- a/Marker/Marker/Utility/Extensions/TextView/UITextViewExtension.swift +++ b/Marker/Marker/Utility/Extensions/TextView/UITextViewExtension.swift @@ -28,6 +28,14 @@ public extension UITextView { /// - markdownText: The Markdown text to be displayed in the text view. /// - textStyle: Text style object containing style information. func setMarkdownText(_ markdownText: String, using textStyle: TextStyle) { + if let linkColor = textStyle.linkColor { + #if swift(>=4.0) + linkTextAttributes = [AttributedStringKey.foregroundColor.rawValue: linkColor] + #else + linkTextAttributes = [AttributedStringKey.foregroundColor: linkColor] + #endif + } + attributedText = attributedMarkdownString(from: markdownText, using: textStyle) } diff --git a/Marker/MarkerTests/ElementParserTests.swift b/Marker/MarkerTests/ElementParserTests.swift new file mode 100644 index 0000000..e995bb6 --- /dev/null +++ b/Marker/MarkerTests/ElementParserTests.swift @@ -0,0 +1,105 @@ +// +// ElementParserTests.swift +// Marker +// +// Created by Htin Linn on 10/31/17. +// Copyright © 2017 Prolific Interactive. All rights reserved. +// + +import XCTest +@testable import Marker + +class ElementParserTests: XCTestCase { + + override func setUp() { + super.setUp() + } + + override func tearDown() { + super.tearDown() + } + + func testParseMatchingSymbolElements() { + do { + let poundRule = Rule(symbol: Symbol(character: "#")) + let dollarRule = Rule(symbol: Symbol(character: "$")) + + let (parsedString, parsedElements) = try ElementParser.parse("#abc# def $ghi$ jkl $mno$", + using: [poundRule, dollarRule]) + + XCTAssert(parsedString == "abc def ghi jkl mno") + XCTAssert(parsedElements.count == 3) + + XCTAssert(parsedElements[0].rule == poundRule) + XCTAssert(parsedElements[0].range == parsedString.range(of: "abc")) + + XCTAssert(parsedElements[1].rule == dollarRule) + XCTAssert(parsedElements[1].range == parsedString.range(of: "ghi")) + + XCTAssert(parsedElements[2].rule == dollarRule) + XCTAssert(parsedElements[2].range == parsedString.range(of: "mno")) + } catch { + XCTFail("Parsing failed.") + } + } + + func testParseNonMatchingSymbolElements() { + do { + let curlyBracketRule = Rule(openingSymbol: Symbol(character: "{"), closingSymbol: Symbol(character: "}")) + let angleBracketRule = Rule(openingSymbol: Symbol(character: "<"), closingSymbol: Symbol(character: ">")) + + let (parsedString, parsedElements) = try ElementParser.parse("Here is a and a {block}.", + using: [curlyBracketRule, angleBracketRule]) + + XCTAssert(parsedString == "Here is a tag and a block.") + XCTAssert(parsedElements.count == 2) + + XCTAssert(parsedElements[0].rule == angleBracketRule) + XCTAssert(parsedElements[0].range == parsedString.range(of: "tag")) + + XCTAssert(parsedElements[1].rule == curlyBracketRule) + XCTAssert(parsedElements[1].range == parsedString.range(of: "block")) + } catch { + XCTFail("Parsing failed.") + } + } + + func testThatOtherMatchingSymbolsAreNotParsed() { + do { + let poundRule = Rule(symbol: Symbol(character: "#")) + + let (parsedString, parsedElements) = try ElementParser.parse("#abc# def $ghi$ _jkl_ *mno*", + using: [poundRule]) + + XCTAssert(parsedString == "abc def $ghi$ _jkl_ *mno*") + XCTAssert(parsedElements.count == 1) + + XCTAssert(parsedElements[0].rule == poundRule) + XCTAssert(parsedElements[0].range == parsedString.range(of: "abc")) + } catch { + XCTFail("Parsing failed.") + } + } + + func testThatRuleSymbolsCanBeEscaped() { + do { + let poundRule = Rule(symbol: Symbol(character: "#")) + let curlyBracketRule = Rule(openingSymbol: Symbol(character: "{"), closingSymbol: Symbol(character: "}")) + + let (parsedString, parsedElements) = try ElementParser.parse("#abc# {def} ghi \\#jkl\\# \\{mno\\}", + using: [poundRule, curlyBracketRule]) + + XCTAssert(parsedString == "abc def ghi #jkl# {mno}") + XCTAssert(parsedElements.count == 2) + + XCTAssert(parsedElements[0].rule == poundRule) + XCTAssert(parsedElements[0].range == parsedString.range(of: "abc")) + + XCTAssert(parsedElements[1].rule == curlyBracketRule) + XCTAssert(parsedElements[1].range == parsedString.range(of: "def")) + } catch { + XCTFail("Parsing failed.") + } + } + +} diff --git a/Marker/MarkerTests/ParserTests.swift b/Marker/MarkerTests/MarkdownParserTests.swift similarity index 59% rename from Marker/MarkerTests/ParserTests.swift rename to Marker/MarkerTests/MarkdownParserTests.swift index 24d3df1..9c33ca9 100644 --- a/Marker/MarkerTests/ParserTests.swift +++ b/Marker/MarkerTests/MarkdownParserTests.swift @@ -1,5 +1,5 @@ // -// ParserTests.swift +// MarkdownParserTests.swift // Marker // // Created by Htin Linn on 7/7/17. @@ -9,7 +9,7 @@ import XCTest @testable import Marker -class ParserTests: XCTestCase { +class MarkdownParserTests: XCTestCase { override func setUp() { super.setUp() @@ -82,13 +82,29 @@ class ParserTests: XCTestCase { XCTFail("Parsing failed.") } } + + func testParseLinkElements() { + do { + let (parsedString, parsedElements) = try MarkdownParser.parse("[abc](https://example.com) def") + + XCTAssert(parsedString == "abc def") + XCTAssert(parsedElements.count == 1) + + XCTAssert(parsedElements[0].isLinkElement()) + XCTAssert(parsedElements[0].linkURLString() == "https://example.com") + + XCTAssert(parsedElements[0].range == parsedString.range(of: "abc")) + } catch { + XCTFail("Parsing failed.") + } + } func testParseMixedElements() { do { - let (parsedString, parsedElements) = try MarkdownParser.parse("*abc* __def__ _ghi_ **jkl** ~~mno~~ ==pqr==") + let (parsedString, parsedElements) = try MarkdownParser.parse("*abc* __def__ _ghi_ **jkl** ~~mno~~ ==pqr== [stu](https://vw.com)") - XCTAssert(parsedString == "abc def ghi jkl mno pqr") - XCTAssert(parsedElements.count == 6) + XCTAssert(parsedString == "abc def ghi jkl mno pqr stu") + XCTAssert(parsedElements.count == 7) XCTAssert(parsedElements[0].isEmElement()) XCTAssert(parsedElements[1].isStrongElement()) @@ -96,6 +112,8 @@ class ParserTests: XCTestCase { XCTAssert(parsedElements[3].isStrongElement()) XCTAssert(parsedElements[4].isStrikethroughElement()) XCTAssert(parsedElements[5].isUnderlineElement()) + XCTAssert(parsedElements[6].isLinkElement()) + XCTAssert(parsedElements[6].linkURLString() == "https://vw.com") XCTAssert(parsedElements[0].range == parsedString.range(of: "abc")) XCTAssert(parsedElements[1].range == parsedString.range(of: "def")) @@ -103,6 +121,7 @@ class ParserTests: XCTestCase { XCTAssert(parsedElements[3].range == parsedString.range(of: "jkl")) XCTAssert(parsedElements[4].range == parsedString.range(of: "mno")) XCTAssert(parsedElements[5].range == parsedString.range(of: "pqr")) + XCTAssert(parsedElements[6].range == parsedString.range(of: "stu")) } catch { XCTFail("Parsing failed.") } @@ -151,7 +170,18 @@ class ParserTests: XCTestCase { XCTFail("Parsing failed.") } } - + + func testEscapeCharacters() { + do { + let (parsedString, parsedElements) = try MarkdownParser.parse("\\*em\\* and \\__strong\\__ escaped.") + + XCTAssert(parsedString == "*em* and __strong__ escaped.") + XCTAssert(parsedElements.count == 0) + } catch { + XCTFail("Parsing failed.") + } + } + func testElementsInMiddleOfWords() { do { let (parsedString, parsedElements) = try MarkdownParser.parse("the_quick_brown*fox*jumps__over__the**lazy**==dog==.") @@ -174,38 +204,113 @@ class ParserTests: XCTestCase { XCTFail("Parsing failed.") } } - - func testThatMismatchedTagsThrowAnError() { + + func testThatNonLinkSquareBracketsAreAllowed() { do { - let _ = try MarkdownParser.parse("This _won't__ work because the tags don't match.") + let (parsedString, parsedElements) = try MarkdownParser.parse("[self dealloc];") + + XCTAssert(parsedString == "[self dealloc];") + XCTAssert(parsedElements.count == 0) } catch { - XCTAssert(error as! ElementParser.ParserError == ElementParser.ParserError.tagMismatch) + XCTFail("Parsing failed.") } - + } + + func testThatNonLinkParenthesesAreAllowed() { do { - let _ = try MarkdownParser.parse("Neither **should* this.") + let (parsedString, parsedElements) = try MarkdownParser.parse("there can be one (or more) parentheses(s).") + + XCTAssert(parsedString == "there can be one (or more) parentheses(s).") + XCTAssert(parsedElements.count == 0) } catch { - XCTAssert(error as! ElementParser.ParserError == ElementParser.ParserError.tagMismatch) + XCTFail("Parsing failed.") } } - + + func testThatSquareBracketsNeedToBeRightBeforeParenthesesForLinks() { + do { + let (parsedString, parsedElements) = try MarkdownParser.parse("This is a [link](https://example.com).") + + XCTAssert(parsedString == "This is a link.") + XCTAssert(parsedElements.count == 1) + + XCTAssert(parsedElements[0].isLinkElement()) + XCTAssert(parsedElements[0].linkURLString() == "https://example.com") + } catch { + XCTFail("Parsing failed.") + } + + do { + let (parsedString, parsedElements) = try MarkdownParser.parse("This is not a [link] (https://example.com).") + + XCTAssert(parsedString == "This is not a [link] (https://example.com).") + XCTAssert(parsedElements.count == 0) + } catch { + XCTFail("Parsing failed.") + } + } + + func testThatMultipleLinksAreAllowed() { + do { + let string = """ + [Link A](https://example.com/a), + [Link B](https://example.com/b) + """ + let (parsedString, parsedElements) = try MarkdownParser.parse(string) + + XCTAssert(parsedString == "Link A,\nLink B") + XCTAssert(parsedElements.count == 2) + + XCTAssert(parsedElements[0].linkURLString() == "https://example.com/a") + XCTAssert(parsedElements[1].linkURLString() == "https://example.com/b") + } catch { + XCTFail("Parsing failed.") + } + } + + func testThatCharactersWithInURLsCanBeEscaped() { + do { + let (parsedString, parsedElements) = try MarkdownParser.parse("[Link](https://en.wikipedia.org/wiki/Link_(The_Legend_of_Zelda\\))") + + XCTAssert(parsedString == "Link") + XCTAssert(parsedElements.count == 1) + + XCTAssert(parsedElements[0].isLinkElement()) + XCTAssert(parsedElements[0].linkURLString() == "https://en.wikipedia.org/wiki/Link_(The_Legend_of_Zelda)") + } catch { + XCTFail("Parsing failed.") + } + } + func testThatUnclosedTagsThrowAnError() { do { let _ = try MarkdownParser.parse("Please *don't _do_ this.") } catch { - XCTAssert(error as! ElementParser.ParserError == ElementParser.ParserError.unclosedTags) + XCTAssert(error as! TokenParser.Error == TokenParser.Error.unclosedTags) } do { let _ = try MarkdownParser.parse("Finish this __sentenc") } catch { - XCTAssert(error as! ElementParser.ParserError == ElementParser.ParserError.unclosedTags) + XCTAssert(error as! TokenParser.Error == TokenParser.Error.unclosedTags) } do { let _ = try MarkdownParser.parse("Not ==correct.") } catch { - XCTAssert(error as! ElementParser.ParserError == ElementParser.ParserError.unclosedTags) + XCTAssert(error as! TokenParser.Error == TokenParser.Error.unclosedTags) + } + + do { + let _ = try MarkdownParser.parse("This _won't__ work because the tags don't match.") + } catch { + XCTAssert(error as! TokenParser.Error == TokenParser.Error.unclosedTags) + } + + do { + let _ = try MarkdownParser.parse("Neither **should* this.") + } catch { + XCTAssert(error as! TokenParser.Error == TokenParser.Error.unclosedTags) } } @@ -215,7 +320,7 @@ private extension MarkdownElement { func isEmElement() -> Bool { switch self { - case .em(_): + case .em: return true default: return false @@ -224,7 +329,7 @@ private extension MarkdownElement { func isStrongElement() -> Bool { switch self { - case .strong(_): + case .strong: return true default: return false @@ -233,7 +338,7 @@ private extension MarkdownElement { func isStrikethroughElement() -> Bool { switch self { - case .strikethrough(_): + case .strikethrough: return true default: return false @@ -242,11 +347,29 @@ private extension MarkdownElement { func isUnderlineElement() -> Bool { switch self { - case .underline(_): + case .underline: return true default: return false } } - + + func isLinkElement() -> Bool { + switch self { + case .link: + return true + default: + return false + } + } + + func linkURLString() -> String? { + switch self { + case .link(_, let urlString): + return urlString + default: + return nil + } + } + } diff --git a/Marker/MarkerTests/TextStyleEquatableTests.swift b/Marker/MarkerTests/TextStyleEquatableTests.swift index d8a0a1f..f0ade16 100644 --- a/Marker/MarkerTests/TextStyleEquatableTests.swift +++ b/Marker/MarkerTests/TextStyleEquatableTests.swift @@ -41,6 +41,8 @@ class TextStyleEquatableTests: XCTestCase { strikethroughColor: Color.red, underlineStyle: .styleSingle, underlineColor: Color.red, + linkFont: Font(name: "Helvetica-Bold", size: 10)!, + linkColor: Color.blue, textTransform: .lowercased) } @@ -70,6 +72,8 @@ class TextStyleEquatableTests: XCTestCase { strikethroughColor: textStyle.strikethroughColor, underlineStyle: textStyle.underlineStyle, underlineColor: textStyle.underlineColor, + linkFont: textStyle.linkFont, + linkColor: textStyle.linkColor, textTransform: textStyle.textTransform) XCTAssertEqual(textStyle, sameTextStyle) @@ -95,6 +99,8 @@ class TextStyleEquatableTests: XCTestCase { let differentStrikethroughColor = textStyle.with(newStrikethroughColor: Color.blue) let differentUnderlineStyle = textStyle.with(newUnderlineStyle: .styleDouble) let differentUnderlineColor = textStyle.with(newUnderlineColor: Color.blue) + let differentLinkFont = textStyle.with(newLinkFont: Font(name: textStyle.linkFont!.fontName, size: textStyle.linkFont!.pointSize + 10)!) + let differentLinkColor = textStyle.with(newLinkColor: Color.red) let differentTextTransform = textStyle.with(newTextTransform: .uppercased) @@ -117,6 +123,8 @@ class TextStyleEquatableTests: XCTestCase { XCTAssertNotEqual(textStyle, differentStrikethroughColor) XCTAssertNotEqual(textStyle, differentUnderlineStyle) XCTAssertNotEqual(textStyle, differentUnderlineColor) + XCTAssertNotEqual(textStyle, differentLinkFont) + XCTAssertNotEqual(textStyle, differentLinkColor) XCTAssertNotEqual(textStyle, differentTextTransform) } diff --git a/Marker/MarkerTests/TextStyleFactoryFunctionTests.swift b/Marker/MarkerTests/TextStyleFactoryFunctionTests.swift index 30e61fb..50bcca5 100644 --- a/Marker/MarkerTests/TextStyleFactoryFunctionTests.swift +++ b/Marker/MarkerTests/TextStyleFactoryFunctionTests.swift @@ -37,6 +37,12 @@ class TextStyleFactoryFunctionTests: XCTestCase { paragraphSpacingBefore: 9, textAlignment: .left, lineBreakMode: .byWordWrapping, + strikethroughStyle: .styleSingle, + strikethroughColor: Color.red, + underlineStyle: .styleSingle, + underlineColor: Color.red, + linkFont: Font(name: "Helvetica-Bold", size: 10)!, + linkColor: Color.blue, textTransform: .lowercased) } @@ -68,6 +74,8 @@ class TextStyleFactoryFunctionTests: XCTestCase { strikethroughColor: textStyle.strikethroughColor, underlineStyle: textStyle.underlineStyle, underlineColor: textStyle.underlineColor, + linkFont: textStyle.linkFont, + linkColor: textStyle.linkColor, textTransform: textStyle.textTransform) XCTAssertEqual(newTextStyle, expectedTextStyle) @@ -95,6 +103,8 @@ class TextStyleFactoryFunctionTests: XCTestCase { strikethroughColor: textStyle.strikethroughColor, underlineStyle: textStyle.underlineStyle, underlineColor: textStyle.underlineColor, + linkFont: textStyle.linkFont, + linkColor: textStyle.linkColor, textTransform: textStyle.textTransform) XCTAssertEqual(newTextStyle, expectedTextStyle) @@ -122,6 +132,8 @@ class TextStyleFactoryFunctionTests: XCTestCase { strikethroughColor: textStyle.strikethroughColor, underlineStyle: textStyle.underlineStyle, underlineColor: textStyle.underlineColor, + linkFont: textStyle.linkFont, + linkColor: textStyle.linkColor, textTransform: textStyle.textTransform) XCTAssertEqual(newTextStyle, expectedTextStyle) @@ -149,6 +161,8 @@ class TextStyleFactoryFunctionTests: XCTestCase { strikethroughColor: textStyle.strikethroughColor, underlineStyle: textStyle.underlineStyle, underlineColor: textStyle.underlineColor, + linkFont: textStyle.linkFont, + linkColor: textStyle.linkColor, textTransform: textStyle.textTransform) XCTAssertEqual(newTextStyle, expectedTextStyle) @@ -176,6 +190,8 @@ class TextStyleFactoryFunctionTests: XCTestCase { strikethroughColor: textStyle.strikethroughColor, underlineStyle: textStyle.underlineStyle, underlineColor: textStyle.underlineColor, + linkFont: textStyle.linkFont, + linkColor: textStyle.linkColor, textTransform: textStyle.textTransform) XCTAssertEqual(newTextStyle, expectedTextStyle) @@ -203,6 +219,8 @@ class TextStyleFactoryFunctionTests: XCTestCase { strikethroughColor: textStyle.strikethroughColor, underlineStyle: textStyle.underlineStyle, underlineColor: textStyle.underlineColor, + linkFont: textStyle.linkFont, + linkColor: textStyle.linkColor, textTransform: textStyle.textTransform) XCTAssertEqual(newTextStyle, expectedTextStyle) @@ -230,6 +248,8 @@ class TextStyleFactoryFunctionTests: XCTestCase { strikethroughColor: textStyle.strikethroughColor, underlineStyle: textStyle.underlineStyle, underlineColor: textStyle.underlineColor, + linkFont: textStyle.linkFont, + linkColor: textStyle.linkColor, textTransform: textStyle.textTransform) XCTAssertEqual(newTextStyle, expectedTextStyle) @@ -257,6 +277,8 @@ class TextStyleFactoryFunctionTests: XCTestCase { strikethroughColor: textStyle.strikethroughColor, underlineStyle: textStyle.underlineStyle, underlineColor: textStyle.underlineColor, + linkFont: textStyle.linkFont, + linkColor: textStyle.linkColor, textTransform: textStyle.textTransform) XCTAssertEqual(newTextStyle, expectedTextStyle) @@ -284,6 +306,8 @@ class TextStyleFactoryFunctionTests: XCTestCase { strikethroughColor: textStyle.strikethroughColor, underlineStyle: textStyle.underlineStyle, underlineColor: textStyle.underlineColor, + linkFont: textStyle.linkFont, + linkColor: textStyle.linkColor, textTransform: textStyle.textTransform) XCTAssertEqual(newTextStyle, expectedTextStyle) @@ -311,6 +335,8 @@ class TextStyleFactoryFunctionTests: XCTestCase { strikethroughColor: textStyle.strikethroughColor, underlineStyle: textStyle.underlineStyle, underlineColor: textStyle.underlineColor, + linkFont: textStyle.linkFont, + linkColor: textStyle.linkColor, textTransform: textStyle.textTransform) XCTAssertEqual(newTextStyle, expectedTextStyle) @@ -338,6 +364,8 @@ class TextStyleFactoryFunctionTests: XCTestCase { strikethroughColor: textStyle.strikethroughColor, underlineStyle: textStyle.underlineStyle, underlineColor: textStyle.underlineColor, + linkFont: textStyle.linkFont, + linkColor: textStyle.linkColor, textTransform: textStyle.textTransform) XCTAssertEqual(newTextStyle, expectedTextStyle) @@ -365,6 +393,8 @@ class TextStyleFactoryFunctionTests: XCTestCase { strikethroughColor: textStyle.strikethroughColor, underlineStyle: textStyle.underlineStyle, underlineColor: textStyle.underlineColor, + linkFont: textStyle.linkFont, + linkColor: textStyle.linkColor, textTransform: textStyle.textTransform) XCTAssertEqual(newTextStyle, expectedTextStyle) @@ -392,6 +422,8 @@ class TextStyleFactoryFunctionTests: XCTestCase { strikethroughColor: textStyle.strikethroughColor, underlineStyle: textStyle.underlineStyle, underlineColor: textStyle.underlineColor, + linkFont: textStyle.linkFont, + linkColor: textStyle.linkColor, textTransform: textStyle.textTransform) XCTAssertEqual(newTextStyle, expectedTextStyle) @@ -419,6 +451,8 @@ class TextStyleFactoryFunctionTests: XCTestCase { strikethroughColor: textStyle.strikethroughColor, underlineStyle: textStyle.underlineStyle, underlineColor: textStyle.underlineColor, + linkFont: textStyle.linkFont, + linkColor: textStyle.linkColor, textTransform: textStyle.textTransform) XCTAssertEqual(newTextStyle, expectedTextStyle) @@ -446,6 +480,8 @@ class TextStyleFactoryFunctionTests: XCTestCase { strikethroughColor: textStyle.strikethroughColor, underlineStyle: textStyle.underlineStyle, underlineColor: textStyle.underlineColor, + linkFont: textStyle.linkFont, + linkColor: textStyle.linkColor, textTransform: textStyle.textTransform) XCTAssertEqual(newTextStyle, expectedTextStyle) @@ -473,6 +509,8 @@ class TextStyleFactoryFunctionTests: XCTestCase { strikethroughColor: textStyle.strikethroughColor, underlineStyle: textStyle.underlineStyle, underlineColor: textStyle.underlineColor, + linkFont: textStyle.linkFont, + linkColor: textStyle.linkColor, textTransform: textStyle.textTransform) XCTAssertEqual(newTextStyle, expectedTextStyle) @@ -500,6 +538,8 @@ class TextStyleFactoryFunctionTests: XCTestCase { strikethroughColor: newStrikethroughColor, underlineStyle: textStyle.underlineStyle, underlineColor: textStyle.underlineColor, + linkFont: textStyle.linkFont, + linkColor: textStyle.linkColor, textTransform: textStyle.textTransform) XCTAssertEqual(newTextStyle, expectedTextStyle) @@ -527,6 +567,8 @@ class TextStyleFactoryFunctionTests: XCTestCase { strikethroughColor: textStyle.strikethroughColor, underlineStyle: newUnderlineStyle, underlineColor: textStyle.underlineColor, + linkFont: textStyle.linkFont, + linkColor: textStyle.linkColor, textTransform: textStyle.textTransform) XCTAssertEqual(newTextStyle, expectedTextStyle) @@ -554,10 +596,71 @@ class TextStyleFactoryFunctionTests: XCTestCase { strikethroughColor: textStyle.strikethroughColor, underlineStyle: textStyle.underlineStyle, underlineColor: newUnderlineColor, + linkFont: textStyle.linkFont, + linkColor: textStyle.linkColor, textTransform: textStyle.textTransform) XCTAssertEqual(newTextStyle, expectedTextStyle) } + + + func testTextStyleFactory_whenNewLinkFont_newLinkFontIsUsed() { + let newLinkFont = Font(name: textStyle.linkFont!.fontName, size: textStyle.linkFont!.pointSize + 10)! + let newTextStyle = textStyle.with(newLinkFont: newLinkFont) + let expectedTextStyle = TextStyle(font: textStyle.font, + emFont: textStyle.emFont, + strongFont: textStyle.strongFont, + textColor: textStyle.textColor, + characterSpacing: textStyle.characterSpacing, + lineSpacing: textStyle.lineSpacing, + lineHeightMultiple: textStyle.lineHeightMultiple, + minimumLineHeight: textStyle.minimumLineHeight, + maximumLineHeight: textStyle.maximumLineHeight, + firstLineHeadIndent: textStyle.firstLineHeadIndent, + headIndent: textStyle.headIndent, + paragraphSpacing: textStyle.paragraphSpacing, + paragraphSpacingBefore: textStyle.paragraphSpacingBefore, + textAlignment: textStyle.textAlignment, + lineBreakMode: textStyle.lineBreakMode, + strikethroughStyle: textStyle.strikethroughStyle, + strikethroughColor: textStyle.strikethroughColor, + underlineStyle: textStyle.underlineStyle, + underlineColor: textStyle.underlineColor, + linkFont: newLinkFont, + linkColor: textStyle.linkColor, + textTransform: textStyle.textTransform) + + XCTAssertEqual(newTextStyle, expectedTextStyle) + } + + func testTextStyleFactory_whenNewLinkColor_NewLinkColorIsUsed() { + let newLinkColor = Color.red + let newTextStyle = textStyle.with(newLinkColor: newLinkColor) + let expectedTextStyle = TextStyle(font: textStyle.font, + emFont: textStyle.emFont, + strongFont: textStyle.strongFont, + textColor: textStyle.textColor, + characterSpacing: textStyle.characterSpacing, + lineSpacing: textStyle.lineSpacing, + lineHeightMultiple: textStyle.lineHeightMultiple, + minimumLineHeight: textStyle.minimumLineHeight, + maximumLineHeight: textStyle.maximumLineHeight, + firstLineHeadIndent: textStyle.firstLineHeadIndent, + headIndent: textStyle.headIndent, + paragraphSpacing: textStyle.paragraphSpacing, + paragraphSpacingBefore: textStyle.paragraphSpacingBefore, + textAlignment: textStyle.textAlignment, + lineBreakMode: textStyle.lineBreakMode, + strikethroughStyle: textStyle.strikethroughStyle, + strikethroughColor: textStyle.strikethroughColor, + underlineStyle: textStyle.underlineStyle, + underlineColor: textStyle.underlineColor, + linkFont: textStyle.linkFont, + linkColor: newLinkColor, + textTransform: textStyle.textTransform) + + XCTAssertEqual(newTextStyle, expectedTextStyle) + } func testTextStyleFactory_whenNewTextTransform_newTextTransformIsUsed() { let newTextTransform: TextTransform = .uppercased @@ -581,6 +684,8 @@ class TextStyleFactoryFunctionTests: XCTestCase { strikethroughColor: textStyle.strikethroughColor, underlineStyle: textStyle.underlineStyle, underlineColor: textStyle.underlineColor, + linkFont: textStyle.linkFont, + linkColor: textStyle.linkColor, textTransform: newTextTransform) XCTAssertEqual(newTextStyle, expectedTextStyle) @@ -609,6 +714,8 @@ class TextStyleFactoryFunctionTests: XCTestCase { strikethroughColor: strikethroughColor, underlineStyle: textStyle.underlineStyle, underlineColor: textStyle.underlineColor, + linkFont: textStyle.linkFont, + linkColor: textStyle.linkColor, textTransform: textStyle.textTransform) XCTAssertEqual(newTextStyle, expectedTextStyle) @@ -637,6 +744,8 @@ class TextStyleFactoryFunctionTests: XCTestCase { strikethroughColor: expectedColor, underlineStyle: textStyle.underlineStyle, underlineColor: textStyle.underlineColor, + linkFont: textStyle.linkFont, + linkColor: textStyle.linkColor, textTransform: textStyle.textTransform) XCTAssertEqual(newTextStyle, expectedTextStyle) @@ -665,6 +774,8 @@ class TextStyleFactoryFunctionTests: XCTestCase { strikethroughColor: textStyle.strikethroughColor, underlineStyle: underlineStyle, underlineColor: underlineColor, + linkFont: textStyle.linkFont, + linkColor: textStyle.linkColor, textTransform: textStyle.textTransform) XCTAssertEqual(newTextStyle, expectedTextStyle) @@ -693,6 +804,8 @@ class TextStyleFactoryFunctionTests: XCTestCase { strikethroughColor: textStyle.strikethroughColor, underlineStyle: expectedStyle, underlineColor: expectedColor, + linkFont: textStyle.linkFont, + linkColor: textStyle.linkColor, textTransform: textStyle.textTransform) XCTAssertEqual(newTextStyle, expectedTextStyle) diff --git a/README.md b/README.md index f4c9c81..7a6c9be 100644 --- a/README.md +++ b/README.md @@ -95,18 +95,36 @@ Marker also supports setting text with common Markdown tags: * Bold (`__` or `**`) * Italic (`_` or `*`) +* Links (`[]()`) As well as convenient Markdown tags specific to Marker: * Strikethrough (`~~`) * Underline (`==`) -To set Markdown text on these elements, use `setMarkdownText(_:using:)` (or `setMarkdownTitleText(_:using:)` for `UIButton`) function. +To set Markdown text on these elements, use `setMarkdownText(_:using:)` (or `setMarkdownTitleText(_:using:)` for buttons) function. **NOTE**: Setting Markdown links work only in text views whereas other Markdown tags can be applied to any UI element with aforementioned `setMarkdownText(_using:)` function. ```swift textField.setMarkdownText("_Hello World_", using: headlineTextStyle) ``` +#### Backslash Escaping + +Both Markdown and custom markup functions support backslash escaping for generating literal characters which are otherwise reserved for mark up purposes. For instance, URLs with ")" character in them would not be parsed correctly without backslash escapes. + +``` +[Wiki](https://en.wikipedia.org/wiki/Wiki_(disambiguation)) +``` + +By default, the parser produces `https://en.wikipedia.org/wiki/Wiki_(disambiguation` as the URL for the above link. + +``` +[Wiki](https://en.wikipedia.org/wiki/Wiki_(disambiguation\)) +``` + +By backslash escaping the ")" character, the parser will treat it as a literal and produce the correct URL, `https://en.wikipedia.org/wiki/Wiki_(disambiguation))`. + + #### Best Practices Parsing operation for custom markup and Markdown strings can be too slow to use for performance-critical views such as `UITableViewCell`. In these cases, it's recommended to cache and reuse `NSAttributedString`'s returned from `Marker` functions instead of calling either `setText(_:using:customMarkup:)` or `setMarkdownText(_:using:)` directly.