Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added Markdown link and backslash escape support #24

Merged
merged 7 commits into from
Nov 8, 2017
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions Marker/Marker.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -50,8 +50,20 @@
27C4E61B1ED5ED6500DDE387 /* TextTransform.swift in Sources */ = {isa = PBXBuildFile; fileRef = 271C85B51ED4E2B700F8BBBB /* TextTransform.swift */; };
27C4E61C1ED5ED6900DDE387 /* Marker.h in Headers */ = {isa = PBXBuildFile; fileRef = 271C858D1ED4E2A000F8BBBB /* Marker.h */; settings = {ATTRIBUTES = (Public, ); }; };
27C4E61D1ED5ED6C00DDE387 /* Marker.h in Headers */ = {isa = PBXBuildFile; fileRef = 271C858D1ED4E2A000F8BBBB /* Marker.h */; settings = {ATTRIBUTES = (Public, ); }; };
480104771FA1355F00F20FF8 /* Rule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 480104741FA1355F00F20FF8 /* Rule.swift */; };
480104781FA1355F00F20FF8 /* Rule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 480104741FA1355F00F20FF8 /* Rule.swift */; };
480104791FA1355F00F20FF8 /* Rule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 480104741FA1355F00F20FF8 /* Rule.swift */; };
4801047A1FA1355F00F20FF8 /* TokenParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 480104751FA1355F00F20FF8 /* TokenParser.swift */; };
4801047B1FA1355F00F20FF8 /* TokenParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 480104751FA1355F00F20FF8 /* TokenParser.swift */; };
4801047C1FA1355F00F20FF8 /* TokenParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 480104751FA1355F00F20FF8 /* TokenParser.swift */; };
4801047D1FA1355F00F20FF8 /* Token.swift in Sources */ = {isa = PBXBuildFile; fileRef = 480104761FA1355F00F20FF8 /* Token.swift */; };
4801047E1FA1355F00F20FF8 /* Token.swift in Sources */ = {isa = PBXBuildFile; fileRef = 480104761FA1355F00F20FF8 /* Token.swift */; };
4801047F1FA1355F00F20FF8 /* Token.swift in Sources */ = {isa = PBXBuildFile; fileRef = 480104761FA1355F00F20FF8 /* Token.swift */; };
480C7A5F1F15832A0094E4EA /* ParserTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 271C85C81ED4E37700F8BBBB /* ParserTests.swift */; };
480C7A601F15832A0094E4EA /* ParserTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 271C85C81ED4E37700F8BBBB /* ParserTests.swift */; };
481303A01FA3BA7C001F1DF1 /* String+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4813039F1FA3BA7C001F1DF1 /* String+Extensions.swift */; };
481303A11FA3BC64001F1DF1 /* String+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4813039F1FA3BA7C001F1DF1 /* String+Extensions.swift */; };
481303A21FA3BC65001F1DF1 /* String+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4813039F1FA3BA7C001F1DF1 /* String+Extensions.swift */; };
484053B21F72EA6000626C55 /* UIButtonExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 484053A21F72EA4F00626C55 /* UIButtonExtension.swift */; };
484053B31F72EA6000626C55 /* Color.swift in Sources */ = {isa = PBXBuildFile; fileRef = 484053A41F72EA4F00626C55 /* Color.swift */; };
484053B41F72EA6000626C55 /* Font.swift in Sources */ = {isa = PBXBuildFile; fileRef = 484053A61F72EA4F00626C55 /* Font.swift */; };
Expand Down Expand Up @@ -140,6 +152,10 @@
27C4E5CF1ED5ED2400DDE387 /* Marker-tvOSTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "Marker-tvOSTests.xctest"; sourceTree = BUILT_PRODUCTS_DIR; };
27C4E5E31ED5ED3000DDE387 /* Marker.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Marker.framework; sourceTree = BUILT_PRODUCTS_DIR; };
27C4E5EB1ED5ED3100DDE387 /* Marker-macOSTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "Marker-macOSTests.xctest"; sourceTree = BUILT_PRODUCTS_DIR; };
480104741FA1355F00F20FF8 /* Rule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Rule.swift; sourceTree = "<group>"; };
480104751FA1355F00F20FF8 /* TokenParser.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TokenParser.swift; sourceTree = "<group>"; };
480104761FA1355F00F20FF8 /* Token.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Token.swift; sourceTree = "<group>"; };
4813039F1FA3BA7C001F1DF1 /* String+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+Extensions.swift"; sourceTree = "<group>"; };
484053A11F72EA4F00626C55 /* NSButtonExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSButtonExtension.swift; sourceTree = "<group>"; };
484053A21F72EA4F00626C55 /* UIButtonExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIButtonExtension.swift; sourceTree = "<group>"; };
484053A41F72EA4F00626C55 /* Color.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Color.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -268,9 +284,13 @@
271C85AD1ED4E2B700F8BBBB /* ElementParser.swift */,
271C85AE1ED4E2B700F8BBBB /* MarkdownElement.swift */,
271C85AF1ED4E2B700F8BBBB /* MarkdownParser.swift */,
480104741FA1355F00F20FF8 /* Rule.swift */,
4813039F1FA3BA7C001F1DF1 /* String+Extensions.swift */,
271C85B01ED4E2B700F8BBBB /* Symbol.swift */,
271C85B11ED4E2B700F8BBBB /* Tag.swift */,
271C85B21ED4E2B700F8BBBB /* TagParser.swift */,
480104761FA1355F00F20FF8 /* Token.swift */,
480104751FA1355F00F20FF8 /* TokenParser.swift */,
);
path = Parser;
sourceTree = "<group>";
Expand Down Expand Up @@ -613,14 +633,18 @@
buildActionMask = 2147483647;
files = (
271C85C51ED4E2B700F8BBBB /* TextStyle.swift in Sources */,
4801047D1FA1355F00F20FF8 /* Token.swift in Sources */,
48F232751F1FB7B600E86D5D /* TextTransform+Extensions.swift in Sources */,
271C85C01ED4E2B700F8BBBB /* MarkdownParser.swift in Sources */,
484053B81F72EA6000626C55 /* UITextFieldExtension.swift in Sources */,
484053B31F72EA6000626C55 /* Color.swift in Sources */,
480104771FA1355F00F20FF8 /* Rule.swift in Sources */,
271C85BB1ED4E2B700F8BBBB /* Markup.swift in Sources */,
271C85BD1ED4E2B700F8BBBB /* Element.swift in Sources */,
271C85BF1ED4E2B700F8BBBB /* MarkdownElement.swift in Sources */,
271C85C31ED4E2B700F8BBBB /* TagParser.swift in Sources */,
4801047A1FA1355F00F20FF8 /* TokenParser.swift in Sources */,
481303A01FA3BA7C001F1DF1 /* String+Extensions.swift in Sources */,
48F232801F1FBA7800E86D5D /* TextStyle+Extensions.swift in Sources */,
484053B21F72EA6000626C55 /* UIButtonExtension.swift in Sources */,
484053BA1F72EA6000626C55 /* UITextViewExtension.swift in Sources */,
Expand Down Expand Up @@ -653,16 +677,20 @@
buildActionMask = 2147483647;
files = (
27C4E6091ED5ED6400DDE387 /* TextStyle.swift in Sources */,
4801047E1FA1355F00F20FF8 /* Token.swift in Sources */,
48F232761F1FB7B600E86D5D /* TextTransform+Extensions.swift in Sources */,
27C4E6041ED5ED6400DDE387 /* MarkdownParser.swift in Sources */,
484053C21F72EA6000626C55 /* UITextFieldExtension.swift in Sources */,
484053BD1F72EA6000626C55 /* Color.swift in Sources */,
480104781FA1355F00F20FF8 /* Rule.swift in Sources */,
27C4E5FF1ED5ED6400DDE387 /* Markup.swift in Sources */,
484053CF1F72EA7A00626C55 /* UIButtonExtension.swift in Sources */,
27C4E6011ED5ED6400DDE387 /* Element.swift in Sources */,
27C4E6031ED5ED6400DDE387 /* MarkdownElement.swift in Sources */,
4801047B1FA1355F00F20FF8 /* TokenParser.swift in Sources */,
27C4E6071ED5ED6400DDE387 /* TagParser.swift in Sources */,
48F232811F1FBA7800E86D5D /* TextStyle+Extensions.swift in Sources */,
481303A11FA3BC64001F1DF1 /* String+Extensions.swift in Sources */,
484053C41F72EA6000626C55 /* UITextViewExtension.swift in Sources */,
484053C01F72EA6000626C55 /* LineBreakMode.swift in Sources */,
27C4E6051ED5ED6400DDE387 /* Symbol.swift in Sources */,
Expand Down Expand Up @@ -695,6 +723,8 @@
27C4E61A1ED5ED6500DDE387 /* TextStyle.swift in Sources */,
484053C51F72EA6100626C55 /* NSButtonExtension.swift in Sources */,
48F232821F1FBA7800E86D5D /* TextStyle+Extensions.swift in Sources */,
4801047C1FA1355F00F20FF8 /* TokenParser.swift in Sources */,
480104791FA1355F00F20FF8 /* Rule.swift in Sources */,
27C4E6151ED5ED6500DDE387 /* MarkdownParser.swift in Sources */,
484053C71F72EA6100626C55 /* Color.swift in Sources */,
48F232771F1FB7B600E86D5D /* TextTransform+Extensions.swift in Sources */,
Expand All @@ -705,6 +735,8 @@
27C4E6141ED5ED6500DDE387 /* MarkdownElement.swift in Sources */,
27C4E6181ED5ED6500DDE387 /* TagParser.swift in Sources */,
484053CA1F72EA6100626C55 /* LineBreakMode.swift in Sources */,
481303A21FA3BC65001F1DF1 /* String+Extensions.swift in Sources */,
4801047F1FA1355F00F20FF8 /* Token.swift in Sources */,
27C4E6161ED5ED6500DDE387 /* Symbol.swift in Sources */,
27C4E6131ED5ED6500DDE387 /* ElementParser.swift in Sources */,
27C4E60B1ED5ED6500DDE387 /* Marker.swift in Sources */,
Expand Down
65 changes: 35 additions & 30 deletions Marker/Marker/Marker.swift
Original file line number Diff line number Diff line change
Expand Up @@ -64,40 +64,39 @@ public func parsedMarkdownString(from markdownText: String,

elements.forEach { (element) in
var font: Font? = nil
var strikethroughStyle: NSUnderlineStyle? = nil
var underlineStyle: NSUnderlineStyle? = nil

switch element {
case .em(_):
case .em:
font = textStyle.emFont
case .strong(_):
case .strong:
font = textStyle.strongFont
case .strikethrough(_):
strikethroughStyle = textStyle.strikethroughStyle
case .underline(_):
underlineStyle = textStyle.underlineStyle
}

if let font = font {
attributedString.addAttributes([AttributedStringKey.font: font], range: NSRange(element.range, in: parsedString))
}

if let strikethroughStyle = strikethroughStyle {
attributedString.addAttributes([AttributedStringKey.strikethroughStyle: strikethroughStyle.rawValue],
range: NSRange(element.range, in: parsedString))

case .strikethrough(let range):
if let strikethroughStyle = textStyle.strikethroughStyle {
attributedString.addAttributes([AttributedStringKey.strikethroughStyle: strikethroughStyle.rawValue],
range: NSRange(range, in: parsedString))
}
if let strikethroughColor = textStyle.strikethroughColor {
attributedString.addAttributes([AttributedStringKey.strikethroughColor: strikethroughColor],
range: NSRange(element.range, in: parsedString))
range: NSRange(range, in: parsedString))
}
case .underline(let range):
if let underlineStyle = textStyle.underlineStyle {
attributedString.addAttributes([AttributedStringKey.underlineStyle: underlineStyle.rawValue],
range: NSRange(range, in: parsedString))
}
}

if let underlineStyle = underlineStyle {
attributedString.addAttributes([AttributedStringKey.underlineStyle: underlineStyle.rawValue], range: NSRange(element.range, in: parsedString))

if let underlineColor = textStyle.underlineColor {
attributedString.addAttributes([AttributedStringKey.underlineColor: underlineColor], range: NSRange(element.range, in: parsedString))
attributedString.addAttributes([AttributedStringKey.underlineColor: underlineColor],
range: NSRange(range, in: parsedString))
}
case .link(let range, let urlString):
attributedString.addAttribute(AttributedStringKey.link,
value: urlString,
range: NSRange(range, in: parsedString))
}

if let font = font {
attributedString.addAttributes([AttributedStringKey.font: font],
range: NSRange(element.range, in: parsedString))
}
}

Expand All @@ -119,17 +118,23 @@ public func parsedMarkupString(from text: String,
return NSAttributedString(string: text, textStyle: textStyle)
}

let (parsedString, elements) = try ElementParser.parse(text, for: markups.map { Symbol(character: $0.0) })

let markupRules = Dictionary(
uniqueKeysWithValues: markups.map { (key, value) in
return (Rule(symbol: Symbol(character: key)), value)
}
)

let (parsedString, elements) = try ElementParser.parse(text, for: Array(markupRules.keys))

let attributedString = NSMutableAttributedString(string: textStyle.textTransform.applied(to: parsedString))
attributedString.addAttributes(textStyle.attributes,
range: NSRange(location: 0, length: parsedString.count))

elements.forEach { (element) in
if let markup = markups[Character(element.symbol.rawValue)] {
if let markup = markupRules[element.rule] {
attributedString.addAttributes(markup.attributes, range: NSRange(element.range, in: parsedString))
}
}

return attributedString
}
2 changes: 1 addition & 1 deletion Marker/Marker/NSAttributedString+Extensions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
// Copyright © 2017 Prolific Interactive. All rights reserved.
//

internal extension NSAttributedString {
extension NSAttributedString {

/// Initializes `NSAttributedString` instance with given string and text style.
///
Expand Down
8 changes: 3 additions & 5 deletions Marker/Marker/Parser/Element.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,11 @@

import Foundation

internal typealias Index = String.CharacterView.Index

/// Markup element. Contains a symbol and the range it applies to.
internal struct Element {
struct Element {

/// Markup symbol.
let symbol: Symbol
/// Markup rule.
let rule: Rule

/// Range that the receiver applies to.
let range: Range<Index>
Expand Down
75 changes: 16 additions & 59 deletions Marker/Marker/Parser/ElementParser.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,79 +8,36 @@

import Foundation

/// Markup element parser.
internal struct ElementParser {

/// Parser error.
///
/// - tagMismatch: Opening tag doesn't match closing tag.
/// - unclosedTags: A tag was left unclosed.
enum ParserError: LocalizedError {

case tagMismatch
case unclosedTags

var errorDescription: String? {
switch self {
case .tagMismatch:
return "Opening tag doesn't match closing tag."
case .unclosedTags:
return "A tag was left unclosed."
}
}

}

/// Bare bones parser that strips `string` for symbols defined in `rules`.
struct ElementParser {

// MARK: - Static functions

/// Parses specified string and returns a tuple containing string stripped of tag characters and an array of markup elements.
/// Parses specified string of symbols defined in `rules` and returns a tuple containing string stripped of matching characters and a list of matched elements.
///
/// - Parameters:
/// - string: String to be parsed.
/// - symbols: Symbols to parse for.
/// - Returns: Tuple containing string stripped of tag characters and an array of markup elements.
/// - rules: Rules with symbols to parse for.
/// - Returns: Tuple containing string stripped of matching characters and a list of matched elements.
/// - Throws: Parser error.
static func parse(_ string: String, for symbols: [Symbol]) throws -> (strippedString: String, elements: [Element]) {
let parser = TagParser(symbols: symbols)
let tags = parser.parse(string)

guard tags.count > 0 else {
static func parse(_ string: String, for rules: [Rule]) throws -> (strippedString: String, elements: [Element]) {
let tokens = try TokenParser.parse(string, using: rules)

guard tokens.count > 0 else {
return (string, [])
}
guard tags.count % 2 == 0 else {
throw ParserError.unclosedTags
}

var strippedString: String = ""
var strippedString = ""
var elements: [Element] = []

var startIndex: Index = string.startIndex
for i in stride(from: 0, to: tags.count, by: 2) {
let openingTag = tags[i]
let closingTag = tags[i + 1]
for token in tokens {
let range = strippedString.append(contentOf: token)

if openingTag.symbol != closingTag.symbol {
throw ParserError.tagMismatch
if let rule = token.rule {
elements.append(Element(rule: rule, range: range))
}

// Add the text from the last closing tag to the current opening tag.
strippedString += string[startIndex..<openingTag.index]

let elementStartIndex = strippedString.endIndex
// Add the text that is in between the opening and closing tags.
strippedString += string[string.index(openingTag.index, offsetBy: openingTag.symbol.length)..<closingTag.index]
let elementEndIndex = strippedString.endIndex

// Create an element that would apply to the new string.
elements.append(Element(symbol: openingTag.symbol, range: elementStartIndex..<elementEndIndex))

// Update the start index for the next iteration.
startIndex = string.index(closingTag.index, offsetBy: closingTag.symbol.length)
}

// Add from the last closing tag to the end of the string.
strippedString += string[startIndex..<string.endIndex]


return (strippedString, elements)
}

Expand Down
20 changes: 8 additions & 12 deletions Marker/Marker/Parser/MarkdownElement.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,23 +14,19 @@ import Foundation
/// - strong: Strong element.
/// - strikethrough: Strikethrough element.
/// - underline: Underline element.
internal enum MarkdownElement {
/// - link: Link element.
enum MarkdownElement {

case em(Range<Index>)
case strong(Range<Index>)
case strikethrough(Range<Index>)
case underline(Range<Index>)
case em(range: Range<Index>)
case strong(range: Range<Index>)
case strikethrough(range: Range<Index>)
case underline(range: Range<Index>)
case link(range: Range<Index>, urlString: String)

/// Range of characters that the elements apply to.
var range: Range<Index> {
switch self {
case .em(let range):
return range
case .strong(let range):
return range
case .strikethrough(let range):
return range
case .underline(let range):
case .em(let range), .strong(let range), .strikethrough(let range), .underline(let range), .link(let range, _):
return range
}
}
Expand Down
Loading