Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,8 @@ Emphasis
_Em_
**Strong**
__Strong__

***Strong Emphasis***
___Strong Emphasis___
````

# Requirements
Expand Down
4 changes: 2 additions & 2 deletions TSMarkdownParser.podspec
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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'
Expand Down
160 changes: 160 additions & 0 deletions TSMarkdownParser/NSAttributedString+Markdown.swift
Original file line number Diff line number Diff line change
@@ -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

3 changes: 3 additions & 0 deletions TSMarkdownParser/TSMarkdownParser.h
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ typedef void (^TSMarkdownParserLinkFormattingBlock)(NSMutableAttributedString *a
@property (nonatomic, strong) NSDictionary<NSString *, id> *monospaceAttributes;
@property (nonatomic, strong) NSDictionary<NSString *, id> *strongAttributes;
@property (nonatomic, strong) NSDictionary<NSString *, id> *emphasisAttributes;
@property (nonatomic, strong) NSDictionary<NSString *, id> *strongAndEmphasisAttributes;
/**
* standardParser setting for NSLinkAttributeName
*
Expand Down Expand Up @@ -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` */
Expand Down
32 changes: 27 additions & 5 deletions TSMarkdownParser/TSMarkdownParser.m
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand Down Expand Up @@ -208,6 +210,10 @@ + (instancetype)standardParser {
[attributedString addAttributes:weakParser.emphasisAttributes range:range];
}];

[defaultParser addStrongAndEmphasisParsingWithFormattingBlock:^(NSMutableAttributedString *attributedString, NSRange range) {
Copy link
Collaborator

@Coeur Coeur Mar 5, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I may move this line before Strong and Emphasis.

[attributedString addAttributes:weakParser.strongAndEmphasisAttributes range:range];
}];

/* unescaping parsing */

[defaultParser addCodeUnescapingParsingWithFormattingBlock:^(NSMutableAttributedString *attributedString, NSRange range) {
Expand Down Expand Up @@ -249,6 +255,7 @@ + (void)addAttributes:(NSArray<NSDictionary<NSString *, id> *> *)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

Expand Down Expand Up @@ -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];
}
}];
}

Expand All @@ -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"))) {
Expand Down