SwiftyMarkdown.swift 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414
  1. //
  2. // SwiftyMarkdown.swift
  3. // SwiftyMarkdown
  4. //
  5. // Created by Simon Fairbairn on 05/03/2016.
  6. // Copyright © 2016 Voyage Travel Apps. All rights reserved.
  7. //
  8. import UIKit
  9. public protocol FontProperties {
  10. var fontName : String? { get set }
  11. var color : UIColor { get set }
  12. }
  13. /**
  14. A struct defining the styles that can be applied to the parsed Markdown. The `fontName` property is optional, and if it's not set then the `fontName` property of the Body style will be applied.
  15. If that is not set, then the system default will be used.
  16. */
  17. public struct BasicStyles : FontProperties {
  18. public var fontName : String? = UIFont.preferredFont(forTextStyle: UIFontTextStyle.body).fontName
  19. public var color = UIColor.black
  20. }
  21. enum LineType : Int {
  22. case h1, h2, h3, h4, h5, h6, body
  23. }
  24. enum LineStyle : Int {
  25. case none
  26. case italic
  27. case bold
  28. case code
  29. case link
  30. static func styleFromString(_ string : String ) -> LineStyle {
  31. if string == "**" || string == "__" {
  32. return .bold
  33. } else if string == "*" || string == "_" {
  34. return .italic
  35. } else if string == "`" {
  36. return .code
  37. } else if string == "[" {
  38. return .link
  39. } else {
  40. return .none
  41. }
  42. }
  43. }
  44. /// 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.
  45. open class SwiftyMarkdown {
  46. /// The styles to apply to any H1 headers found in the Markdown
  47. open var h1 = BasicStyles()
  48. /// The styles to apply to any H2 headers found in the Markdown
  49. open var h2 = BasicStyles()
  50. /// The styles to apply to any H3 headers found in the Markdown
  51. open var h3 = BasicStyles()
  52. /// The styles to apply to any H4 headers found in the Markdown
  53. open var h4 = BasicStyles()
  54. /// The styles to apply to any H5 headers found in the Markdown
  55. open var h5 = BasicStyles()
  56. /// The styles to apply to any H6 headers found in the Markdown
  57. open var h6 = BasicStyles()
  58. /// The default body styles. These are the base styles and will be used for e.g. headers if no other styles override them.
  59. open var body = BasicStyles()
  60. /// The styles to apply to any links found in the Markdown
  61. open var link = BasicStyles()
  62. /// The styles to apply to any bold text found in the Markdown
  63. open var bold = BasicStyles()
  64. /// The styles to apply to any italic text found in the Markdown
  65. open var italic = BasicStyles()
  66. /// The styles to apply to any code blocks or inline code text found in the Markdown
  67. open var code = BasicStyles()
  68. var currentType : LineType = .body
  69. let string : String
  70. let instructionSet = CharacterSet(charactersIn: "[\\*_`")
  71. /**
  72. - parameter string: A string containing [Markdown](https://daringfireball.net/projects/markdown/) syntax to be converted to an NSAttributedString
  73. - returns: An initialized SwiftyMarkdown object
  74. */
  75. public init(string : String ) {
  76. self.string = string
  77. }
  78. /**
  79. A failable initializer that takes a URL and attempts to read it as a UTF-8 string
  80. - parameter url: The location of the file to read
  81. - returns: An initialized SwiftyMarkdown object, or nil if the string couldn't be read
  82. */
  83. public init?(url : URL ) {
  84. do {
  85. self.string = try NSString(contentsOf: url, encoding: String.Encoding.utf8.rawValue) as String
  86. } catch {
  87. self.string = ""
  88. return nil
  89. }
  90. }
  91. /**
  92. 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.
  93. - returns: An NSAttributedString with the styles applied
  94. */
  95. open func attributedString() -> NSAttributedString {
  96. let attributedString = NSMutableAttributedString(string: "")
  97. let lines = self.string.components(separatedBy: CharacterSet.newlines)
  98. var lineCount = 0
  99. let headings = ["# ", "## ", "### ", "#### ", "##### ", "###### "]
  100. var skipLine = false
  101. for theLine in lines {
  102. lineCount += 1
  103. if skipLine {
  104. skipLine = false
  105. continue
  106. }
  107. var line = theLine == "" ? " " : theLine
  108. for heading in headings {
  109. if let range = line.range(of: heading) , range.lowerBound == line.startIndex {
  110. let startHeadingString = line.replacingCharacters(in: range, with: "")
  111. // Remove ending
  112. let endHeadingString = heading.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines)
  113. line = startHeadingString.replacingOccurrences(of: endHeadingString, with: "").trimmingCharacters(in: CharacterSet.whitespacesAndNewlines)
  114. currentType = LineType(rawValue: headings.index(of: heading)!)!
  115. // We found a heading so break out of the inner loop
  116. break
  117. }
  118. }
  119. // Look for underlined headings
  120. if lineCount < lines.count {
  121. let nextLine = lines[lineCount]
  122. if let range = nextLine.range(of: "=") , range.lowerBound == nextLine.startIndex {
  123. // Make H1
  124. currentType = .h1
  125. // We need to skip the next line
  126. skipLine = true
  127. }
  128. if let range = nextLine.range(of: "-") , range.lowerBound == nextLine.startIndex {
  129. // Make H2
  130. currentType = .h2
  131. // We need to skip the next line
  132. skipLine = true
  133. }
  134. }
  135. // If this is not an empty line...
  136. if line.characters.count > 0 {
  137. // ...start scanning
  138. let scanner = Scanner(string: line)
  139. // We want to be aware of spaces
  140. scanner.charactersToBeSkipped = nil
  141. while !scanner.isAtEnd {
  142. var string : NSString?
  143. // Get all the characters up to the ones we are interested in
  144. if scanner.scanUpToCharacters(from: instructionSet, into: &string) {
  145. if let hasString = string as String? {
  146. let bodyString = attributedStringFromString(hasString, withStyle: .none)
  147. attributedString.append(bodyString)
  148. let location = scanner.scanLocation
  149. let matchedCharacters = tagFromScanner(scanner).foundCharacters
  150. // If the next string after the characters is a space, then add it to the final string and continue
  151. let set = NSMutableCharacterSet.whitespace()
  152. set.formUnion(with: CharacterSet.punctuationCharacters)
  153. if scanner.scanUpToCharacters(from: set as CharacterSet, into: nil) {
  154. scanner.scanLocation = location
  155. attributedString.append(self.attributedStringFromScanner(scanner))
  156. } else if matchedCharacters == "[" {
  157. scanner.scanLocation = location
  158. attributedString.append(self.attributedStringFromScanner(scanner))
  159. } else {
  160. let charAtts = attributedStringFromString(matchedCharacters, withStyle: .none)
  161. attributedString.append(charAtts)
  162. }
  163. }
  164. } else {
  165. attributedString.append(self.attributedStringFromScanner(scanner, atStartOfLine: true))
  166. }
  167. }
  168. }
  169. // Append a new line character to the end of the processed line
  170. // if lineCount < lines.count {
  171. attributedString.append(NSAttributedString(string: "\n"))
  172. // }
  173. currentType = .body
  174. }
  175. return attributedString
  176. }
  177. func attributedStringFromScanner( _ scanner : Scanner, atStartOfLine start : Bool = false) -> NSAttributedString {
  178. var followingString : NSString?
  179. let results = self.tagFromScanner(scanner)
  180. var style = LineStyle.styleFromString(results.foundCharacters)
  181. var attributes = [String : AnyObject]()
  182. if style == .link {
  183. var linkText : NSString?
  184. var linkURL : NSString?
  185. let linkCharacters = CharacterSet(charactersIn: "]()")
  186. scanner.scanUpToCharacters(from: linkCharacters, into: &linkText)
  187. scanner.scanCharacters(from: linkCharacters, into: nil)
  188. scanner.scanUpToCharacters(from: linkCharacters, into: &linkURL)
  189. scanner.scanCharacters(from: linkCharacters, into: nil)
  190. if let hasLink = linkText, let hasURL = linkURL {
  191. followingString = hasLink
  192. attributes[NSLinkAttributeName] = hasURL
  193. } else {
  194. style = .none
  195. }
  196. } else {
  197. scanner.scanUpToCharacters(from: instructionSet, into: &followingString)
  198. }
  199. let attributedString = attributedStringFromString(results.escapedCharacters, withStyle: style).mutableCopy() as! NSMutableAttributedString
  200. if let hasString = followingString as String? {
  201. let prefix = ( style == .code && start ) ? "\t" : ""
  202. let attString = attributedStringFromString(prefix + hasString, withStyle: style, attributes: attributes)
  203. attributedString.append(attString)
  204. }
  205. let suffix = self.tagFromScanner(scanner)
  206. attributedString.append(attributedStringFromString(suffix.escapedCharacters, withStyle: style))
  207. return attributedString
  208. }
  209. func tagFromScanner( _ scanner : Scanner ) -> (foundCharacters : String, escapedCharacters : String) {
  210. var matchedCharacters : String = ""
  211. var tempCharacters : NSString?
  212. // Scan the ones we are interested in
  213. while scanner.scanCharacters(from: instructionSet, into: &tempCharacters) {
  214. if let chars = tempCharacters as String? {
  215. matchedCharacters = matchedCharacters + chars
  216. }
  217. }
  218. var foundCharacters : String = ""
  219. while matchedCharacters.contains("\\") {
  220. if let hasRange = matchedCharacters.range(of: "\\") {
  221. if matchedCharacters.characters.count > 1 {
  222. let newRange = hasRange.lowerBound..<matchedCharacters.index(hasRange.upperBound, offsetBy: 1)
  223. foundCharacters = foundCharacters + matchedCharacters.substring(with: newRange)
  224. matchedCharacters.removeSubrange(newRange)
  225. } else {
  226. break
  227. }
  228. }
  229. }
  230. return (matchedCharacters, foundCharacters.replacingOccurrences(of: "\\", with: ""))
  231. }
  232. // Make H1
  233. func attributedStringFromString(_ string : String, withStyle style : LineStyle, attributes : [String : AnyObject] = [:] ) -> NSAttributedString {
  234. let textStyle : UIFontTextStyle
  235. var fontName : String?
  236. var attributes = attributes
  237. // What type are we and is there a font name set?
  238. switch currentType {
  239. case .h1:
  240. fontName = h1.fontName
  241. if #available(iOS 9, *) {
  242. textStyle = UIFontTextStyle.title1
  243. } else {
  244. textStyle = UIFontTextStyle.headline
  245. }
  246. attributes[NSForegroundColorAttributeName] = h1.color
  247. case .h2:
  248. fontName = h2.fontName
  249. if #available(iOS 9, *) {
  250. textStyle = UIFontTextStyle.title2
  251. } else {
  252. textStyle = UIFontTextStyle.headline
  253. }
  254. attributes[NSForegroundColorAttributeName] = h2.color
  255. case .h3:
  256. fontName = h3.fontName
  257. if #available(iOS 9, *) {
  258. textStyle = UIFontTextStyle.title2
  259. } else {
  260. textStyle = UIFontTextStyle.subheadline
  261. }
  262. attributes[NSForegroundColorAttributeName] = h3.color
  263. case .h4:
  264. fontName = h4.fontName
  265. textStyle = UIFontTextStyle.headline
  266. attributes[NSForegroundColorAttributeName] = h4.color
  267. case .h5:
  268. fontName = h5.fontName
  269. textStyle = UIFontTextStyle.subheadline
  270. attributes[NSForegroundColorAttributeName] = h5.color
  271. case .h6:
  272. fontName = h6.fontName
  273. textStyle = UIFontTextStyle.footnote
  274. attributes[NSForegroundColorAttributeName] = h6.color
  275. default:
  276. fontName = body.fontName
  277. textStyle = UIFontTextStyle.body
  278. attributes[NSForegroundColorAttributeName] = body.color
  279. break
  280. }
  281. // Check for code
  282. if style == .code {
  283. fontName = code.fontName
  284. attributes[NSForegroundColorAttributeName] = code.color
  285. }
  286. if style == .link {
  287. fontName = link.fontName
  288. attributes[NSForegroundColorAttributeName] = link.color
  289. }
  290. // Fallback to body
  291. if let _ = fontName {
  292. } else {
  293. fontName = body.fontName
  294. }
  295. let font = UIFont.preferredFont(forTextStyle: textStyle)
  296. let styleDescriptor = font.fontDescriptor
  297. let styleSize = styleDescriptor.fontAttributes[UIFontDescriptorSizeAttribute] as? CGFloat ?? CGFloat(14)
  298. var finalFont : UIFont
  299. if let finalFontName = fontName, let font = UIFont(name: finalFontName, size: styleSize) {
  300. finalFont = font
  301. } else {
  302. finalFont = UIFont.preferredFont(forTextStyle: textStyle)
  303. }
  304. let finalFontDescriptor = finalFont.fontDescriptor
  305. if style == .italic {
  306. if let italicDescriptor = finalFontDescriptor.withSymbolicTraits(.traitItalic) {
  307. finalFont = UIFont(descriptor: italicDescriptor, size: styleSize)
  308. }
  309. }
  310. if style == .bold {
  311. if let boldDescriptor = finalFontDescriptor.withSymbolicTraits(.traitBold) {
  312. finalFont = UIFont(descriptor: boldDescriptor, size: styleSize)
  313. }
  314. }
  315. attributes[NSFontAttributeName] = finalFont
  316. return NSAttributedString(string: string, attributes: attributes)
  317. }
  318. }