Эх сурвалжийг харах

Implements new tokenisation engine and updates readme

Simon Fairbairn 5 жил өмнө
parent
commit
6d6f83d4b0

+ 98 - 3
README.md

@@ -1,22 +1,32 @@
-# SwiftyMarkdown
+# SwiftyMarkdown 1.0
 
-SwiftyMarkdown converts Markdown files and strings into NSAttributedString using sensible defaults and a Swift-style syntax. It uses dynamic type to set the font size correctly with whatever font you'd like to use
+SwiftyMarkdown converts Markdown files and strings into NSAttributedString using sensible defaults and a Swift-style syntax. It uses dynamic type to set the font size correctly with whatever font you'd like to use.
+
+## Fully Rebuilt For 2020!
+
+Now features a more robust, rules-based line processing and tokenisation engine. Now supports images, codeblocks, blockquotes, and unordered lists!
 
 ## Installation
 
-CocoaPods:
+### CocoaPods:
 
 `pod 'SwiftyMarkdown'`
 
+### SPM: 
+
+In Xcode, `File -> Swift Packages -> Add Package Dependency` and add the GitHub URL. 
+
 ## Usage
 
 Text string
+
 ```swift
 let md = SwiftyMarkdown(string: "# Heading\nMy *Markdown* string")
 md.attributedString()
 ```
 
 URL 
+
 ```swift
 if let url = Bundle.main.url(forResource: "file", withExtension: "md"), md = SwiftyMarkdown(url: url ) {
 	md.attributedString()
@@ -37,6 +47,13 @@ if let url = Bundle.main.url(forResource: "file", withExtension: "md"), md = Swi
     
     `code`
     [Links](http://voyagetravelapps.com/)
+    ![Images](<Name of asset in bundle, or URL>)
+    
+    > Blockquotes
+		
+		Indented code blocks
+
+  
 
 ## Customisation 
 ```swift
@@ -52,4 +69,82 @@ md.h1.fontSize = 16
 
 ![Screenshot](http://f.cl.ly/items/12332k3f2s0s0C281h2u/swiftymarkdown.png)
 
+## Advanced Customisation
+
+SwiftyMarkdown uses a rules-based line processing and customisation engine that is no longer limited to Markdown. Rules are processed in order, from top to bottom. Line processing happens first, then character styles are applied based on the character rules. 
+
+For example, here's how a small subset of Markdown line tags are set up within SwiftyMarkdown:
+
+	enum MarkdownLineStyle : LineStyling {
+		case h1
+		case h2
+		case previousH1
+		case codeblock
+		case body
+		
+		var shouldTokeniseLine: Bool {
+			switch self {
+			case .codeblock:
+				return false
+			default:
+				return true
+			}
+		}
+		
+		func styleIfFoundStyleAffectsPreviousLine() -> LineStyling? {
+			switch self {
+			case .previousH1:
+				return MarkdownLineStyle.h1
+			default :
+				return nil
+			}
+		}
+	}
+
+	static let lineRules = [
+		LineRule(token: "    ",type : MarkdownLineStyle.codeblock, removeFrom: .leading),
+		LineRule(token: "=",type : MarkdownLineStyle.previousH1, removeFrom: .entireLine, changeAppliesTo: .previous),
+		LineRule(token: "## ",type : MarkdownLineStyle.h2, removeFrom: .both),
+		LineRule(token: "# ",type : MarkdownLineStyle.h1, removeFrom: .both)
+	]
+	
+	let lineProcessor = SwiftyLineProcessor(rules: SwiftyMarkdown.lineRules, default: MarkdownLineStyle.body)
+	
+Similarly, the character styles all follow rules:
+	
+	enum CharacterStyle : CharacterStyling {
+		case link, bold, italic, code
+	}
+	
+	static let characterRules = [
+		CharacterRule(openTag: "[", intermediateTag: "](", closingTag: ")", escapeCharacter: "\\", styles: [1 : [CharacterStyle.link]], maxTags: 1),
+		CharacterRule(openTag: "`", intermediateTag: nil, closingTag: nil, escapeCharacter: "\\", styles: [1 : [CharacterStyle.code]], maxTags: 1),
+		CharacterRule(openTag: "*", intermediateTag: nil, closingTag: nil, escapeCharacter: "\\", styles: [1 : [CharacterStyle.italic], 2 : [CharacterStyle.bold], 3 : [CharacterStyle.bold, CharacterStyle.italic]], maxTags: 3),
+		CharacterRule(openTag: "_", intermediateTag: nil, closingTag: nil, escapeCharacter: "\\", styles: [1 : [CharacterStyle.italic], 2 : [CharacterStyle.bold], 3 : [CharacterStyle.bold, CharacterStyle.italic]], maxTags: 3)
+	]
+
+If you wanted to create a rule that applied a style of `Elf` to a range of characters between "The elf will speak now: %Here is my elf speaking%", you could set things up like this:
+
+	enum Characters : CharacterStyling {
+		case elf
+	}
+	
+	let characterRules = [
+		CharacterRule(openTag: "%", intermediateTag: nil, closingTag: nil, escapeCharacter: "\\", styles: [1 : [CharacterStyle.elf]], maxTags: 1)
+	]
+	
+	let processor = SwiftyTokeniser( with : characterRules )
+	let string = "The elf will speak now: %Here is my elf speaking%"
+	let tokens = processor.process(string)
+
+The output is an array of tokens would be equivalent to:
+
+	[
+		Token(type: .string, inputString: "The elf will speak now: ", characterStyles: []),
+		Token(type: .openTag, inputString: "%", characterStyles: []),
+		Token(type: .string, inputString: "Here is my elf speaking", characterStyles: [.elf]),
+		Token(type: .openTag, inputString: "%", characterStyles: [])
+	]
+
+
 

+ 498 - 11
SwiftyMarkdown.playground/Pages/Fucking Again.xcplaygroundpage/Contents.swift

@@ -1,10 +1,477 @@
 //: [Previous](@previous)
 
+
 import Foundation
+import os.log
 
+extension OSLog {
+	private static var subsystem = "SwiftyTokeniser"
+	static let tokenising = OSLog(subsystem: subsystem, category: "Tokenising")
+	static let styling = OSLog(subsystem: subsystem, category: "Styling")
+}
 
 // Tag definition
+public protocol CharacterStyling {
+	
+}
+
+public enum SpaceAllowed {
+	case no
+	case bothSides
+	case oneSide
+	case leadingSide
+	case trailingSide
+}
+
+public enum Cancel {
+    case none
+    case allRemaining
+    case currentSet
+}
+
+public struct SwiftyTagging {
+	public let openTag : String
+	public let intermediateTag : String?
+	public let closingTag : String?
+	public let escapeCharacter : Character?
+	public let styles : [Int : [CharacterStyling]]
+	public var maxTags : Int = 1
+	public var spacesAllowed : SpaceAllowed = .oneSide
+	public var cancels : Cancel = .none
+	
+	public init(openTag: String, intermediateTag: String? = nil, closingTag: String? = nil, escapeCharacter: Character? = nil, styles: [Int : [CharacterStyling]] = [:], maxTags : Int = 1) {
+		self.openTag = openTag
+		self.intermediateTag = intermediateTag
+		self.closingTag = closingTag
+		self.escapeCharacter = escapeCharacter
+		self.styles = styles
+		self.maxTags = maxTags
+	}
+}
+
+// Token definition
+public enum TokenType {
+	case openTag
+	case intermediateTag
+	case closeTag
+	case processed
+	case string
+	case escape
+	case metadata
+}
+
+
+
+public struct Token {
+	public let id = UUID().uuidString
+	public var type : TokenType
+	public let inputString : String
+	public var metadataString : String? = nil
+	public var characterStyles : [CharacterStyling] = []
+	public var count : Int = 0
+	public var shouldSkip : Bool = false
+	public var outputString : String {
+		get {
+			switch self.type {
+			case .openTag, .closeTag, .intermediateTag:
+				if count == 0 {
+					return ""
+				} else {
+					let range = inputString.startIndex..<inputString.index(inputString.startIndex, offsetBy: self.count)
+					return String(inputString[range])
+				}
+			case .metadata, .processed:
+				return ""
+			case .escape, .string:
+				return inputString
+			}
+		}
+	}
+	public init( type : TokenType, inputString : String, characterStyles : [CharacterStyling] = []) {
+		self.type = type
+		self.inputString = inputString
+		self.characterStyles = characterStyles
+	}
+}
+
+public class SwiftyTokeniser {
+	let rules : [SwiftyTagging]
+	
+	public init( with rules : [SwiftyTagging] ) {
+		self.rules = rules
+	}
+	
+	public func process( _ inputString : String ) -> [Token] {
+		guard rules.count > 0 else {
+			return [Token(type: .string, inputString: inputString)]
+		}
+
+		var currentTokens : [Token] = []
+		var mutableRules = self.rules
+		while !mutableRules.isEmpty {
+			let nextRule = mutableRules.removeFirst()
+			if currentTokens.isEmpty {
+				// This means it's the first time through
+				currentTokens = self.applyStyles(to: self.scan(inputString, with: nextRule), usingRule: nextRule)
+				continue
+			}
+			
+			// Each string could have additional tokens within it, so they have to be scanned as well with the current rule.
+			// The one string token might then be exploded into multiple more tokens
+			var replacements : [Int : [Token]] = [:]
+			for (idx,token) in currentTokens.enumerated() {
+				switch token.type {
+				case .string:
+					let nextTokens = self.scan(token.outputString, with: nextRule)
+					replacements[idx] = self.applyStyles(to: nextTokens, usingRule: nextRule)
+				default:
+					break
+				}
+			}
+			
+			// This replaces the individual string tokens with the new token arrays
+			// making sure to apply any previously found styles to the new tokens.
+			for key in replacements.keys.sorted(by: { $0 > $1 }) {
+				let existingToken = currentTokens[key]
+				var newTokens : [Token] = []
+				for token in replacements[key]! {
+					var newToken = token
+					newToken.characterStyles.append(contentsOf: existingToken.characterStyles)
+					newTokens.append(newToken)
+				}
+				currentTokens.replaceSubrange(key...key, with: newTokens)
+			}
+		}
+		return currentTokens
+	}
+	
+	func handleClosingTagFromOpenTag(withIndex index : Int, in tokens: inout [Token], following rule : SwiftyTagging ) {
+		
+		guard rule.closingTag != nil else {
+			return
+		}
+		
+		var metadataIndex = index
+		// If there's an intermediate tag, get the index of that
+		if rule.intermediateTag != nil {
+			guard let nextTokenIdx = tokens.firstIndex(where: { $0.type == .intermediateTag }) else {
+				return
+			}
+			metadataIndex = nextTokenIdx
+			let styles : [CharacterStyling] = rule.styles[1] ?? []
+			for i in index..<nextTokenIdx {
+				for style in styles {
+					tokens[i].characterStyles.append(style)
+				}
+			}
+		}
+		
+		guard let closeTokenIdx = tokens.firstIndex(where: { $0.type == .closeTag }) else {
+			return
+		}
+		var metadataString : String = ""
+		for i in metadataIndex..<closeTokenIdx {
+			var otherTokens = tokens[i]
+			otherTokens.type = .metadata
+			tokens[i] = otherTokens
+			metadataString.append(otherTokens.outputString)
+		}
+		tokens[closeTokenIdx].type = .processed
+		tokens[metadataIndex].type = .processed
+		tokens[index].type = .processed
+		tokens[index].metadataString = metadataString
+		
+		
+		
+	}
+	
+	
+	func applyStyles( to tokens : [Token], usingRule rule : SwiftyTagging ) -> [Token] {
+		var nextTokens : [Token] = []
+		var mutableTokens : [Token] = tokens
+		print( tokens.map( { ( $0.outputString, $0.count )}))
+		for idx in 0..<mutableTokens.count {
+			let token = mutableTokens[idx]
+			switch token.type {
+			case .escape:
+				print( "Found escape (\(token.inputString))" )
+				nextTokens.append(token)
+			case .openTag:
+				let theToken = mutableTokens[idx]
+				print ("Found open tag with tag count \(theToken.count) tags: \(theToken.inputString). Current rule open tag = \(rule.openTag)" )
+				
+				guard rule.closingTag == nil else {
+					
+					// If there's an intermediate tag, get the index of that
+					self.handleClosingTagFromOpenTag(withIndex: idx, in: &mutableTokens, following: rule)
+					// Get the index of the closing tag
+					
+					continue
+				}
+				
+				guard theToken.count > 0 else {
+					nextTokens.append(theToken)
+					continue
+				}
+				
+				let startIdx = idx
+				var endIdx : Int? = nil
+				
+				if let nextTokenIdx = mutableTokens.firstIndex(where: { $0.inputString == theToken.inputString && $0.type == theToken.type && $0.count == theToken.count && $0.id != theToken.id }) {
+					endIdx = nextTokenIdx
+				}
+				guard let existentEnd = endIdx else {
+					nextTokens.append(theToken)
+					continue
+				}
+				
+				let styles : [CharacterStyling] = rule.styles[theToken.count] ?? []
+				for i in startIdx..<existentEnd {
+					var otherTokens = mutableTokens[i]
+					for style in styles {
+						otherTokens.characterStyles.append(style)
+					}
+					mutableTokens[i] = otherTokens
+				}
+				var newToken = theToken
+				newToken.count = 0
+				nextTokens.append(newToken)
+				mutableTokens[idx] = newToken
+				
+				var closeToken = mutableTokens[existentEnd]
+				closeToken.count = 0
+				mutableTokens[existentEnd] = closeToken
+			case .intermediateTag:
+				let theToken = mutableTokens[idx]
+				print ("Found intermediate tag with tag count \(theToken.count) tags: \(theToken.inputString)" )
+				nextTokens.append(theToken)
+			case .closeTag:
+				let theToken = mutableTokens[idx]
+				print ("Found close tag with tag count \(theToken.count) tags: \(theToken.inputString)" )
+				nextTokens.append(theToken)
+			case .string:
+				let theToken = mutableTokens[idx]
+				print ("Found String: \(theToken.inputString)" )
+				nextTokens.append(theToken)
+			case .metadata:
+				let theToken = mutableTokens[idx]
+				print ("Found metadata: \(theToken.inputString)" )
+				nextTokens.append(theToken)
+			case .processed:
+				let theToken = mutableTokens[idx]
+				print ("Found already processed tag: \(theToken.inputString)" )
+				nextTokens.append(theToken)
+			}
+		}
+		return nextTokens
+	}
+	
+	
+	func scan( _ string : String, with rule : SwiftyTagging) -> [Token] {
+		let scanner = Scanner(string: string)
+		scanner.charactersToBeSkipped = nil
+		var tokens : [Token] = []
+		var set = CharacterSet(charactersIn: "\(rule.openTag)\(rule.intermediateTag ?? "")\(rule.closingTag ?? "")")
+		if let existentEscape = rule.escapeCharacter {
+			set.insert(charactersIn: String(existentEscape))
+		}
+		
+		var openingString = ""
+		while !scanner.isAtEnd {
+			
+			if #available(iOS 13.0, *) {
+				if let start = scanner.scanUpToCharacters(from: set) {
+					openingString.append(start)
+				}
+			} else {
+				var string : NSString?
+				scanner.scanUpToCharacters(from: set, into: &string)
+				if let existentString = string as String? {
+					openingString.append(existentString)
+				}
+				// Fallback on earlier versions
+			}
+			
+			let lastChar : String?
+			if #available(iOS 13.0, *) {
+				lastChar = ( scanner.currentIndex > string.startIndex ) ? String(string[string.index(before: scanner.currentIndex)..<scanner.currentIndex]) : nil
+			} else {
+				let scanLocation = string.index(string.startIndex, offsetBy: scanner.scanLocation)
+				lastChar = ( scanLocation > string.startIndex ) ? String(string[string.index(before: scanLocation)..<scanLocation]) : nil
+			}
+			let maybeFoundChars : String?
+			if #available(iOS 13.0, *) {
+				maybeFoundChars = scanner.scanCharacters(from: set )
+			} else {
+				var string : NSString?
+				scanner.scanCharacters(from: set, into: &string)
+				maybeFoundChars = string as String?
+			}
+			
+			let nextChar : String?
+			if #available(iOS 13.0, *) {
+				 nextChar = (scanner.currentIndex != string.endIndex) ? String(string[scanner.currentIndex]) : nil
+			} else {
+				let scanLocation = string.index(string.startIndex, offsetBy: scanner.scanLocation)
+				nextChar = (scanLocation != string.endIndex) ? String(string[scanLocation]) : nil
+			}
+			
+			guard let foundChars = maybeFoundChars else {
+				tokens.append(Token(type: .string, inputString: "\(openingString)"))
+				continue
+			}
+			
+			
+			
+			if !validateSpacing(nextCharacter: nextChar, previousCharacter: lastChar, with: rule) {
+				let escapeString = String("\(rule.escapeCharacter ?? Character(""))")
+				var escaped = foundChars.replacingOccurrences(of: "\(escapeString)\(rule.openTag)", with: rule.openTag)
+				if let hasIntermediateTag = rule.intermediateTag {
+					escaped = foundChars.replacingOccurrences(of: "\(escapeString)\(hasIntermediateTag)", with: hasIntermediateTag)
+				}
+				if let existentClosingTag = rule.closingTag {
+					escaped = foundChars.replacingOccurrences(of: "\(escapeString)\(existentClosingTag)", with: existentClosingTag)
+				}
+				
+				openingString.append(escaped)
+				continue
+			}
 
+			var cumulativeString = ""
+			var openString = ""
+			var intermediateString = ""
+			var closedString = ""
+			var maybeEscapeNext = false
+			
+			
+			func addToken( for type : TokenType ) {
+				var inputString : String
+				switch type {
+				case .openTag:
+					inputString = openString
+				case .intermediateTag:
+					inputString = intermediateString
+				case .closeTag:
+					inputString = closedString
+				default:
+					inputString = ""
+				}
+				guard !inputString.isEmpty else {
+					return
+				}
+				if !openingString.isEmpty {
+					tokens.append(Token(type: .string, inputString: "\(openingString)"))
+					openingString = ""
+				}
+				var token = Token(type: type, inputString: inputString)
+				if rule.closingTag == nil {
+					token.count = inputString.count
+				}
+				
+				tokens.append(token)
+				
+				switch type {
+				case .openTag:
+					openString = ""
+				case .intermediateTag:
+					intermediateString = ""
+				case .closeTag:
+					closedString = ""
+				default:
+					break
+				}
+			}
+			
+			// Here I am going through and adding the characters in the found set to a cumulative string.
+			// If there is an escape character, then the loop stops and any open tags are tokenised.
+			for char in foundChars {
+				cumulativeString.append(char)
+				if maybeEscapeNext {
+					
+					var escaped = cumulativeString
+					if String(char) == rule.openTag || String(char) == rule.intermediateTag || String(char) == rule.closingTag {
+						escaped = String(cumulativeString.replacingOccurrences(of: String(rule.escapeCharacter ?? Character("")), with: ""))
+					}
+					
+					openingString.append(escaped)
+					cumulativeString = ""
+					maybeEscapeNext = false
+				}
+				if let existentEscape = rule.escapeCharacter {
+					if cumulativeString == String(existentEscape) {
+						maybeEscapeNext = true
+						addToken(for: .openTag)
+						addToken(for: .intermediateTag)
+						addToken(for: .closeTag)
+						continue
+					}
+				}
+				
+				
+				if cumulativeString == rule.openTag {
+					openString.append(char)
+					cumulativeString = ""
+				} else if cumulativeString == rule.intermediateTag {
+					intermediateString.append(cumulativeString)
+					cumulativeString = ""
+				} else if cumulativeString == rule.closingTag {
+					closedString.append(char)
+					cumulativeString = ""
+				}
+			}
+			
+			// If we're here, it means that an escape character was found but without a corresponding
+			// tag, which means it might belong to a different rule.
+			// It should be added to the next group of regular characters
+			if maybeEscapeNext {
+				openingString.append( cumulativeString )
+			}
+			addToken(for: .openTag)
+			addToken(for: .intermediateTag)
+			addToken(for: .closeTag)
+		
+		}
+		return tokens
+	}
+	
+	func validateSpacing( nextCharacter : String?, previousCharacter : String?, with rule : SwiftyTagging ) -> Bool {
+		switch rule.spacesAllowed {
+		case .leadingSide:
+			guard nextCharacter != nil else {
+				return true
+			}
+			if nextCharacter == " "  {
+				return false
+			}
+		case .trailingSide:
+			guard previousCharacter != nil else {
+				return true
+			}
+			if previousCharacter == " " {
+				return false
+			}
+		case .no:
+			switch (previousCharacter, nextCharacter) {
+			case (nil, nil), ( " ", _ ), (  _, " " ):
+				return false
+			default:
+				return true
+			}
+		
+		case .oneSide:
+			switch (previousCharacter, nextCharacter) {
+			case  (nil, " " ), (" ", nil), (" ", " " ):
+				return false
+			default:
+				return true
+			}
+		default:
+			break
+		}
+		return true
+	}
+	
+}
 
 
 // Example customisation
@@ -13,6 +480,8 @@ public enum CharacterStyle : CharacterStyling {
 	case bold
 	case italic
 	case code
+	case link
+	
 }
 
 
@@ -46,9 +515,7 @@ let challenge3 = Test(input: " * ", output : " * ", tokens : [
 ])
 let challenge4 = Test(input: "**AAAA*BB\\*BB*AAAAAA**", output : "AAAABB*BBAAAAAA", tokens : [
 	Token(type: .string, inputString: "AAAA", characterStyles: [CharacterStyle.bold]),
-	Token(type: .string, inputString: "BB", characterStyles: [CharacterStyle.bold, CharacterStyle.italic]),
-	Token(type: .string, inputString: "*", characterStyles: [CharacterStyle.bold, CharacterStyle.italic]),
-	Token(type: .string, inputString: "BB", characterStyles: [CharacterStyle.bold, CharacterStyle.italic]),
+	Token(type: .string, inputString: "BB*BB", characterStyles: [CharacterStyle.bold, CharacterStyle.italic]),
 	Token(type: .string, inputString: "AAAAAA", characterStyles: [CharacterStyle.bold]),
 ])
 let challenge5 = Test(input: "*Italic* \\_\\_Not Bold\\_\\_ **Bold**", output : "Italic __Not Bold__ Bold", tokens : [
@@ -67,29 +534,44 @@ let challenge7 = Test(input: " *\\**Italic*\\** ", output : " *Italic* ", tokens
 	Token(type: .string, inputString: " ", characterStyles: []),
 ])
 
+let challenge8 = Test(input: "[*Link*](https://www.neverendingvoyage.com/)", output : "Link", tokens : [
+	Token(type: .string, inputString: "Link", characterStyles: [CharacterStyle.link, CharacterStyle.italic])
+])
 
-let challenges = [challenge1]
+let challenge9 = Test(input: "`Code (should not be indented)`", output: "Code (should not be indented)", tokens: [
+	Token(type: .string, inputString: "Link", characterStyles: [CharacterStyle.link, CharacterStyle.italic])
+])
 
-var codeblock = SwiftyTagging(openTag: "`", intermediateTag: nil, closingTag: nil, escapeString: "\\", styles: [1 : [CharacterStyle.code]], maxTags: 1)
+let challenge10 = Test(input: "A string with a **bold** word", output: "A string with a bold word",  tokens: [
+	Token(type: .string, inputString: "A string with a ", characterStyles: []),
+	Token(type: .string, inputString: "bold", characterStyles: [CharacterStyle.bold]),
+	Token(type: .string, inputString: " word", characterStyles: [])
+])
+
+
+let challenges = [challenge10]
+
+var links = SwiftyTagging(openTag: "[", intermediateTag: "](", closingTag: ")", escapeCharacter: "\\", styles: [1 : [CharacterStyle.link]], maxTags: 1)
+var codeblock = SwiftyTagging(openTag: "`", intermediateTag: nil, closingTag: nil, escapeCharacter: "\\", styles: [1 : [CharacterStyle.code]], maxTags: 1)
 codeblock.cancels = .allRemaining
-let asterisks = SwiftyTagging(openTag: "*", intermediateTag: nil, closingTag: "*", escapeString: "\\", styles: [1 : [.italic], 2 : [.bold], 3 : [CharacterStyle.bold, CharacterStyle.italic]], maxTags: 3)
-let underscores = SwiftyTagging(openTag: "_", intermediateTag: nil, closingTag: nil, escapeString: "\\", styles: [1 : [.italic], 2 : [.bold], 3 : [CharacterStyle.bold, CharacterStyle.italic]], maxTags: 3)
+let asterisks = SwiftyTagging(openTag: "*", intermediateTag: nil, closingTag: nil, escapeCharacter: "\\", styles: [1 : [.italic], 2 : [.bold], 3 : [CharacterStyle.bold, CharacterStyle.italic]], maxTags: 3)
+let underscores = SwiftyTagging(openTag: "_", intermediateTag: nil, closingTag: nil, escapeCharacter: "\\", styles: [1 : [.italic], 2 : [.bold], 3 : [CharacterStyle.bold, CharacterStyle.italic]], maxTags: 3)
 
-let scan = SwiftyTokeniser(with: [ asterisks, underscores])
+let scan = SwiftyTokeniser(with: [ links, asterisks])
 
 for challenge in challenges {
 	let finalTokens = scan.process(challenge.input)
 	let stringTokens = finalTokens.filter({ $0.type == .string })
 	
 	guard stringTokens.count == challenge.tokens.count else {
-		print("Token count check failed. Expected: \(challenge.tokens.count). Found: \(finalTokens.count)")
+		print("Token count check failed. Expected: \(challenge.tokens.count). Found: \(stringTokens.count)")
 		print("-------EXPECTED--------")
 		for token in challenge.tokens {
 			switch token.type {
 			case .string:
 				print("\(token.outputString): \(token.characterStyles)")
 			default:
-				break
+				print("\(token.outputString)")
 			}
 		}
 		print("-------OUTPUT--------")
@@ -98,11 +580,16 @@ for challenge in challenges {
 			case .string:
 				print("\(token.outputString): \(token.characterStyles)")
 			default:
-				break
+				if !token.outputString.isEmpty {
+					print("\(token.outputString)")
+				}
+				
 			}
 		}
 		continue
 	}
+	
+	print("-----EXPECTATIONS-----")
 	for (idx, token) in stringTokens.enumerated() {
 		let expected = challenge.tokens[idx]
 

+ 10 - 10
SwiftyMarkdown.playground/Pages/SwiftyMarkdown.xcplaygroundpage/Contents.swift

@@ -120,11 +120,11 @@ func replaceTokens( in line : SwiftyLine ) -> SwiftyLine {
     var replacementString : String = line.line
     
     var newTokens : [Token] = []
-    for var token in line.tokens {
-        replacementString = token.replaceToken(in: replacementString)
-        newTokens.append(token)
-    }
-    return Line(line: replacementString, lineStyle: line.lineStyle, tokens: newTokens)
+//    for var token in line.tokens {
+//        replacementString = token.replaceToken(in: replacementString)
+//        newTokens.append(token)
+//    }
+    return line // SwiftyLine(line: replacementString, lineStyle: line.lineStyle)
 }
 
 func tokenisePre13( _ line : SwiftyLine ) -> SwiftyLine {
@@ -135,9 +135,9 @@ func tokenisePre13( _ line : SwiftyLine ) -> SwiftyLine {
 func tokenise( _ line : SwiftyLine ) -> SwiftyLine {
     
     // Do nothing if it's a codeblock
-    if !line.lineStyle.tokenise {
-        return line
-    }
+//    if !line.lineStyle.tokenise {
+//        return line
+//    }
     var output : String = ""
     let textScanner = Scanner(string: line.line)
     textScanner.charactersToBeSkipped = nil
@@ -197,7 +197,7 @@ func tokenise( _ line : SwiftyLine ) -> SwiftyLine {
             break
         }
     }
-    return Line(line: output, lineStyle: line.lineStyle, tokens: tokens)
+    return line // SwiftyLine(line: output, lineStyle: line.lineStyle)
 }
 
 func handleLinks( in token : Token ) -> Token {
@@ -352,7 +352,7 @@ func process( _ tokens : [Token] ) -> [Token] {
     return doneTokens
 }
 
-func attributedString( for line : Line ) -> NSAttributedString {
+func attributedString( for line : SwiftyLine ) -> NSAttributedString {
     return NSAttributedString(string: line.line)
 }
 

+ 3 - 325
SwiftyMarkdown.playground/Pages/Untitled Page.xcplaygroundpage/Contents.swift

@@ -1,325 +1,3 @@
-//: Playground - noun: a place where people can play
-
-import UIKit
-import PlaygroundSupport
-
-let containerView = UIView(frame: CGRect(x: 0.0, y: 0.0, width: 400.0, height: 600))
-
-PlaygroundPage.current.liveView = containerView
-let label = UITextView(frame: containerView.frame)
-containerView.addSubview(label)
-
-var foundCharacters : String = ""
-var matchedCharacters : String = "\\Some string ''\\"
-if let hasRange = matchedCharacters.range(of: "\\") {
-	
-	let newRange  = hasRange.lowerBound..<hasRange.upperBound
-	foundCharacters = foundCharacters + matchedCharacters[newRange]
-	
-	matchedCharacters.removeSubrange(newRange)
-}
-
-
-
-//
-//public protocol FontProperties {
-//	var fontName : String { get set }
-//	var color : UIColor { get set }
-//}
-//
-//
-//public struct BasicStyles : FontProperties {
-//	public var fontName = UIFont.preferredFontForTextStyle(UIFontTextStyleBody).fontName
-//	public var color = UIColor.blackColor()
-//}
-//
-//enum LineType : Int {
-//	case H1, H2, H3, H4, H5, H6, Body, Italic, Bold, Code
-//}
-//
-//
-//public class SwiftyMarkdown {
-//	
-//	public var h1 = BasicStyles()
-//	public var h2 = BasicStyles()
-//	public var h3 = BasicStyles()
-//	public var h4 = BasicStyles()
-//	public var h5 = BasicStyles()
-//	public var h6 = BasicStyles()
-//	
-//	public var body = BasicStyles()
-//	public var link = BasicStyles()
-//	public var italic = BasicStyles()
-//	public var code = BasicStyles()
-//	public var bold = BasicStyles()
-//	
-//	let string : String
-//	let instructionSet = NSCharacterSet(charactersInString: "\\*_`")
-//	
-//	public init(string : String ) {
-//		self.string = string
-//	}
-//	
-//	public init?(url : NSURL ) {
-//		
-//		do {
-//			self.string = try NSString(contentsOfURL: url, encoding: NSUTF8StringEncoding) as String
-//			
-//		} catch {
-//			self.string = ""
-//			fatalError("Couldn't read string")
-//			return nil
-//		}
-//	}
-//	
-//	public func attributedString() -> NSAttributedString {
-//		let attributedString = NSMutableAttributedString(string: "")
-//		
-//		let lines = self.string.componentsSeparatedByCharactersInSet(NSCharacterSet.newlineCharacterSet())
-//		
-//		var lineCount = 0
-//		
-//		let headings = ["# ", "## ", "### ", "#### ", "##### ", "###### "]
-//		
-//		
-//		var skipLine = false
-//		for line in lines {
-//			lineCount++
-//			if skipLine {
-//				skipLine = false
-//				continue
-//			}
-//			var headingFound = false
-//			for heading in headings {
-//				
-//				if let range =  line.rangeOfString(heading) where range.startIndex == line.startIndex {
-//					
-//					let startHeadingString = line.stringByReplacingCharactersInRange(range, withString: "")
-//					let endHeadingHash = " " + heading.stringByTrimmingCharactersInSet(NSCharacterSet.whitespaceAndNewlineCharacterSet())
-//					
-//					let finalHeadingString = startHeadingString.stringByReplacingOccurrencesOfString(endHeadingHash, withString: "")
-//					
-//					// Make Hx where x == current index
-//					let string = attributedStringFromString(finalHeadingString, withType: LineType(rawValue: headings.indexOf(heading)!)!)
-//					attributedString.appendAttributedString(string)
-//					headingFound = true
-//				}
-//			}
-//			if headingFound {
-//				continue
-//			}
-//			
-//			
-//			if lineCount  < lines.count {
-//				let nextLine = lines[lineCount]
-//				
-//				if let range = nextLine.rangeOfString("=") where range.startIndex == nextLine.startIndex {
-//					// Make H1
-//					let string = attributedStringFromString(line, withType: .H1)
-//					attributedString.appendAttributedString(string)
-//					skipLine = true
-//					continue
-//				}
-//				
-//				if let range = nextLine.rangeOfString("-") where range.startIndex == nextLine.startIndex {
-//					
-//					
-//					// Make H1
-//					let string = attributedStringFromString(line, withType: .H2)
-//					attributedString.appendAttributedString(string)
-//					skipLine = true
-//					continue
-//				}
-//			}
-//			
-//			if line.characters.count > 0 {
-//				
-//				let scanner = NSScanner(string: line)
-//				
-//				
-//				scanner.charactersToBeSkipped = nil
-//
-//				while !scanner.atEnd {
-//					
-//					var followingString : NSString?
-//					var string : NSString?
-//					// Get all the characters up to the ones we are interested in
-//					if scanner.scanUpToCharactersFromSet(instructionSet, intoString: &string) {
-//						if let hasString = string as? String {
-//							let bodyString = attributedStringFromString(hasString, withType: .Body)
-//							attributedString.appendAttributedString(bodyString)
-//
-//							var matchedCharacters = self.tagFromScanner(scanner)
-//							
-//							
-//							let location = scanner.scanLocation
-//							// If the next string after the characters is a space, then add it to the final string and continue
-//							if !scanner.scanUpToString(" ", intoString: nil) {
-//								
-//								let charAtts = attributedStringFromString(matchedCharacters, withType: .Body)
-//								
-//								attributedString.appendAttributedString(charAtts)
-//							} else {
-//								scanner.scanLocation = location
-//								scanner.scanUpToCharactersFromSet(instructionSet, intoString: &followingString)
-//								if let hasString = followingString as? String {
-//									let attString : NSAttributedString
-//									
-//									if matchedCharacters.containsString("\\") {
-//										attString = attributedStringFromString(matchedCharacters + hasString, withType: .Body)
-//									} else if matchedCharacters == "**" || matchedCharacters == "__" {
-//										attString = attributedStringFromString(hasString, withType: .Bold)
-//									} else {
-//										attString = attributedStringFromString(hasString, withType: .Italic)
-//									}
-//									attributedString.appendAttributedString(attString)
-//								}
-//								matchedCharacters = self.tagFromScanner(scanner)
-//								
-//								if matchedCharacters.containsString("\\") {
-//									let attString = attributedStringFromString(matchedCharacters, withType: .Body)
-//									
-//									attributedString.appendAttributedString(attString)
-//								}
-//								
-//							}
-//						}
-//					} else {
-//						var matchedCharacters = self.tagFromScanner(scanner)
-//
-//						scanner.scanUpToCharactersFromSet(instructionSet, intoString: &followingString)
-//						if let hasString = followingString as? String {
-//							let attString : NSAttributedString
-//							
-//							if matchedCharacters.containsString("\\") {
-//								attString = attributedStringFromString(matchedCharacters + hasString, withType: .Body)
-//							} else if matchedCharacters == "**" || matchedCharacters == "__" {
-//								attString = attributedStringFromString(hasString, withType: .Bold)
-//							} else {
-//								attString = attributedStringFromString(hasString, withType: .Italic)
-//							}
-//							attributedString.appendAttributedString(attString)
-//						}
-//						matchedCharacters = self.tagFromScanner(scanner)
-//						
-//						if matchedCharacters.containsString("\\") {
-//							let attString = attributedStringFromString(matchedCharacters, withType: .Body)
-//							
-//							attributedString.appendAttributedString(attString)
-//						}
-//						
-//					}
-//				}
-//			}
-//			attributedString.appendAttributedString(NSAttributedString(string: "\n"))
-//		}
-//		
-//		return attributedString
-//	}
-//	
-//	func tagFromScanner( scanner : NSScanner ) -> String {
-//		var matchedCharacters : String = ""
-//		var tempCharacters : NSString?
-//		
-//		// Scan the ones we are interested in
-//		while scanner.scanCharactersFromSet(instructionSet, intoString: &tempCharacters) {
-//			if let chars = tempCharacters as? String {
-//				matchedCharacters = matchedCharacters + chars
-//			}
-//		}
-//		return matchedCharacters
-//	}
-//	
-//	
-//	// Make H1
-//	
-//	func attributedStringFromString(string : String, withType type : LineType ) -> NSAttributedString {
-//		var attributes : [String : AnyObject]
-//		let textStyle : String
-//		let fontName : String
-//		
-//		var appendNewLine = true
-//		
-//		switch type {
-//		case .H1:
-//			fontName = h1.fontName
-//			
-//			if #available(iOS 9, *) {
-//				textStyle = UIFontTextStyleTitle1
-//			}
-//			attributes = [NSForegroundColorAttributeName : h1.color]
-//		case .H2:
-//			fontName = h2.fontName
-//			textStyle = UIFontTextStyleTitle2
-//			attributes = [NSForegroundColorAttributeName : h2.color]
-//		case .H3:
-//			fontName = h3.fontName
-//			textStyle = UIFontTextStyleTitle3
-//			attributes = [NSForegroundColorAttributeName : h3.color]
-//		case .H4:
-//			fontName = h4.fontName
-//			textStyle = UIFontTextStyleHeadline
-//			attributes = [NSForegroundColorAttributeName : h4.color]
-//		case .H5:
-//			fontName = h5.fontName
-//			textStyle = UIFontTextStyleSubheadline
-//			attributes = [NSForegroundColorAttributeName : h5.color]
-//		case .H6:
-//			fontName = h6.fontName
-//			textStyle = UIFontTextStyleFootnote
-//			attributes = [NSForegroundColorAttributeName : h6.color]
-//		case .Italic:
-//			fontName = italic.fontName
-//			attributes = [NSForegroundColorAttributeName : italic.color]
-//			textStyle = UIFontTextStyleBody
-//			appendNewLine = false
-//		case .Bold:
-//			fontName = bold.fontName
-//			attributes = [NSForegroundColorAttributeName : bold.color]
-//			appendNewLine = false
-//			textStyle = UIFontTextStyleBody
-//		default:
-//			appendNewLine = false
-//			fontName = body.fontName
-//			textStyle = UIFontTextStyleBody
-//			attributes = [NSForegroundColorAttributeName:body.color]
-//			break
-//		}
-//		
-//		let font = UIFont.preferredFontForTextStyle(textStyle)
-//		let styleDescriptor = font.fontDescriptor()
-//		let styleSize = styleDescriptor.fontAttributes()[UIFontDescriptorSizeAttribute] as? CGFloat ?? CGFloat(14)
-//		
-//		var finalFont : UIFont
-//		if let font = UIFont(name: fontName, size: styleSize) {
-//			finalFont = font
-//		} else {
-//			finalFont = UIFont.preferredFontForTextStyle(textStyle)
-//		}
-//		
-//		let finalFontDescriptor = finalFont.fontDescriptor()
-//		if type == .Italic {
-//			let italicDescriptor = finalFontDescriptor.fontDescriptorWithSymbolicTraits(.TraitItalic)
-//			finalFont = UIFont(descriptor: italicDescriptor, size: styleSize)
-//		}
-//		if type == .Bold {
-//			let boldDescriptor = finalFontDescriptor.fontDescriptorWithSymbolicTraits(.TraitBold)
-//			finalFont = UIFont(descriptor: boldDescriptor, size: styleSize)
-//		}
-//		
-//		
-//		attributes[NSFontAttributeName] = finalFont
-//		
-//		if appendNewLine {
-//			return NSAttributedString(string: string + "\n", attributes: attributes)
-//		} else {
-//			return NSAttributedString(string: string, attributes: attributes)
-//		}
-//	}
-//}
-//
-//if let url = NSBundle.mainBundle().URLForResource("test", withExtension: "md"), md = SwiftyMarkdown(url: url) {
-//	
-//	label.attributedText = md.attributedString()
-//}
-//
+var nums = [10, 20, 30, 40, 50]
+nums.replaceSubrange(1...1, with: repeatElement(1, count: 5))
+print(nums)

+ 97 - 4
SwiftyMarkdown/SwiftyMarkdown.swift

@@ -13,6 +13,7 @@ enum CharacterStyle : CharacterStyling {
 	case bold
 	case italic
 	case code
+	case link
 }
 
 enum MarkdownLineStyle : LineStyling {
@@ -87,9 +88,10 @@ If that is not set, then the system default will be used.
 	]
 	
 	static let characterRules = [
-		SwiftyTagging(openTag: "`", intermediateTag: nil, closingTag: nil, escapeString: "\\", styles: [1 : [CharacterStyle.code]], maxTags: 1),
-		SwiftyTagging(openTag: "*", intermediateTag: nil, closingTag: "*", escapeString: "\\", styles: [1 : [.italic], 2 : [.bold], 3 : [CharacterStyle.bold, CharacterStyle.italic]], maxTags: 3),
-		SwiftyTagging(openTag: "_", intermediateTag: nil, closingTag: nil, escapeString: "\\", styles: [1 : [.italic], 2 : [.bold], 3 : [CharacterStyle.bold, CharacterStyle.italic]], maxTags: 3)
+		CharacterRule(openTag: "[", intermediateTag: "](", closingTag: ")", escapeCharacter: "\\", styles: [1 : [CharacterStyle.link]], maxTags: 1),
+		CharacterRule(openTag: "`", intermediateTag: nil, closingTag: nil, escapeCharacter: "\\", styles: [1 : [CharacterStyle.code]], maxTags: 1),
+		CharacterRule(openTag: "*", intermediateTag: nil, closingTag: nil, escapeCharacter: "\\", styles: [1 : [CharacterStyle.italic], 2 : [CharacterStyle.bold], 3 : [CharacterStyle.bold, CharacterStyle.italic]], maxTags: 3),
+		CharacterRule(openTag: "_", intermediateTag: nil, closingTag: nil, escapeCharacter: "\\", styles: [1 : [CharacterStyle.italic], 2 : [CharacterStyle.bold], 3 : [CharacterStyle.bold, CharacterStyle.italic]], maxTags: 3)
 	]
 	
 	let lineProcessor = SwiftyLineProcessor(rules: SwiftyMarkdown.lineRules, defaultRule: MarkdownLineStyle.body)
@@ -223,15 +225,19 @@ If that is not set, then the system default will be used.
 		
 		var strings : [String] = []
 		for line in foundAttributes {
+			
+			attributedString.append(attributedStringFor(line))
+			
 			let finalTokens = self.tokeniser.process(line.line)
 			
+			
 			let string = finalTokens.map({ $0.outputString }).joined()
 			strings.append(string)
 		}
 		
 		let finalString = strings.joined(separator: "\n")
 		
-		return NSAttributedString(string: finalString)
+		return attributedString
 	}
 	
 	
@@ -239,6 +245,93 @@ If that is not set, then the system default will be used.
 
 extension SwiftyMarkdown {
 	
+	func attributedStringFor( _ line : SwiftyLine ) -> NSAttributedString {
+		var outputLine = line.line
+		if let style = line.lineStyle as? MarkdownLineStyle, style == .codeblock {
+			outputLine = "\t\(outputLine)"
+		}
+		let textStyle : UIFont.TextStyle
+		var fontName : String?
+		var attributes : [NSAttributedString.Key : AnyObject] = [:]
+		var fontSize : CGFloat?
+		
+		// What type are we and is there a font name set?
+		
+		
+		switch line.lineStyle as! MarkdownLineStyle {
+		case .h1:
+			fontName = h1.fontName
+			fontSize = h1.fontSize
+			if #available(iOS 9, *) {
+				textStyle = UIFont.TextStyle.title1
+			} else {
+				textStyle = UIFont.TextStyle.headline
+			}
+			attributes[NSAttributedString.Key.foregroundColor] = h1.color
+		case .h2:
+			fontName = h2.fontName
+			fontSize = h2.fontSize
+			if #available(iOS 9, *) {
+				textStyle = UIFont.TextStyle.title2
+			} else {
+				textStyle = UIFont.TextStyle.headline
+			}
+			attributes[NSAttributedString.Key.foregroundColor] = h2.color
+		case .h3:
+			fontName = h3.fontName
+			fontSize = h3.fontSize
+			if #available(iOS 9, *) {
+				textStyle = UIFont.TextStyle.title2
+			} else {
+				textStyle = UIFont.TextStyle.subheadline
+			}
+			attributes[NSAttributedString.Key.foregroundColor] = h3.color
+		case .h4:
+			fontName = h4.fontName
+			fontSize = h4.fontSize
+			textStyle = UIFont.TextStyle.headline
+			attributes[NSAttributedString.Key.foregroundColor] = h4.color
+		case .h5:
+			fontName = h5.fontName
+			fontSize = h5.fontSize
+			textStyle = UIFont.TextStyle.subheadline
+			attributes[NSAttributedString.Key.foregroundColor] = h5.color
+		case .h6:
+			fontName = h6.fontName
+			fontSize = h6.fontSize
+			textStyle = UIFont.TextStyle.footnote
+			attributes[NSAttributedString.Key.foregroundColor] = h6.color
+		default:
+			fontName = body.fontName
+			fontSize = body.fontSize
+			textStyle = UIFont.TextStyle.body
+			attributes[NSAttributedString.Key.foregroundColor] = body.color
+			break
+		}
+
+		if let _ = fontName {
+			
+		} else {
+			fontName = body.fontName
+		}
+		
+		fontSize = fontSize == 0.0 ? nil : fontSize
+		let font = UIFont.preferredFont(forTextStyle: textStyle)
+		let styleDescriptor = font.fontDescriptor
+		let styleSize = fontSize ?? styleDescriptor.fontAttributes[UIFontDescriptor.AttributeName.size] as? CGFloat ?? CGFloat(14)
+		
+		var finalFont : UIFont
+		if let finalFontName = fontName, let font = UIFont(name: finalFontName, size: styleSize) {
+			finalFont = font
+		} else {
+			finalFont = UIFont.preferredFont(forTextStyle:  textStyle)
+		}
+
+		attributes[NSAttributedString.Key.font] = finalFont
+		
+		return NSAttributedString(string: outputLine, attributes: attributes)
+	}
+	
 	func attributedStringFromString(_ string : String, withStyle style : MarkdownLineStyle, attributes : [NSAttributedString.Key : AnyObject] = [:] ) -> NSAttributedString {
 		let textStyle : UIFont.TextStyle
 		var fontName : String?

+ 180 - 70
SwiftyMarkdown/SwiftyTokeniser.swift

@@ -5,9 +5,16 @@
 //  Created by Simon Fairbairn on 16/12/2019.
 //  Copyright © 2019 Voyage Travel Apps. All rights reserved.
 //
-
 import Foundation
+import os.log
+
+extension OSLog {
+	private static var subsystem = "SwiftyTokeniser"
+	static let tokenising = OSLog(subsystem: subsystem, category: "Tokenising")
+	static let styling = OSLog(subsystem: subsystem, category: "Styling")
+}
 
+// Tag definition
 public protocol CharacterStyling {
 	
 }
@@ -26,21 +33,21 @@ public enum Cancel {
     case currentSet
 }
 
-public struct SwiftyTagging {
+public struct CharacterRule {
 	public let openTag : String
 	public let intermediateTag : String?
 	public let closingTag : String?
-	public let escapeString : String?
+	public let escapeCharacter : Character?
 	public let styles : [Int : [CharacterStyling]]
 	public var maxTags : Int = 1
 	public var spacesAllowed : SpaceAllowed = .oneSide
 	public var cancels : Cancel = .none
 	
-	public init(openTag: String, intermediateTag: String? = nil, closingTag: String? = nil, escapeString: String? = nil, styles: [Int : [CharacterStyling]] = [:], maxTags : Int = 1) {
+	public init(openTag: String, intermediateTag: String? = nil, closingTag: String? = nil, escapeCharacter: Character? = nil, styles: [Int : [CharacterStyling]] = [:], maxTags : Int = 1) {
 		self.openTag = openTag
 		self.intermediateTag = intermediateTag
 		self.closingTag = closingTag
-		self.escapeString = escapeString
+		self.escapeCharacter = escapeCharacter
 		self.styles = styles
 		self.maxTags = maxTags
 	}
@@ -51,16 +58,19 @@ public enum TokenType {
 	case openTag
 	case intermediateTag
 	case closeTag
+	case processed
 	case string
 	case escape
+	case metadata
 }
 
 
 
 public struct Token {
 	public let id = UUID().uuidString
-	public let type : TokenType
+	public var type : TokenType
 	public let inputString : String
+	public var metadataString : String? = nil
 	public var characterStyles : [CharacterStyling] = []
 	public var count : Int = 0
 	public var shouldSkip : Bool = false
@@ -74,7 +84,9 @@ public struct Token {
 					let range = inputString.startIndex..<inputString.index(inputString.startIndex, offsetBy: self.count)
 					return String(inputString[range])
 				}
-			default:
+			case .metadata, .processed:
+				return ""
+			case .escape, .string:
 				return inputString
 			}
 		}
@@ -87,9 +99,9 @@ public struct Token {
 }
 
 public class SwiftyTokeniser {
-	let rules : [SwiftyTagging]
+	let rules : [CharacterRule]
 	
-	public init( with rules : [SwiftyTagging] ) {
+	public init( with rules : [CharacterRule] ) {
 		self.rules = rules
 	}
 	
@@ -97,63 +109,127 @@ public class SwiftyTokeniser {
 		guard rules.count > 0 else {
 			return [Token(type: .string, inputString: inputString)]
 		}
-		var tagLookup : [String : [Int : [CharacterStyling]]] = [:]
-		var finalTokens : [Token] = []
+
+		var currentTokens : [Token] = []
 		var mutableRules = self.rules
-		let firstRule = mutableRules.removeFirst()
-		tagLookup[firstRule.openTag] = firstRule.styles
-		var tokens = self.scan(inputString, with: firstRule)
-		if firstRule.cancels != .allRemaining {
-			while !mutableRules.isEmpty {
-				let nextRule = mutableRules.removeFirst()
-				tagLookup[nextRule.openTag] = nextRule.styles
+		while !mutableRules.isEmpty {
+			let nextRule = mutableRules.removeFirst()
+			if currentTokens.isEmpty {
+				// This means it's the first time through
+				currentTokens = self.applyStyles(to: self.scan(inputString, with: nextRule), usingRule: nextRule)
+				continue
+			}
+			
+			// Each string could have additional tokens within it, so they have to be scanned as well with the current rule.
+			// The one string token might then be exploded into multiple more tokens
+			var replacements : [Int : [Token]] = [:]
+			for (idx,token) in currentTokens.enumerated() {
+				switch token.type {
+				case .string:
+					let nextTokens = self.scan(token.outputString, with: nextRule)
+					replacements[idx] = self.applyStyles(to: nextTokens, usingRule: nextRule)
+				default:
+					break
+				}
+			}
+			
+			// This replaces the individual string tokens with the new token arrays
+			// making sure to apply any previously found styles to the new tokens.
+			for key in replacements.keys.sorted(by: { $0 > $1 }) {
+				let existingToken = currentTokens[key]
 				var newTokens : [Token] = []
-				for token in tokens {
-					switch token.type {
-					case .string:
-						newTokens.append(contentsOf: self.scan(token.outputString, with: nextRule))
-					default:
-						newTokens.append(token)
-					}
+				for token in replacements[key]! {
+					var newToken = token
+					newToken.characterStyles.append(contentsOf: existingToken.characterStyles)
+					newTokens.append(newToken)
 				}
-				tokens = newTokens
-	//			let tokens = self.scan(string, with: rule)
-				switch nextRule.cancels {
-				case .allRemaining:
-					break
-				default:
-					continue
+				currentTokens.replaceSubrange(key...key, with: newTokens)
+			}
+		}
+		return currentTokens
+	}
+	
+	func handleClosingTagFromOpenTag(withIndex index : Int, in tokens: inout [Token], following rule : CharacterRule ) {
+		
+		guard rule.closingTag != nil else {
+			return
+		}
+		
+		var metadataIndex = index
+		// If there's an intermediate tag, get the index of that
+		if rule.intermediateTag != nil {
+			guard let nextTokenIdx = tokens.firstIndex(where: { $0.type == .intermediateTag }) else {
+				return
+			}
+			metadataIndex = nextTokenIdx
+			let styles : [CharacterStyling] = rule.styles[1] ?? []
+			for i in index..<nextTokenIdx {
+				for style in styles {
+					tokens[i].characterStyles.append(style)
 				}
 			}
 		}
-
+		
+		guard let closeTokenIdx = tokens.firstIndex(where: { $0.type == .closeTag }) else {
+			return
+		}
+		var metadataString : String = ""
+		for i in metadataIndex..<closeTokenIdx {
+			var otherTokens = tokens[i]
+			otherTokens.type = .metadata
+			tokens[i] = otherTokens
+			metadataString.append(otherTokens.outputString)
+		}
+		tokens[closeTokenIdx].type = .processed
+		tokens[metadataIndex].type = .processed
+		tokens[index].type = .processed
+		tokens[index].metadataString = metadataString
+		
+		
+		
+	}
+	
+	
+	func applyStyles( to tokens : [Token], usingRule rule : CharacterRule ) -> [Token] {
+		var nextTokens : [Token] = []
 		var mutableTokens : [Token] = tokens
-		for (idx, token) in tokens.enumerated() {
+		print( tokens.map( { ( $0.outputString, $0.count )}))
+		for idx in 0..<mutableTokens.count {
+			let token = mutableTokens[idx]
 			switch token.type {
 			case .escape:
 				print( "Found escape (\(token.inputString))" )
-				finalTokens.append(token)
+				nextTokens.append(token)
 			case .openTag:
-				
 				let theToken = mutableTokens[idx]
-				print ("Found open tag with tag count \(theToken.count) tags: \(theToken.inputString)" )
+				print ("Found open tag with tag count \(theToken.count) tags: \(theToken.inputString). Current rule open tag = \(rule.openTag)" )
+				
+				guard rule.closingTag == nil else {
+					
+					// If there's an intermediate tag, get the index of that
+					self.handleClosingTagFromOpenTag(withIndex: idx, in: &mutableTokens, following: rule)
+					// Get the index of the closing tag
+					
+					continue
+				}
+				
 				guard theToken.count > 0 else {
-					finalTokens.append(theToken)
+					nextTokens.append(theToken)
 					continue
 				}
 				
 				let startIdx = idx
 				var endIdx : Int? = nil
-
+				
 				if let nextTokenIdx = mutableTokens.firstIndex(where: { $0.inputString == theToken.inputString && $0.type == theToken.type && $0.count == theToken.count && $0.id != theToken.id }) {
 					endIdx = nextTokenIdx
 				}
 				guard let existentEnd = endIdx else {
-					finalTokens.append(theToken)
+					nextTokens.append(theToken)
 					continue
 				}
 				
-				let styles : [CharacterStyling] = tagLookup[String(theToken.inputString.first!)]?[theToken.count] ?? []
+				let styles : [CharacterStyling] = rule.styles[theToken.count] ?? []
 				for i in startIdx..<existentEnd {
 					var otherTokens = mutableTokens[i]
 					for style in styles {
@@ -163,30 +239,47 @@ public class SwiftyTokeniser {
 				}
 				var newToken = theToken
 				newToken.count = 0
-				finalTokens.append(newToken)
+				nextTokens.append(newToken)
 				mutableTokens[idx] = newToken
 				
 				var closeToken = mutableTokens[existentEnd]
 				closeToken.count = 0
 				mutableTokens[existentEnd] = closeToken
-				
+			case .intermediateTag:
+				let theToken = mutableTokens[idx]
+				print ("Found intermediate tag with tag count \(theToken.count) tags: \(theToken.inputString)" )
+				nextTokens.append(theToken)
+			case .closeTag:
+				let theToken = mutableTokens[idx]
+				print ("Found close tag with tag count \(theToken.count) tags: \(theToken.inputString)" )
+				nextTokens.append(theToken)
 			case .string:
 				let theToken = mutableTokens[idx]
 				print ("Found String: \(theToken.inputString)" )
-				finalTokens.append(theToken)
-			default:
-				break
+				nextTokens.append(theToken)
+			case .metadata:
+				let theToken = mutableTokens[idx]
+				print ("Found metadata: \(theToken.inputString)" )
+				nextTokens.append(theToken)
+			case .processed:
+				let theToken = mutableTokens[idx]
+				print ("Found already processed tag: \(theToken.inputString)" )
+				nextTokens.append(theToken)
 			}
 		}
-		return finalTokens
-		
+		return nextTokens
 	}
 	
-	func scan( _ string : String, with rule : SwiftyTagging) -> [Token] {
+	
+	func scan( _ string : String, with rule : CharacterRule) -> [Token] {
 		let scanner = Scanner(string: string)
 		scanner.charactersToBeSkipped = nil
 		var tokens : [Token] = []
-		let set = CharacterSet(charactersIn: "\(rule.openTag)\(rule.intermediateTag ?? "")\(rule.closingTag ?? "")\(rule.escapeString ?? "")")
+		var set = CharacterSet(charactersIn: "\(rule.openTag)\(rule.intermediateTag ?? "")\(rule.closingTag ?? "")")
+		if let existentEscape = rule.escapeCharacter {
+			set.insert(charactersIn: String(existentEscape))
+		}
+		
 		var openingString = ""
 		while !scanner.isAtEnd {
 			
@@ -235,33 +328,33 @@ public class SwiftyTokeniser {
 			
 			
 			if !validateSpacing(nextCharacter: nextChar, previousCharacter: lastChar, with: rule) {
-				var escaped = foundChars.replacingOccurrences(of: "\(rule.escapeString ?? "")\(rule.openTag)", with: rule.openTag)
+				let escapeString = String("\(rule.escapeCharacter ?? Character(""))")
+				var escaped = foundChars.replacingOccurrences(of: "\(escapeString)\(rule.openTag)", with: rule.openTag)
 				if let hasIntermediateTag = rule.intermediateTag {
-					escaped = foundChars.replacingOccurrences(of: "\(rule.escapeString ?? "")\(hasIntermediateTag)", with: hasIntermediateTag)
+					escaped = foundChars.replacingOccurrences(of: "\(escapeString)\(hasIntermediateTag)", with: hasIntermediateTag)
 				}
 				if let existentClosingTag = rule.closingTag {
-					escaped = foundChars.replacingOccurrences(of: "\(rule.escapeString ?? "")\(existentClosingTag)", with: existentClosingTag)
+					escaped = foundChars.replacingOccurrences(of: "\(escapeString)\(existentClosingTag)", with: existentClosingTag)
 				}
 				
 				openingString.append(escaped)
 				continue
 			}
-			if !openingString.isEmpty {
-				tokens.append(Token(type: .string, inputString: "\(openingString)"))
-				openingString = ""
-			}
-			
-			
+
 			var cumulativeString = ""
 			var openString = ""
+			var intermediateString = ""
 			var closedString = ""
 			var maybeEscapeNext = false
 			
+			
 			func addToken( for type : TokenType ) {
 				var inputString : String
 				switch type {
 				case .openTag:
 					inputString = openString
+				case .intermediateTag:
+					inputString = intermediateString
 				case .closeTag:
 					inputString = closedString
 				default:
@@ -270,13 +363,22 @@ public class SwiftyTokeniser {
 				guard !inputString.isEmpty else {
 					return
 				}
+				if !openingString.isEmpty {
+					tokens.append(Token(type: .string, inputString: "\(openingString)"))
+					openingString = ""
+				}
 				var token = Token(type: type, inputString: inputString)
-				token.count = inputString.count
+				if rule.closingTag == nil {
+					token.count = inputString.count
+				}
+				
 				tokens.append(token)
 				
 				switch type {
 				case .openTag:
 					openString = ""
+				case .intermediateTag:
+					intermediateString = ""
 				case .closeTag:
 					closedString = ""
 				default:
@@ -284,31 +386,38 @@ public class SwiftyTokeniser {
 				}
 			}
 			
-			
+			// Here I am going through and adding the characters in the found set to a cumulative string.
+			// If there is an escape character, then the loop stops and any open tags are tokenised.
 			for char in foundChars {
 				cumulativeString.append(char)
 				if maybeEscapeNext {
 					
 					var escaped = cumulativeString
 					if String(char) == rule.openTag || String(char) == rule.intermediateTag || String(char) == rule.closingTag {
-						escaped = String(cumulativeString.replacingOccurrences(of: rule.escapeString ?? "", with: ""))
+						escaped = String(cumulativeString.replacingOccurrences(of: String(rule.escapeCharacter ?? Character("")), with: ""))
 					}
 					
-					tokens.append(Token(type: .string, inputString: escaped ))
+					openingString.append(escaped)
 					cumulativeString = ""
 					maybeEscapeNext = false
 				}
-				
-				if cumulativeString == rule.escapeString {
-					maybeEscapeNext = true
-					addToken(for: .openTag)
-					addToken(for: .closeTag)
-					continue
+				if let existentEscape = rule.escapeCharacter {
+					if cumulativeString == String(existentEscape) {
+						maybeEscapeNext = true
+						addToken(for: .openTag)
+						addToken(for: .intermediateTag)
+						addToken(for: .closeTag)
+						continue
+					}
 				}
 				
+				
 				if cumulativeString == rule.openTag {
 					openString.append(char)
 					cumulativeString = ""
+				} else if cumulativeString == rule.intermediateTag {
+					intermediateString.append(cumulativeString)
+					cumulativeString = ""
 				} else if cumulativeString == rule.closingTag {
 					closedString.append(char)
 					cumulativeString = ""
@@ -322,13 +431,14 @@ public class SwiftyTokeniser {
 				openingString.append( cumulativeString )
 			}
 			addToken(for: .openTag)
+			addToken(for: .intermediateTag)
 			addToken(for: .closeTag)
 		
 		}
 		return tokens
 	}
 	
-	func validateSpacing( nextCharacter : String?, previousCharacter : String?, with rule : SwiftyTagging ) -> Bool {
+	func validateSpacing( nextCharacter : String?, previousCharacter : String?, with rule : CharacterRule ) -> Bool {
 		switch rule.spacesAllowed {
 		case .leadingSide:
 			guard nextCharacter != nil else {

+ 100 - 48
SwiftyMarkdownTests/SwiftyMarkdownTests.swift

@@ -11,6 +11,7 @@ import XCTest
 
 class SwiftyMarkdownTests: XCTestCase {
     
+	
     override func setUp() {
         super.setUp()
         // Put setup code here. This method is called before the invocation of each test method in the class.
@@ -27,72 +28,129 @@ class SwiftyMarkdownTests: XCTestCase {
 		var acutalOutput : String = ""
 	}
 	
-
-    
+	struct TokenTest {
+		let input : String
+		let output : String
+		let tokens : [Token]
+	}
+	
 	func testThatOctothorpeHeadersAreHandledCorrectly() {
 		
 		let heading1 = StringTest(input: "# Heading 1", expectedOutput: "Heading 1")
+		var smd = SwiftyMarkdown(string:heading1.input )
+		XCTAssertEqual(smd.attributedString().string, heading1.expectedOutput)
+		
 		let heading2 = StringTest(input: "## Heading 2", expectedOutput: "Heading 2")
+		smd = SwiftyMarkdown(string:heading2.input )
+		XCTAssertEqual(smd.attributedString().string, heading2.expectedOutput)
+		
 		let heading3 = StringTest(input: "### #Heading #3", expectedOutput: "#Heading #3")
+		smd = SwiftyMarkdown(string:heading3.input )
+		XCTAssertEqual(smd.attributedString().string, heading3.expectedOutput)
+		
 		let heading4 = StringTest(input: "  #### #Heading 4 ####", expectedOutput: "#Heading 4")
-		let heading5 = StringTest(input: " ##### Heading 5 ####   ", expectedOutput: "Heading 5 ####")
-		let heading6 = StringTest(input: " ##### Heading 5 #### More ", expectedOutput: "Heading 5 #### More")
-		let heading7 = StringTest(input: "# **Bold Header 1** ", expectedOutput: "Bold Header 1")
-		let heading8 = StringTest(input: "## Header 2 _With Italics_", expectedOutput: "Header 2 With Italics")
-		let tricksterHeading = StringTest(input: "    # Heading 1", expectedOutput: "# Heading 1")
+		smd = SwiftyMarkdown(string:heading4.input )
+		XCTAssertEqual(smd.attributedString().string, heading4.expectedOutput)
 		
-		let inputStrings = [heading1, heading2, heading3, heading4, heading5, heading6, heading7, heading8, tricksterHeading]
+		let heading5 = StringTest(input: " ##### Heading 5 ####   ", expectedOutput: "Heading 5 ####")
+		smd = SwiftyMarkdown(string:heading5.input )
+		XCTAssertEqual(smd.attributedString().string, heading5.expectedOutput)
 		
-		let inputString = inputStrings.map({ $0.input }).joined(separator: "\n")
+		let heading6 = StringTest(input: " ##### Heading 5 #### More ", expectedOutput: "Heading 5 #### More")
+		smd = SwiftyMarkdown(string:heading6.input )
+		XCTAssertEqual(smd.attributedString().string, heading6.expectedOutput)
 		
-		let swiftyMarkdown = SwiftyMarkdown(string: inputString)
-		XCTAssertEqual(swiftyMarkdown.attributedString().string, inputStrings.map({ $0.expectedOutput}).joined(separator: "\n"))
+		let heading7 = StringTest(input: "# **Bold Header 1** ", expectedOutput: "Bold Header 1")
+		smd = SwiftyMarkdown(string:heading7.input )
+		XCTAssertEqual(smd.attributedString().string, heading7.expectedOutput)
 		
-		///
+		let heading8 = StringTest(input: "## Header 2 _With Italics_", expectedOutput: "Header 2 With Italics")
+		smd = SwiftyMarkdown(string:heading8.input )
+		XCTAssertEqual(smd.attributedString().string, heading8.expectedOutput)
 		
-		var headerString = "# Header 1\n## Header 2 ##\n### Header 3 ### \n#### Header 4#### \n##### Header 5\n###### Header 6"
-		let headerStringWithBold = "# **Bold Header 1**"
-		let headerStringWithItalic = "## Header 2 _With Italics_"
+		let heading9 = StringTest(input: "    # Heading 1", expectedOutput: "# Heading 1")
+		smd = SwiftyMarkdown(string:heading9.input )
+		XCTAssertEqual(smd.attributedString().string, heading9.expectedOutput)
+
+		let allHeaders = [heading1, heading2, heading3, heading4, heading5, heading6, heading7, heading8, heading9]
+		smd = SwiftyMarkdown(string: allHeaders.map({ $0.input }).joined(separator: "\n"))
+		XCTAssertEqual(smd.attributedString().string, allHeaders.map({ $0.expectedOutput}).joined(separator: "\n"))
 		
-		var md = SwiftyMarkdown(string: headerString)
-		XCTAssertEqual(md.attributedString().string, "Header 1\nHeader 2\nHeader 3\nHeader 4\nHeader 5\nHeader 6")
+		let headerString = StringTest(input: "# Header 1\n## Header 2 ##\n### Header 3 ### \n#### Header 4#### \n##### Header 5\n###### Header 6", expectedOutput: "Header 1\nHeader 2\nHeader 3\nHeader 4\nHeader 5\nHeader 6")
+		smd = SwiftyMarkdown(string: headerString.input)
+		XCTAssertEqual(smd.attributedString().string, headerString.expectedOutput)
 		
-		 md = SwiftyMarkdown(string: headerStringWithBold)
-		XCTAssertEqual(md.attributedString().string, "Bold Header 1")
+		let headerStringWithBold = StringTest(input: "# **Bold Header 1**", expectedOutput: "Bold Header 1")
+		smd = SwiftyMarkdown(string: headerStringWithBold.input)
+		XCTAssertEqual(smd.attributedString().string, headerStringWithBold.expectedOutput )
 		
-		md = SwiftyMarkdown(string: headerStringWithItalic)
-		XCTAssertEqual(md.attributedString().string, "Header 2 With Italics")
+		let headerStringWithItalic = StringTest(input: "## Header 2 _With Italics_", expectedOutput: "Header 2 With Italics")
+		smd = SwiftyMarkdown(string: headerStringWithItalic.input)
+		XCTAssertEqual(smd.attributedString().string, headerStringWithItalic.expectedOutput)
 		
 	}
+
 	
 	func testThatUndelinedHeadersAreHandledCorrectly() {
-		let h1String = "Header 1\n===\nSome following text"
-		let h2String = "Header 2\n---\nSome following text"
+
+		let h1String = StringTest(input: "Header 1\n===\nSome following text", expectedOutput: "Header 1\nSome following text")
+		var md = SwiftyMarkdown(string: h1String.input)
+		XCTAssertEqual(md.attributedString().string, h1String.expectedOutput)
 		
-		let h1StringWithBold = "Header 1 **With Bold**\n===\nSome following text"
-		let h2StringWithItalic = "Header 2 _With Italic_\n---\nSome following text"
-		let h2StringWithCode = "Header 2 `With Code`\n---\nSome following text"
+		let h2String = StringTest(input: "Header 2\n---\nSome following text", expectedOutput: "Header 2\nSome following text")
+		md = SwiftyMarkdown(string: h2String.input)
+		XCTAssertEqual(md.attributedString().string, h2String.expectedOutput)
 		
-		var md = SwiftyMarkdown(string: h1String)
-		XCTAssertEqual(md.attributedString().string, "Header 1\nSome following text")
+		let h1StringWithBold = StringTest(input: "Header 1 **With Bold**\n===\nSome following text", expectedOutput: "Header 1 With Bold\nSome following text")
+		md = SwiftyMarkdown(string: h1StringWithBold.input)
+		XCTAssertEqual(md.attributedString().string, h1StringWithBold.expectedOutput)
 		
-		md = SwiftyMarkdown(string: h2String)
-		XCTAssertEqual(md.attributedString().string, "Header 2\nSome following text")
+		let h2StringWithItalic = StringTest(input: "Header 2 _With Italic_\n---\nSome following text", expectedOutput: "Header 2 With Italic\nSome following text")
+		md = SwiftyMarkdown(string: h2StringWithItalic.input)
+		XCTAssertEqual(md.attributedString().string, h2StringWithItalic.expectedOutput)
 		
-		md = SwiftyMarkdown(string: h1StringWithBold)
-		XCTAssertEqual(md.attributedString().string, "Header 1 With Bold\nSome following text")
+		let h2StringWithCode = StringTest(input: "Header 2 `With Code`\n---\nSome following text", expectedOutput: "Header 2 With Code\nSome following text")
+		md = SwiftyMarkdown(string: h2StringWithCode.input)
+		XCTAssertEqual(md.attributedString().string, h2StringWithCode.expectedOutput)
+	}
+	
+	func attempt( _ challenge : TokenTest ) {
+		let md = SwiftyMarkdown(string: challenge.input)
+		let tokeniser = SwiftyTokeniser(with: SwiftyMarkdown.characterRules)
+		let tokens = tokeniser.process(challenge.input)
+		let stringTokens = tokens.filter({ $0.type == .string })
+		XCTAssertEqual(challenge.tokens.count, stringTokens.count)
+		XCTAssertEqual(tokens.map({ $0.outputString }).joined(), challenge.output)
+		
+		let existentTokenStyles = stringTokens.compactMap({ $0.characterStyles as? [CharacterStyle] })
+		let expectedStyles = challenge.tokens.compactMap({ $0.characterStyles as? [CharacterStyle] })
+		
+		XCTAssertEqual(existentTokenStyles, expectedStyles)
+
+		let attributedString = md.attributedString()
+		XCTAssertEqual(attributedString.string, challenge.output)
 		
-		md = SwiftyMarkdown(string: h2StringWithItalic)
-		XCTAssertEqual(md.attributedString().string, "Header 2 With Italic\nSome following text")
+		let att = attributedString.attribute(.font, at: 0, effectiveRange: nil)
+		XCTAssertNotNil(att)
 		
-		md = SwiftyMarkdown(string: h2StringWithCode)
-		XCTAssertEqual(md.attributedString().string, "Header 2 With Code\nSome following text")
 	}
 	
 	func testThatRegularTraitsAreParsedCorrectly() {
-		let boldAtStartOfString = "**A bold string**"
-		let boldWithinString = "A string with a **bold** word"
-		let codeAtStartOfString = "`Code (should be indented)`"
+
+		let challenge1 = TokenTest(input: "**A bold string**", output: "A bold string",  tokens: [
+			Token(type: .string, inputString: "A bold string", characterStyles: [CharacterStyle.bold])
+		])
+		self.attempt(challenge1)
+		
+		let challenge2 = TokenTest(input: "A string with a **bold** word", output: "A string with a bold word",  tokens: [
+			Token(type: .string, inputString: "A string with a ", characterStyles: []),
+			Token(type: .string, inputString: "bold", characterStyles: [CharacterStyle.bold]),
+			Token(type: .string, inputString: " word", characterStyles: [])
+		])
+		self.attempt(challenge2)
+		
+		
+		let codeAtStartOfString = "`Code (should not be indented)`"
 		let codeWithinString = "A string with `code` (should not be indented)"
 		let italicAtStartOfString = "*An italicised string*"
 		let italicWithinString = "A string with *italicised* text"
@@ -104,14 +162,8 @@ class SwiftyMarkdownTests: XCTestCase {
 		let longMixedString = "_An italic string_, **follwed by a bold one**, `with some code`, \\*\\*and some\\*\\* \\_escaped\\_ \\`characters\\`, `ending` *with* __more__ variety."
 		
 		
-		var md = SwiftyMarkdown(string: boldAtStartOfString)
-		XCTAssertEqual(md.attributedString().string, "A bold string")
-		
-		md = SwiftyMarkdown(string: boldWithinString)
-		XCTAssertEqual(md.attributedString().string, "A string with a bold word")
-		
-		md = SwiftyMarkdown(string: codeAtStartOfString)
-		XCTAssertEqual(md.attributedString().string, "\tCode (should be indented)")
+		var md = SwiftyMarkdown(string: codeAtStartOfString)
+		XCTAssertEqual(md.attributedString().string, "Code (should not be indented)")
 		
 		md = SwiftyMarkdown(string: codeWithinString)
 		XCTAssertEqual(md.attributedString().string, "A string with code (should not be indented)")
@@ -126,7 +178,7 @@ class SwiftyMarkdownTests: XCTestCase {
 		XCTAssertEqual(md.attributedString().string, "A bold string with a mix of bold styles")
 		
 		md = SwiftyMarkdown(string: multipleCodeWords)
-		XCTAssertEqual(md.attributedString().string, "\tA code string with multiple code instances")
+		XCTAssertEqual(md.attributedString().string, "A code string with multiple code instances")
 		
 		md = SwiftyMarkdown(string: multipleItalicWords)
 		XCTAssertEqual(md.attributedString().string, "An italic string with a mix of italic styles")