Browse Source

Implements new tokenisation engine and updates readme

Simon Fairbairn 5 years ago
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
 ## Installation
 
 
-CocoaPods:
+### CocoaPods:
 
 
 `pod 'SwiftyMarkdown'`
 `pod 'SwiftyMarkdown'`
 
 
+### SPM: 
+
+In Xcode, `File -> Swift Packages -> Add Package Dependency` and add the GitHub URL. 
+
 ## Usage
 ## Usage
 
 
 Text string
 Text string
+
 ```swift
 ```swift
 let md = SwiftyMarkdown(string: "# Heading\nMy *Markdown* string")
 let md = SwiftyMarkdown(string: "# Heading\nMy *Markdown* string")
 md.attributedString()
 md.attributedString()
 ```
 ```
 
 
 URL 
 URL 
+
 ```swift
 ```swift
 if let url = Bundle.main.url(forResource: "file", withExtension: "md"), md = SwiftyMarkdown(url: url ) {
 if let url = Bundle.main.url(forResource: "file", withExtension: "md"), md = SwiftyMarkdown(url: url ) {
 	md.attributedString()
 	md.attributedString()
@@ -37,6 +47,13 @@ if let url = Bundle.main.url(forResource: "file", withExtension: "md"), md = Swi
     
     
     `code`
     `code`
     [Links](http://voyagetravelapps.com/)
     [Links](http://voyagetravelapps.com/)
+    ![Images](<Name of asset in bundle, or URL>)
+    
+    > Blockquotes
+		
+		Indented code blocks
+
+  
 
 
 ## Customisation 
 ## Customisation 
 ```swift
 ```swift
@@ -52,4 +69,82 @@ md.h1.fontSize = 16
 
 
 ![Screenshot](http://f.cl.ly/items/12332k3f2s0s0C281h2u/swiftymarkdown.png)
 ![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)
 //: [Previous](@previous)
 
 
+
 import Foundation
 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
 // 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
 // Example customisation
@@ -13,6 +480,8 @@ public enum CharacterStyle : CharacterStyling {
 	case bold
 	case bold
 	case italic
 	case italic
 	case code
 	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 : [
 let challenge4 = Test(input: "**AAAA*BB\\*BB*AAAAAA**", output : "AAAABB*BBAAAAAA", tokens : [
 	Token(type: .string, inputString: "AAAA", characterStyles: [CharacterStyle.bold]),
 	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]),
 	Token(type: .string, inputString: "AAAAAA", characterStyles: [CharacterStyle.bold]),
 ])
 ])
 let challenge5 = Test(input: "*Italic* \\_\\_Not Bold\\_\\_ **Bold**", output : "Italic __Not Bold__ Bold", tokens : [
 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: []),
 	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
 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 {
 for challenge in challenges {
 	let finalTokens = scan.process(challenge.input)
 	let finalTokens = scan.process(challenge.input)
 	let stringTokens = finalTokens.filter({ $0.type == .string })
 	let stringTokens = finalTokens.filter({ $0.type == .string })
 	
 	
 	guard stringTokens.count == challenge.tokens.count else {
 	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--------")
 		print("-------EXPECTED--------")
 		for token in challenge.tokens {
 		for token in challenge.tokens {
 			switch token.type {
 			switch token.type {
 			case .string:
 			case .string:
 				print("\(token.outputString): \(token.characterStyles)")
 				print("\(token.outputString): \(token.characterStyles)")
 			default:
 			default:
-				break
+				print("\(token.outputString)")
 			}
 			}
 		}
 		}
 		print("-------OUTPUT--------")
 		print("-------OUTPUT--------")
@@ -98,11 +580,16 @@ for challenge in challenges {
 			case .string:
 			case .string:
 				print("\(token.outputString): \(token.characterStyles)")
 				print("\(token.outputString): \(token.characterStyles)")
 			default:
 			default:
-				break
+				if !token.outputString.isEmpty {
+					print("\(token.outputString)")
+				}
+				
 			}
 			}
 		}
 		}
 		continue
 		continue
 	}
 	}
+	
+	print("-----EXPECTATIONS-----")
 	for (idx, token) in stringTokens.enumerated() {
 	for (idx, token) in stringTokens.enumerated() {
 		let expected = challenge.tokens[idx]
 		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 replacementString : String = line.line
     
     
     var newTokens : [Token] = []
     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 {
 func tokenisePre13( _ line : SwiftyLine ) -> SwiftyLine {
@@ -135,9 +135,9 @@ func tokenisePre13( _ line : SwiftyLine ) -> SwiftyLine {
 func tokenise( _ line : SwiftyLine ) -> SwiftyLine {
 func tokenise( _ line : SwiftyLine ) -> SwiftyLine {
     
     
     // Do nothing if it's a codeblock
     // Do nothing if it's a codeblock
-    if !line.lineStyle.tokenise {
-        return line
-    }
+//    if !line.lineStyle.tokenise {
+//        return line
+//    }
     var output : String = ""
     var output : String = ""
     let textScanner = Scanner(string: line.line)
     let textScanner = Scanner(string: line.line)
     textScanner.charactersToBeSkipped = nil
     textScanner.charactersToBeSkipped = nil
@@ -197,7 +197,7 @@ func tokenise( _ line : SwiftyLine ) -> SwiftyLine {
             break
             break
         }
         }
     }
     }
-    return Line(line: output, lineStyle: line.lineStyle, tokens: tokens)
+    return line // SwiftyLine(line: output, lineStyle: line.lineStyle)
 }
 }
 
 
 func handleLinks( in token : Token ) -> Token {
 func handleLinks( in token : Token ) -> Token {
@@ -352,7 +352,7 @@ func process( _ tokens : [Token] ) -> [Token] {
     return doneTokens
     return doneTokens
 }
 }
 
 
-func attributedString( for line : Line ) -> NSAttributedString {
+func attributedString( for line : SwiftyLine ) -> NSAttributedString {
     return NSAttributedString(string: line.line)
     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 bold
 	case italic
 	case italic
 	case code
 	case code
+	case link
 }
 }
 
 
 enum MarkdownLineStyle : LineStyling {
 enum MarkdownLineStyle : LineStyling {
@@ -87,9 +88,10 @@ If that is not set, then the system default will be used.
 	]
 	]
 	
 	
 	static let characterRules = [
 	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)
 	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] = []
 		var strings : [String] = []
 		for line in foundAttributes {
 		for line in foundAttributes {
+			
+			attributedString.append(attributedStringFor(line))
+			
 			let finalTokens = self.tokeniser.process(line.line)
 			let finalTokens = self.tokeniser.process(line.line)
 			
 			
+			
 			let string = finalTokens.map({ $0.outputString }).joined()
 			let string = finalTokens.map({ $0.outputString }).joined()
 			strings.append(string)
 			strings.append(string)
 		}
 		}
 		
 		
 		let finalString = strings.joined(separator: "\n")
 		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 {
 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 {
 	func attributedStringFromString(_ string : String, withStyle style : MarkdownLineStyle, attributes : [NSAttributedString.Key : AnyObject] = [:] ) -> NSAttributedString {
 		let textStyle : UIFont.TextStyle
 		let textStyle : UIFont.TextStyle
 		var fontName : String?
 		var fontName : String?

+ 180 - 70
SwiftyMarkdown/SwiftyTokeniser.swift

@@ -5,9 +5,16 @@
 //  Created by Simon Fairbairn on 16/12/2019.
 //  Created by Simon Fairbairn on 16/12/2019.
 //  Copyright © 2019 Voyage Travel Apps. All rights reserved.
 //  Copyright © 2019 Voyage Travel Apps. All rights reserved.
 //
 //
-
 import Foundation
 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 protocol CharacterStyling {
 	
 	
 }
 }
@@ -26,21 +33,21 @@ public enum Cancel {
     case currentSet
     case currentSet
 }
 }
 
 
-public struct SwiftyTagging {
+public struct CharacterRule {
 	public let openTag : String
 	public let openTag : String
 	public let intermediateTag : String?
 	public let intermediateTag : String?
 	public let closingTag : String?
 	public let closingTag : String?
-	public let escapeString : String?
+	public let escapeCharacter : Character?
 	public let styles : [Int : [CharacterStyling]]
 	public let styles : [Int : [CharacterStyling]]
 	public var maxTags : Int = 1
 	public var maxTags : Int = 1
 	public var spacesAllowed : SpaceAllowed = .oneSide
 	public var spacesAllowed : SpaceAllowed = .oneSide
 	public var cancels : Cancel = .none
 	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.openTag = openTag
 		self.intermediateTag = intermediateTag
 		self.intermediateTag = intermediateTag
 		self.closingTag = closingTag
 		self.closingTag = closingTag
-		self.escapeString = escapeString
+		self.escapeCharacter = escapeCharacter
 		self.styles = styles
 		self.styles = styles
 		self.maxTags = maxTags
 		self.maxTags = maxTags
 	}
 	}
@@ -51,16 +58,19 @@ public enum TokenType {
 	case openTag
 	case openTag
 	case intermediateTag
 	case intermediateTag
 	case closeTag
 	case closeTag
+	case processed
 	case string
 	case string
 	case escape
 	case escape
+	case metadata
 }
 }
 
 
 
 
 
 
 public struct Token {
 public struct Token {
 	public let id = UUID().uuidString
 	public let id = UUID().uuidString
-	public let type : TokenType
+	public var type : TokenType
 	public let inputString : String
 	public let inputString : String
+	public var metadataString : String? = nil
 	public var characterStyles : [CharacterStyling] = []
 	public var characterStyles : [CharacterStyling] = []
 	public var count : Int = 0
 	public var count : Int = 0
 	public var shouldSkip : Bool = false
 	public var shouldSkip : Bool = false
@@ -74,7 +84,9 @@ public struct Token {
 					let range = inputString.startIndex..<inputString.index(inputString.startIndex, offsetBy: self.count)
 					let range = inputString.startIndex..<inputString.index(inputString.startIndex, offsetBy: self.count)
 					return String(inputString[range])
 					return String(inputString[range])
 				}
 				}
-			default:
+			case .metadata, .processed:
+				return ""
+			case .escape, .string:
 				return inputString
 				return inputString
 			}
 			}
 		}
 		}
@@ -87,9 +99,9 @@ public struct Token {
 }
 }
 
 
 public class SwiftyTokeniser {
 public class SwiftyTokeniser {
-	let rules : [SwiftyTagging]
+	let rules : [CharacterRule]
 	
 	
-	public init( with rules : [SwiftyTagging] ) {
+	public init( with rules : [CharacterRule] ) {
 		self.rules = rules
 		self.rules = rules
 	}
 	}
 	
 	
@@ -97,63 +109,127 @@ public class SwiftyTokeniser {
 		guard rules.count > 0 else {
 		guard rules.count > 0 else {
 			return [Token(type: .string, inputString: inputString)]
 			return [Token(type: .string, inputString: inputString)]
 		}
 		}
-		var tagLookup : [String : [Int : [CharacterStyling]]] = [:]
-		var finalTokens : [Token] = []
+
+		var currentTokens : [Token] = []
 		var mutableRules = self.rules
 		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] = []
 				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
 		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 {
 			switch token.type {
 			case .escape:
 			case .escape:
 				print( "Found escape (\(token.inputString))" )
 				print( "Found escape (\(token.inputString))" )
-				finalTokens.append(token)
+				nextTokens.append(token)
 			case .openTag:
 			case .openTag:
-				
 				let theToken = mutableTokens[idx]
 				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 {
 				guard theToken.count > 0 else {
-					finalTokens.append(theToken)
+					nextTokens.append(theToken)
 					continue
 					continue
 				}
 				}
 				
 				
 				let startIdx = idx
 				let startIdx = idx
 				var endIdx : Int? = nil
 				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 }) {
 				if let nextTokenIdx = mutableTokens.firstIndex(where: { $0.inputString == theToken.inputString && $0.type == theToken.type && $0.count == theToken.count && $0.id != theToken.id }) {
 					endIdx = nextTokenIdx
 					endIdx = nextTokenIdx
 				}
 				}
 				guard let existentEnd = endIdx else {
 				guard let existentEnd = endIdx else {
-					finalTokens.append(theToken)
+					nextTokens.append(theToken)
 					continue
 					continue
 				}
 				}
 				
 				
-				let styles : [CharacterStyling] = tagLookup[String(theToken.inputString.first!)]?[theToken.count] ?? []
+				let styles : [CharacterStyling] = rule.styles[theToken.count] ?? []
 				for i in startIdx..<existentEnd {
 				for i in startIdx..<existentEnd {
 					var otherTokens = mutableTokens[i]
 					var otherTokens = mutableTokens[i]
 					for style in styles {
 					for style in styles {
@@ -163,30 +239,47 @@ public class SwiftyTokeniser {
 				}
 				}
 				var newToken = theToken
 				var newToken = theToken
 				newToken.count = 0
 				newToken.count = 0
-				finalTokens.append(newToken)
+				nextTokens.append(newToken)
 				mutableTokens[idx] = newToken
 				mutableTokens[idx] = newToken
 				
 				
 				var closeToken = mutableTokens[existentEnd]
 				var closeToken = mutableTokens[existentEnd]
 				closeToken.count = 0
 				closeToken.count = 0
 				mutableTokens[existentEnd] = closeToken
 				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:
 			case .string:
 				let theToken = mutableTokens[idx]
 				let theToken = mutableTokens[idx]
 				print ("Found String: \(theToken.inputString)" )
 				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)
 		let scanner = Scanner(string: string)
 		scanner.charactersToBeSkipped = nil
 		scanner.charactersToBeSkipped = nil
 		var tokens : [Token] = []
 		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 = ""
 		var openingString = ""
 		while !scanner.isAtEnd {
 		while !scanner.isAtEnd {
 			
 			
@@ -235,33 +328,33 @@ public class SwiftyTokeniser {
 			
 			
 			
 			
 			if !validateSpacing(nextCharacter: nextChar, previousCharacter: lastChar, with: rule) {
 			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 {
 				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 {
 				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)
 				openingString.append(escaped)
 				continue
 				continue
 			}
 			}
-			if !openingString.isEmpty {
-				tokens.append(Token(type: .string, inputString: "\(openingString)"))
-				openingString = ""
-			}
-			
-			
+
 			var cumulativeString = ""
 			var cumulativeString = ""
 			var openString = ""
 			var openString = ""
+			var intermediateString = ""
 			var closedString = ""
 			var closedString = ""
 			var maybeEscapeNext = false
 			var maybeEscapeNext = false
 			
 			
+			
 			func addToken( for type : TokenType ) {
 			func addToken( for type : TokenType ) {
 				var inputString : String
 				var inputString : String
 				switch type {
 				switch type {
 				case .openTag:
 				case .openTag:
 					inputString = openString
 					inputString = openString
+				case .intermediateTag:
+					inputString = intermediateString
 				case .closeTag:
 				case .closeTag:
 					inputString = closedString
 					inputString = closedString
 				default:
 				default:
@@ -270,13 +363,22 @@ public class SwiftyTokeniser {
 				guard !inputString.isEmpty else {
 				guard !inputString.isEmpty else {
 					return
 					return
 				}
 				}
+				if !openingString.isEmpty {
+					tokens.append(Token(type: .string, inputString: "\(openingString)"))
+					openingString = ""
+				}
 				var token = Token(type: type, inputString: inputString)
 				var token = Token(type: type, inputString: inputString)
-				token.count = inputString.count
+				if rule.closingTag == nil {
+					token.count = inputString.count
+				}
+				
 				tokens.append(token)
 				tokens.append(token)
 				
 				
 				switch type {
 				switch type {
 				case .openTag:
 				case .openTag:
 					openString = ""
 					openString = ""
+				case .intermediateTag:
+					intermediateString = ""
 				case .closeTag:
 				case .closeTag:
 					closedString = ""
 					closedString = ""
 				default:
 				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 {
 			for char in foundChars {
 				cumulativeString.append(char)
 				cumulativeString.append(char)
 				if maybeEscapeNext {
 				if maybeEscapeNext {
 					
 					
 					var escaped = cumulativeString
 					var escaped = cumulativeString
 					if String(char) == rule.openTag || String(char) == rule.intermediateTag || String(char) == rule.closingTag {
 					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 = ""
 					cumulativeString = ""
 					maybeEscapeNext = false
 					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 {
 				if cumulativeString == rule.openTag {
 					openString.append(char)
 					openString.append(char)
 					cumulativeString = ""
 					cumulativeString = ""
+				} else if cumulativeString == rule.intermediateTag {
+					intermediateString.append(cumulativeString)
+					cumulativeString = ""
 				} else if cumulativeString == rule.closingTag {
 				} else if cumulativeString == rule.closingTag {
 					closedString.append(char)
 					closedString.append(char)
 					cumulativeString = ""
 					cumulativeString = ""
@@ -322,13 +431,14 @@ public class SwiftyTokeniser {
 				openingString.append( cumulativeString )
 				openingString.append( cumulativeString )
 			}
 			}
 			addToken(for: .openTag)
 			addToken(for: .openTag)
+			addToken(for: .intermediateTag)
 			addToken(for: .closeTag)
 			addToken(for: .closeTag)
 		
 		
 		}
 		}
 		return tokens
 		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 {
 		switch rule.spacesAllowed {
 		case .leadingSide:
 		case .leadingSide:
 			guard nextCharacter != nil else {
 			guard nextCharacter != nil else {

+ 100 - 48
SwiftyMarkdownTests/SwiftyMarkdownTests.swift

@@ -11,6 +11,7 @@ import XCTest
 
 
 class SwiftyMarkdownTests: XCTestCase {
 class SwiftyMarkdownTests: XCTestCase {
     
     
+	
     override func setUp() {
     override func setUp() {
         super.setUp()
         super.setUp()
         // Put setup code here. This method is called before the invocation of each test method in the class.
         // 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 = ""
 		var acutalOutput : String = ""
 	}
 	}
 	
 	
-
-    
+	struct TokenTest {
+		let input : String
+		let output : String
+		let tokens : [Token]
+	}
+	
 	func testThatOctothorpeHeadersAreHandledCorrectly() {
 	func testThatOctothorpeHeadersAreHandledCorrectly() {
 		
 		
 		let heading1 = StringTest(input: "# Heading 1", expectedOutput: "Heading 1")
 		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")
 		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")
 		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 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() {
 	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() {
 	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 codeWithinString = "A string with `code` (should not be indented)"
 		let italicAtStartOfString = "*An italicised string*"
 		let italicAtStartOfString = "*An italicised string*"
 		let italicWithinString = "A string with *italicised* text"
 		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."
 		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)
 		md = SwiftyMarkdown(string: codeWithinString)
 		XCTAssertEqual(md.attributedString().string, "A string with code (should not be indented)")
 		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")
 		XCTAssertEqual(md.attributedString().string, "A bold string with a mix of bold styles")
 		
 		
 		md = SwiftyMarkdown(string: multipleCodeWords)
 		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)
 		md = SwiftyMarkdown(string: multipleItalicWords)
 		XCTAssertEqual(md.attributedString().string, "An italic string with a mix of italic styles")
 		XCTAssertEqual(md.attributedString().string, "An italic string with a mix of italic styles")