Converts Markdown files and strings into NSAttributedStrings with lots of customisation options.

Overview

SwiftyMarkdown 1.0

SwiftyMarkdown converts Markdown files and strings into NSAttributedStrings using sensible defaults and a Swift-style syntax. It uses dynamic type to set the font size correctly with whatever font you'd like to use.

Fully Rebuilt For 2020!

SwiftyMarkdown now features a more robust and reliable rules-based line processing and character tokenisation engine. It has added support for images stored in the bundle (![Image](<Name In bundle>)), codeblocks, blockquotes, and unordered lists!

Line-level attributes can now have a paragraph alignment applied to them (e.g. h2.aligment = .center), and links can be optionally underlined by setting underlineLinks to true.

It also uses the system color .label as the default font color on iOS 13 and above for Dark Mode support out of the box.

Support for all of Apple's platforms has been enabled.

Installation

CocoaPods:

pod 'SwiftyMarkdown'

SPM:

In Xcode, File -> Swift Packages -> Add Package Dependency and add the GitHub URL.

How To Use SwiftyMarkdown

Read Markdown from a text string...

let md = SwiftyMarkdown(string: "# Heading\nMy *Markdown* string")
md.attributedString()

...or from a URL.

if let url = Bundle.main.url(forResource: "file", withExtension: "md"), md = SwiftyMarkdown(url: url ) {
	md.attributedString()
}

If you want to use a different string once SwiftyMarkdown has been initialised, you can now do so like this:

let md = SwiftyMarkdown(string: "# Heading\nMy *Markdown* string")
md.attributedString(from: "A **SECOND** Markdown string. *Fancy!*")

The attributed string can then be assigned to any label or text control that has support for attributed text.

let md = SwiftyMarkdown(string: "# Heading\nMy *Markdown* string")
let label = UILabel()
label.attributedText = md.attributedString()

Supported Markdown Features

*italics* or _italics_
**bold** or __bold__
~~Linethrough~~Strikethroughs. 
`code`

# Header 1

or

Header 1
====

## Header 2

or

Header 2
---

### Header 3
#### Header 4
##### Header 5 #####
###### Header 6 ######

	Indented code blocks (spaces or tabs)

[Links](http://voyagetravelapps.com/)
![Images](<Name of asset in bundle>)

[Referenced Links][1]
![Referenced Images][2]

[1]: http://voyagetravelapps.com/
[2]: <Name of asset in bundle>

> Blockquotes

- Bulleted
- Lists
	- Including indented lists
		- Up to three levels
- Neat!

1. Ordered
1. Lists
	1. Including indented lists
		- Up to three levels

Compound rules also work, for example:

It recognises **[Bold Links](http://voyagetravelapps.com/)**

Or [**Bold Links**](http://voyagetravelapps.com/)

Images will be inserted into the returned NSAttributedString as an NSTextAttachment (sadly, this will not work on watchOS as NSTextAttachment is not available).

Customisation

Set the attributes of every paragraph and character style type using straightforward dot syntax:

md.body.fontName = "AvenirNextCondensed-Medium"

md.h1.color = UIColor.redColor()
md.h1.fontName = "AvenirNextCondensed-Bold"
md.h1.fontSize = 16
md.h1.alignmnent = .center

md.italic.color = UIColor.blueColor()

md.underlineLinks = true

md.bullet = "🍏"

On iOS, Specified font sizes will be adjusted relative to the the user's dynamic type settings.

Screenshot

Screenshot

There's an example project included in the repository. Open the Example/SwiftyMarkdown.xcodeproj file to get started.

Front Matter

SwiftyMarkdown recognises YAML front matter and will populate the frontMatterAttributes property with the key-value pairs that it fines.

Appendix

A) All Customisable Properties

h1.fontName : String
h1.fontSize : CGFloat
h1.color : UI/NSColor
h1.fontStyle : FontStyle
h1.alignment : NSTextAlignment

h2.fontName : String
h2.fontSize : CGFloat
h2.color : UI/NSColor
h2.fontStyle : FontStyle
h2.alignment : NSTextAlignment

h3.fontName : String
h3.fontSize : CGFloat
h3.color : UI/NSColor
h3.fontStyle : FontStyle
h3.alignment : NSTextAlignment

h4.fontName : String
h4.fontSize : CGFloat
h4.color : UI/NSColor
h4.fontStyle : FontStyle
h4.alignment : NSTextAlignment

h5.fontName : String
h5.fontSize : CGFloat
h5.color : UI/NSColor
h5.fontStyle : FontStyle
h5.alignment : NSTextAlignment

h6.fontName : String
h6.fontSize : CGFloat
h6.color : UI/NSColor
h6.fontStyle : FontStyle
h6.alignment : NSTextAlignment

body.fontName : String
body.fontSize : CGFloat
body.color : UI/NSColor
body.fontStyle : FontStyle
body.alignment : NSTextAlignment

blockquotes.fontName : String
blockquotes.fontSize : CGFloat
blockquotes.color : UI/NSColor
blockquotes.fontStyle : FontStyle
blockquotes.alignment : NSTextAlignment

link.fontName : String
link.fontSize : CGFloat
link.color : UI/NSColor
link.fontStyle : FontStyle

bold.fontName : String
bold.fontSize : CGFloat
bold.color : UI/NSColor
bold.fontStyle : FontStyle

italic.fontName : String
italic.fontSize : CGFloat
italic.color : UI/NSColor
italic.fontStyle : FontStyle

code.fontName : String
code.fontSize : CGFloat
code.color : UI/NSColor
code.fontStyle : FontStyle

strikethrough.fontName : String
strikethrough.fontSize : CGFloat
strikethrough.color : UI/NSColor
strikethrough.fontStyle : FontStyle

underlineLinks : Bool

bullet : String

FontStyle is an enum with these cases: normal, bold, italic, and bolditalic to give you more precise control over how lines and character styles should look. For example, perhaps you want blockquotes to default to having the italic style:

md.blockquotes.fontStyle = .italic

Or, if you like a bit of chaos:

md.bold.fontStyle = .italic
md.italic.fontStyle = .bold

B) Advanced Customisation

SwiftyMarkdown uses a rules-based line processing and customisation engine that is no longer limited to Markdown. Rules are processed in order, from top to bottom. Line processing happens first, then character styles are applied based on the character rules.

For example, here's how a small subset of Markdown line tags are set up within SwiftyMarkdown:

enum MarkdownLineStyle : LineStyling {
	case h1
	case h2
	case previousH1
	case codeblock
	case body
	
	var shouldTokeniseLine: Bool {
		switch self {
		case .codeblock:
			return false
		default:
			return true
		}
	}
	
	func styleIfFoundStyleAffectsPreviousLine() -> LineStyling? {
		switch self {
		case .previousH1:
			return MarkdownLineStyle.h1
		default :
			return nil
		}
	}
}

static public var lineRules = [
	LineRule(token: "    ",type : MarkdownLineStyle.codeblock, removeFrom: .leading),
	LineRule(token: "=",type : MarkdownLineStyle.previousH1, removeFrom: .entireLine, changeAppliesTo: .previous),
	LineRule(token: "## ",type : MarkdownLineStyle.h2, removeFrom: .both),
	LineRule(token: "# ",type : MarkdownLineStyle.h1, removeFrom: .both)
]

let lineProcessor = SwiftyLineProcessor(rules: SwiftyMarkdown.lineRules, default: MarkdownLineStyle.body)

Similarly, the character styles all follow rules:

enum CharacterStyle : CharacterStyling {
	case link, bold, italic, code
}

static public var characterRules = [
    CharacterRule(primaryTag: CharacterRuleTag(tag: "[", type: .open), otherTags: [
			CharacterRuleTag(tag: "]", type: .close),
			CharacterRuleTag(tag: "[", type: .metadataOpen),
			CharacterRuleTag(tag: "]", type: .metadataClose)
	], styles: [1 : CharacterStyle.link], metadataLookup: true, 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: [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)
]

These Character Rules are defined by SwiftyMarkdown:

public struct CharacterRule : CustomStringConvertible {

	public let primaryTag : CharacterRuleTag
	public let tags : [CharacterRuleTag]
	public let escapeCharacters : [Character]
	public let styles : [Int : CharacterStyling]
	public let minTags : Int
	public let maxTags : Int
	public var metadataLookup : Bool = false
	public var definesBoundary = false
	public var shouldCancelRemainingRules = false
	public var balancedTags = false
}
  1. primaryTag: Each rule must have at least one tag and it can be one of repeating, open, close, metadataOpen, or metadataClose. repeating tags are tags that have identical open and close characters (and often have more than 1 style depending on how many are in a group). For example, the * tag used in Markdown.
  2. tags: An array of other tags that the rule can look for. This is where you would put the close tag for a custom rule, for example.
  3. escapeCharacters: The characters that appear prior to any of the tag characters that tell the scanner to ignore the tag.
  4. styles: The styles that should be applied to every character between the opening and closing tags.
  5. minTags: The minimum number of repeating characters to be considered a successful match. For example, setting the primaryTag to * and the minTag to 2 would mean that **foo** would be a successful match wheras *bar* would not.
  6. maxTags: The maximum number of repeating characters to be considered a successful match.
  7. metadataLookup: Used for Markdown reference links. Tells the scanner to try to look up the metadata from this dictionary, rather than from the inline result.
  8. definesBoundary: In order for open and close tags to be successful, the boundaryCount for a given location in the string needs to be the same. Setting this property to true means that this rule will increase the boundaryCount for every character between its opening and closing tags. For example, the [ rule defines a boundary. After it is applied, the string *foo[bar*] becomes *foobar* with a boundaryCount 00001111. Applying the * rule results in the output *foobar* because the opening * tag and the closing * tag now have different boundaryCount values. It's basically a way to fix the **[should not be bold**](url) problem in Markdown.
  9. shouldCancelRemainingTags: A successful match will mark every character between the opening and closing tags as complete, thereby preventing any further rules from being applied to those characters.
  10. balancedTags: This flag requires that the opening and closing tags be of exactly equal length. E.g. If this is set to true, **foo* would result in **foo*. If it was false, the output would be *foo.

Rule Subsets

If you want to only support a small subset of Markdown, it's now easy to do.

This example would only process strings with * and _ characters, ignoring links, images, code, and all line-level attributes (headings, blockquotes, etc.)

SwiftyMarkdown.lineRules = []

SwiftyMarkdown.characterRules = [
	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)
]

Custom Rules

If you wanted to create a rule that applied a style of Elf to a range of characters between "The elf will speak now: %Here is my elf speaking%", you could set things up like this:

enum Characters : CharacterStyling {
	case elf

	func isEqualTo( _ other : CharacterStyling) -> Bool {
		if let other = other as? Characters else {
			return false
		}
		return other == self
	}
}

let characterRules = [
	CharacterRule(primaryTag: CharacterRuleTag(tag: "%", type: .repeating), otherTags: [], styles: [1 : CharacterStyle.elf])
]

let processor = SwiftyTokeniser( with : characterRules )
let string = "The elf will speak now: %Here is my elf speaking%"
let tokens = processor.process(string)

The output is an array of tokens would be equivalent to:

[
	Token(type: .string, inputString: "The elf will speak now: ", characterStyles: []),
	Token(type: .repeatingTag, inputString: "%", characterStyles: []),
	Token(type: .string, inputString: "Here is my elf speaking", characterStyles: [.elf]),
	Token(type: .repeatingTag, inputString: "%", characterStyles: [])
]

C) SpriteKit Support

Did you know that SKLabelNode supports attributed text? I didn't.

let smd = SwiftyMarkdown(string: "My Character's **Dialogue**")

let label = SKLabelNode()
label.preferredMaxLayoutWidth = 500
label.numberOfLines = 0
label.attributedText = smd.attributedString()
Comments
  • adds support for optionally setting a fixed font size

    adds support for optionally setting a fixed font size

    Sometimes you just need to fix the font size for design purposes. This PR adds the ability to override the dynamic text font size by setting the fontSize on the respective FontProperties.

    opened by mightea 9
  • Attributes

    Attributes

    Addresses https://github.com/SimonFairbairn/SwiftyMarkdown/issues/16 and https://github.com/SimonFairbairn/SwiftyMarkdown/pull/12

    The idea here is to let users define NSAttributedString attributes for individual styles and default to Dynamic Typing if these are not set. This provides much needed flexibility without giving up Dynamic Typing.

    To see this in action, open SwiftyMarkdown.playground and compare Dynamic Typing with Attributes:

    Attributes

    image

    Dynamic Typing

    image

    Feedback and comments welcome.

    opened by arielelkin 5
  • Font size ignored

    Font size ignored

    Changing the font size makes no difference. Also if I do this for all style but the issue exists: setFontSizeForAllStyles gets ignored Using SwiftyMarkdown 1.2.3 and tried 1.2 let swiftytext = SwiftyMarkdown(string: mdString) swiftytext.body.color = UIColor.darkGray swiftytext.h2.fontStyle = .bold swiftytext.body.fontSize = 50

    opened by fmatosic 4
  • Cannot set link.color

    Cannot set link.color

    Using link.color doesn't seem to change the color for links. link.fontName works fine

    let md = SwiftyMarkdown(string: "![Screenshot](http://f.cl.ly/items/12332k3f2s0s0C281h2u/swiftymarkdown.png)") md.link.color = UIColor.greenColor() md.link.fontName = "ArvilSans" textAreaView.attributedText = md.attributedString()

    Any ideas why?

    opened by pbridi 4
  • Support for inline images

    Support for inline images

    Using NSTextAttachment:

    let img = UIImage(named:"image")
    let textAttachment = NSTextAttachment(data: nil, ofType: nil)
    textAttachment.image = img
    let imageString = NSAttributedString(attachment: textAttachment)
    
    enhancement 
    opened by SimonFairbairn 4
  • Phone URL not working and crashing the app after long press

    Phone URL not working and crashing the app after long press

    Xcode 11.3, iOS 13.3, SDK 1.1.0

    Repro steps:

    • Create a UITextView on a storyboard
    textView.isSelectable = true
    textView.isEditable = false
    textView.delegate = self
    textView.dataDetectorTypes = [.phoneNumber]
    let text = """
    Call us at [0 123 456](tel://0 123 456). We'll answer your call. Or go to [our website](https://www.google.com).
    """
    let markdown = SwiftyMarkdown(string: text)
    textView.attributedString = markdown.attributedString() 
    

    Result:

    • Tapping the phone number doesn't call textView(_ textView: UITextView, shouldInteractWith URL: URL, in characterRange: NSRange, interaction: UITextItemInteraction)
    • Long pressing the phone number causes the app to crash with an ambiguous stack trace.
    opened by kacper1703 3
  • Indented list color incorrect

    Indented list color incorrect

    Issue

    The colors for indented lists are incorrect (unless I am setting the colors incorrectly). We support light and dark mode users can switch between them. Notice in the picture below, that in dark mode, the indented lists don't change colors.

    This is what I set in code:

    markdown.body.color = .red
    
    Screen Shot 2020-01-30 at 3 58 21 PM Screen Shot 2020-01-30 at 3 58 14 PM
    opened by spike-hue 3
  • "Fatal error: String index is out of bounds" for particular string

    A string index out of bounds fatal error is produced when converting this text into an attributed string:

    let text = "[**\\!bang**](https://duckduckgo.com/bang) "
    let md = SwiftyMarkdown(string: text)
    let string = md.attributedString()
    
    Screen Shot 2019-04-16 at 10 00 39 AM
    opened by christoph-verse 3
  • Support for Links inside bold

    Support for Links inside bold

    I was using this library to work on Reddit's markdown for Reddit-MVVM-Benchmark and I hit a wall when it comes to tags inside tags, especially this use case:

    **Al Kindle, captain of [Team Half Fast Astronaut](https://www.facebook.com/Teamhfa):**

    Is this a library limitation or rather a bug on how the parsing is working?

    opened by ivanbruel 3
  • Fix build break:

    Fix build break: "Cannot capture lastElement before it is declared"

    @SimonFairbairn not sure if this is exactly the cause, but my project runs on Swift 5 and fails to build because of the issue in the title. Moving the lastElement declaration fixes the issue.

    opened by julo15 2
  • [Bug] Links surrounded by brackets don't render correctly

    [Bug] Links surrounded by brackets don't render correctly

    For the markdown string SwiftyMarkdown(string: "Hello ([hello](https://www.google.com))"), the rendered attributed string shows up incorrectly.

    In SwiftyMarkdown v0.6.2, this shows up as: image (missing the end bracket)

    In SwiftyMarkdown v1.1.0, this shows up as: image (doesn't tokenize the link at all)

    I would expect this to show up as:

    Hello (hello)

    opened by julo15 2
  • How to detect and navigate for link from UILabel

    How to detect and navigate for link from UILabel

    Can't able to perform navigation for link present in UILabel. Could you please provide any solution for UILabel instead of using UITextview? I really appreciate any help you can provide.

    opened by Elamuruga 0
  • New `ignoreDynamicFontSize` property for iOS

    New `ignoreDynamicFontSize` property for iOS

    This PR adds a new ignoreDynamicFontSize property that is necessary if you want to set sizes for other styles then italic or bold without setting the fontName (you cannot get SFUI font via name).

    Inspired by an old but still opened PR.

    opened by Claes34 5
  • iOS 16 lines truncated when text contains multiple paragraphs.

    iOS 16 lines truncated when text contains multiple paragraphs.

    Everything works fine prior to the iOS 16 betas.

    If you have a string that contains multiple paragaphs all paragraphs after the first one will be truncated after the first line. This happens even if the markdown string passed to SwiftyMarkdown contains no special markdown formatting.

    For example if passed some multi paragraph text it would render like this:

    This is the first paragraph were text wraps properly and lines are not truncated. It can carry on for multiple lines just fine.

    But every paragraph after the first will a its text trun....

    And the third paragraph will appear truncated as we...

    If you pass the exact same text as an NSAttributedString directly (not using SwiftyMarkdown) then it lays out correctly.

    I'm not sure what is causing this as I don't quite understand the inner workings or how attributed strings are built. I don't know if Apple changed something behind the scenes here or if it's just a bug with the beta but it's probably worth looking into.

    Thanks

    opened by tylerjames 3
  • Fixing issue #129

    Fixing issue #129

    Fixing issue https://github.com/SimonFairbairn/SwiftyMarkdown/issues/129

    there's a bug about the close tag.

    the origin markdown below: [Links(asd)](http://voyagetravelapps.com/). and result image below: image

    opened by ZhouBoo 0
  • the close tag bug

    the close tag bug

    swift 5.5 SwiftyMarkdown 1.2.4


    I think there's a bug about the close tag.

    the origin markdown below: [Links(asd)](http://voyagetravelapps.com/). and result image below: image

    opened by ZhouBoo 0
Owner
Simon Fairbairn
Creator of Trail Wallet and traveller of the world.
Simon Fairbairn
Generate SwiftUI Text or AttributedString from markdown strings with custom style names.

iOS 15.0 / macOS 12.0 / tvOS 15.0 / watchOS 8.0 StyledMarkdown is a mini library that lets you define custom styles in code and use them in your local

null 19 Dec 7, 2022
LDOMarkdownParser - Parse (some) markdown attributes into an NSAttributedString

LDOMarkdownParser Description Convert markdown text styling into an NSAttributed

Lurado 1 Feb 4, 2022
Markdown parsing and rendering for iOS and OS X

CocoaMarkdown Markdown parsing and rendering for iOS and macOS CocoaMarkdown is a cross-platform framework for parsing and rendering Markdown, built o

Indragie Karunaratne 1.2k Dec 12, 2022
A simple and customizable Markdown Parser for Swift

MarkdownKit MarkdownKit is a customizable and extensible Markdown parser for iOS and macOS. It supports many of the standard Markdown elements through

Bruno Oliveira 687 Dec 18, 2022
MarkdownView is a WKWebView based UI element, and internally use bootstrap, highlight.js, markdown-it.

MarkdownView is a WKWebView based UI element, and internally use bootstrap, highlight.js, markdown-it.

Keita Oouchi 1.8k Dec 21, 2022
Markdown in SwiftUI, and some other interesting components.

RoomTime RoomTime is a bundle of tools developed in my app RoomTime Lite. ( ?? RoomTime Lite is still in development) Features TextArea AutoWrap Markd

Chen SiWei 56 Dec 20, 2022
Notepad - A fully themeable iOS markdown editor with live syntax highlighting.

Notepad is just like any other UITextView, but you need to use the convenience initializer in order to use the themes. To create a new theme, copy one of the existing themes and edit the JSON.

Rudd Fawcett 802 Dec 31, 2022
Markdown parser for iOS

Marky Mark Marky Mark is a parser written in Swift that converts markdown into native views. The way it looks it highly customizable and the supported

M2mobi 254 Jun 11, 2021
Swift markdown library

Markdown ![Swift version](https://img.shields.io/badge/Swift-2.1 | 2.2-blue.svg) ![GitHub license](https://img.shields.io/badge/license-LGPL v3-green.

Crossroad Labs 79 Oct 9, 2022
A Pure Swift implementation of the markdown mark-up language

SmarkDown A pure Swift markdown implementation consistent with Gruber's 1.0.1 version. It is released under the BSD license so please feel free to use

Swift Studies 67 Jan 24, 2022
`resultBuilder` support for `swift-markdown`

SwiftMarkdownBuilder resultBuilder support for swift-markdown. The default way to build Markdown in swift-markdown is to use varargs initializers, e.g

DocZ 9 May 31, 2022
Leverages Apple's Swift-based Markdown parser to output NSAttributedString.

Markdownosaur ?? Markdownosaur uses Apple's excellent and relatively new Swift Markdown library to analyze a Markdown source, and then takes that anal

Christian Selig 232 Dec 20, 2022
AttributedString Markdown initializer with custom styling

AttributedString Markdown initializer with custom styling AttributedString in iOS 15 and macOS 12 comes with a Markdown initializer. But: There is no

Frank Rausch 41 Dec 19, 2022
An Objective-C framework for converting Markdown to HTML.

MMMarkdown MMMarkdown is an Objective-C framework for converting Markdown to HTML. It is compatible with OS X 10.7+, iOS 8.0+, tvOS, and watchOS. Unli

Matt Diephouse 1.2k Dec 14, 2022
Markdown syntax highlighter for iOS

Marklight Markdown syntax highlighter for iOS and macOS. Description Marklight is a drop in component to easily add realtime Markdown syntax highlight

Matteo Gavagnin 539 Dec 29, 2022
Rich Markdown editing control for iOS

MarkdownTextView Rich Markdown Editing for iOS MarkdownTextView is an iOS framework for adding rich Markdown editing capabilities. Support for Markdow

Indragie Karunaratne 676 Dec 7, 2022
Blazing fast Markdown / CommonMark rendering in Swift, built upon cmark.

Down Blazing fast Markdown (CommonMark) rendering in Swift, built upon cmark v0.29.0. Is your app using it? Let us know! If you're looking for iwasrob

John Nguyen 2k Dec 19, 2022
Markdown parser for iOS

Marky Mark Marky Mark is a parser written in Swift that converts markdown into native views. The way it looks it highly customizable and the supported

M2mobi 262 Nov 23, 2021
A Github action for creating generic run report using Markdown

create-report A Github action for creating generic run report (using Markdown!) - uses: michaelhenry/[email protected] with: report-title: "

Michael Henry 4 Apr 19, 2022