Simon Fairbairn před 5 roky
rodič
revize
898a1ca2ed

+ 1 - 1
Example/SwiftyMarkdownExample/example.md

@@ -1 +1 @@
-An ![Image](imageName)
+[a](b)

+ 45 - 49
Sources/SwiftyMarkdown/CharacterRule.swift

@@ -21,32 +21,6 @@ public enum Cancel {
 	case currentSet
 	case currentSet
 }
 }
 
 
-public struct v2_CharacterRule {
-	public let openTag : String
-	public let closeTag : String?
-	public let escapeCharacter : Character?
-	public let styles : [Int : [CharacterStyling]]
-	public let minTags : Int
-	public let maxTags : Int
-	public let metadataOpen : Character?
-	public let metadataClose : Character?
-}
-
-
-public enum EscapeCharacterRule {
-	case keep
-	case remove
-}
-
-public struct EscapeCharacter {
-	let character : Character
-	let rule : EscapeCharacterRule
-	public init( character : Character, rule : EscapeCharacterRule ) {
-		self.character = character
-		self.rule = rule
-	}
-}
-
 public enum CharacterRuleTagType {
 public enum CharacterRuleTagType {
 	case open
 	case open
 	case close
 	case close
@@ -58,57 +32,79 @@ public enum CharacterRuleTagType {
 
 
 public struct CharacterRuleTag {
 public struct CharacterRuleTag {
 	let tag : String
 	let tag : String
-	let escapeCharacters : [EscapeCharacter]
 	let type : CharacterRuleTagType
 	let type : CharacterRuleTagType
-	let min : Int
-	let max : Int
 	
 	
-	public init( tag : String, type : CharacterRuleTagType, escapeCharacters : [EscapeCharacter] = [EscapeCharacter(character: "\\", rule: .remove)], min : Int = 1, max : Int = 1) {
+	public init( tag : String, type : CharacterRuleTagType ) {
 		self.tag = tag
 		self.tag = tag
 		self.type = type
 		self.type = type
-		self.escapeCharacters = escapeCharacters
-		self.min = min
-		self.max = max
-	}
-	
-	public func escapeCharacter( for character : Character ) -> EscapeCharacter? {
-		return self.escapeCharacters.filter({ $0.character == character }).first
 	}
 	}
 }
 }
 
 
 public struct CharacterRule : CustomStringConvertible {
 public struct CharacterRule : CustomStringConvertible {
 	
 	
-
+	public let primaryTag : CharacterRuleTag
 	public let tags : [CharacterRuleTag]
 	public let tags : [CharacterRuleTag]
-	public let styles : [Int : [CharacterStyling]]
-	public var minTags : Int = 1
-	public var maxTags : Int = 1
-	public var spacesAllowed : SpaceAllowed = .oneSide
-	public var cancels : Cancel = .none
+	public let escapeCharacters : [Character]
+	public let styles : [Int : CharacterStyling]
+	public let minTags : Int
+	public let maxTags : Int
 	public var metadataLookup : Bool = false
 	public var metadataLookup : Bool = false
 	public var isRepeatingTag : Bool {
 	public var isRepeatingTag : Bool {
 		return self.primaryTag.type == .repeating
 		return self.primaryTag.type == .repeating
 	}
 	}
-	public var isSelfContained = false
+	public var definesBoundary = false
+	public var shouldCancelRemainingTags = false
+	public var balancedTags = false
 	
 	
 	public var description: String {
 	public var description: String {
 		return "Character Rule with Open tag: \(self.primaryTag.tag) and current styles : \(self.styles) "
 		return "Character Rule with Open tag: \(self.primaryTag.tag) and current styles : \(self.styles) "
 	}
 	}
 	
 	
-	public let primaryTag : CharacterRuleTag
+	
 	
 	
 	public func tag( for type : CharacterRuleTagType ) -> CharacterRuleTag? {
 	public func tag( for type : CharacterRuleTagType ) -> CharacterRuleTag? {
 		return self.tags.filter({ $0.type == type }).first ?? nil
 		return self.tags.filter({ $0.type == type }).first ?? nil
 	}
 	}
 	
 	
-	public init(primaryTag: CharacterRuleTag, otherTags: [CharacterRuleTag], styles: [Int : [CharacterStyling]] = [:], cancels : Cancel = .none, metadataLookup : Bool = false, spacesAllowed: SpaceAllowed = .oneSide, isSelfContained : Bool = false) {
+	public init(primaryTag: CharacterRuleTag, otherTags: [CharacterRuleTag], escapeCharacters : [Character] = ["\\"], styles: [Int : CharacterStyling] = [:], minTags : Int = 1, maxTags : Int = 1, metadataLookup : Bool = false, definesBoundary : Bool = false, shouldCancelRemainingTags : Bool = false, balancedTags : Bool = false) {
 		self.primaryTag = primaryTag
 		self.primaryTag = primaryTag
 		self.tags = otherTags
 		self.tags = otherTags
+		self.escapeCharacters = escapeCharacters
 		self.styles = styles
 		self.styles = styles
-		self.cancels = cancels
 		self.metadataLookup = metadataLookup
 		self.metadataLookup = metadataLookup
-		self.spacesAllowed = spacesAllowed
-		self.isSelfContained = isSelfContained
+		self.definesBoundary = definesBoundary
+		self.shouldCancelRemainingTags = shouldCancelRemainingTags
+		self.minTags = maxTags < minTags ? maxTags : minTags
+		self.maxTags = minTags > maxTags ? minTags : maxTags
+		self.balancedTags = balancedTags
 	}
 	}
 }
 }
 
 
+
+
+enum ElementType {
+	case tag
+	case escape
+	case string
+	case space
+	case newline
+	case metadata
+}
+
+struct Element {
+	let character : Character
+	var type : ElementType
+	var boundaryCount : Int = 0
+	var isComplete : Bool = false
+	var styles : [CharacterStyling] = []
+	var metadata : [String] = []
+}
+
+extension CharacterSet {
+    func containsUnicodeScalars(of character: Character) -> Bool {
+        return character.unicodeScalars.allSatisfy(contains(_:))
+    }
+}
+
+
+

+ 17 - 17
Sources/SwiftyMarkdown/SwiftyMarkdown.swift

@@ -17,7 +17,7 @@ extension OSLog {
 	static let swiftyMarkdownPerformance = OSLog(subsystem: subsystem, category: "Swifty Markdown Performance")
 	static let swiftyMarkdownPerformance = OSLog(subsystem: subsystem, category: "Swifty Markdown Performance")
 }
 }
 
 
-enum CharacterStyle : CharacterStyling {
+public enum CharacterStyle : CharacterStyling {
 	case none
 	case none
 	case bold
 	case bold
 	case italic
 	case italic
@@ -28,7 +28,7 @@ enum CharacterStyle : CharacterStyling {
 	case referencedImage
 	case referencedImage
 	case strikethrough
 	case strikethrough
 	
 	
-	func isEqualTo(_ other: CharacterStyling) -> Bool {
+	public func isEqualTo(_ other: CharacterStyling) -> Bool {
 		guard let other = other as? CharacterStyle else {
 		guard let other = other as? CharacterStyle else {
 			return false
 			return false
 		}
 		}
@@ -172,21 +172,21 @@ If that is not set, then the system default will be used.
 				CharacterRuleTag(tag: "]", type: .close),
 				CharacterRuleTag(tag: "]", type: .close),
 				CharacterRuleTag(tag: "(", type: .metadataOpen),
 				CharacterRuleTag(tag: "(", type: .metadataOpen),
 				CharacterRuleTag(tag: ")", type: .metadataClose)
 				CharacterRuleTag(tag: ")", type: .metadataClose)
-		], styles: [1 : [CharacterStyle.image]], metadataLookup: false, spacesAllowed: .bothSides, isSelfContained: true),
-		CharacterRule(primaryTag: CharacterRuleTag(tag: "[", type: .open, escapeCharacters: [EscapeCharacter(character: "\\", rule: .remove),EscapeCharacter(character: "!", rule: .keep)]), otherTags: [
+		], styles: [1 : CharacterStyle.image], metadataLookup: false, definesBoundary: true),
+		CharacterRule(primaryTag: CharacterRuleTag(tag: "[", type: .open), otherTags: [
 				CharacterRuleTag(tag: "]", type: .close),
 				CharacterRuleTag(tag: "]", type: .close),
 				CharacterRuleTag(tag: "[", type: .metadataOpen),
 				CharacterRuleTag(tag: "[", type: .metadataOpen),
 				CharacterRuleTag(tag: "]", type: .metadataClose)
 				CharacterRuleTag(tag: "]", type: .metadataClose)
-		], styles: [1 : [CharacterStyle.referencedLink]], metadataLookup: true, spacesAllowed: .bothSides, isSelfContained: true),
-		CharacterRule(primaryTag: CharacterRuleTag(tag: "[", type: .open, escapeCharacters: [EscapeCharacter(character: "\\", rule: .remove),EscapeCharacter(character: "!", rule: .keep)]), otherTags: [
+		], styles: [1 : CharacterStyle.link], metadataLookup: true, definesBoundary: true),
+		CharacterRule(primaryTag: CharacterRuleTag(tag: "[", type: .open), otherTags: [
 				CharacterRuleTag(tag: "]", type: .close),
 				CharacterRuleTag(tag: "]", type: .close),
 				CharacterRuleTag(tag: "(", type: .metadataOpen),
 				CharacterRuleTag(tag: "(", type: .metadataOpen),
 				CharacterRuleTag(tag: ")", type: .metadataClose)
 				CharacterRuleTag(tag: ")", type: .metadataClose)
-		], styles: [1 : [CharacterStyle.link]], metadataLookup: true, spacesAllowed: .bothSides, isSelfContained: true),
-		CharacterRule(primaryTag: CharacterRuleTag(tag: "`", type: .repeating), otherTags: [], styles: [1 : [CharacterStyle.code]], cancels: .allRemaining),
-		CharacterRule(primaryTag:CharacterRuleTag(tag: "~", type: .repeating, min: 2, max: 2), otherTags : [], styles: [2 : [CharacterStyle.strikethrough]]),
-		CharacterRule(primaryTag: CharacterRuleTag(tag: "*", type: .repeating, min: 1, max: 3), otherTags: [], styles: [1 : [CharacterStyle.italic], 2 : [CharacterStyle.bold], 3 : [CharacterStyle.bold, CharacterStyle.italic]]),
-		CharacterRule(primaryTag: CharacterRuleTag(tag: "_", type: .repeating, min: 1, max: 3), otherTags: [], styles: [1 : [CharacterStyle.italic], 2 : [CharacterStyle.bold], 3 : [CharacterStyle.bold, CharacterStyle.italic]])
+		], styles: [1 : CharacterStyle.link], metadataLookup: false, definesBoundary: true),
+		CharacterRule(primaryTag: CharacterRuleTag(tag: "`", type: .repeating), otherTags: [], styles: [1 : CharacterStyle.code], shouldCancelRemainingTags: true, balancedTags: true),
+		CharacterRule(primaryTag:CharacterRuleTag(tag: "~", type: .repeating), otherTags : [], styles: [2 : CharacterStyle.strikethrough], minTags:2 , maxTags:2),
+		CharacterRule(primaryTag: CharacterRuleTag(tag: "*", type: .repeating), otherTags: [], styles: [1 : CharacterStyle.italic, 2 : CharacterStyle.bold], minTags:1 , maxTags:2),
+		CharacterRule(primaryTag: CharacterRuleTag(tag: "_", type: .repeating), otherTags: [], styles: [1 : CharacterStyle.italic, 2 : CharacterStyle.bold], minTags:1 , maxTags:2)
 	]
 	]
 	
 	
 	let lineProcessor = SwiftyLineProcessor(rules: SwiftyMarkdown.lineRules, defaultRule: MarkdownLineStyle.body, frontMatterRules: SwiftyMarkdown.frontMatterRules)
 	let lineProcessor = SwiftyLineProcessor(rules: SwiftyMarkdown.lineRules, defaultRule: MarkdownLineStyle.body, frontMatterRules: SwiftyMarkdown.frontMatterRules)
@@ -550,16 +550,16 @@ extension SwiftyMarkdown {
 				attributes[.foregroundColor] = self.bold.color
 				attributes[.foregroundColor] = self.bold.color
 			}
 			}
 			
 			
-			if styles.contains(.link), let url = token.metadataString {
+			if let linkIdx = styles.firstIndex(of: .link), linkIdx < token.metadataStrings.count {
 				attributes[.foregroundColor] = self.link.color
 				attributes[.foregroundColor] = self.link.color
 				attributes[.font] = self.font(for: line, characterOverride: .link)
 				attributes[.font] = self.font(for: line, characterOverride: .link)
-				attributes[.link] = url as AnyObject
+				attributes[.link] = token.metadataStrings[linkIdx] as AnyObject
 				
 				
 				if underlineLinks {
 				if underlineLinks {
 					attributes[.underlineStyle] = NSUnderlineStyle.single.rawValue as AnyObject
 					attributes[.underlineStyle] = NSUnderlineStyle.single.rawValue as AnyObject
 				}
 				}
 			}
 			}
-			
+						
 			if styles.contains(.strikethrough) {
 			if styles.contains(.strikethrough) {
 				attributes[.font] = self.font(for: line, characterOverride: .strikethrough)
 				attributes[.font] = self.font(for: line, characterOverride: .strikethrough)
 				attributes[.strikethroughStyle] = NSUnderlineStyle.single.rawValue as AnyObject
 				attributes[.strikethroughStyle] = NSUnderlineStyle.single.rawValue as AnyObject
@@ -567,18 +567,18 @@ extension SwiftyMarkdown {
 			}
 			}
 			
 			
 			#if !os(watchOS)
 			#if !os(watchOS)
-			if styles.contains(.image), let imageName = token.metadataString {
+			if let imgIdx = styles.firstIndex(of: .image), imgIdx < token.metadataStrings.count {
 				if !self.applyAttachments {
 				if !self.applyAttachments {
 					continue
 					continue
 				}
 				}
 				#if !os(macOS)
 				#if !os(macOS)
 				let image1Attachment = NSTextAttachment()
 				let image1Attachment = NSTextAttachment()
-				image1Attachment.image = UIImage(named: imageName)
+				image1Attachment.image = UIImage(named: token.metadataStrings[imgIdx])
 				let str = NSAttributedString(attachment: image1Attachment)
 				let str = NSAttributedString(attachment: image1Attachment)
 				finalAttributedString.append(str)
 				finalAttributedString.append(str)
 				#elseif !os(watchOS)
 				#elseif !os(watchOS)
 				let image1Attachment = NSTextAttachment()
 				let image1Attachment = NSTextAttachment()
-				image1Attachment.image = NSImage(named: imageName)
+				image1Attachment.image = NSImage(named: token.metadataStrings[imgIdx])
 				let str = NSAttributedString(attachment: image1Attachment)
 				let str = NSAttributedString(attachment: image1Attachment)
 				finalAttributedString.append(str)
 				finalAttributedString.append(str)
 				#endif
 				#endif

+ 487 - 165
Sources/SwiftyMarkdown/SwiftyScannerNonRepeating.swift

@@ -15,235 +15,557 @@
 import Foundation
 import Foundation
 import os.log
 import os.log
 
 
-enum v2_TokenType {
-	case string
-	case link
-	case metadata
-	case tag
+
+extension OSLog {
+	private static var subsystem = "SwiftyScanner"
+	static let swiftyScannerScanner = OSLog(subsystem: subsystem, category: "Swifty Scanner Scanner")
+	static let swiftyScannerScannerPerformance = OSLog(subsystem: subsystem, category: "Swifty Scanner Scanner Peformance")
+}
+
+enum RepeatingTagType {
+	case open
+	case either
+	case close
+	case neither
 }
 }
 
 
-struct v2_Token {
-	var type : v2_TokenType
-	let string : String
-	var metadata : String = ""
-//	let startIndex : String.Index
+struct TagGroup {
+	let groupID  = UUID().uuidString
+	var tagRanges : [ClosedRange<Int>]
+	var tagType : RepeatingTagType = .open
+	var count = 1
 }
 }
 
 
-class SwiftyScannerNonRepeating : SwiftyScanning {
-	var metadataLookup: [String : String]
+class SwiftyScannerNonRepeating {
+	var elements : [Element]
+	let rule : CharacterRule
+	let metadata : [String : String]
+	var pointer : Int = 0
 	
 	
-	var str : String
-	var currentIndex : String.Index
+	var tagGroups : [TagGroup] = []
 	
 	
-	var rule : CharacterRule
-	var tokens : [Token]
-
-	var openIndices : [Int] = []
-	var accumulatedStr : String = ""
-	var stringList : [v2_Token] = []
-
+	var isMetadataOpen = false
+	
+	var enableLog = (ProcessInfo.processInfo.environment["SwiftyScannerScanner"] != nil)
 	
 	
-	init( tokens : [Token], rule : CharacterRule, metadataLookup : [String : String] = [:] ) {
-		self.tokens = tokens
+	let currentPerfomanceLog = PerformanceLog(with: "SwiftyScannerScannerPerformanceLogging", identifier: "Scanner", log: OSLog.swiftyScannerPerformance)
+	let log = PerformanceLog(with: "SwiftyScannerScanner", identifier: "Scanner", log: OSLog.swiftyScannerScanner)
+		
+	enum Position {
+		case forward(Int)
+		case backward(Int)
+	}
+	
+	init( withElements elements : [Element], rule : CharacterRule, metadata : [String : String]) {
+		self.elements = elements
 		self.rule = rule
 		self.rule = rule
-		self.str = tokens.map({ $0.inputString }).joined()
-		self.currentIndex = self.str.startIndex
-		self.metadataLookup = metadataLookup
+		self.currentPerfomanceLog.start()
+		self.metadata = metadata
 	}
 	}
 	
 	
-	func scan() -> [Token] {
+	func elementsBetweenCurrentPosition( and newPosition : Position ) -> [Element]? {
 		
 		
-		if !self.str.contains(rule.primaryTag.tag) {
-			return self.tokens
+		let newIdx : Int
+		var isForward = true
+		switch newPosition {
+		case .backward(let positions):
+			isForward = false
+			newIdx = pointer - positions
+			if newIdx < 0 {
+				return nil
+			}
+		case .forward(let positions):
+			newIdx = pointer + positions
+			if newIdx >= self.elements.count {
+				return nil
+			}
 		}
 		}
-		self.process()
-		return self.convertTokens()
+		
+		
+		let range : ClosedRange<Int> = ( isForward ) ? self.pointer...newIdx : newIdx...self.pointer
+		return Array(self.elements[range])
 	}
 	}
 	
 	
-	func emptyAccumulatedString() {
-		if !accumulatedStr.isEmpty {
-			stringList.append(v2_Token(type: .string, string: accumulatedStr))
-			accumulatedStr.removeAll()
+	
+	func element( for position : Position ) -> Element? {
+		let newIdx : Int
+		switch position {
+		case .backward(let positions):
+			newIdx = pointer - positions
+			if newIdx < 0 {
+				return nil
+			}
+		case .forward(let positions):
+			newIdx = pointer + positions
+			if newIdx >= self.elements.count {
+				return nil
+			}
+		}
+		return self.elements[newIdx]
+	}
+	
+	
+	func positionIsEqualTo( character : Character, direction : Position ) -> Bool {
+		guard let validElement = self.element(for: direction) else {
+			return false
+		}
+		return validElement.character == character
+	}
+	
+	func positionContains( characters : [Character], direction : Position ) -> Bool {
+		guard let validElement = self.element(for: direction) else {
+			return false
 		}
 		}
+		return characters.contains(validElement.character)
 	}
 	}
 	
 	
-	func process() {
-		var tokens : [Token] = []
+	func isEscaped() -> Bool {
+		let isEscaped = self.positionContains(characters: self.rule.escapeCharacters, direction: .backward(1))
+		if isEscaped {
+			self.elements[self.pointer - 1].type = .escape
+		}
+		return isEscaped
+	}
+	
+	func range( for tag : String? ) -> ClosedRange<Int>? {
 
 
-		let openTagStart = rule.primaryTag.tag[rule.primaryTag.tag.startIndex]
-		let closeTagStart = ( rule.tag(for: .close)?.tag != nil ) ? rule.tag(for: .close)?.tag[rule.tag(for: .close)!.tag.startIndex] : nil
+		guard let tag = tag else {
+			return nil
+		}
 		
 		
-
-
+		guard let openChar = tag.first else {
+			return nil
+		}
+		
+		if self.pointer == self.elements.count {
+			return nil
+		}
+		
+		if self.elements[self.pointer].character != openChar {
+			return nil
+		}
 		
 		
+		if isEscaped() {
+			return nil
+		}
+		
+		let range : ClosedRange<Int>
+		if tag.count > 1 {
+			guard let elements = self.elementsBetweenCurrentPosition(and: .forward(tag.count - 1) ) else {
+				return nil
+			}
+			// If it's already a tag, then it should be ignored
+			if elements.filter({ $0.type != .string }).count > 0 {
+				return nil
+			}
+			if elements.map( { String($0.character) }).joined() != tag {
+				return nil
+			}
+			let endIdx = (self.pointer + tag.count - 1)
+			for i in self.pointer...endIdx {
+				self.elements[i].type = .tag
+			}
+			range = self.pointer...endIdx
+			self.pointer += tag.count
+		} else {
+			// If it's already a tag, then it should be ignored
+			if self.elements[self.pointer].type != .string {
+				return nil
+			}
+			self.elements[self.pointer].type = .tag
+			range = self.pointer...self.pointer
+			self.pointer += 1
+		}
+		return range
+	}
+	
+	
+	func resetTagGroup( withID id : String ) {
+		if let idx = self.tagGroups.firstIndex(where: { $0.groupID == id }) {
+			for range in self.tagGroups[idx].tagRanges {
+				self.resetTag(in: range)
+			}
+			self.tagGroups.remove(at: idx)
+		}
+		self.isMetadataOpen = false
+	}
+	
+	func resetTag( in range : ClosedRange<Int>) {
+		for idx in range {
+			self.elements[idx].type = .string
+		}
+	}
+	
+	func resetLastTag( for range : inout [ClosedRange<Int>]) {
+		guard let last = range.last else {
+			return
+		}
+		for idx in last {
+			self.elements[idx].type = .string
+		}
+	}
+	
+	func closeTag( _ tag : String, withGroupID id : String ) {
+		guard var tagGroup = self.tagGroups.first(where: { $0.groupID == id }) else {
+			return
+		}
 		
 		
-		while currentIndex != str.endIndex {
-			let char = str[currentIndex]
+		var metadataString = ""
+		if self.isMetadataOpen {
+			let metadataCloseRange = tagGroup.tagRanges.removeLast()
+			let metadataOpenRange = tagGroup.tagRanges.removeLast()
 			
 			
-			if str[currentIndex] != openTagStart && str[currentIndex] != closeTagStart {
-				movePointer(&currentIndex, addCharacter: char)
-				continue
+			if metadataOpenRange.upperBound + 1 == (metadataCloseRange.lowerBound) {
+				if self.enableLog {
+					os_log("Nothing between the tags", log: OSLog.swiftyScannerScanner, type:.info , self.rule.description)
+				}
+			} else {
+				for idx in (metadataOpenRange.upperBound)...(metadataCloseRange.lowerBound) {
+					self.elements[idx].type = .metadata
+					if self.rule.definesBoundary {
+						self.elements[idx].boundaryCount += 1
+					}
+				}
+				
+				
+				let key = self.elements[metadataOpenRange.upperBound + 1..<metadataCloseRange.lowerBound].map( { String( $0.character )}).joined()
+				if self.rule.metadataLookup {
+					metadataString = self.metadata[key] ?? ""
+				} else {
+					metadataString = key
+				}
 			}
 			}
+		}
+		
+		let closeRange = tagGroup.tagRanges.removeLast()
+		let openRange = tagGroup.tagRanges.removeLast()
 
 
-			
-			// We have the first character of a possible open tag
-			if char == openTagStart {
-				// Checks to see if there is an escape character before this one
-				if let prevIndex = str.index(currentIndex, offsetBy: -1, limitedBy: str.startIndex) {
-					if let escapeChar = self.rule.primaryTag.escapeCharacter(for: str[prevIndex]) {
-						switch escapeChar.rule {
-						case .remove:
-							if !accumulatedStr.isEmpty {
-								accumulatedStr.removeLast()
-							}
-						case .keep:
-							break
+		if self.rule.balancedTags && closeRange.count != openRange.count {
+			return
+		}
+		
+		
+		var shouldRemove = true
+		var styles : [CharacterStyling] = []
+		if openRange.upperBound + 1 == (closeRange.lowerBound) {
+			if self.enableLog {
+				os_log("Nothing between the tags", log: OSLog.swiftyScannerScanner, type:.info , self.rule.description)
+			}
+		} else {
+			var remainingTags = min(openRange.upperBound - openRange.lowerBound, closeRange.upperBound - closeRange.lowerBound) + 1
+			while remainingTags > 0 {
+				if remainingTags >= self.rule.maxTags {
+					remainingTags -= self.rule.maxTags
+					if let style = self.rule.styles[ self.rule.maxTags ] {
+						if !styles.contains(where: { $0.isEqualTo(style)}) {
+							styles.append(style)
 						}
 						}
-						movePointer(&currentIndex, addCharacter: char)
-						continue
 					}
 					}
 				}
 				}
+				if let style = self.rule.styles[remainingTags] {
+					remainingTags -= remainingTags
+					if !styles.contains(where: { $0.isEqualTo(style)}) {
+						styles.append(style)
+					}
+				}
+			}
+			
+			for idx in (openRange.upperBound)...(closeRange.lowerBound) {
+				self.elements[idx].styles.append(contentsOf: styles)
+				self.elements[idx].metadata.append(metadataString)
+				if self.rule.definesBoundary {
+					self.elements[idx].boundaryCount += 1
+				}
+				if self.rule.shouldCancelRemainingTags {
+					self.elements[idx].boundaryCount = 1000
+				}
+			}
+			
+			if self.rule.isRepeatingTag {
+				let difference = ( openRange.upperBound - openRange.lowerBound ) - (closeRange.upperBound - closeRange.lowerBound)
+				switch difference {
+				case 1...:
+					for idx in openRange.upperBound - (difference - 1)...openRange.upperBound {
+						self.elements[idx].type = .string
+					}
+				case ...(-1):
+					for idx in closeRange.upperBound - (abs(difference) - 1)...closeRange.upperBound{
+						self.elements[idx].type = .string
+					}
+				default:
+					break
+				}
+			}
+			
+		}
+		if shouldRemove {
+			self.tagGroups.removeAll(where: { $0.groupID == id })
+			self.isMetadataOpen = false
+		}
+	}
+	
+	func emptyRanges( _ ranges : inout [ClosedRange<Int>] ) {
+		while !ranges.isEmpty {
+			self.resetLastTag(for: &ranges)
+			ranges.removeLast()
+		}
+	}
+	
+	func scanNonRepeatingTags() {
+		var groupID = ""
+		let closeTag = self.rule.tag(for: .close)?.tag
+		let metadataOpen = self.rule.tag(for: .metadataOpen)?.tag
+		let metadataClose = self.rule.tag(for: .metadataClose)?.tag
+		
+		while self.pointer < self.elements.count {
+			if self.enableLog {
+				os_log("CHARACTER: %@", log: OSLog.swiftyScannerScanner, type:.info , String(self.elements[self.pointer].character))
+			}
+			
+			if let range = self.range(for: metadataClose) {
+				if self.isMetadataOpen {
+					guard let groupIdx = self.tagGroups.firstIndex(where: { $0.groupID == groupID }) else {
+						self.pointer += 1
+						continue
+					}
+					
+					guard !self.tagGroups.isEmpty else {
+						self.resetTagGroup(withID: groupID)
+						continue
+					}
 				
 				
-				emptyAccumulatedString()
-				
-				guard let nextIdx = str.index(currentIndex, offsetBy: rule.primaryTag.tag.count, limitedBy: str.endIndex) else {
-					movePointer(&currentIndex, addCharacter: char)
+					guard self.isMetadataOpen else {
+						
+						self.resetTagGroup(withID: groupID)
+						continue
+					}
+					if self.enableLog {
+						os_log("Closing metadata tag found. Closing tag with ID %@", log: OSLog.swiftyScannerScanner, type:.info , groupID)
+					}
+					self.tagGroups[groupIdx].tagRanges.append(range)
+					self.closeTag(closeTag!, withGroupID: groupID)
+					self.isMetadataOpen = false
 					continue
 					continue
+				} else {
+					self.resetTag(in: range)
+					self.pointer -= metadataClose!.count
 				}
 				}
-				let tag = String(str[currentIndex..<nextIdx])
-				if tag != rule.primaryTag.tag {
-					movePointer(&currentIndex, addCharacter: char)
-					continue
+
+			}
+			
+			if let openRange = self.range(for: self.rule.primaryTag.tag) {
+				if self.isMetadataOpen {
+					self.resetTagGroup(withID: groupID)
 				}
 				}
 				
 				
-				openIndices.append(stringList.count)
-				stringList.append(v2_Token(type: .tag, string: tag))
-				currentIndex = str.index(currentIndex, offsetBy: rule.primaryTag.tag.count, limitedBy: str.endIndex) ?? str.endIndex
+				let tagGroup = TagGroup(tagRanges: [openRange])
+				groupID = tagGroup.groupID
+				if self.enableLog {
+					os_log("New open tag found. Starting new Group with ID %@", log: OSLog.swiftyScannerScanner, type:.info , groupID)
+				}
+				if self.rule.isRepeatingTag {
+					
+				}
+				
+				self.tagGroups.append(tagGroup)
 				continue
 				continue
 			}
 			}
-			if char == closeTagStart {
-				
-				emptyAccumulatedString()
-				
-				guard let closeTag = rule.tag(for: .close)?.tag else {
-					movePointer(&currentIndex, addCharacter: char)
+	
+			if let range = self.range(for: closeTag) {
+				guard !self.tagGroups.isEmpty else {
+					if self.enableLog {
+						os_log("No open tags exist, resetting this close tag", log: OSLog.swiftyScannerScanner, type:.info)
+					}
+					self.resetTag(in: range)
 					continue
 					continue
 				}
 				}
-				
-				guard let nextIdx = str.index(currentIndex, offsetBy: closeTag.count, limitedBy: str.endIndex) else {
-					movePointer(&currentIndex, addCharacter: char)
+				self.tagGroups[self.tagGroups.count - 1].tagRanges.append(range)
+				groupID = self.tagGroups[self.tagGroups.count - 1].groupID
+				if self.enableLog {
+					os_log("New close tag found. Appending to group with ID %@", log: OSLog.swiftyScannerScanner, type:.info , groupID)
+				}
+				guard metadataOpen != nil else {
+					if self.enableLog {
+						os_log("No metadata tags exist, closing valid tag with ID %@", log: OSLog.swiftyScannerScanner, type:.info , groupID)
+					}
+					self.closeTag(closeTag!, withGroupID: groupID)
 					continue
 					continue
 				}
 				}
-				let tag = String(str[currentIndex..<nextIdx])
-				if tag != closeTag {
-					movePointer(&currentIndex, addCharacter: char)
+				
+				guard self.pointer != self.elements.count else {
 					continue
 					continue
 				}
 				}
-				if openIndices.isEmpty {
-					stringList.append(v2_Token(type: .string, string: String(char)))
-					movePointer(&currentIndex)
+				
+				guard let range = self.range(for: metadataOpen) else {
+					if self.enableLog {
+						os_log("No metadata tag found, resetting group with ID %@", log: OSLog.swiftyScannerScanner, type:.info , groupID)
+					}
+					self.resetTagGroup(withID: groupID)
 					continue
 					continue
 				}
 				}
+				self.tagGroups[self.tagGroups.count - 1].tagRanges.append(range)
+				self.isMetadataOpen = true
+				continue
+			}
+			
 
 
-				// At this point we have gathered a valid close tag and we have a valid open tag
+			if let range = self.range(for: metadataOpen) {
+				if self.enableLog {
+					os_log("Multiple open metadata tags found!", log: OSLog.swiftyScannerScanner, type:.info , groupID)
+				}
+				self.resetTag(in: range)
+				self.resetTagGroup(withID: groupID)
+				self.isMetadataOpen = false
+				continue
+			}
+			self.pointer += 1
+		}
+	}
+	
+	var spaceAndNewLine = CharacterSet.whitespacesAndNewlines
+	
+	
+	
+	
+	func scanRepeatingTags() {
+				
+		var groupID = ""
+		let escapeCharacters = "" //self.rule.escapeCharacters.map( { String( $0 ) }).joined()
+		let unionSet = spaceAndNewLine.union(CharacterSet(charactersIn: escapeCharacters))
+		while self.pointer < self.elements.count {
+			if self.enableLog {
+				os_log("CHARACTER: %@", log: OSLog.swiftyScannerScanner, type:.info , String(self.elements[self.pointer].character))
+			}
+			
+			if var openRange = self.range(for: self.rule.primaryTag.tag) {
 				
 				
-				guard let metadataOpen = rule.tag(for: .metadataOpen), let metadataClose = rule.tag(for: .metadataClose) else {
-					currentIndex = nextIdx
-					addLink()
+				if self.elements[openRange].first?.boundaryCount == 1000 {
+					self.resetTag(in: openRange)
 					continue
 					continue
 				}
 				}
-				if nextIdx == str.endIndex {
-					movePointer(&currentIndex, addCharacter: char)
-					continue
+				
+				var count = 1
+				var tagType : RepeatingTagType = .open
+				if let prevElement = self.element(for: .backward(self.rule.primaryTag.tag.count + 1))  {
+					if !unionSet.containsUnicodeScalars(of: prevElement.character) {
+						tagType = .either
+					}
+				} else {
+					tagType = .open
 				}
 				}
-				guard str[nextIdx] == metadataOpen.tag.first else {
-					movePointer(&currentIndex, addCharacter: char)
-					continue
+				
+				while let nextRange = self.range(for: self.rule.primaryTag.tag)  {
+					count += 1
+					openRange = openRange.lowerBound...nextRange.upperBound
 				}
 				}
 				
 				
-				let substr = str[nextIdx..<str.endIndex]
-				guard let closeIdx = substr.firstIndex(of: metadataClose.tag.first!) else {
-					movePointer(&currentIndex, addCharacter: char)
-					continue
+				if self.rule.minTags > 1 {
+					if (openRange.upperBound - openRange.lowerBound) + 1 < self.rule.minTags {
+						self.resetTag(in: openRange)
+						os_log("Tag does not meet minimum length", log: .swiftyScannerScanner, type: .info)
+						continue
+					}
+				}
+				
+				var validTagGroup = true
+				if let nextElement = self.element(for: .forward(0)) {
+					if unionSet.containsUnicodeScalars(of: nextElement.character) {
+						if tagType == .either {
+							tagType = .close
+						} else {
+							validTagGroup = tagType != .open
+						}
+					}
+				} else {
+					if tagType == .either {
+						tagType = .close
+					} else {
+						validTagGroup = tagType != .open
+					}
 				}
 				}
-				let open = substr.index(nextIdx, offsetBy: 1, limitedBy: substr.endIndex) ?? substr.endIndex
-				let metadataStr = String(substr[open..<closeIdx])
 				
 				
-				guard !metadataStr.contains(rule.primaryTag.tag) else {
-					movePointer(&currentIndex, addCharacter: char)
-					continue
 
 
+				
+				
+				if !validTagGroup {
+					if self.enableLog {
+						os_log("Tag has whitespace on both sides", log: .swiftyScannerScanner, type: .info)
+					}
+					self.resetTag(in: openRange)
+					continue
 				}
 				}
 				
 				
-				currentIndex = str.index(closeIdx, offsetBy: 1, limitedBy: str.endIndex) ?? closeIdx
+				if let idx = tagGroups.firstIndex(where: { $0.groupID == groupID }) {
+					
+					if tagType == .either {
+						if tagGroups[idx].count == count {
+							self.tagGroups[idx].tagRanges.append(openRange)
+							self.closeTag(self.rule.primaryTag.tag, withGroupID: groupID)
+							
+							if let last = self.tagGroups.last {
+								groupID = last.groupID
+							}
+							
+							continue
+						}
+					} else {
+						if let prevRange = tagGroups[idx].tagRanges.first {
+							if self.elements[prevRange].first?.boundaryCount == self.elements[openRange].first?.boundaryCount {
+								self.tagGroups[idx].tagRanges.append(openRange)
+								self.closeTag(self.rule.primaryTag.tag, withGroupID: groupID)
+							}
+						}
+						continue
+					}
+					
 
 
-				addLink(with: metadataStr)
+	
+				}
+				var tagGroup = TagGroup(tagRanges: [openRange])
+				groupID = tagGroup.groupID
+				tagGroup.tagType = tagType
+				tagGroup.count = count
+				
+				if self.enableLog {
+					os_log("New open tag found. Starting new Group with ID %@", log: OSLog.swiftyScannerScanner, type:.info , groupID)
+				}
+				
+				self.tagGroups.append(tagGroup)
+				continue
 			}
 			}
-		}
-
-		if !accumulatedStr.isEmpty {
-			stringList.append(v2_Token(type: .string, string: accumulatedStr))
-		}
-	}
 	
 	
-	func movePointer( _ idx : inout String.Index, addCharacter char : Character? = nil ) {
-		idx = str.index(idx, offsetBy: 1, limitedBy: str.endIndex) ?? str.endIndex
-		if let character = char {
-			accumulatedStr.append(character)
+			self.pointer += 1
 		}
 		}
 	}
 	}
 	
 	
-	func addLink(with metadataStr : String? = nil) {
-		let openIndex = openIndices.removeLast()
-		stringList.remove(at: openIndex)
-		let subarray = stringList[openIndex..<stringList.count]
-		stringList.removeSubrange(openIndex..<stringList.count)
-		stringList.append(v2_Token(type: .link, string: subarray.map({ $0.string }).joined(), metadata: metadataStr ?? ""))
-	}
 	
 	
-	func convertTokens() -> [Token] {
-		if !stringList.contains(where: { $0.type == .link }) {
-			return [Token(type: .string, inputString: stringList.map({ $0.string}).joined())]
+	func scan() -> [Element] {
+		
+		guard self.elements.filter({ $0.type == .string }).map({ String($0.character) }).joined().contains(self.rule.primaryTag.tag) else {
+			return self.elements
 		}
 		}
-		var tokens : [Token] = []
-		var allStrings : [v2_Token] = []
-		for tok in stringList {
-			if tok.type == .link {
-				if !allStrings.isEmpty {
-					tokens.append(Token(type: .string, inputString: allStrings.map({ $0.string }).joined()))
-					allStrings.removeAll()
-				}
-				let ruleStyles = self.rule.styles[1] ?? []
-				let charStyles = ( rule.isSelfContained ) ? [] : ruleStyles
-				var token = Token(type: .string, inputString: tok.string, characterStyles: charStyles)
-				token.metadataString = tok.metadata
-				
-				if rule.isSelfContained {
-					var parentToken = Token(type: .string, inputString: token.id, characterStyles: ruleStyles)
-					parentToken.children = [token]
-					tokens.append(parentToken)
-				} else {
-					tokens.append(token)
-				}
-			} else {
-				allStrings.append(tok)
-			}
+		
+		self.currentPerfomanceLog.tag(with: "Beginning \(self.rule.primaryTag.tag)")
+		
+		if self.enableLog {
+			os_log("RULE: %@", log: OSLog.swiftyScannerScanner, type:.info , self.rule.description)
 		}
 		}
-		if !allStrings.isEmpty {
-			tokens.append(Token(type: .string, inputString: allStrings.map({ $0.string }).joined()))
+		
+		if self.rule.isRepeatingTag {
+			self.scanRepeatingTags()
+		} else {
+			self.scanNonRepeatingTags()
 		}
 		}
 		
 		
-		return tokens
-	}
-	
-	// Old
-	
-	func scan( _ tokens : [Token], with rule : CharacterRule ) -> [Token] {
-		self.tokens = tokens
-		return self.scan(tokens.map({ $0.inputString }).joined(), with: rule)
-	}
-	
-	func scan(_ string: String, with rule: CharacterRule) -> [Token] {
-		return []
+		for tagGroup in self.tagGroups {
+			self.resetTagGroup(withID: tagGroup.groupID)
+		}
+		
+		if self.enableLog {
+			for element in self.elements {
+				print(element)
+			}
+		}
+		return self.elements
 	}
 	}
 }
 }

+ 55 - 30
Sources/SwiftyMarkdown/SwiftyTokeniser.swift

@@ -26,6 +26,10 @@ public class SwiftyTokeniser {
 	var scanner : SwiftyScanning!
 	var scanner : SwiftyScanning!
 	public var metadataLookup : [String : String] = [:]
 	public var metadataLookup : [String : String] = [:]
 	
 	
+	let newlines = CharacterSet.newlines
+	let spaces = CharacterSet.whitespaces
+
+	
 	public init( with rules : [CharacterRule] ) {
 	public init( with rules : [CharacterRule] ) {
 		self.rules = rules
 		self.rules = rules
 		
 		
@@ -60,6 +64,22 @@ public class SwiftyTokeniser {
 		}
 		}
 		
 		
 		self.currentPerfomanceLog.start()
 		self.currentPerfomanceLog.start()
+	
+		var elementArray : [Element] = []
+		for char in inputString {
+			if newlines.containsUnicodeScalars(of: char) {
+				let element = Element(character: char, type: .newline)
+				elementArray.append(element)
+				continue
+			}
+			if spaces.containsUnicodeScalars(of: char) {
+				let element = Element(character: char, type: .space)
+				elementArray.append(element)
+				continue
+			}
+			let element = Element(character: char, type: .string)
+			elementArray.append(element)
+		}
 		
 		
 		while !mutableRules.isEmpty {
 		while !mutableRules.isEmpty {
 			let nextRule = mutableRules.removeFirst()
 			let nextRule = mutableRules.removeFirst()
@@ -68,32 +88,50 @@ public class SwiftyTokeniser {
 				self.scanner = SwiftyScanner()
 				self.scanner = SwiftyScanner()
 				self.scanner.metadataLookup = self.metadataLookup
 				self.scanner.metadataLookup = self.metadataLookup
 			}
 			}
-			
-			
+
 			if enableLog {
 			if enableLog {
 				os_log("------------------------------", log: .tokenising, type: .info)
 				os_log("------------------------------", log: .tokenising, type: .info)
 				os_log("RULE: %@", log: OSLog.tokenising, type:.info , nextRule.description)
 				os_log("RULE: %@", log: OSLog.tokenising, type:.info , nextRule.description)
 			}
 			}
 			self.currentPerfomanceLog.tag(with: "(start rule %@)")
 			self.currentPerfomanceLog.tag(with: "(start rule %@)")
 			
 			
-			for (idx,token) in currentTokens.enumerated() {
-				if !token.children.isEmpty {
-					if nextRule.isRepeatingTag {
-						currentTokens[idx].children = self.scanner.scan(token.children, with: nextRule)
-					} else {
-						let scanner = SwiftyScannerNonRepeating(tokens: token.children, rule: nextRule, metadataLookup: self.metadataLookup)
-						currentTokens[idx].children = scanner.scan()
-					}
-					
-				}
+			let scanner = SwiftyScannerNonRepeating(withElements: elementArray, rule: nextRule, metadata: self.metadataLookup)
+			elementArray = scanner.scan()
+		}
+		
+		var output : [Token] = []
+		
+		
+		func empty( _ string : inout String, into tokens : inout [Token] )  {
+			guard !string.isEmpty else {
+				return
+			}
+			var token = Token(type: .string, inputString: string)
+			token.metadataStrings.append(contentsOf: lastElement.metadata) 
+			token.characterStyles = lastElement.styles
+			string.removeAll()
+			tokens.append(token)
+		}
+		
+		
+		var lastElement = elementArray.first!
+		var accumulatedString = ""
+		for element in elementArray {
+			guard element.type != .escape else {
+				continue
+			}
+			
+			guard element.type == .string || element.type == .space || element.type == .newline else {
+				empty(&accumulatedString, into: &output)
+				continue
 			}
 			}
-			if nextRule.isRepeatingTag {
-				currentTokens = self.scanner.scan(currentTokens, with: nextRule)
-			} else {
-				let scanner = SwiftyScannerNonRepeating(tokens: currentTokens, rule: nextRule, metadataLookup: self.metadataLookup)
-				currentTokens = scanner.scan()
+			if lastElement.styles as? [CharacterStyle] != element.styles as? [CharacterStyle] {
+				empty(&accumulatedString, into: &output)
 			}
 			}
+			accumulatedString.append(element.character)
+			lastElement = element
 		}
 		}
+		empty(&accumulatedString, into: &output)
 		
 		
 		self.currentPerfomanceLog.tag(with: "(finished all rules)")
 		self.currentPerfomanceLog.tag(with: "(finished all rules)")
 		
 		
@@ -101,22 +139,9 @@ public class SwiftyTokeniser {
 			os_log("=====RULE PROCESSING COMPLETE=====", log: .tokenising, type: .info)
 			os_log("=====RULE PROCESSING COMPLETE=====", log: .tokenising, type: .info)
 			os_log("==================================", log: .tokenising, type: .info)
 			os_log("==================================", log: .tokenising, type: .info)
 		}
 		}
-		var output = self.flatten(currentTokens)
 		return output
 		return output
 	}
 	}
 	
 	
-	func flatten( _ tokens : [Token], with styles : [CharacterStyling] = []) -> [Token] {
-		var output : [Token] = []
-		for var token in tokens {
-			if !token.children.isEmpty {
-				output.append(contentsOf: self.flatten(token.children, with: token.characterStyles))
-			} else {
-				token.characterStyles.append(contentsOf: styles)
-				output.append(token)
-			}
-		}
-		return output
-	}
 	
 	
 //
 //
 //
 //

+ 2 - 2
Sources/SwiftyMarkdown/Token.swift

@@ -27,7 +27,7 @@ public struct Token {
 	public let id = UUID().uuidString
 	public let id = UUID().uuidString
 	public let type : TokenType
 	public let type : TokenType
 	public let inputString : String
 	public let inputString : String
-	public var metadataString : String? = nil
+	public var metadataStrings : [String] = []
 	public internal(set) var group : Int = 0
 	public internal(set) var group : Int = 0
 	public internal(set) var characterStyles : [CharacterStyling] = []
 	public internal(set) var characterStyles : [CharacterStyling] = []
 	public internal(set) var count : Int = 0
 	public internal(set) var count : Int = 0
@@ -67,7 +67,7 @@ public struct Token {
 	
 	
 	func newToken( fromSubstring string: String,  isReplacement : Bool) -> Token {
 	func newToken( fromSubstring string: String,  isReplacement : Bool) -> Token {
 		var newToken = Token(type: (isReplacement) ? .replacement : .string , inputString: string, characterStyles: self.characterStyles)
 		var newToken = Token(type: (isReplacement) ? .replacement : .string , inputString: string, characterStyles: self.characterStyles)
-		newToken.metadataString = self.metadataString
+		newToken.metadataStrings = self.metadataStrings
 		newToken.isMetadata = self.isMetadata
 		newToken.isMetadata = self.isMetadata
 		newToken.isProcessed = self.isProcessed
 		newToken.isProcessed = self.isProcessed
 		return newToken
 		return newToken

+ 4 - 4
SwiftyMarkdown.xcodeproj/project.pbxproj

@@ -25,7 +25,7 @@
 		F4ACB6CE23E8A88400EA665D /* Token.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4ACB6CD23E8A88400EA665D /* Token.swift */; };
 		F4ACB6CE23E8A88400EA665D /* Token.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4ACB6CD23E8A88400EA665D /* Token.swift */; };
 		F4ACB6D023E8A8A500EA665D /* CharacterRule.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4ACB6CF23E8A8A500EA665D /* CharacterRule.swift */; };
 		F4ACB6D023E8A8A500EA665D /* CharacterRule.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4ACB6CF23E8A8A500EA665D /* CharacterRule.swift */; };
 		F4ACB6D223E8B08400EA665D /* PerfomanceLog.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4ACB6D123E8B08400EA665D /* PerfomanceLog.swift */; };
 		F4ACB6D223E8B08400EA665D /* PerfomanceLog.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4ACB6D123E8B08400EA665D /* PerfomanceLog.swift */; };
-		F4ACB6D423E8B5C500EA665D /* SwiftyMarkdown+CharacterRule.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4ACB6D323E8B5C500EA665D /* SwiftyMarkdown+CharacterRule.swift */; };
+		F4C95126243ECB320059AB15 /* SwiftyScannerNonRepeating.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4C95125243ECB320059AB15 /* SwiftyScannerNonRepeating.swift */; };
 		OBJ_40 /* String+SwiftyMarkdown.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_9 /* String+SwiftyMarkdown.swift */; };
 		OBJ_40 /* String+SwiftyMarkdown.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_9 /* String+SwiftyMarkdown.swift */; };
 		OBJ_41 /* SwiftyLineProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_10 /* SwiftyLineProcessor.swift */; };
 		OBJ_41 /* SwiftyLineProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_10 /* SwiftyLineProcessor.swift */; };
 		OBJ_42 /* SwiftyMarkdown+iOS.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_11 /* SwiftyMarkdown+iOS.swift */; };
 		OBJ_42 /* SwiftyMarkdown+iOS.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_11 /* SwiftyMarkdown+iOS.swift */; };
@@ -64,7 +64,7 @@
 		F4ACB6CD23E8A88400EA665D /* Token.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Token.swift; sourceTree = "<group>"; };
 		F4ACB6CD23E8A88400EA665D /* Token.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Token.swift; sourceTree = "<group>"; };
 		F4ACB6CF23E8A8A500EA665D /* CharacterRule.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CharacterRule.swift; sourceTree = "<group>"; };
 		F4ACB6CF23E8A8A500EA665D /* CharacterRule.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CharacterRule.swift; sourceTree = "<group>"; };
 		F4ACB6D123E8B08400EA665D /* PerfomanceLog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PerfomanceLog.swift; sourceTree = "<group>"; };
 		F4ACB6D123E8B08400EA665D /* PerfomanceLog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PerfomanceLog.swift; sourceTree = "<group>"; };
-		F4ACB6D323E8B5C500EA665D /* SwiftyMarkdown+CharacterRule.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SwiftyMarkdown+CharacterRule.swift"; sourceTree = "<group>"; };
+		F4C95125243ECB320059AB15 /* SwiftyScannerNonRepeating.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SwiftyScannerNonRepeating.swift; sourceTree = "<group>"; };
 		OBJ_10 /* SwiftyLineProcessor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwiftyLineProcessor.swift; sourceTree = "<group>"; };
 		OBJ_10 /* SwiftyLineProcessor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwiftyLineProcessor.swift; sourceTree = "<group>"; };
 		OBJ_11 /* SwiftyMarkdown+iOS.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SwiftyMarkdown+iOS.swift"; sourceTree = "<group>"; };
 		OBJ_11 /* SwiftyMarkdown+iOS.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SwiftyMarkdown+iOS.swift"; sourceTree = "<group>"; };
 		OBJ_12 /* SwiftyMarkdown+macOS.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SwiftyMarkdown+macOS.swift"; sourceTree = "<group>"; };
 		OBJ_12 /* SwiftyMarkdown+macOS.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SwiftyMarkdown+macOS.swift"; sourceTree = "<group>"; };
@@ -176,10 +176,10 @@
 				OBJ_11 /* SwiftyMarkdown+iOS.swift */,
 				OBJ_11 /* SwiftyMarkdown+iOS.swift */,
 				OBJ_12 /* SwiftyMarkdown+macOS.swift */,
 				OBJ_12 /* SwiftyMarkdown+macOS.swift */,
 				OBJ_13 /* SwiftyMarkdown.swift */,
 				OBJ_13 /* SwiftyMarkdown.swift */,
-				F4ACB6D323E8B5C500EA665D /* SwiftyMarkdown+CharacterRule.swift */,
 				OBJ_14 /* SwiftyTokeniser.swift */,
 				OBJ_14 /* SwiftyTokeniser.swift */,
 				F4ACB6CD23E8A88400EA665D /* Token.swift */,
 				F4ACB6CD23E8A88400EA665D /* Token.swift */,
 				F4ACB6CA23E8A5C500EA665D /* SwiftyScanner.swift */,
 				F4ACB6CA23E8A5C500EA665D /* SwiftyScanner.swift */,
+				F4C95125243ECB320059AB15 /* SwiftyScannerNonRepeating.swift */,
 				F4ACB6CF23E8A8A500EA665D /* CharacterRule.swift */,
 				F4ACB6CF23E8A8A500EA665D /* CharacterRule.swift */,
 				F4ACB6D123E8B08400EA665D /* PerfomanceLog.swift */,
 				F4ACB6D123E8B08400EA665D /* PerfomanceLog.swift */,
 			);
 			);
@@ -271,13 +271,13 @@
 			isa = PBXSourcesBuildPhase;
 			isa = PBXSourcesBuildPhase;
 			buildActionMask = 0;
 			buildActionMask = 0;
 			files = (
 			files = (
-				F4ACB6D423E8B5C500EA665D /* SwiftyMarkdown+CharacterRule.swift in Sources */,
 				OBJ_40 /* String+SwiftyMarkdown.swift in Sources */,
 				OBJ_40 /* String+SwiftyMarkdown.swift in Sources */,
 				OBJ_41 /* SwiftyLineProcessor.swift in Sources */,
 				OBJ_41 /* SwiftyLineProcessor.swift in Sources */,
 				F4ACB6D223E8B08400EA665D /* PerfomanceLog.swift in Sources */,
 				F4ACB6D223E8B08400EA665D /* PerfomanceLog.swift in Sources */,
 				F4ACB6CB23E8A5C500EA665D /* SwiftyScanner.swift in Sources */,
 				F4ACB6CB23E8A5C500EA665D /* SwiftyScanner.swift in Sources */,
 				OBJ_42 /* SwiftyMarkdown+iOS.swift in Sources */,
 				OBJ_42 /* SwiftyMarkdown+iOS.swift in Sources */,
 				OBJ_43 /* SwiftyMarkdown+macOS.swift in Sources */,
 				OBJ_43 /* SwiftyMarkdown+macOS.swift in Sources */,
+				F4C95126243ECB320059AB15 /* SwiftyScannerNonRepeating.swift in Sources */,
 				OBJ_44 /* SwiftyMarkdown.swift in Sources */,
 				OBJ_44 /* SwiftyMarkdown.swift in Sources */,
 				OBJ_45 /* SwiftyTokeniser.swift in Sources */,
 				OBJ_45 /* SwiftyTokeniser.swift in Sources */,
 				F4ACB6D023E8A8A500EA665D /* CharacterRule.swift in Sources */,
 				F4ACB6D023E8A8A500EA665D /* CharacterRule.swift in Sources */,

+ 42 - 0
SwiftyMarkdown.xcodeproj/xcshareddata/xcbaselines/SwiftyMarkdown::SwiftyMarkdownTests.xcbaseline/88991ED5-B954-422F-B610-BDC9A4AEC008.plist

@@ -0,0 +1,42 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+	<key>classNames</key>
+	<dict>
+		<key>SwiftyMarkdownPerformanceTests</key>
+		<dict>
+			<key>testThatFilesAreProcessedQuickly()</key>
+			<dict>
+				<key>com.apple.XCTPerformanceMetric_WallClockTime</key>
+				<dict>
+					<key>baselineAverage</key>
+					<real>0.1</real>
+					<key>baselineIntegrationDisplayName</key>
+					<string>Local Baseline</string>
+				</dict>
+			</dict>
+			<key>testThatStringsAreProcessedQuickly()</key>
+			<dict>
+				<key>com.apple.XCTPerformanceMetric_WallClockTime</key>
+				<dict>
+					<key>baselineAverage</key>
+					<real>0.1</real>
+					<key>baselineIntegrationDisplayName</key>
+					<string>Local Baseline</string>
+				</dict>
+			</dict>
+			<key>testThatVeryLongStringsAreProcessedQuickly()</key>
+			<dict>
+				<key>com.apple.XCTPerformanceMetric_WallClockTime</key>
+				<dict>
+					<key>baselineAverage</key>
+					<real>0.1</real>
+					<key>baselineIntegrationDisplayName</key>
+					<string>Local Baseline</string>
+				</dict>
+			</dict>
+		</dict>
+	</dict>
+</dict>
+</plist>

+ 24 - 0
SwiftyMarkdown.xcodeproj/xcshareddata/xcbaselines/SwiftyMarkdown::SwiftyMarkdownTests.xcbaseline/Info.plist

@@ -4,6 +4,30 @@
 <dict>
 <dict>
 	<key>runDestinationsByUUID</key>
 	<key>runDestinationsByUUID</key>
 	<dict>
 	<dict>
+		<key>88991ED5-B954-422F-B610-BDC9A4AEC008</key>
+		<dict>
+			<key>localComputer</key>
+			<dict>
+				<key>busSpeedInMHz</key>
+				<integer>400</integer>
+				<key>cpuCount</key>
+				<integer>1</integer>
+				<key>cpuKind</key>
+				<string>8-Core Intel Core i9</string>
+				<key>cpuSpeedInMHz</key>
+				<integer>2400</integer>
+				<key>logicalCPUCoresPerPackage</key>
+				<integer>16</integer>
+				<key>modelCode</key>
+				<string>MacBookPro16,1</string>
+				<key>physicalCPUCoresPerPackage</key>
+				<integer>8</integer>
+				<key>platformIdentifier</key>
+				<string>com.apple.platform.macosx</string>
+			</dict>
+			<key>targetArchitecture</key>
+			<string>x86_64h</string>
+		</dict>
 		<key>AD1DF83E-20BC-4E7E-8C14-683818ED0A26</key>
 		<key>AD1DF83E-20BC-4E7E-8C14-683818ED0A26</key>
 		<dict>
 		<dict>
 			<key>localComputer</key>
 			<key>localComputer</key>

+ 10 - 0
SwiftyMarkdown.xcodeproj/xcshareddata/xcschemes/SwiftyMarkdown-Package.xcscheme

@@ -56,6 +56,16 @@
             value = ""
             value = ""
             isEnabled = "NO">
             isEnabled = "NO">
          </EnvironmentVariable>
          </EnvironmentVariable>
+         <EnvironmentVariable
+            key = "SwiftyScannerScanner"
+            value = ""
+            isEnabled = "NO">
+         </EnvironmentVariable>
+         <EnvironmentVariable
+            key = "SwiftyScannerScannerPerformanceLogging"
+            value = ""
+            isEnabled = "NO">
+         </EnvironmentVariable>
          <EnvironmentVariable
          <EnvironmentVariable
             key = "SwiftyLineProcessorPerformanceLogging"
             key = "SwiftyLineProcessorPerformanceLogging"
             value = ""
             value = ""

+ 14 - 21
Tests/SwiftyMarkdownTests/SwiftyMarkdownCharacterTests.swift

@@ -12,12 +12,11 @@ import XCTest
 class SwiftyMarkdownStylingTests: SwiftyMarkdownCharacterTests {
 class SwiftyMarkdownStylingTests: SwiftyMarkdownCharacterTests {
 	
 	
 	func testIsolatedCase() {
 	func testIsolatedCase() {
-		
-		challenge = TokenTest(input: "An ![Image](imageName)", output: "An ", tokens: [
-			Token(type: .string, inputString: "An ", characterStyles: []),
-			Token(type: .string, inputString: "Image", characterStyles: [CharacterStyle.image])
+
+		challenge = TokenTest(input: "```code`", output: "```code`", tokens : [
+			Token(type: .string, inputString: "```code`", characterStyles: [])
 		])
 		])
-		results = self.attempt(challenge, rules: [.links, .images])
+		results = self.attempt(challenge)
 		if results.stringTokens.count == challenge.tokens.count {
 		if results.stringTokens.count == challenge.tokens.count {
 			for (idx, token) in results.stringTokens.enumerated() {
 			for (idx, token) in results.stringTokens.enumerated() {
 				XCTAssertEqual(token.inputString, challenge.tokens[idx].inputString)
 				XCTAssertEqual(token.inputString, challenge.tokens[idx].inputString)
@@ -26,14 +25,8 @@ class SwiftyMarkdownStylingTests: SwiftyMarkdownCharacterTests {
 		} else {
 		} else {
 			XCTAssertEqual(results.stringTokens.count, challenge.tokens.count)
 			XCTAssertEqual(results.stringTokens.count, challenge.tokens.count)
 		}
 		}
-		XCTAssertEqual(results.attributedString.string, challenge.output)
 		XCTAssertEqual(results.foundStyles, results.expectedStyles)
 		XCTAssertEqual(results.foundStyles, results.expectedStyles)
-		var links = results.tokens.filter({ $0.type == .string && (($0.characterStyles as? [CharacterStyle])?.contains(.image) ?? false) })
-		if links.count == 1 {
-			XCTAssertEqual(links[0].metadataString, "imageName")
-		} else {
-			XCTFail("Incorrect link count. Expecting 1, found \(links.count)")
-		}
+		XCTAssertEqual(results.attributedString.string, challenge.output)
 		
 		
 		return
 		return
 		
 		
@@ -70,7 +63,7 @@ class SwiftyMarkdownStylingTests: SwiftyMarkdownCharacterTests {
 		XCTAssertEqual(results.foundStyles, results.expectedStyles)
 		XCTAssertEqual(results.foundStyles, results.expectedStyles)
 		XCTAssertEqual(results.attributedString.string, challenge.output)
 		XCTAssertEqual(results.attributedString.string, challenge.output)
 		if results.links.count == 1 {
 		if results.links.count == 1 {
-			XCTAssertEqual(results.links[0].metadataString, "https://www.neverendingvoyage.com/")
+			XCTAssertEqual(results.links[0].metadataStrings.first, "https://www.neverendingvoyage.com/")
 		} else {
 		} else {
 			XCTFail("Incorrect link count. Expecting 1, found \(results.links.count)")
 			XCTFail("Incorrect link count. Expecting 1, found \(results.links.count)")
 		}
 		}
@@ -516,7 +509,7 @@ class SwiftyMarkdownStylingTests: SwiftyMarkdownCharacterTests {
 	}
 	}
 	
 	
 	func testForExtremeEscapeCombinations() {
 	func testForExtremeEscapeCombinations() {
-		challenge = TokenTest(input: "Before *\\***\\****A bold string*\\***\\****\\ After", output: "Before ***A bold string***\\", tokens : [
+		challenge = TokenTest(input: "Before *\\***\\****A bold string*\\***\\****\\ After", output: "Before ***A bold string***\\ After", tokens : [
 			Token(type: .string, inputString: "Before ", characterStyles: []),
 			Token(type: .string, inputString: "Before ", characterStyles: []),
 			Token(type: .string, inputString: "*", characterStyles: [CharacterStyle.italic]),
 			Token(type: .string, inputString: "*", characterStyles: [CharacterStyle.italic]),
 			Token(type: .string, inputString: "**", characterStyles: [CharacterStyle.bold]),
 			Token(type: .string, inputString: "**", characterStyles: [CharacterStyle.bold]),
@@ -552,9 +545,9 @@ class SwiftyMarkdownStylingTests: SwiftyMarkdownCharacterTests {
 		XCTAssertEqual(results.foundStyles, results.expectedStyles)
 		XCTAssertEqual(results.foundStyles, results.expectedStyles)
 		XCTAssertEqual(results.attributedString.string, challenge.output)
 		XCTAssertEqual(results.attributedString.string, challenge.output)
 		
 		
-		challenge = TokenTest(input: "A string with a ****bold italic**** word", output: "A string with a *bold italic* word",  tokens: [
+		challenge = 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: "A string with a ", characterStyles: []),
-			Token(type: .string, inputString: "*bold italic*", characterStyles: [CharacterStyle.bold, CharacterStyle.italic]),
+			Token(type: .string, inputString: "bold", characterStyles: [CharacterStyle.bold]),
 			Token(type: .string, inputString: " word", characterStyles: [])
 			Token(type: .string, inputString: " word", characterStyles: [])
 		])
 		])
 		results = self.attempt(challenge)
 		results = self.attempt(challenge)
@@ -607,7 +600,7 @@ class SwiftyMarkdownStylingTests: SwiftyMarkdownCharacterTests {
 		challenge = TokenTest(input: "A string with a **bold*italic*bold** word", output: "A string with a bolditalicbold word",  tokens: [
 		challenge = TokenTest(input: "A string with a **bold*italic*bold** word", output: "A string with a bolditalicbold word",  tokens: [
 			Token(type: .string, inputString: "A string with a ", characterStyles: []),
 			Token(type: .string, inputString: "A string with a ", characterStyles: []),
 			Token(type: .string, inputString: "bold", characterStyles: [CharacterStyle.bold]),
 			Token(type: .string, inputString: "bold", characterStyles: [CharacterStyle.bold]),
-			Token(type: .string, inputString: "italic", characterStyles: [CharacterStyle.bold, CharacterStyle.italic]),
+			Token(type: .string, inputString: "italic", characterStyles: [CharacterStyle.italic, CharacterStyle.bold]),
 			Token(type: .string, inputString: "bold", characterStyles: [CharacterStyle.bold]),
 			Token(type: .string, inputString: "bold", characterStyles: [CharacterStyle.bold]),
 			Token(type: .string, inputString: " word", characterStyles: [])
 			Token(type: .string, inputString: " word", characterStyles: [])
 		])
 		])
@@ -623,8 +616,8 @@ class SwiftyMarkdownStylingTests: SwiftyMarkdownCharacterTests {
 		XCTAssertEqual(results.foundStyles, results.expectedStyles)
 		XCTAssertEqual(results.foundStyles, results.expectedStyles)
 		XCTAssertEqual(results.attributedString.string, challenge.output)
 		XCTAssertEqual(results.attributedString.string, challenge.output)
 		
 		
-		challenge = TokenTest(input: "A string with ```code`", output: "A string with ``code", tokens : [
-			Token(type: .string, inputString: "A string with ``", characterStyles: []),
+		challenge = TokenTest(input: "A string with ```code`", output: "A string with ```code`", tokens : [
+			Token(type: .string, inputString: "A string with ```code`", characterStyles: []),
 			Token(type: .string, inputString: "code", characterStyles: [CharacterStyle.code])
 			Token(type: .string, inputString: "code", characterStyles: [CharacterStyle.code])
 		])
 		])
 		results = self.attempt(challenge)
 		results = self.attempt(challenge)
@@ -634,7 +627,7 @@ class SwiftyMarkdownStylingTests: SwiftyMarkdownCharacterTests {
 				XCTAssertEqual(token.characterStyles as? [CharacterStyle], challenge.tokens[idx].characterStyles as?  [CharacterStyle])
 				XCTAssertEqual(token.characterStyles as? [CharacterStyle], challenge.tokens[idx].characterStyles as?  [CharacterStyle])
 			}
 			}
 		} else {
 		} else {
-			
+			XCTAssertEqual(results.stringTokens.count, challenge.tokens.count)
 		}
 		}
 		XCTAssertEqual(results.foundStyles, results.expectedStyles)
 		XCTAssertEqual(results.foundStyles, results.expectedStyles)
 		XCTAssertEqual(results.attributedString.string, challenge.output)
 		XCTAssertEqual(results.attributedString.string, challenge.output)
@@ -744,7 +737,7 @@ class SwiftyMarkdownStylingTests: SwiftyMarkdownCharacterTests {
 	
 	
 	func testReportedCrashingStrings() {
 	func testReportedCrashingStrings() {
 		challenge = TokenTest(input: "[**\\!bang**](https://duckduckgo.com/bang)", output: "\\!bang", tokens: [
 		challenge = TokenTest(input: "[**\\!bang**](https://duckduckgo.com/bang)", output: "\\!bang", tokens: [
-			Token(type: .string, inputString: "\\!bang", characterStyles: [CharacterStyle.bold, CharacterStyle.link])
+			Token(type: .string, inputString: "\\!bang", characterStyles: [CharacterStyle.link, CharacterStyle.bold])
 		])
 		])
 		results = self.attempt(challenge)
 		results = self.attempt(challenge)
 		if results.stringTokens.count == challenge.tokens.count {
 		if results.stringTokens.count == challenge.tokens.count {

+ 57 - 40
Tests/SwiftyMarkdownTests/SwiftyMarkdownLinkTests.swift

@@ -15,7 +15,7 @@ class SwiftyMarkdownLinkTests: SwiftyMarkdownCharacterTests {
 		challenge = TokenTest(input: "[a](b)", output: "a", tokens: [
 		challenge = TokenTest(input: "[a](b)", output: "a", tokens: [
 			Token(type: .string, inputString: "a", characterStyles: [CharacterStyle.link])
 			Token(type: .string, inputString: "a", characterStyles: [CharacterStyle.link])
 		])
 		])
-		results = self.attempt(challenge)
+		results = self.attempt(challenge, rules: [.links])
 		if results.stringTokens.count == challenge.tokens.count {
 		if results.stringTokens.count == challenge.tokens.count {
 			for (idx, token) in results.stringTokens.enumerated() {
 			for (idx, token) in results.stringTokens.enumerated() {
 				XCTAssertEqual(token.inputString, challenge.tokens[idx].inputString)
 				XCTAssertEqual(token.inputString, challenge.tokens[idx].inputString)
@@ -27,7 +27,7 @@ class SwiftyMarkdownLinkTests: SwiftyMarkdownCharacterTests {
 		XCTAssertEqual(results.foundStyles, results.expectedStyles)
 		XCTAssertEqual(results.foundStyles, results.expectedStyles)
 		XCTAssertEqual(results.attributedString.string, challenge.output)
 		XCTAssertEqual(results.attributedString.string, challenge.output)
 		if let existentOpen = results.tokens.filter({ $0.type == .string && (($0.characterStyles as? [CharacterStyle])?.contains(.link) ?? false) }).first {
 		if let existentOpen = results.tokens.filter({ $0.type == .string && (($0.characterStyles as? [CharacterStyle])?.contains(.link) ?? false) }).first {
-			XCTAssertEqual(existentOpen.metadataString, "b")
+			XCTAssertEqual(existentOpen.metadataStrings.first, "b")
 		} else {
 		} else {
 			XCTFail("Failed to find an open link tag")
 			XCTFail("Failed to find an open link tag")
 		}
 		}
@@ -48,7 +48,7 @@ class SwiftyMarkdownLinkTests: SwiftyMarkdownCharacterTests {
 		XCTAssertEqual(results.foundStyles, results.expectedStyles)
 		XCTAssertEqual(results.foundStyles, results.expectedStyles)
 		XCTAssertEqual(results.attributedString.string, challenge.output)
 		XCTAssertEqual(results.attributedString.string, challenge.output)
 		if let existentOpen = results.tokens.filter({ $0.type == .string && (($0.characterStyles as? [CharacterStyle])?.contains(.link) ?? false) }).first {
 		if let existentOpen = results.tokens.filter({ $0.type == .string && (($0.characterStyles as? [CharacterStyle])?.contains(.link) ?? false) }).first {
-			XCTAssertEqual(existentOpen.metadataString, "http://voyagetravelapps.com/")
+			XCTAssertEqual(existentOpen.metadataStrings.first, "http://voyagetravelapps.com/")
 		} else {
 		} else {
 			XCTFail("Failed to find an open link tag")
 			XCTFail("Failed to find an open link tag")
 		}
 		}
@@ -103,8 +103,9 @@ class SwiftyMarkdownLinkTests: SwiftyMarkdownCharacterTests {
 		XCTAssertEqual(results.foundStyles, results.expectedStyles)
 		XCTAssertEqual(results.foundStyles, results.expectedStyles)
 		XCTAssertEqual(results.attributedString.string, challenge.output)
 		XCTAssertEqual(results.attributedString.string, challenge.output)
 		
 		
-		challenge = TokenTest(input: "![a](b)", output: "![a](b)", tokens: [
-			Token(type: .string, inputString: "![a](b)", characterStyles: [])
+		challenge = TokenTest(input: "![a](b)", output: "!a", tokens: [
+			Token(type: .string, inputString: "!", characterStyles: []),
+			Token(type: .string, inputString: "a", characterStyles: [CharacterStyle.link])
 		])
 		])
 		results = self.attempt(challenge, rules: [.links])
 		results = self.attempt(challenge, rules: [.links])
 		if results.stringTokens.count == challenge.tokens.count {
 		if results.stringTokens.count == challenge.tokens.count {
@@ -139,8 +140,8 @@ class SwiftyMarkdownLinkTests: SwiftyMarkdownCharacterTests {
 		XCTAssertEqual(results.attributedString.string, challenge.output)
 		XCTAssertEqual(results.attributedString.string, challenge.output)
 		var links = results.tokens.filter({ $0.type == .string && (($0.characterStyles as? [CharacterStyle])?.contains(.link) ?? false) })
 		var links = results.tokens.filter({ $0.type == .string && (($0.characterStyles as? [CharacterStyle])?.contains(.link) ?? false) })
 		if links.count == 2 {
 		if links.count == 2 {
-			XCTAssertEqual(links[0].metadataString, "http://voyagetravelapps.com/")
-			XCTAssertEqual(links[1].metadataString, "https://www.neverendingvoyage.com/")
+			XCTAssertEqual(links[0].metadataStrings.first, "http://voyagetravelapps.com/")
+			XCTAssertEqual(links[1].metadataStrings.first, "https://www.neverendingvoyage.com/")
 		} else {
 		} else {
 			XCTFail("Incorrect number of links found. Expecting 2, found \(links.count)")
 			XCTFail("Incorrect number of links found. Expecting 2, found \(links.count)")
 		}
 		}
@@ -164,8 +165,8 @@ class SwiftyMarkdownLinkTests: SwiftyMarkdownCharacterTests {
 		XCTAssertEqual(results.attributedString.string, challenge.output)
 		XCTAssertEqual(results.attributedString.string, challenge.output)
 		links = results.tokens.filter({ $0.type == .string && (($0.characterStyles as? [CharacterStyle])?.contains(.link) ?? false) })
 		links = results.tokens.filter({ $0.type == .string && (($0.characterStyles as? [CharacterStyle])?.contains(.link) ?? false) })
 		if links.count == 2 {
 		if links.count == 2 {
-			XCTAssertEqual(links[0].metadataString, "http://voyagetravelapps.com/")
-			XCTAssertEqual(links[1].metadataString, "https://www.neverendingvoyage.com/")
+			XCTAssertEqual(links[0].metadataStrings.first, "http://voyagetravelapps.com/")
+			XCTAssertEqual(links[1].metadataStrings.first, "https://www.neverendingvoyage.com/")
 		} else {
 		} else {
 			XCTFail("Incorrect number of links found. Expecting 2, found \(links.count)")
 			XCTFail("Incorrect number of links found. Expecting 2, found \(links.count)")
 		}
 		}
@@ -190,8 +191,8 @@ class SwiftyMarkdownLinkTests: SwiftyMarkdownCharacterTests {
 		XCTAssertEqual(results.attributedString.string, challenge.output)
 		XCTAssertEqual(results.attributedString.string, challenge.output)
 		links = results.tokens.filter({ $0.type == .string && (($0.characterStyles as? [CharacterStyle])?.contains(.link) ?? false) })
 		links = results.tokens.filter({ $0.type == .string && (($0.characterStyles as? [CharacterStyle])?.contains(.link) ?? false) })
 		if links.count == 2 {
 		if links.count == 2 {
-			XCTAssertEqual(links[0].metadataString, "http://voyagetravelapps.com/")
-			XCTAssertEqual(links[1].metadataString, "https://www.neverendingvoyage.com/")
+			XCTAssertEqual(links[0].metadataStrings.first, "http://voyagetravelapps.com/")
+			XCTAssertEqual(links[1].metadataStrings.first, "https://www.neverendingvoyage.com/")
 		} else {
 		} else {
 			XCTFail("Incorrect number of links found. Expecting 2, found \(links.count)")
 			XCTFail("Incorrect number of links found. Expecting 2, found \(links.count)")
 		}
 		}
@@ -215,8 +216,8 @@ class SwiftyMarkdownLinkTests: SwiftyMarkdownCharacterTests {
 		XCTAssertEqual(results.attributedString.string, challenge.output)
 		XCTAssertEqual(results.attributedString.string, challenge.output)
 		links = results.tokens.filter({ $0.type == .string && (($0.characterStyles as? [CharacterStyle])?.contains(.link) ?? false) })
 		links = results.tokens.filter({ $0.type == .string && (($0.characterStyles as? [CharacterStyle])?.contains(.link) ?? false) })
 		if links.count == 2 {
 		if links.count == 2 {
-			XCTAssertEqual(links[0].metadataString, "http://voyagetravelapps.com/")
-			XCTAssertEqual(links[1].metadataString, "https://www.neverendingvoyage.com/")
+			XCTAssertEqual(links[0].metadataStrings.first, "http://voyagetravelapps.com/")
+			XCTAssertEqual(links[1].metadataStrings.first, "https://www.neverendingvoyage.com/")
 		} else {
 		} else {
 			XCTFail("Incorrect number of links found. Expecting 2, found \(links.count)")
 			XCTFail("Incorrect number of links found. Expecting 2, found \(links.count)")
 		}
 		}
@@ -247,8 +248,8 @@ class SwiftyMarkdownLinkTests: SwiftyMarkdownCharacterTests {
 		XCTAssertEqual(results.attributedString.string, challenge.output)
 		XCTAssertEqual(results.attributedString.string, challenge.output)
 		let links = results.tokens.filter({ $0.type == .string && (($0.characterStyles as? [CharacterStyle])?.contains(.link) ?? false) })
 		let links = results.tokens.filter({ $0.type == .string && (($0.characterStyles as? [CharacterStyle])?.contains(.link) ?? false) })
 		if links.count == 2 {
 		if links.count == 2 {
-			XCTAssertEqual(links[0].metadataString, "mailto:simon@voyagetravelapps.com")
-			XCTAssertEqual(links[1].metadataString, "twitter://user?screen_name=VoyageTravelApp")
+			XCTAssertEqual(links[0].metadataStrings.first, "mailto:simon@voyagetravelapps.com")
+			XCTAssertEqual(links[1].metadataStrings.first, "twitter://user?screen_name=VoyageTravelApp")
 		} else {
 		} else {
 			XCTFail("Incorrect number of links found. Expecting 2, found \(links.count)")
 			XCTFail("Incorrect number of links found. Expecting 2, found \(links.count)")
 		}
 		}
@@ -274,7 +275,7 @@ class SwiftyMarkdownLinkTests: SwiftyMarkdownCharacterTests {
 		XCTAssertEqual(results.attributedString.string, challenge.output)
 		XCTAssertEqual(results.attributedString.string, challenge.output)
 		var links = results.tokens.filter({ $0.type == .string && (($0.characterStyles as? [CharacterStyle])?.contains(.link) ?? false) })
 		var links = results.tokens.filter({ $0.type == .string && (($0.characterStyles as? [CharacterStyle])?.contains(.link) ?? false) })
 		if links.count == 1 {
 		if links.count == 1 {
-			XCTAssertEqual(links[0].metadataString, "https://www.neverendingvoyage.com/")
+			XCTAssertEqual(links[0].metadataStrings.first, "https://www.neverendingvoyage.com/")
 		} else {
 		} else {
 			XCTFail("Incorrect number of links found. Expecting 2, found \(links.count)")
 			XCTFail("Incorrect number of links found. Expecting 2, found \(links.count)")
 		}
 		}
@@ -297,7 +298,7 @@ class SwiftyMarkdownLinkTests: SwiftyMarkdownCharacterTests {
 		XCTAssertEqual(results.attributedString.string, challenge.output)
 		XCTAssertEqual(results.attributedString.string, challenge.output)
 		links = results.tokens.filter({ $0.type == .string && (($0.characterStyles as? [CharacterStyle])?.contains(.link) ?? false) })
 		links = results.tokens.filter({ $0.type == .string && (($0.characterStyles as? [CharacterStyle])?.contains(.link) ?? false) })
 		if links.count == 1 {
 		if links.count == 1 {
-			XCTAssertEqual(links[0].metadataString, "https://www.neverendingvoyage.com/")
+			XCTAssertEqual(links[0].metadataStrings.first, "https://www.neverendingvoyage.com/")
 		} else {
 		} else {
 			XCTFail("Incorrect number of links found. Expecting 2, found \(links.count)")
 			XCTFail("Incorrect number of links found. Expecting 2, found \(links.count)")
 		}
 		}
@@ -329,12 +330,26 @@ class SwiftyMarkdownLinkTests: SwiftyMarkdownCharacterTests {
 		XCTAssertEqual(results.foundStyles, results.expectedStyles)
 		XCTAssertEqual(results.foundStyles, results.expectedStyles)
 		XCTAssertEqual(results.attributedString.string, challenge.output)
 		XCTAssertEqual(results.attributedString.string, challenge.output)
 		
 		
-		challenge = TokenTest(input: "[A link](((url)", output: "A link", tokens: [
-			Token(type: .string, inputString: "A link", characterStyles: [CharacterStyle.link])
+		challenge = TokenTest(input: "[A link](((url)", output: "[A link](((url)", tokens: [
+			Token(type: .string, inputString: "[A link](((url)", characterStyles: [])
 		])
 		])
 		results = self.attempt(challenge, rules: [.images, .links])
 		results = self.attempt(challenge, rules: [.images, .links])
+		if results.stringTokens.count == challenge.tokens.count {
+			for (idx, token) in results.stringTokens.enumerated() {
+				XCTAssertEqual(token.inputString, challenge.tokens[idx].inputString)
+				XCTAssertEqual(token.characterStyles as? [CharacterStyle], challenge.tokens[idx].characterStyles as?  [CharacterStyle])
+			}
+		} else {
+			XCTAssertEqual(results.stringTokens.count, challenge.tokens.count)
+		}
+		XCTAssertEqual(results.foundStyles, results.expectedStyles)
+		XCTAssertEqual(results.attributedString.string, challenge.output)
+		XCTAssertEqual(results.links.count, 0)
 		
 		
-		
+		challenge = TokenTest(input: "[[a](((b)](c)", output: "[a](((b)", tokens: [
+			Token(type: .string, inputString: "[a](((b)", characterStyles: [CharacterStyle.link])
+		])
+		results = self.attempt(challenge, rules: [.images, .links])
 		if results.stringTokens.count == challenge.tokens.count {
 		if results.stringTokens.count == challenge.tokens.count {
 			for (idx, token) in results.stringTokens.enumerated() {
 			for (idx, token) in results.stringTokens.enumerated() {
 				XCTAssertEqual(token.inputString, challenge.tokens[idx].inputString)
 				XCTAssertEqual(token.inputString, challenge.tokens[idx].inputString)
@@ -347,11 +362,13 @@ class SwiftyMarkdownLinkTests: SwiftyMarkdownCharacterTests {
 		XCTAssertEqual(results.attributedString.string, challenge.output)
 		XCTAssertEqual(results.attributedString.string, challenge.output)
 		XCTAssertEqual(results.links.count, 1)
 		XCTAssertEqual(results.links.count, 1)
 		if results.links.count == 1 {
 		if results.links.count == 1 {
-			XCTAssertEqual(results.links[0].metadataString, "((url")
+			XCTAssertEqual(results.links[0].metadataStrings.first, "c")
 		} else {
 		} else {
 			XCTFail("Incorrect link count. Expecting 1, found \(results.links.count)")
 			XCTFail("Incorrect link count. Expecting 1, found \(results.links.count)")
 		}
 		}
 		
 		
+		
+		
 		challenge = TokenTest(input: "[Link with missing square(http://voyagetravelapps.com/)", output: "[Link with missing square(http://voyagetravelapps.com/)", tokens: [
 		challenge = TokenTest(input: "[Link with missing square(http://voyagetravelapps.com/)", output: "[Link with missing square(http://voyagetravelapps.com/)", tokens: [
 			Token(type: .string, inputString: "[Link with missing square(http://voyagetravelapps.com/)", characterStyles: [])
 			Token(type: .string, inputString: "[Link with missing square(http://voyagetravelapps.com/)", characterStyles: [])
 		])
 		])
@@ -384,7 +401,7 @@ class SwiftyMarkdownLinkTests: SwiftyMarkdownCharacterTests {
 		XCTAssertEqual(results.attributedString.string, challenge.output)
 		XCTAssertEqual(results.attributedString.string, challenge.output)
 		XCTAssertEqual(results.links.count, 1)
 		XCTAssertEqual(results.links.count, 1)
 		if results.links.count == 1 {
 		if results.links.count == 1 {
-			XCTAssertEqual(results.links[0].metadataString, "http://voyagetravelapps.com/")
+			XCTAssertEqual(results.links[0].metadataStrings.first, "http://voyagetravelapps.com/")
 		} else {
 		} else {
 			XCTFail("Incorrect link count. Expecting 1, found \(results.links.count)")
 			XCTFail("Incorrect link count. Expecting 1, found \(results.links.count)")
 		}
 		}
@@ -419,7 +436,7 @@ class SwiftyMarkdownLinkTests: SwiftyMarkdownCharacterTests {
 		XCTAssertEqual(results.attributedString.string, challenge.output)
 		XCTAssertEqual(results.attributedString.string, challenge.output)
 		XCTAssertEqual(results.links.count, 1)
 		XCTAssertEqual(results.links.count, 1)
 		if results.links.count == 1 {
 		if results.links.count == 1 {
-			XCTAssertEqual(results.links[0].metadataString, "http://voyagetravelapps.com/")
+			XCTAssertEqual(results.links[0].metadataStrings.first, "http://voyagetravelapps.com/")
 		} else {
 		} else {
 			XCTFail("Incorrect link count. Expecting 1, found \(results.links.count)")
 			XCTFail("Incorrect link count. Expecting 1, found \(results.links.count)")
 		}
 		}
@@ -441,7 +458,7 @@ class SwiftyMarkdownLinkTests: SwiftyMarkdownCharacterTests {
 		XCTAssertEqual(results.attributedString.string, challenge.output)
 		XCTAssertEqual(results.attributedString.string, challenge.output)
 		XCTAssertEqual(results.links.count, 1)
 		XCTAssertEqual(results.links.count, 1)
 		if results.links.count == 1 {
 		if results.links.count == 1 {
-			XCTAssertEqual(results.links[0].metadataString, "http://voyagetravelapps.com/")
+			XCTAssertEqual(results.links[0].metadataStrings.first, "http://voyagetravelapps.com/")
 		} else {
 		} else {
 			XCTFail("Incorrect link count. Expecting 1, found \(results.links.count)")
 			XCTFail("Incorrect link count. Expecting 1, found \(results.links.count)")
 		}
 		}
@@ -463,7 +480,7 @@ class SwiftyMarkdownLinkTests: SwiftyMarkdownCharacterTests {
 		XCTAssertEqual(results.attributedString.string, challenge.output)
 		XCTAssertEqual(results.attributedString.string, challenge.output)
 		XCTAssertEqual(results.links.count, 1)
 		XCTAssertEqual(results.links.count, 1)
 		if results.links.count == 1 {
 		if results.links.count == 1 {
-			XCTAssertEqual(results.links[0].metadataString, "http://voyagetravelapps.com/")
+			XCTAssertEqual(results.links[0].metadataStrings.first, "http://voyagetravelapps.com/")
 		} else {
 		} else {
 			XCTFail("Incorrect link count. Expecting 1, found \(results.links.count)")
 			XCTFail("Incorrect link count. Expecting 1, found \(results.links.count)")
 		}
 		}
@@ -505,14 +522,14 @@ class SwiftyMarkdownLinkTests: SwiftyMarkdownCharacterTests {
 		XCTAssertEqual(results.foundStyles, results.expectedStyles)
 		XCTAssertEqual(results.foundStyles, results.expectedStyles)
 		XCTAssertEqual(results.links.count, 1)
 		XCTAssertEqual(results.links.count, 1)
 		if results.links.count == 1 {
 		if results.links.count == 1 {
-			XCTAssertEqual(results.links[0].metadataString, "http://voyagetravelapps.com/")
+			XCTAssertEqual(results.links[0].metadataStrings.first, "http://voyagetravelapps.com/")
 		} else {
 		} else {
 			XCTFail("Incorrect link count. Expecting 1, found \(results.links.count)")
 			XCTFail("Incorrect link count. Expecting 1, found \(results.links.count)")
 		}
 		}
 		
 		
 		challenge = TokenTest(input: "A Bold [**Link**](http://voyagetravelapps.com/)", output: "A Bold Link", tokens: [
 		challenge = TokenTest(input: "A Bold [**Link**](http://voyagetravelapps.com/)", output: "A Bold Link", tokens: [
 			Token(type: .string, inputString: "A Bold ", characterStyles: []),
 			Token(type: .string, inputString: "A Bold ", characterStyles: []),
-			Token(type: .string, inputString: "Link", characterStyles: [CharacterStyle.bold, CharacterStyle.link])
+			Token(type: .string, inputString: "Link", characterStyles: [ CharacterStyle.link, CharacterStyle.bold])
 		])
 		])
 		results = self.attempt(challenge)
 		results = self.attempt(challenge)
 		if results.stringTokens.count == challenge.tokens.count {
 		if results.stringTokens.count == challenge.tokens.count {
@@ -527,7 +544,7 @@ class SwiftyMarkdownLinkTests: SwiftyMarkdownCharacterTests {
 		XCTAssertEqual(results.attributedString.string, challenge.output)
 		XCTAssertEqual(results.attributedString.string, challenge.output)
 		XCTAssertEqual(results.links.count, 1)
 		XCTAssertEqual(results.links.count, 1)
 		if results.links.count == 1 {
 		if results.links.count == 1 {
-			XCTAssertEqual(results.links[0].metadataString, "http://voyagetravelapps.com/")
+			XCTAssertEqual(results.links[0].metadataStrings.first, "http://voyagetravelapps.com/")
 		} else {
 		} else {
 			XCTFail("Incorrect link count. Expecting 1, found \(results.links.count)")
 			XCTFail("Incorrect link count. Expecting 1, found \(results.links.count)")
 		}
 		}
@@ -557,7 +574,7 @@ class SwiftyMarkdownLinkTests: SwiftyMarkdownCharacterTests {
 			Token(type: .string, inputString: "An ", characterStyles: []),
 			Token(type: .string, inputString: "An ", characterStyles: []),
 			Token(type: .string, inputString: "Image", characterStyles: [CharacterStyle.image])
 			Token(type: .string, inputString: "Image", characterStyles: [CharacterStyle.image])
 		])
 		])
-		results = self.attempt(challenge, rules: [.links, .images])
+		results = self.attempt(challenge)
 		if results.stringTokens.count == challenge.tokens.count {
 		if results.stringTokens.count == challenge.tokens.count {
 			for (idx, token) in results.stringTokens.enumerated() {
 			for (idx, token) in results.stringTokens.enumerated() {
 				XCTAssertEqual(token.inputString, challenge.tokens[idx].inputString)
 				XCTAssertEqual(token.inputString, challenge.tokens[idx].inputString)
@@ -570,14 +587,14 @@ class SwiftyMarkdownLinkTests: SwiftyMarkdownCharacterTests {
 		XCTAssertEqual(results.foundStyles, results.expectedStyles)
 		XCTAssertEqual(results.foundStyles, results.expectedStyles)
 		var links = results.tokens.filter({ $0.type == .string && (($0.characterStyles as? [CharacterStyle])?.contains(.image) ?? false) })
 		var links = results.tokens.filter({ $0.type == .string && (($0.characterStyles as? [CharacterStyle])?.contains(.image) ?? false) })
 		if links.count == 1 {
 		if links.count == 1 {
-			XCTAssertEqual(links[0].metadataString, "imageName")
+			XCTAssertEqual(links[0].metadataStrings.first, "imageName")
 		} else {
 		} else {
 			XCTFail("Incorrect link count. Expecting 1, found \(links.count)")
 			XCTFail("Incorrect link count. Expecting 1, found \(links.count)")
 		}
 		}
 		
 		
-		challenge = TokenTest(input: "An [![Image](imageName)](https://www.neverendingvoyage.com/)", output: "An Image", tokens: [
+		challenge = TokenTest(input: "An [![Image](imageName)](https://www.neverendingvoyage.com/)", output: "An ", tokens: [
 			Token(type: .string, inputString: "An ", characterStyles: []),
 			Token(type: .string, inputString: "An ", characterStyles: []),
-			Token(type: .string, inputString: "Image", characterStyles: [CharacterStyle.image])
+			Token(type: .string, inputString: "Image", characterStyles: [CharacterStyle.image, CharacterStyle.link])
 		])
 		])
 		results = self.attempt(challenge)
 		results = self.attempt(challenge)
 		if results.stringTokens.count == challenge.tokens.count {
 		if results.stringTokens.count == challenge.tokens.count {
@@ -592,13 +609,13 @@ class SwiftyMarkdownLinkTests: SwiftyMarkdownCharacterTests {
 		XCTAssertEqual(results.foundStyles, results.expectedStyles)
 		XCTAssertEqual(results.foundStyles, results.expectedStyles)
 		links = results.tokens.filter({ $0.type == .string && (($0.characterStyles as? [CharacterStyle])?.contains(.image) ?? false) })
 		links = results.tokens.filter({ $0.type == .string && (($0.characterStyles as? [CharacterStyle])?.contains(.image) ?? false) })
 		if links.count == 1 {
 		if links.count == 1 {
-			XCTAssertEqual(links[0].metadataString, "imageName")
+			XCTAssertEqual(links[0].metadataStrings.first, "imageName")
 		} else {
 		} else {
 			XCTFail("Incorrect link count. Expecting 1, found \(links.count)")
 			XCTFail("Incorrect link count. Expecting 1, found \(links.count)")
 		}
 		}
 		links = results.tokens.filter({ $0.type == .string && (($0.characterStyles as? [CharacterStyle])?.contains(.link) ?? false) })
 		links = results.tokens.filter({ $0.type == .string && (($0.characterStyles as? [CharacterStyle])?.contains(.link) ?? false) })
 		if links.count == 1 {
 		if links.count == 1 {
-			XCTAssertEqual(links[0].metadataString, "https://www.neverendingvoyage.com/")
+			XCTAssertEqual(links[0].metadataStrings.last, "https://www.neverendingvoyage.com/")
 		} else {
 		} else {
 			XCTFail("Incorrect link count. Expecting 1, found \(links.count)")
 			XCTFail("Incorrect link count. Expecting 1, found \(links.count)")
 		}
 		}
@@ -621,7 +638,7 @@ class SwiftyMarkdownLinkTests: SwiftyMarkdownCharacterTests {
 		XCTAssertEqual(results.foundStyles, results.expectedStyles)
 		XCTAssertEqual(results.foundStyles, results.expectedStyles)
 		XCTAssertEqual(results.attributedString.string, challenge.output)
 		XCTAssertEqual(results.attributedString.string, challenge.output)
 		if results.links.count == 1 {
 		if results.links.count == 1 {
-			XCTAssertEqual(results.links[0].metadataString, "https://www.neverendingvoyage.com/")
+			XCTAssertEqual(results.links[0].metadataStrings.first, "https://www.neverendingvoyage.com/")
 		} else {
 		} else {
 			XCTFail("Incorrect link count. Expecting 1, found \(results.links.count)")
 			XCTFail("Incorrect link count. Expecting 1, found \(results.links.count)")
 		}
 		}
@@ -641,12 +658,12 @@ class SwiftyMarkdownLinkTests: SwiftyMarkdownCharacterTests {
 		}
 		}
 		XCTAssertEqual(results.foundStyles, results.expectedStyles)
 		XCTAssertEqual(results.foundStyles, results.expectedStyles)
 		if results.links.count == 1 {
 		if results.links.count == 1 {
-			XCTAssertEqual(results.links[0].metadataString, "https://www.neverendingvoyage.com/")
+			XCTAssertEqual(results.links[0].metadataStrings.first, "https://www.neverendingvoyage.com/")
 		} else {
 		} else {
 			XCTFail("Incorrect link count. Expecting 1, found \(results.links.count)")
 			XCTFail("Incorrect link count. Expecting 1, found \(results.links.count)")
 		}
 		}
 		
 		
-		challenge = TokenTest(input: "An *\\*italic\\** [referenced link][link]", output: "An *italic* referenced link", tokens: [
+		challenge = TokenTest(input: "An *\\*italic\\** [referenced link][a]\n[a]: link", output: "An *italic* referenced link", tokens: [
 			Token(type: .string, inputString: "An ", characterStyles: []),
 			Token(type: .string, inputString: "An ", characterStyles: []),
 			Token(type: .string, inputString: "*italic*", characterStyles: [CharacterStyle.italic]),
 			Token(type: .string, inputString: "*italic*", characterStyles: [CharacterStyle.italic]),
 			Token(type: .string, inputString: " ", characterStyles: []),
 			Token(type: .string, inputString: " ", characterStyles: []),
@@ -663,7 +680,7 @@ class SwiftyMarkdownLinkTests: SwiftyMarkdownCharacterTests {
 		}
 		}
 		XCTAssertEqual(results.foundStyles, results.expectedStyles)
 		XCTAssertEqual(results.foundStyles, results.expectedStyles)
 		if results.links.count == 1 {
 		if results.links.count == 1 {
-			XCTAssertEqual(results.links[0].metadataString, "link")
+			XCTAssertEqual(results.links[0].metadataStrings.first, "link")
 		} else {
 		} else {
 			XCTFail("Incorrect link count. Expecting 1, found \(results.links.count)")
 			XCTFail("Incorrect link count. Expecting 1, found \(results.links.count)")
 		}
 		}
@@ -688,8 +705,8 @@ class SwiftyMarkdownLinkTests: SwiftyMarkdownCharacterTests {
 		}
 		}
 		XCTAssertEqual(results.foundStyles, results.expectedStyles)
 		XCTAssertEqual(results.foundStyles, results.expectedStyles)
 		if results.links.count == 2 {
 		if results.links.count == 2 {
-			XCTAssertEqual(results.links[0].metadataString, "https://www.neverendingvoyage.com/")
-			XCTAssertEqual(results.links[1].metadataString, "http://voyagetravelapps.com/")
+			XCTAssertEqual(results.links[0].metadataStrings.first, "https://www.neverendingvoyage.com/")
+			XCTAssertEqual(results.links[1].metadataStrings.first, "http://voyagetravelapps.com/")
 		} else {
 		} else {
 			XCTFail("Incorrect link count. Expecting 1, found \(results.links.count)")
 			XCTFail("Incorrect link count. Expecting 1, found \(results.links.count)")
 		}
 		}

+ 1 - 1
Tests/SwiftyMarkdownTests/SwiftyMarkdownPerformanceTests.swift

@@ -33,7 +33,7 @@ class SwiftyMarkdownPerformanceTests: XCTestCase {
 		}
 		}
 	}
 	}
 	
 	
-	func testThatVeryLongStringsAreProcessedQuickly() {
+ 	func testThatVeryLongStringsAreProcessedQuickly() {
 		let string = "SwiftyMarkdown converts Markdown files and strings into `NSAttributedString`s using sensible defaults and a *Swift*-style syntax. It uses **dynamic type** to set the font size correctly with [whatever](https://www.neverendingvoyage.com/) font you'd like to use. SwiftyMarkdown converts Markdown files and strings into `NSAttributedString`s using sensible defaults and a *Swift*-style syntax. It uses **dynamic type** to set the font size correctly with [whatever](https://www.neverendingvoyage.com/) font you'd like to use. SwiftyMarkdown converts Markdown files and strings into `NSAttributedString`s using sensible defaults and a *Swift*-style syntax. It uses **dynamic type** to set the font size correctly with [whatever](https://www.neverendingvoyage.com/) font you'd like to use. SwiftyMarkdown converts Markdown files and strings into `NSAttributedString`s using sensible defaults and a *Swift*-style syntax. It uses **dynamic type** to set the font size correctly with [whatever](https://www.neverendingvoyage.com/) font you'd like to use. SwiftyMarkdown converts Markdown files and strings into `NSAttributedString`s using sensible defaults and a *Swift*-style syntax. It uses **dynamic type** to set the font size correctly with [whatever](https://www.neverendingvoyage.com/) font you'd like to use. SwiftyMarkdown converts Markdown files and strings into `NSAttributedString`s using sensible defaults and a *Swift*-style syntax. It uses **dynamic type** to set the font size correctly with [whatever](https://www.neverendingvoyage.com/) font you'd like to use."
 		let string = "SwiftyMarkdown converts Markdown files and strings into `NSAttributedString`s using sensible defaults and a *Swift*-style syntax. It uses **dynamic type** to set the font size correctly with [whatever](https://www.neverendingvoyage.com/) font you'd like to use. SwiftyMarkdown converts Markdown files and strings into `NSAttributedString`s using sensible defaults and a *Swift*-style syntax. It uses **dynamic type** to set the font size correctly with [whatever](https://www.neverendingvoyage.com/) font you'd like to use. SwiftyMarkdown converts Markdown files and strings into `NSAttributedString`s using sensible defaults and a *Swift*-style syntax. It uses **dynamic type** to set the font size correctly with [whatever](https://www.neverendingvoyage.com/) font you'd like to use. SwiftyMarkdown converts Markdown files and strings into `NSAttributedString`s using sensible defaults and a *Swift*-style syntax. It uses **dynamic type** to set the font size correctly with [whatever](https://www.neverendingvoyage.com/) font you'd like to use. SwiftyMarkdown converts Markdown files and strings into `NSAttributedString`s using sensible defaults and a *Swift*-style syntax. It uses **dynamic type** to set the font size correctly with [whatever](https://www.neverendingvoyage.com/) font you'd like to use. SwiftyMarkdown converts Markdown files and strings into `NSAttributedString`s using sensible defaults and a *Swift*-style syntax. It uses **dynamic type** to set the font size correctly with [whatever](https://www.neverendingvoyage.com/) font you'd like to use."
 		let md = SwiftyMarkdown(string: string)
 		let md = SwiftyMarkdown(string: string)
 		measure {
 		measure {

+ 7 - 20
Tests/SwiftyMarkdownTests/XCTest+SwiftyMarkdown.swift

@@ -31,32 +31,19 @@ enum Rule {
 	func asCharacterRule() -> CharacterRule {
 	func asCharacterRule() -> CharacterRule {
 		switch self {
 		switch self {
 		case .images:
 		case .images:
-			return CharacterRule(primaryTag: CharacterRuleTag(tag: "![", type: .open), otherTags: [
-					CharacterRuleTag(tag: "]", type: .close),
-					CharacterRuleTag(tag: "(", type: .metadataOpen),
-					CharacterRuleTag(tag: ")", type: .metadataClose)
-			], styles: [1 : [CharacterStyle.image]], metadataLookup: false, spacesAllowed: .bothSides, isSelfContained: true)
+			return SwiftyMarkdown.characterRules.filter({ $0.primaryTag.tag == "![" && !$0.metadataLookup  }).first!
 		case .links:
 		case .links:
-			return CharacterRule(primaryTag: CharacterRuleTag(tag: "[", type: .open, escapeCharacters: [EscapeCharacter(character: "\\", rule: .remove),EscapeCharacter(character: "!", rule: .keep)]), otherTags: [
-					CharacterRuleTag(tag: "]", type: .close),
-					CharacterRuleTag(tag: "(", type: .metadataOpen),
-					CharacterRuleTag(tag: ")", type: .metadataClose)
-			], styles: [1 : [CharacterStyle.link]], metadataLookup: true, spacesAllowed: .bothSides, isSelfContained: true)
+			return SwiftyMarkdown.characterRules.filter({ $0.primaryTag.tag == "[" && !$0.metadataLookup  }).first!
 		case .backticks:
 		case .backticks:
-			return CharacterRule(primaryTag: CharacterRuleTag(tag: "`", type: .repeating), otherTags: [], styles: [1 : [CharacterStyle.code]], cancels: .allRemaining)
+			return SwiftyMarkdown.characterRules.filter({ $0.primaryTag.tag == "`" }).first!
 		case .strikethroughs:
 		case .strikethroughs:
-			return CharacterRule(primaryTag:CharacterRuleTag(tag: "~", type: .repeating, min: 2, max: 2), otherTags : [], styles: [2 : [CharacterStyle.strikethrough]])
+			return SwiftyMarkdown.characterRules.filter({ $0.primaryTag.tag == "~" }).first!
 		case .asterisks:
 		case .asterisks:
-			return CharacterRule(primaryTag: CharacterRuleTag(tag: "*", type: .repeating, min: 1, max: 3), otherTags: [], styles: [1 : [CharacterStyle.italic], 2 : [CharacterStyle.bold], 3 : [CharacterStyle.bold, CharacterStyle.italic]])
+			return SwiftyMarkdown.characterRules.filter({ $0.primaryTag.tag == "*" }).first!
 		case .underscores:
 		case .underscores:
-			return CharacterRule(primaryTag: CharacterRuleTag(tag: "_", type: .repeating, min: 1, max: 3), otherTags: [], styles: [1 : [CharacterStyle.italic], 2 : [CharacterStyle.bold], 3 : [CharacterStyle.bold, CharacterStyle.italic]])
+			return SwiftyMarkdown.characterRules.filter({ $0.primaryTag.tag == "_" }).first!
 		case .referencedLinks:
 		case .referencedLinks:
-			return CharacterRule(primaryTag: CharacterRuleTag(tag: "[", type: .open, escapeCharacters: [EscapeCharacter(character: "\\", rule: .remove),EscapeCharacter(character: "!", rule: .keep)]), otherTags: [
-					CharacterRuleTag(tag: "]", type: .close),
-					CharacterRuleTag(tag: "[", type: .metadataOpen),
-					CharacterRuleTag(tag: "]", type: .metadataClose)
-			], styles: [1 : [CharacterStyle.referencedLink]], metadataLookup: true, spacesAllowed: .bothSides, isSelfContained: true)
-				
+			return SwiftyMarkdown.characterRules.filter({ $0.primaryTag.tag == "[" && $0.metadataLookup  }).first!		
 		}
 		}
 	}
 	}
 }
 }