Many iOS applications give you the ability to write plain text in a UITextView and format that text based upon simple rules. TextMarkupKit makes it easy to add "format as you type" capabilities to any iOS application.
It consists of several interrelated components:
- One set of components let you write a Parsing Expression Grammar to define how to parse the user's input. Because writing grammars is hard, TextMarkupKit lets you design "extensible grammars." Extensible grammars have explicit extension points where people can introduce new rules rather than writing an entire language grammar from scratch.
TextMarkupKitalso provides an extensible grammar for a subset of Markdown syntax calledMiniMarkdownGrammar. You can extendMiniMarkdownGrammarby providing additional parsing rules for block or inline styles. - An implementation of Dubroy & Warth's Incremental Packrat Parsing algorithm to efficiently re-parse text content as the user types in the
UITextView. - A system to format an
NSAttributedStringbased upon the parse tree for its-stringcontents. TextMarkupKit's formatting support was designed around the needs of lightweight "human markup languages" like Markdown instead of syntax highlighting of programming languages. In addition to changing the attributes associated with text, TextMarkupKit's formatting rules let you transform the displayed text itself. For example, you may choose to change a space to a tab when formatting a list, or not show the special formatting delimiters in some modes, or replace an image markup sequence with an actual image attachment. TextMarkupKit supports all of these modes. - A way to efficiently integrate the formatted
NSAttributedStringwith TextKit so it can be used with aUITextView.
TextMarkupKit provides the parsing / formatting support for my application Grail Diary.
Install TextMarkupKit using Swift Package Manager.
dependencies: [
.package(url: "https://github.com/bdewey/TextMarkupKit", from: "0.7.0"),
],
Please note that TextMarkupKit is not yet at Version 1.0.0 -- the API is changing frequently and dramatically as I adopt code written for one specific application for general use.
While TextMarkupKit is designed to support custom formatting and custom markup languages, you can get started with a subset of Markdown out-of-the box. Using UIKit:
import TextMarkupKit
import UIKit
// textStorage will hold the characters and formatting (determined by the markup rules).
//
// MiniMarkdownGrammar.defaultEditingStyle():
// - Tells `ParsedAttributedString` to use the rules of MiniMarkdownGrammar to parse the text
// - Provides a default set of formatters to style the parsed text.
let textStorage = ParsedAttributedString(string: "# Hello, world!\n", style: MiniMarkdownGrammar.defaultEditingStyle())
// MarkupFormattingTextView is a subclass of UITextView and you can use it anywhere you would use a UITextView.
let textView = MarkupFormattingTextView(parsedAttributedString: textStorage)Using SwiftUI:
import SwiftUI
import TextMarkupKit
struct ContentView: View {
@Binding var document: TextMarkupKitSampleDocument
var body: some View {
// `MarkupFormattedTextEditor` is a SwiftUI wrapper around `MarkupFormattingTextView` that commits its changes back to the
// text binding when editing is complete. By default it uses `MiniMarkdownGrammar.defaultEditingStyle()`, but you can provide
// a custom style with the `style:` parameter.
MarkupFormattedTextEditor(text: $document.text)
}
}That's it! You now have a view that will format plain text and automatically adjust as the content changes. Check out the sample application to see this in action.
Since this project started for personal use, documentation is sparse. While I build it up, this is an overview of the important areas of code.
ParsingRuleis an abstract base class. The job of a parsing rule is to evaluate the input text at specific offset and produce aParsingResult, which is a struct that indicates:- If the parsing rule succeeded at that location
- If the rule succeeded, how much of the input string is consumed by the rule. Parsing continues after the consumed input.
- How much of the input string the
ParsingRulehad to look at at to make its success/fail decision.
PackratGrammaris a protocol something that defines a complete grammar through a graph ofParsingRules.PackratGrammarexposes a single rule,start, that will be used when attempting to parse a string.MemoizationTableimplements the core incremental packrat parsing algorithm.
Additionally, ParsingRule.swift defines many simple rules that you can combine to build much more complex rules for constructing your grammar.
DotRulematches any character.Charactersmatches any character defined by aCharacterSet.Literalmatches a string literal.InOrdertakes an array of child rules and succeeds if every one of the child rules succeeds in sequence.Choicealso takes an array of child rules, but matches the first of the child rules in the array.AssertionRuletakes a single child rule. It succeeds if its child rule succeeds but it does not consume the input.NotAssertionRule, likeAssertionRule, takes a single child rule. It will succeed if its child rule fails and vice versa, and never consumes input.RangeRuletakes a single child rule and will try repeatedly match the rule to the input. It succeeds if the number of successful repetitions of the child rule falls within a specified range.
PieceTableimplements the piece table data structure for efficient text editing.PieceTableStringis a subclass ofNSMutableStringthat uses aPieceTablefor its internal storage.ParsedAttributedStringis a subclass ofNSMutableAttributedStringthat:- Uses a
PieceTableStringfor character storage; - Uses a
MemoizationTableto parse the string and incrementally re-parse the string on each change; - Applies a set of formatting rules based upon the parsed syntax tree to determine the formatting of the string.
- Uses a
ObjectiveCTextStorageWrapperis anNSTextStorageimplementation that lets you use aParsedAttributedStringas the backing storage forTextKit, likeUITextView.
