diff --git a/README.md b/README.md index c8cbc20..e77a3ad 100644 --- a/README.md +++ b/README.md @@ -53,7 +53,8 @@ Emphasis _Em_ **Strong** __Strong__ - +***Strong Emphasis*** +___Strong Emphasis___ ```` # Requirements diff --git a/TSMarkdownParser.podspec b/TSMarkdownParser.podspec index c6b6f80..4c6806f 100644 --- a/TSMarkdownParser.podspec +++ b/TSMarkdownParser.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = "TSMarkdownParser" - s.version = "2.1.5" + s.version = "2.2.0" s.summary = "A markdown to NSAttributedString parser for iOS and OSX" s.description = <<-DESC @@ -17,7 +17,7 @@ Pod::Spec.new do |s| s.tvos.deployment_target = "9.0" s.osx.deployment_target = "10.7" s.source = { :git => "https://github.com/laptobbe/TSMarkdownParser.git", :tag => s.version.to_s } - s.source_files = "TSMarkdownParser/**/*.{h,m}" + s.source_files = "TSMarkdownParser/**/*.{h,m,swift}" s.requires_arc = true s.ios.framework = 'UIKit' s.tvos.framework = 'UIKit' diff --git a/TSMarkdownParser/NSAttributedString+Markdown.swift b/TSMarkdownParser/NSAttributedString+Markdown.swift new file mode 100644 index 0000000..2f82640 --- /dev/null +++ b/TSMarkdownParser/NSAttributedString+Markdown.swift @@ -0,0 +1,160 @@ +// +// NSAttributedString+Markdown.swift +// TSMarkdownParserLib +// +// Created by Stephan Heilner on 10/30/17. +// Copyright © 2017 Computertalk Sweden. All rights reserved. +// + +#if !os(OSX) + +import UIKit +import Foundation + +public extension NSAttributedString { + + public func markdownString() -> String { + let bulletCharacter = Character("\u{2022}") + let nonBreakingSpaceCharacter = Character("\u{00A0}") + + var markdownString = "" + + enum FormattingChange { + case enable + case disable + case keep + + static func getFormattingChange(_ before: Bool, after: Bool) -> FormattingChange { + if !before && after { return .enable } + if before && !after { return .disable } + return .keep + } + } + + var stringHasBoldEnabled = false + var stringHasItalicEnabled = false + var closingString = "" + var characterOnBulletedListLine = false + var openedNumberedListStarter = false + var characterOnNumberedListLine = false + var numberedListIsFirstLine = false + var previousCharacter: Character? + + enumerateAttributes(in: NSRange(location: 0, length: length), options: []) { attributes, range, shouldStop in + if let traits = (attributes[NSAttributedStringKey.font] as? UIFont)?.fontDescriptor.symbolicTraits { + let boldChange = FormattingChange.getFormattingChange(stringHasBoldEnabled, after: traits.contains(.traitBold)) + let italicChange = FormattingChange.getFormattingChange(stringHasItalicEnabled, after: traits.contains(.traitItalic)) + var formatString = "" + switch boldChange { + case .enable: + formatString += "**" + closingString = "**\(closingString)" + case .disable: + if stringHasItalicEnabled && italicChange == .keep { + formatString += "_**_" + closingString = "_" + } else { + formatString += "**" + closingString = "" + } + case .keep: + break + } + + switch italicChange { + case .enable: + formatString += "_" + closingString = "_\(closingString)" + case .disable: + if stringHasBoldEnabled && boldChange == .keep { + formatString = "**_**\(formatString)" + closingString = "**" + } else { + formatString = "_\(formatString)" + closingString = "" + } + case .keep: + break + } + + markdownString += formatString + + stringHasBoldEnabled = traits.contains(.traitBold) + stringHasItalicEnabled = traits.contains(.traitItalic) + } + + let preprocessedString = (self.string as NSString).substring(with: range) + let processedString = preprocessedString.characters.reduce("") { resultString, character in + var stringToAppend = "" + + switch character { + case "\\", "`", "*", "_", "{", "}", "[", "]", "(", ")", "#", "+", "-", "!": + stringToAppend = "\\\(character)" + case "\n", "\u{2028}": + stringToAppend = "\(closingString)\(character)" + if !characterOnBulletedListLine && !characterOnNumberedListLine { + stringToAppend += String(closingString.characters.reversed()) + } + + characterOnBulletedListLine = false + characterOnNumberedListLine = false + case "1", "2", "3", "4", "5", "6", "7", "8", "9", "0": + if previousCharacter == "\n" || previousCharacter == nil || previousCharacter == nonBreakingSpaceCharacter { + openedNumberedListStarter = true + } + + numberedListIsFirstLine = previousCharacter == nil ? true : numberedListIsFirstLine + stringToAppend = "\(character)" + case bulletCharacter: + characterOnBulletedListLine = true + stringToAppend = "+ \(previousCharacter != nil ? String(closingString.characters.reversed()) : markdownString)" + markdownString = previousCharacter == nil ? "" : markdownString + case ".": + if openedNumberedListStarter { + openedNumberedListStarter = false + characterOnNumberedListLine = true + + stringToAppend = "\(character) \(!numberedListIsFirstLine ? String(closingString.characters.reversed()) : markdownString)" + + if numberedListIsFirstLine { + markdownString = "" + numberedListIsFirstLine = false + } + break + } + stringToAppend = "\\\(character)" + case nonBreakingSpaceCharacter: + if characterOnBulletedListLine || characterOnNumberedListLine { + break + } + stringToAppend = " " + default: + if (previousCharacter == "\n" || previousCharacter == "\u{2028}") && characterOnBulletedListLine { + characterOnBulletedListLine = false + stringToAppend = "\(String(closingString.characters.reversed()))\(character)" + } else { + stringToAppend = "\(character)" + } + } + + previousCharacter = character + return "\(resultString)\(stringToAppend)" + } + markdownString += processedString + } + markdownString += closingString + markdownString = markdownString.replacingOccurrences(of: "**__**", with: "").replacingOccurrences(of: "****", with: "") + .replacingOccurrences(of: "__", with: "") + // Help the user because they probably didn't intend to have empty bullets and it will make markdown have a + if we leave them + markdownString = markdownString.replacingOccurrences(of: "+ \n", with: "") + if markdownString.hasSuffix("+ ") { + markdownString = (markdownString as NSString).substring(to: markdownString.characters.count - 2) + } + + return markdownString + } + +} + +#endif + diff --git a/TSMarkdownParser/TSMarkdownParser.h b/TSMarkdownParser/TSMarkdownParser.h index 785a3ee..159874f 100644 --- a/TSMarkdownParser/TSMarkdownParser.h +++ b/TSMarkdownParser/TSMarkdownParser.h @@ -28,6 +28,7 @@ typedef void (^TSMarkdownParserLinkFormattingBlock)(NSMutableAttributedString *a @property (nonatomic, strong) NSDictionary *monospaceAttributes; @property (nonatomic, strong) NSDictionary *strongAttributes; @property (nonatomic, strong) NSDictionary *emphasisAttributes; +@property (nonatomic, strong) NSDictionary *strongAndEmphasisAttributes; /** * standardParser setting for NSLinkAttributeName * @@ -126,6 +127,8 @@ typedef void (^TSMarkdownParserLinkFormattingBlock)(NSMutableAttributedString *a - (void)addStrongParsingWithFormattingBlock:(TSMarkdownParserFormattingBlock)formattingBlock; /// accepts "*text*", "_text_" - (void)addEmphasisParsingWithFormattingBlock:(TSMarkdownParserFormattingBlock)formattingBlock; +/// accepts "***text***", "___text___" +- (void)addStrongAndEmphasisParsingWithFormattingBlock:(TSMarkdownParserFormattingBlock)formattingBlock; /* 7. examples unescaping parsing */ /* to use together with `addEscapingParsing` or `addCodeEscapingParsing` */ diff --git a/TSMarkdownParser/TSMarkdownParser.m b/TSMarkdownParser/TSMarkdownParser.m index c48090c..6153afe 100644 --- a/TSMarkdownParser/TSMarkdownParser.m +++ b/TSMarkdownParser/TSMarkdownParser.m @@ -77,6 +77,8 @@ - (instancetype)init { NSForegroundColorAttributeName: [UIColor colorWithSRGBRed:0.95 green:0.54 blue:0.55 alpha:1] }; _strongAttributes = @{ NSFontAttributeName: [UIFont boldSystemFontOfSize:defaultSize] }; + _strongAndEmphasisAttributes = @{ NSFontAttributeName: [UIFont fontWithDescriptor:[[[UIFont systemFontOfSize:defaultSize] fontDescriptor] fontDescriptorWithSymbolicTraits:(UIFontDescriptorTraitBold | UIFontDescriptorTraitItalic)] size:defaultSize] }; + return self; } @@ -208,6 +210,10 @@ + (instancetype)standardParser { [attributedString addAttributes:weakParser.emphasisAttributes range:range]; }]; + [defaultParser addStrongAndEmphasisParsingWithFormattingBlock:^(NSMutableAttributedString *attributedString, NSRange range) { + [attributedString addAttributes:weakParser.strongAndEmphasisAttributes range:range]; + }]; + /* unescaping parsing */ [defaultParser addCodeUnescapingParsingWithFormattingBlock:^(NSMutableAttributedString *attributedString, NSRange range) { @@ -249,6 +255,7 @@ + (void)addAttributes:(NSArray *> *)attributesArray static NSString *const TSMarkdownMonospaceRegex = @"(`+)(\\s*.*?[^`]\\s*)(\\1)(?!`)"; static NSString *const TSMarkdownStrongRegex = @"(\\*\\*|__)(.+?)(\\1)"; static NSString *const TSMarkdownEmRegex = @"(\\*|_)(.+?)(\\1)"; +static NSString *const TSMarkdownStrongEmRegex = @"(((\\*\\*\\*)(.|\\s)*(\\*\\*\\*))|((___)(.|\\s)*(___)))"; #pragma mark escaping parsing @@ -433,11 +440,22 @@ - (void)addEnclosedParsingWithPattern:(NSString *)pattern formattingBlock:(TSMar NSRegularExpression *parsing = [NSRegularExpression regularExpressionWithPattern:pattern options:(NSRegularExpressionOptions)0 error:nil]; [self addParsingRuleWithRegularExpression:parsing block:^(NSTextCheckingResult *match, NSMutableAttributedString *attributedString) { // deleting trailing markdown - [attributedString deleteCharactersInRange:[match rangeAtIndex:3]]; - // formatting string (may alter the length) - formattingBlock(attributedString, [match rangeAtIndex:2]); - // deleting leading markdown - [attributedString deleteCharactersInRange:[match rangeAtIndex:1]]; + NSRange match3 = [match rangeAtIndex:3]; + if (match3.location != NSNotFound) { + [attributedString deleteCharactersInRange:match3]; + } + + NSRange match2 = [match rangeAtIndex:2]; + if (match2.location != NSNotFound) { + // formatting string (may alter the length) + formattingBlock(attributedString, match2); + } + + NSRange match1 = [match rangeAtIndex:1]; + if (match1.location != NSNotFound) { + // deleting leading markdown + [attributedString deleteCharactersInRange:match1]; + } }]; } @@ -453,6 +471,10 @@ - (void)addEmphasisParsingWithFormattingBlock:(TSMarkdownParserFormattingBlock)f [self addEnclosedParsingWithPattern:TSMarkdownEmRegex formattingBlock:formattingBlock]; } +- (void)addStrongAndEmphasisParsingWithFormattingBlock:(TSMarkdownParserFormattingBlock)formattingBlock { + [self addEnclosedParsingWithPattern:TSMarkdownStrongEmRegex formattingBlock:formattingBlock]; +} + #pragma mark link detection - (void)addLinkDetectionWithFormattingBlock:(TSMarkdownParserFormattingBlock)formattingBlock __attribute__((deprecated("use addLinkDetectionWithLinkFormattingBlock: instead"))) {