From 1feb1bea9b21dcf9edfbf8dd4e6161365eb9434a Mon Sep 17 00:00:00 2001 From: Nick Date: Mon, 26 Sep 2022 11:24:39 -0400 Subject: [PATCH] Adding method to handle multiple parsed elements from markdown string. --- Sources/SwiftyMarkdown/SwiftyMarkdown.swift | 1089 ++++++++++--------- 1 file changed, 583 insertions(+), 506 deletions(-) diff --git a/Sources/SwiftyMarkdown/SwiftyMarkdown.swift b/Sources/SwiftyMarkdown/SwiftyMarkdown.swift index 1e9ef16..33d6a6b 100644 --- a/Sources/SwiftyMarkdown/SwiftyMarkdown.swift +++ b/Sources/SwiftyMarkdown/SwiftyMarkdown.swift @@ -13,27 +13,27 @@ import UIKit #endif extension OSLog { - private static var subsystem = "SwiftyMarkdown" - static let swiftyMarkdownPerformance = OSLog(subsystem: subsystem, category: "Swifty Markdown Performance") + private static var subsystem = "SwiftyMarkdown" + static let swiftyMarkdownPerformance = OSLog(subsystem: subsystem, category: "Swifty Markdown Performance") } public enum CharacterStyle : CharacterStyling { - case none - case bold - case italic - case code - case link - case image - case referencedLink - case referencedImage - case strikethrough - - public func isEqualTo(_ other: CharacterStyling) -> Bool { - guard let other = other as? CharacterStyle else { - return false - } - return other == self - } + case none + case bold + case italic + case code + case link + case image + case referencedLink + case referencedImage + case strikethrough + + public func isEqualTo(_ other: CharacterStyling) -> Bool { + guard let other = other as? CharacterStyle else { + return false + } + return other == self + } } enum MarkdownLineStyle : LineStyling { @@ -59,13 +59,13 @@ enum MarkdownLineStyle : LineStyling { case blockquote case codeblock case unorderedList - case unorderedListIndentFirstOrder - case unorderedListIndentSecondOrder + case unorderedListIndentFirstOrder + case unorderedListIndentSecondOrder case orderedList - case orderedListIndentFirstOrder - case orderedListIndentSecondOrder - case referencedLink - + case orderedListIndentFirstOrder + case orderedListIndentSecondOrder + case referencedLink + func styleIfFoundStyleAffectsPreviousLine() -> LineStyling? { switch self { case .previousH1: @@ -79,31 +79,31 @@ enum MarkdownLineStyle : LineStyling { } @objc public enum FontStyle : Int { - case normal - case bold - case italic - case boldItalic + case normal + case bold + case italic + case boldItalic } #if os(macOS) @objc public protocol FontProperties { - var fontName : String? { get set } - var color : NSColor { get set } - var fontSize : CGFloat { get set } - var fontStyle : FontStyle { get set } + var fontName : String? { get set } + var color : NSColor { get set } + var fontSize : CGFloat { get set } + var fontStyle : FontStyle { get set } } #else @objc public protocol FontProperties { - var fontName : String? { get set } - var color : UIColor { get set } - var fontSize : CGFloat { get set } - var fontStyle : FontStyle { get set } + var fontName : String? { get set } + var color : UIColor { get set } + var fontSize : CGFloat { get set } + var fontStyle : FontStyle { get set } } #endif @objc public protocol LineProperties { - var alignment : NSTextAlignment { get set } + var alignment : NSTextAlignment { get set } var lineSpacing: CGFloat { get set } var paragraphSpacing: CGFloat { get set } } @@ -115,460 +115,537 @@ A class defining the styles that can be applied to the parsed Markdown. The `fon If that is not set, then the system default will be used. */ @objc open class BasicStyles : NSObject, FontProperties { - public var fontName : String? - #if os(macOS) - public var color = NSColor.black - #else - public var color = UIColor.black - #endif - public var fontSize : CGFloat = 0.0 - public var fontStyle : FontStyle = .normal + public var fontName : String? + #if os(macOS) + public var color = NSColor.black + #else + public var color = UIColor.black + #endif + public var fontSize : CGFloat = 0.0 + public var fontStyle : FontStyle = .normal } @objc open class LineStyles : NSObject, FontProperties, LineProperties { - public var fontName : String? - #if os(macOS) - public var color = NSColor.black - #else - public var color = UIColor.black - #endif - public var fontSize : CGFloat = 0.0 - public var fontStyle : FontStyle = .normal - public var alignment: NSTextAlignment = .left + public var fontName : String? + #if os(macOS) + public var color = NSColor.black + #else + public var color = UIColor.black + #endif + public var fontSize : CGFloat = 0.0 + public var fontStyle : FontStyle = .normal + public var alignment: NSTextAlignment = .left public var lineSpacing : CGFloat = 0.0 public var paragraphSpacing : CGFloat = 0.0 } @objc open class LinkStyles : BasicStyles { public var underlineStyle: NSUnderlineStyle = .single - #if os(macOS) - public lazy var underlineColor = self.color - #else - public lazy var underlineColor = self.color - #endif + #if os(macOS) + public lazy var underlineColor = self.color + #else + public lazy var underlineColor = self.color + #endif } /// A class that takes a [Markdown](https://daringfireball.net/projects/markdown/) string or file and returns an NSAttributedString with the applied styles. Supports Dynamic Type. @objc open class SwiftyMarkdown: NSObject { - - static public var frontMatterRules = [ - FrontMatterRule(openTag: "---", closeTag: "---", keyValueSeparator: ":") - ] - - static public var lineRules = [ - LineRule(token: "=", type: MarkdownLineStyle.previousH1, removeFrom: .entireLine, changeAppliesTo: .previous), - LineRule(token: "-", type: MarkdownLineStyle.previousH2, removeFrom: .entireLine, changeAppliesTo: .previous), - LineRule(token: "\t\t- ", type: MarkdownLineStyle.unorderedListIndentSecondOrder, removeFrom: .leading, shouldTrim: false), - LineRule(token: "\t- ", type: MarkdownLineStyle.unorderedListIndentFirstOrder, removeFrom: .leading, shouldTrim: false), - LineRule(token: "- ",type : MarkdownLineStyle.unorderedList, removeFrom: .leading), - LineRule(token: "\t\t* ", type: MarkdownLineStyle.unorderedListIndentSecondOrder, removeFrom: .leading, shouldTrim: false), - LineRule(token: "\t* ", type: MarkdownLineStyle.unorderedListIndentFirstOrder, removeFrom: .leading, shouldTrim: false), - LineRule(token: "\t\t1. ", type: MarkdownLineStyle.orderedListIndentSecondOrder, removeFrom: .leading, shouldTrim: false), - LineRule(token: "\t1. ", type: MarkdownLineStyle.orderedListIndentFirstOrder, removeFrom: .leading, shouldTrim: false), - LineRule(token: "1. ",type : MarkdownLineStyle.orderedList, removeFrom: .leading), - LineRule(token: "* ",type : MarkdownLineStyle.unorderedList, removeFrom: .leading), - LineRule(token: " ", type: MarkdownLineStyle.codeblock, removeFrom: .leading, shouldTrim: false), - LineRule(token: "\t", type: MarkdownLineStyle.codeblock, removeFrom: .leading, shouldTrim: false), - LineRule(token: ">",type : MarkdownLineStyle.blockquote, removeFrom: .leading), - LineRule(token: "###### ",type : MarkdownLineStyle.h6, removeFrom: .both), - LineRule(token: "##### ",type : MarkdownLineStyle.h5, removeFrom: .both), - LineRule(token: "#### ",type : MarkdownLineStyle.h4, removeFrom: .both), - LineRule(token: "### ",type : MarkdownLineStyle.h3, removeFrom: .both), - LineRule(token: "## ",type : MarkdownLineStyle.h2, removeFrom: .both), - LineRule(token: "# ",type : MarkdownLineStyle.h1, removeFrom: .both) - ] - - static public var characterRules = [ - CharacterRule(primaryTag: CharacterRuleTag(tag: "![", type: .open), otherTags: [ - CharacterRuleTag(tag: "]", type: .close), - CharacterRuleTag(tag: "[", type: .metadataOpen), - CharacterRuleTag(tag: "]", type: .metadataClose) - ], styles: [1 : CharacterStyle.image], metadataLookup: true, definesBoundary: true), - CharacterRule(primaryTag: CharacterRuleTag(tag: "![", type: .open), otherTags: [ - CharacterRuleTag(tag: "]", type: .close), - CharacterRuleTag(tag: "(", type: .metadataOpen), - CharacterRuleTag(tag: ")", type: .metadataClose) - ], styles: [1 : CharacterStyle.image], metadataLookup: false, definesBoundary: true), - CharacterRule(primaryTag: CharacterRuleTag(tag: "[", type: .open), otherTags: [ - CharacterRuleTag(tag: "]", type: .close), - CharacterRuleTag(tag: "[", type: .metadataOpen), - CharacterRuleTag(tag: "]", type: .metadataClose) - ], styles: [1 : CharacterStyle.link], metadataLookup: true, definesBoundary: true), - CharacterRule(primaryTag: CharacterRuleTag(tag: "[", type: .open), otherTags: [ - CharacterRuleTag(tag: "]", type: .close), - CharacterRuleTag(tag: "(", type: .metadataOpen), - CharacterRuleTag(tag: ")", type: .metadataClose) - ], styles: [1 : CharacterStyle.link], metadataLookup: false, definesBoundary: true), - CharacterRule(primaryTag: CharacterRuleTag(tag: "`", type: .repeating), otherTags: [], styles: [1 : CharacterStyle.code], shouldCancelRemainingRules: true, balancedTags: true), - CharacterRule(primaryTag:CharacterRuleTag(tag: "~", type: .repeating), otherTags : [], styles: [2 : CharacterStyle.strikethrough], minTags:2 , maxTags:2), - CharacterRule(primaryTag: CharacterRuleTag(tag: "*", type: .repeating), otherTags: [], styles: [1 : CharacterStyle.italic, 2 : CharacterStyle.bold], minTags:1 , maxTags:2), - CharacterRule(primaryTag: CharacterRuleTag(tag: "_", type: .repeating), otherTags: [], styles: [1 : CharacterStyle.italic, 2 : CharacterStyle.bold], minTags:1 , maxTags:2) - ] - - let lineProcessor = SwiftyLineProcessor(rules: SwiftyMarkdown.lineRules, defaultRule: MarkdownLineStyle.body, frontMatterRules: SwiftyMarkdown.frontMatterRules) - let tokeniser = SwiftyTokeniser(with: SwiftyMarkdown.characterRules) - - /// The styles to apply to any H1 headers found in the Markdown - open var h1 = LineStyles() - - /// The styles to apply to any H2 headers found in the Markdown - open var h2 = LineStyles() - - /// The styles to apply to any H3 headers found in the Markdown - open var h3 = LineStyles() - - /// The styles to apply to any H4 headers found in the Markdown - open var h4 = LineStyles() - - /// The styles to apply to any H5 headers found in the Markdown - open var h5 = LineStyles() - - /// The styles to apply to any H6 headers found in the Markdown - open var h6 = LineStyles() - - /// The default body styles. These are the base styles and will be used for e.g. headers if no other styles override them. - open var body = LineStyles() - - /// The styles to apply to any blockquotes found in the Markdown - open var blockquotes = LineStyles() - - /// The styles to apply to any links found in the Markdown - open var link = LinkStyles() - - /// The styles to apply to any bold text found in the Markdown - open var bold = BasicStyles() - - /// The styles to apply to any italic text found in the Markdown - open var italic = BasicStyles() - - /// The styles to apply to any code blocks or inline code text found in the Markdown - open var code = BasicStyles() - - open var strikethrough = BasicStyles() - - public var bullet : String = "・" - - public var underlineLinks : Bool = false - - public var frontMatterAttributes : [String : String] { - get { - return self.lineProcessor.frontMatterAttributes - } - } - - var currentType : MarkdownLineStyle = .body - - var string : String + + static public var frontMatterRules = [ + FrontMatterRule(openTag: "---", closeTag: "---", keyValueSeparator: ":") + ] + + static public var lineRules = [ + LineRule(token: "=", type: MarkdownLineStyle.previousH1, removeFrom: .entireLine, changeAppliesTo: .previous), + LineRule(token: "-", type: MarkdownLineStyle.previousH2, removeFrom: .entireLine, changeAppliesTo: .previous), + LineRule(token: "\t\t- ", type: MarkdownLineStyle.unorderedListIndentSecondOrder, removeFrom: .leading, shouldTrim: false), + LineRule(token: "\t- ", type: MarkdownLineStyle.unorderedListIndentFirstOrder, removeFrom: .leading, shouldTrim: false), + LineRule(token: "- ",type : MarkdownLineStyle.unorderedList, removeFrom: .leading), + LineRule(token: "\t\t* ", type: MarkdownLineStyle.unorderedListIndentSecondOrder, removeFrom: .leading, shouldTrim: false), + LineRule(token: "\t* ", type: MarkdownLineStyle.unorderedListIndentFirstOrder, removeFrom: .leading, shouldTrim: false), + LineRule(token: "\t\t1. ", type: MarkdownLineStyle.orderedListIndentSecondOrder, removeFrom: .leading, shouldTrim: false), + LineRule(token: "\t1. ", type: MarkdownLineStyle.orderedListIndentFirstOrder, removeFrom: .leading, shouldTrim: false), + LineRule(token: "1. ",type : MarkdownLineStyle.orderedList, removeFrom: .leading), + LineRule(token: "* ",type : MarkdownLineStyle.unorderedList, removeFrom: .leading), + LineRule(token: " ", type: MarkdownLineStyle.codeblock, removeFrom: .leading, shouldTrim: false), + LineRule(token: "\t", type: MarkdownLineStyle.codeblock, removeFrom: .leading, shouldTrim: false), + LineRule(token: ">",type : MarkdownLineStyle.blockquote, removeFrom: .leading), + LineRule(token: "###### ",type : MarkdownLineStyle.h6, removeFrom: .both), + LineRule(token: "##### ",type : MarkdownLineStyle.h5, removeFrom: .both), + LineRule(token: "#### ",type : MarkdownLineStyle.h4, removeFrom: .both), + LineRule(token: "### ",type : MarkdownLineStyle.h3, removeFrom: .both), + LineRule(token: "## ",type : MarkdownLineStyle.h2, removeFrom: .both), + LineRule(token: "# ",type : MarkdownLineStyle.h1, removeFrom: .both) + ] + + static public var characterRules = [ + CharacterRule(primaryTag: CharacterRuleTag(tag: "![", type: .open), otherTags: [ + CharacterRuleTag(tag: "]", type: .close), + CharacterRuleTag(tag: "[", type: .metadataOpen), + CharacterRuleTag(tag: "]", type: .metadataClose) + ], styles: [1 : CharacterStyle.image], metadataLookup: true, definesBoundary: true), + CharacterRule(primaryTag: CharacterRuleTag(tag: "![", type: .open), otherTags: [ + CharacterRuleTag(tag: "]", type: .close), + CharacterRuleTag(tag: "(", type: .metadataOpen), + CharacterRuleTag(tag: ")", type: .metadataClose) + ], styles: [1 : CharacterStyle.image], metadataLookup: false, definesBoundary: true), + CharacterRule(primaryTag: CharacterRuleTag(tag: "[", type: .open), otherTags: [ + CharacterRuleTag(tag: "]", type: .close), + CharacterRuleTag(tag: "[", type: .metadataOpen), + CharacterRuleTag(tag: "]", type: .metadataClose) + ], styles: [1 : CharacterStyle.link], metadataLookup: true, definesBoundary: true), + CharacterRule(primaryTag: CharacterRuleTag(tag: "[", type: .open), otherTags: [ + CharacterRuleTag(tag: "]", type: .close), + CharacterRuleTag(tag: "(", type: .metadataOpen), + CharacterRuleTag(tag: ")", type: .metadataClose) + ], styles: [1 : CharacterStyle.link], metadataLookup: false, definesBoundary: true), + CharacterRule(primaryTag: CharacterRuleTag(tag: "`", type: .repeating), otherTags: [], styles: [1 : CharacterStyle.code], shouldCancelRemainingRules: true, balancedTags: true), + CharacterRule(primaryTag:CharacterRuleTag(tag: "~", type: .repeating), otherTags : [], styles: [2 : CharacterStyle.strikethrough], minTags:2 , maxTags:2), + CharacterRule(primaryTag: CharacterRuleTag(tag: "*", type: .repeating), otherTags: [], styles: [1 : CharacterStyle.italic, 2 : CharacterStyle.bold], minTags:1 , maxTags:2), + CharacterRule(primaryTag: CharacterRuleTag(tag: "_", type: .repeating), otherTags: [], styles: [1 : CharacterStyle.italic, 2 : CharacterStyle.bold], minTags:1 , maxTags:2) + ] + + let lineProcessor = SwiftyLineProcessor(rules: SwiftyMarkdown.lineRules, defaultRule: MarkdownLineStyle.body, frontMatterRules: SwiftyMarkdown.frontMatterRules) + let tokeniser = SwiftyTokeniser(with: SwiftyMarkdown.characterRules) + + /// The styles to apply to any H1 headers found in the Markdown + open var h1 = LineStyles() + + /// The styles to apply to any H2 headers found in the Markdown + open var h2 = LineStyles() + + /// The styles to apply to any H3 headers found in the Markdown + open var h3 = LineStyles() + + /// The styles to apply to any H4 headers found in the Markdown + open var h4 = LineStyles() + + /// The styles to apply to any H5 headers found in the Markdown + open var h5 = LineStyles() + + /// The styles to apply to any H6 headers found in the Markdown + open var h6 = LineStyles() + + /// The default body styles. These are the base styles and will be used for e.g. headers if no other styles override them. + open var body = LineStyles() + + /// The styles to apply to any blockquotes found in the Markdown + open var blockquotes = LineStyles() + + /// The styles to apply to any links found in the Markdown + open var link = LinkStyles() + + /// The styles to apply to any bold text found in the Markdown + open var bold = BasicStyles() + + /// The styles to apply to any italic text found in the Markdown + open var italic = BasicStyles() + + /// The styles to apply to any code blocks or inline code text found in the Markdown + open var code = BasicStyles() + + open var strikethrough = BasicStyles() + + public var bullet : String = "・" + + public var underlineLinks : Bool = false + + public var frontMatterAttributes : [String : String] { + get { + return self.lineProcessor.frontMatterAttributes + } + } + + var currentType : MarkdownLineStyle = .body + + var string : String - var orderedListCount = 0 - var orderedListIndentFirstOrderCount = 0 - var orderedListIndentSecondOrderCount = 0 - - var previouslyFoundTokens : [Token] = [] - - var applyAttachments = true - - let perfomanceLog = PerformanceLog(with: "SwiftyMarkdownPerformanceLogging", identifier: "Swifty Markdown", log: .swiftyMarkdownPerformance) - - /** - - - parameter string: A string containing [Markdown](https://daringfireball.net/projects/markdown/) syntax to be converted to an NSAttributedString - - - returns: An initialized SwiftyMarkdown object - */ - public init(string : String ) { - self.string = string - super.init() - self.setup() - } - - /** - A failable initializer that takes a URL and attempts to read it as a UTF-8 string - - - parameter url: The location of the file to read - - - returns: An initialized SwiftyMarkdown object, or nil if the string couldn't be read - */ - public init?(url : URL ) { - - do { - self.string = try NSString(contentsOf: url, encoding: String.Encoding.utf8.rawValue) as String - - } catch { - self.string = "" - return nil - } - super.init() - self.setup() - } - - func setup() { - #if os(macOS) - self.setFontColorForAllStyles(with: .labelColor) - #elseif !os(watchOS) - if #available(iOS 13.0, tvOS 13.0, *) { - self.setFontColorForAllStyles(with: .label) - } - #endif - } - - /** - Set font size for all styles - - - parameter size: size of font - */ - open func setFontSizeForAllStyles(with size: CGFloat) { - h1.fontSize = size - h2.fontSize = size - h3.fontSize = size - h4.fontSize = size - h5.fontSize = size - h6.fontSize = size - body.fontSize = size - italic.fontSize = size - bold.fontSize = size - code.fontSize = size - link.fontSize = size - link.fontSize = size - strikethrough.fontSize = size - } - - #if os(macOS) - open func setFontColorForAllStyles(with color: NSColor) { - h1.color = color - h2.color = color - h3.color = color - h4.color = color - h5.color = color - h6.color = color - body.color = color - italic.color = color - bold.color = color - code.color = color - link.color = color - blockquotes.color = color - strikethrough.color = color - } - #else - open func setFontColorForAllStyles(with color: UIColor) { - h1.color = color - h2.color = color - h3.color = color - h4.color = color - h5.color = color - h6.color = color - body.color = color - italic.color = color - bold.color = color - code.color = color - link.color = color - blockquotes.color = color - strikethrough.color = color - } - #endif - - open func setFontNameForAllStyles(with name: String) { - h1.fontName = name - h2.fontName = name - h3.fontName = name - h4.fontName = name - h5.fontName = name - h6.fontName = name - body.fontName = name - italic.fontName = name - bold.fontName = name - code.fontName = name - link.fontName = name - blockquotes.fontName = name - strikethrough.fontName = name - } - - - /** - Generates an NSAttributedString from the string or URL passed at initialisation. Custom fonts or styles are applied to the appropriate elements when this method is called. - - - returns: An NSAttributedString with the styles applied - */ - open func attributedString(from markdownString : String? = nil) -> NSAttributedString { - - self.previouslyFoundTokens.removeAll() - self.perfomanceLog.start() - - if let existentMarkdownString = markdownString { - self.string = existentMarkdownString - } - let attributedString = NSMutableAttributedString(string: "") - self.lineProcessor.processEmptyStrings = MarkdownLineStyle.body - let foundAttributes : [SwiftyLine] = lineProcessor.process(self.string) - - let references : [SwiftyLine] = foundAttributes.filter({ $0.line.starts(with: "[") && $0.line.contains("]:") }) - let referencesRemoved : [SwiftyLine] = foundAttributes.filter({ !($0.line.starts(with: "[") && $0.line.contains("]:") ) }) - var keyValuePairs : [String : String] = [:] - for line in references { - let strings = line.line.components(separatedBy: "]:") - guard strings.count >= 2 else { - continue - } - var key : String = strings[0] - if !key.isEmpty { - let newstart = key.index(key.startIndex, offsetBy: 1) - let range : Range = newstart.. 0 { - attributedString.append(NSAttributedString(string: "\n")) - } - let finalTokens = self.tokeniser.process(line.line) - self.previouslyFoundTokens.append(contentsOf: finalTokens) - self.perfomanceLog.tag(with: "(tokenising complete for line \(idx)") - - attributedString.append(attributedStringFor(tokens: finalTokens, in: line)) - - } - - self.perfomanceLog.end() - - return attributedString - } - + var orderedListCount = 0 + var orderedListIndentFirstOrderCount = 0 + var orderedListIndentSecondOrderCount = 0 + + var previouslyFoundTokens : [Token] = [] + + var applyAttachments = true + + let perfomanceLog = PerformanceLog(with: "SwiftyMarkdownPerformanceLogging", identifier: "Swifty Markdown", log: .swiftyMarkdownPerformance) + + /** + + - parameter string: A string containing [Markdown](https://daringfireball.net/projects/markdown/) syntax to be converted to an NSAttributedString + + - returns: An initialized SwiftyMarkdown object + */ + public init(string : String ) { + self.string = string + super.init() + self.setup() + } + + /** + A failable initializer that takes a URL and attempts to read it as a UTF-8 string + + - parameter url: The location of the file to read + + - returns: An initialized SwiftyMarkdown object, or nil if the string couldn't be read + */ + public init?(url : URL ) { + + do { + self.string = try NSString(contentsOf: url, encoding: String.Encoding.utf8.rawValue) as String + + } catch { + self.string = "" + return nil + } + super.init() + self.setup() + } + + func setup() { + #if os(macOS) + self.setFontColorForAllStyles(with: .labelColor) + #elseif !os(watchOS) + if #available(iOS 13.0, tvOS 13.0, *) { + self.setFontColorForAllStyles(with: .label) + } + #endif + } + + /** + Set font size for all styles + + - parameter size: size of font + */ + open func setFontSizeForAllStyles(with size: CGFloat) { + h1.fontSize = size + h2.fontSize = size + h3.fontSize = size + h4.fontSize = size + h5.fontSize = size + h6.fontSize = size + body.fontSize = size + italic.fontSize = size + bold.fontSize = size + code.fontSize = size + link.fontSize = size + link.fontSize = size + strikethrough.fontSize = size + } + + #if os(macOS) + open func setFontColorForAllStyles(with color: NSColor) { + h1.color = color + h2.color = color + h3.color = color + h4.color = color + h5.color = color + h6.color = color + body.color = color + italic.color = color + bold.color = color + code.color = color + link.color = color + blockquotes.color = color + strikethrough.color = color + } + #else + open func setFontColorForAllStyles(with color: UIColor) { + h1.color = color + h2.color = color + h3.color = color + h4.color = color + h5.color = color + h6.color = color + body.color = color + italic.color = color + bold.color = color + code.color = color + link.color = color + blockquotes.color = color + strikethrough.color = color + } + #endif + + open func setFontNameForAllStyles(with name: String) { + h1.fontName = name + h2.fontName = name + h3.fontName = name + h4.fontName = name + h5.fontName = name + h6.fontName = name + body.fontName = name + italic.fontName = name + bold.fontName = name + code.fontName = name + link.fontName = name + blockquotes.fontName = name + strikethrough.fontName = name + } + + + /** + Generates an NSAttributedString from the string or URL passed at initialisation. Custom fonts or styles are applied to the appropriate elements when this method is called. + + - returns: An NSAttributedString with the styles applied + */ + open func attributedString(from markdownString : String? = nil) -> NSAttributedString { + + self.previouslyFoundTokens.removeAll() + self.perfomanceLog.start() + + if let existentMarkdownString = markdownString { + self.string = existentMarkdownString + } + let attributedString = NSMutableAttributedString(string: "") + self.lineProcessor.processEmptyStrings = MarkdownLineStyle.body + let foundAttributes : [SwiftyLine] = lineProcessor.process(self.string) + + let references : [SwiftyLine] = foundAttributes.filter({ $0.line.starts(with: "[") && $0.line.contains("]:") }) + let referencesRemoved : [SwiftyLine] = foundAttributes.filter({ !($0.line.starts(with: "[") && $0.line.contains("]:") ) }) + var keyValuePairs : [String : String] = [:] + for line in references { + let strings = line.line.components(separatedBy: "]:") + guard strings.count >= 2 else { + continue + } + var key : String = strings[0] + if !key.isEmpty { + let newstart = key.index(key.startIndex, offsetBy: 1) + let range : Range = newstart.. 0 { + attributedString.append(NSAttributedString(string: "\n")) + } + let finalTokens = self.tokeniser.process(line.line) + self.previouslyFoundTokens.append(contentsOf: finalTokens) + self.perfomanceLog.tag(with: "(tokenising complete for line \(idx)") + + attributedString.append(attributedStringFor(tokens: finalTokens, in: line)) + + } + + self.perfomanceLog.end() + + return attributedString + } + + public enum ParsedMarkdownElement: Identifiable { + public var id: UUID { + return UUID() + } + case string(NSMutableAttributedString) + case image(caption: String, urlString: String) + } + // Cases for detcted markdown lines that can be used as delimiter + public enum ParsingMakrdownDelimiterItem: String { + case image = "![" + } + + + open func getParsedMarkdownElements(from markdownString : String? = nil) -> [ParsedMarkdownElement] { + + self.previouslyFoundTokens.removeAll() + self.perfomanceLog.start() + + if let existentMarkdownString = markdownString { + self.string = existentMarkdownString + } + var attributedString = NSMutableAttributedString(string: "") + + self.lineProcessor.processEmptyStrings = MarkdownLineStyle.body + let foundAttributes : [SwiftyLine] = lineProcessor.process(self.string) + + let references : [SwiftyLine] = foundAttributes.filter({ $0.line.starts(with: "[") && $0.line.contains("]:") }) + let referencesRemoved : [SwiftyLine] = foundAttributes.filter({ !($0.line.starts(with: "[") && $0.line.contains("]:") ) }) + var keyValuePairs : [String : String] = [:] + for line in references { + let strings = line.line.components(separatedBy: "]:") + guard strings.count >= 2 else { + continue + } + var key : String = strings[0] + if !key.isEmpty { + let newstart = key.index(key.startIndex, offsetBy: 1) + let range : Range = newstart.. 0 { + attributedString.append(NSAttributedString(string: "\n")) + } + if line.line.hasPrefix(ParsingMakrdownDelimiterItem.image.rawValue) { + let finalTokens = self.tokeniser.process(line.line) + self.perfomanceLog.tag(with: "(tokenising complete for line \(idx)") + let imageElement = ParsedMarkdownElement.image(caption: finalTokens.first?.inputString ?? "", urlString: finalTokens.first?.metadataStrings.first ?? "") + result.append(.string(attributedString)) + result.append(imageElement) + attributedString = NSMutableAttributedString(string: "") + } + // Append more 'if' cases handling when new delimiter added. + else { + let finalTokens = self.tokeniser.process(line.line) + self.previouslyFoundTokens.append(contentsOf: finalTokens) + self.perfomanceLog.tag(with: "(tokenising complete for line \(idx)") + attributedString.append(attributedStringFor(tokens: finalTokens, in: line)) + } + } + if !attributedString.string.isEmpty { + result.append(.string(attributedString)) + } + + self.perfomanceLog.end() + return result + } + } extension SwiftyMarkdown { - - func attributedStringFor( tokens : [Token], in line : SwiftyLine ) -> NSAttributedString { - - var finalTokens = tokens - let finalAttributedString = NSMutableAttributedString() - var attributes : [NSAttributedString.Key : AnyObject] = [:] - - guard let markdownLineStyle = line.lineStyle as? MarkdownLineStyle else { - preconditionFailure("The passed line style is not a valid Markdown Line Style") - } - - var listItem = self.bullet - switch markdownLineStyle { - case .orderedList: - self.orderedListCount += 1 - self.orderedListIndentFirstOrderCount = 0 - self.orderedListIndentSecondOrderCount = 0 - listItem = "\(self.orderedListCount)." - case .orderedListIndentFirstOrder, .unorderedListIndentFirstOrder: - self.orderedListIndentFirstOrderCount += 1 - self.orderedListIndentSecondOrderCount = 0 - if markdownLineStyle == .orderedListIndentFirstOrder { - listItem = "\(self.orderedListIndentFirstOrderCount)." - } - - case .orderedListIndentSecondOrder, .unorderedListIndentSecondOrder: - self.orderedListIndentSecondOrderCount += 1 - if markdownLineStyle == .orderedListIndentSecondOrder { - listItem = "\(self.orderedListIndentSecondOrderCount)." - } - - default: - self.orderedListCount = 0 - self.orderedListIndentFirstOrderCount = 0 - self.orderedListIndentSecondOrderCount = 0 - } + + func attributedStringFor( tokens : [Token], in line : SwiftyLine ) -> NSAttributedString { + + var finalTokens = tokens + let finalAttributedString = NSMutableAttributedString() + var attributes : [NSAttributedString.Key : AnyObject] = [:] + + guard let markdownLineStyle = line.lineStyle as? MarkdownLineStyle else { + preconditionFailure("The passed line style is not a valid Markdown Line Style") + } + + var listItem = self.bullet + switch markdownLineStyle { + case .orderedList: + self.orderedListCount += 1 + self.orderedListIndentFirstOrderCount = 0 + self.orderedListIndentSecondOrderCount = 0 + listItem = "\(self.orderedListCount)." + case .orderedListIndentFirstOrder, .unorderedListIndentFirstOrder: + self.orderedListIndentFirstOrderCount += 1 + self.orderedListIndentSecondOrderCount = 0 + if markdownLineStyle == .orderedListIndentFirstOrder { + listItem = "\(self.orderedListIndentFirstOrderCount)." + } + + case .orderedListIndentSecondOrder, .unorderedListIndentSecondOrder: + self.orderedListIndentSecondOrderCount += 1 + if markdownLineStyle == .orderedListIndentSecondOrder { + listItem = "\(self.orderedListIndentSecondOrderCount)." + } + + default: + self.orderedListCount = 0 + self.orderedListIndentFirstOrderCount = 0 + self.orderedListIndentSecondOrderCount = 0 + } - let lineProperties : LineProperties - switch markdownLineStyle { - case .h1: - lineProperties = self.h1 - case .h2: - lineProperties = self.h2 - case .h3: - lineProperties = self.h3 - case .h4: - lineProperties = self.h4 - case .h5: - lineProperties = self.h5 - case .h6: - lineProperties = self.h6 - case .codeblock: - lineProperties = body - let paragraphStyle = NSMutableParagraphStyle() - paragraphStyle.firstLineHeadIndent = 20.0 - attributes[.paragraphStyle] = paragraphStyle - case .blockquote: - lineProperties = self.blockquotes - let paragraphStyle = NSMutableParagraphStyle() - paragraphStyle.firstLineHeadIndent = 20.0 - paragraphStyle.headIndent = 20.0 - attributes[.paragraphStyle] = paragraphStyle - case .unorderedList, .unorderedListIndentFirstOrder, .unorderedListIndentSecondOrder, .orderedList, .orderedListIndentFirstOrder, .orderedListIndentSecondOrder: - - let interval : CGFloat = 30 - var addition = interval - var indent = "" - switch line.lineStyle as! MarkdownLineStyle { - case .unorderedListIndentFirstOrder, .orderedListIndentFirstOrder: - addition = interval * 2 - indent = "\t" - case .unorderedListIndentSecondOrder, .orderedListIndentSecondOrder: - addition = interval * 3 - indent = "\t\t" - default: - break - } - - lineProperties = body - - let paragraphStyle = NSMutableParagraphStyle() - paragraphStyle.tabStops = [NSTextTab(textAlignment: .left, location: interval, options: [:]), NSTextTab(textAlignment: .left, location: interval, options: [:])] - paragraphStyle.defaultTabInterval = interval - paragraphStyle.headIndent = addition + let lineProperties : LineProperties + switch markdownLineStyle { + case .h1: + lineProperties = self.h1 + case .h2: + lineProperties = self.h2 + case .h3: + lineProperties = self.h3 + case .h4: + lineProperties = self.h4 + case .h5: + lineProperties = self.h5 + case .h6: + lineProperties = self.h6 + case .codeblock: + lineProperties = body + let paragraphStyle = NSMutableParagraphStyle() + paragraphStyle.firstLineHeadIndent = 20.0 + attributes[.paragraphStyle] = paragraphStyle + case .blockquote: + lineProperties = self.blockquotes + let paragraphStyle = NSMutableParagraphStyle() + paragraphStyle.firstLineHeadIndent = 20.0 + paragraphStyle.headIndent = 20.0 + attributes[.paragraphStyle] = paragraphStyle + case .unorderedList, .unorderedListIndentFirstOrder, .unorderedListIndentSecondOrder, .orderedList, .orderedListIndentFirstOrder, .orderedListIndentSecondOrder: + + let interval : CGFloat = 30 + var addition = interval + var indent = "" + switch line.lineStyle as! MarkdownLineStyle { + case .unorderedListIndentFirstOrder, .orderedListIndentFirstOrder: + addition = interval * 2 + indent = "\t" + case .unorderedListIndentSecondOrder, .orderedListIndentSecondOrder: + addition = interval * 3 + indent = "\t\t" + default: + break + } + + lineProperties = body + + let paragraphStyle = NSMutableParagraphStyle() + paragraphStyle.tabStops = [NSTextTab(textAlignment: .left, location: interval, options: [:]), NSTextTab(textAlignment: .left, location: interval, options: [:])] + paragraphStyle.defaultTabInterval = interval + paragraphStyle.headIndent = addition - attributes[.paragraphStyle] = paragraphStyle - finalTokens.insert(Token(type: .string, inputString: "\(indent)\(listItem)\t"), at: 0) - - case .yaml: - lineProperties = body - case .previousH1: - lineProperties = body - case .previousH2: - lineProperties = body - case .body: - lineProperties = body - case .referencedLink: - lineProperties = body - } - + attributes[.paragraphStyle] = paragraphStyle + finalTokens.insert(Token(type: .string, inputString: "\(indent)\(listItem)\t"), at: 0) + + case .yaml: + lineProperties = body + case .previousH1: + lineProperties = body + case .previousH2: + lineProperties = body + case .body: + lineProperties = body + case .referencedLink: + lineProperties = body + } + let paragraphStyle = attributes[.paragraphStyle] as? NSMutableParagraphStyle ?? NSMutableParagraphStyle() - if lineProperties.alignment != .left { - paragraphStyle.alignment = lineProperties.alignment - } + if lineProperties.alignment != .left { + paragraphStyle.alignment = lineProperties.alignment + } paragraphStyle.lineSpacing = lineProperties.lineSpacing paragraphStyle.paragraphSpacing = lineProperties.paragraphSpacing attributes[.paragraphStyle] = paragraphStyle - - - for token in finalTokens { - attributes[.font] = self.font(for: line) - attributes[.link] = nil - attributes[.strikethroughStyle] = nil - attributes[.foregroundColor] = self.color(for: line) + + + for token in finalTokens { + attributes[.font] = self.font(for: line) + attributes[.link] = nil + attributes[.strikethroughStyle] = nil + attributes[.foregroundColor] = self.color(for: line) attributes[.underlineStyle] = nil - guard let styles = token.characterStyles as? [CharacterStyle] else { - continue - } - if styles.contains(.italic) { - attributes[.font] = self.font(for: line, characterOverride: .italic) - attributes[.foregroundColor] = self.italic.color - } - if styles.contains(.bold) { - attributes[.font] = self.font(for: line, characterOverride: .bold) - attributes[.foregroundColor] = self.bold.color - } - + guard let styles = token.characterStyles as? [CharacterStyle] else { + continue + } + if styles.contains(.italic) { + attributes[.font] = self.font(for: line, characterOverride: .italic) + attributes[.foregroundColor] = self.italic.color + } + if styles.contains(.bold) { + attributes[.font] = self.font(for: line, characterOverride: .bold) + attributes[.foregroundColor] = self.bold.color + } + if let linkIdx = styles.firstIndex(of: .link), linkIdx < token.metadataStrings.count { attributes[.foregroundColor] = self.link.color attributes[.font] = self.font(for: line, characterOverride: .link) @@ -579,43 +656,43 @@ extension SwiftyMarkdown { attributes[.underlineColor] = self.link.underlineColor } } - - if styles.contains(.strikethrough) { - attributes[.font] = self.font(for: line, characterOverride: .strikethrough) - attributes[.strikethroughStyle] = NSUnderlineStyle.single.rawValue as AnyObject - attributes[.foregroundColor] = self.strikethrough.color - } - - #if !os(watchOS) - if let imgIdx = styles.firstIndex(of: .image), imgIdx < token.metadataStrings.count { - if !self.applyAttachments { - continue - } - #if !os(macOS) - let image1Attachment = NSTextAttachment() - image1Attachment.image = UIImage(named: token.metadataStrings[imgIdx]) - let str = NSAttributedString(attachment: image1Attachment) - finalAttributedString.append(str) - #elseif !os(watchOS) - let image1Attachment = NSTextAttachment() - image1Attachment.image = NSImage(named: token.metadataStrings[imgIdx]) - let str = NSAttributedString(attachment: image1Attachment) - finalAttributedString.append(str) - #endif - continue - } - #endif - - if styles.contains(.code) { - attributes[.foregroundColor] = self.code.color - attributes[.font] = self.font(for: line, characterOverride: .code) - } else { - // Switch back to previous font - } - let str = NSAttributedString(string: token.outputString, attributes: attributes) - finalAttributedString.append(str) - } - - return finalAttributedString - } + + if styles.contains(.strikethrough) { + attributes[.font] = self.font(for: line, characterOverride: .strikethrough) + attributes[.strikethroughStyle] = NSUnderlineStyle.single.rawValue as AnyObject + attributes[.foregroundColor] = self.strikethrough.color + } + + #if !os(watchOS) + if let imgIdx = styles.firstIndex(of: .image), imgIdx < token.metadataStrings.count { + if !self.applyAttachments { + continue + } + #if !os(macOS) + let image1Attachment = NSTextAttachment() + image1Attachment.image = UIImage(named: token.metadataStrings[imgIdx]) + let str = NSAttributedString(attachment: image1Attachment) + finalAttributedString.append(str) + #elseif !os(watchOS) + let image1Attachment = NSTextAttachment() + image1Attachment.image = NSImage(named: token.metadataStrings[imgIdx]) + let str = NSAttributedString(attachment: image1Attachment) + finalAttributedString.append(str) + #endif + continue + } + #endif + + if styles.contains(.code) { + attributes[.foregroundColor] = self.code.color + attributes[.font] = self.font(for: line, characterOverride: .code) + } else { + // Switch back to previous font + } + let str = NSAttributedString(string: token.outputString, attributes: attributes) + finalAttributedString.append(str) + } + + return finalAttributedString + } }