Exposing the MultiMarkdown 6 Library to Swift, Part 1: Swifty Enums

During the time between Christmas and New Year, which we tend to call the time "between the years" where I live in Germany, I wanted to do something fun, but not too fun so I don't get spoiled.

That's how I turned up experimenting to use libMultiMarkdown from within a Swift app. The amount of fun I have when reading and writing C code is negligible, which made it a perfect fit.

And, frankly, I think using the official MultiMarkdown library is a good approach to perform proper syntax highlighting in an editor. Much better than my home-brewed stuff can ever be. While writing a "minimum viable" Markdown highlighter yourself is pretty easy, the edge cases are aplenty, and the long term maintenance overhead is too big for my taste. So I really don't want to bother making my own highlighter useful much longer and switch to a better, more performant solution.

Getting the current MultiMarkdown 6 project to generate an Xcode project with a local build directory and output the library header file in the first place was a bit of a chore. More about that some other day.

Exposing the token types as enums

In the wild west of C code, you end up with a header file with free functions. Calling these from Swift is straightforward. More bewildering to use are the enums which you need to do anything with the resulting tokens. They look innocent enough, like this:

enum token_types {
    DOC_START_TOKEN = 0,    //!< DOC_START_TOKEN must be type 0

    BLOCK_BLOCKQUOTE = 50,        //!< This must start *after* the largest number in parser.h
    BLOCK_CODE_FENCED,
    BLOCK_CODE_INDENTED,
    BLOCK_DEFLIST,
    BLOCK_DEFINITION,
    BLOCK_DEF_ABBREVIATION,
    BLOCK_DEF_CITATION,
    BLOCK_DEF_GLOSSARY,
    BLOCK_DEF_FOOTNOTE,
    BLOCK_DEF_LINK,
    BLOCK_EMPTY,
    BLOCK_HEADING,                //!< Placeholder for theme cascading
    BLOCK_H1,                    //!< Leave H1, H2, etc. in order
    BLOCK_H2,
    // ...
}

But the resulting Swift code is very noisy because these C enum cases are exported as root-level constants, and the resulting types don't even match Swift's expectations, so you have tons of type casts:

let token: UnsafeMutablePointer<token> = // ...
switch token.pointee.type {
case UInt16(BLOCK_BLOCKQUOTE.rawValue): // ...
case UInt16(BLOCK_DEFLIST.rawValue): // ...
default: break
}

We get useful Swift enums when we use NS_ENUM in Objective-C. Since libMultiMarkdown is not a Swift module but a C library, I think a proper NS_ENUM in Objective-C is a good start. With that in place, you can use it from both Objective-C and Swift.

The expected code would look like this:

typedef NS_ENUM(NSUInteger, MMD6TokenType) {
    MMD6TokenTypeDocStartToken = DOC_START_TOKEN,
    MMD6TokenTypeBlockBlockquote = BLOCK_BLOCKQUOTE,
    MMD6TokenTypeBlockCodeFenced = BLOCK_CODE_FENCED,
    MMD6TokenTypeBlockCodeIndented = BLOCK_CODE_INDENTED,
    MMD6TokenTypeBlockDeflist = BLOCK_DEFLIST,
    MMD6TokenTypeBlockDefinition = BLOCK_DEFINITION,
    MMD6TokenTypeBlockDefAbbreviation = BLOCK_DEF_ABBREVIATION,
    MMD6TokenTypeBlockDefCitation = BLOCK_DEF_CITATION,
    MMD6TokenTypeBlockDefGlossary = BLOCK_DEF_GLOSSARY,
    MMD6TokenTypeBlockDefFootnote = BLOCK_DEF_FOOTNOTE,
    MMD6TokenTypeBlockDefLink = BLOCK_DEF_LINK,
    MMD6TokenTypeBlockEmpty = BLOCK_EMPTY,
    MMD6TokenTypeBlockHeading = BLOCK_HEADING,
    MMD6TokenTypeBlockH1 = BLOCK_H1,
    MMD6TokenTypeBlockH2 = BLOCK_H2,
    MMD6TokenTypeBlockH3 = BLOCK_H3,
    MMD6TokenTypeBlockH4 = BLOCK_H4,
    // ...
} NS_SWIFT_NAME(TokenType);

I wrote a Ruby script to generate the header file code for this.

Note about type names: In C, it makes sense to pluralize the enum's name, as in token_types, because it reads like a grouping; when you prefix the NS_ENUM cases with the type name, you drop it, because you address the case individually, not like a collection: MMD6TokenTypeBlockHeading, not MMD6TokenTypesBlockHeading. In Swift, you don't pluralize enum type names, either, using TokenType.blockHeading and passing tokenType: TokenType parameters around, not tokenTypes: TokenTypes, which again read like collections. So the plural S needs to be dropped. The script has brittle hard-coded depluralization built in.

Get the code generator as a Gist.

Run the generator like this to generate the NS_ENUM code:

ruby cocoaconv.rb path/to/libMultiMarkdown.h

You can also generate better readable Swift enum case descriptions:

ruby cocoaconv.rb -m swift path/to/libMultiMarkdown.h

The latter will print out code like the following:

extension TokenType: CustomStringConvertible {
    public var description: String {
        switch self {
            case .docStartToken: return "TokenType.docStartToken"
            case .blockBlockquote: return "TokenType.blockBlockquote"
            case .blockCodeFenced: return "TokenType.blockCodeFenced"
            case .blockCodeIndented: return "TokenType.blockCodeIndented"
            case .blockDeflist: return "TokenType.blockDeflist"
            case .blockDefinition: return "TokenType.blockDefinition"
            case .blockDefAbbreviation: return "TokenType.blockDefAbbreviation"
            case .blockDefCitation: return "TokenType.blockDefCitation"
            case .blockDefGlossary: return "TokenType.blockDefGlossary"
            case .blockDefFootnote: return "TokenType.blockDefFootnote"
            case .blockDefLink: return "TokenType.blockDefLink"
            case .blockEmpty: return "TokenType.blockEmpty"
            case .blockHeading: return "TokenType.blockHeading"
            // etc.
        }
    }
}

Maybe this'll make its way into the code repository. I'm working on a series of Pull Requests right now to make the library more accessible for Cocoa developers, both Objective-C and Swift.

Using the new enums in a Cocoa project

With the power of my script, these are the steps you need to perform:

  1. Generate the NS_ENUM code,
  2. Store it in a .h file in your project (e.g. MMD6Enums.h),
  3. Add the following to the top of the file:
#import <Foundation/Foundation.h>
#import <libMultiMarkdown/libMultiMarkdown.h>
#import <libMultiMarkdown/token.h>
#import <libMultiMarkdown/d_string.h>

If you're not coding in Objective-C, you then need to import the MMD6Enums.h in your TARGETNAME-Bridging-Header.h file to expose the results to Swift.

You can then use it like so:

func applyHighlightingForTokenTree(_ token: UnsafeMutablePointer<token>) {
    let tokenType = TokenType(rawValue: UInt(token.pointee.type))!

    switch tokenType {
    case .blockBlockquote: // highlight blockquote block
    case .pairEmph: // highlight emphasized span
    case .emphStart, .emphStop: // highlight the asterisks or underscores differently
    // ...
    }
}

All of this is still too much work for my taste. Nobody wants to do that. When I figure out what else I want to change to make libMultiMarkdown usable in Swift, I'll create a wrapper library for Swift PM, Carthage, CocoaPods and whatnot. I'm not certain if that should be part of the MMD6 project itself or a separate repository, though, but I think Fletcher, the author of MultiMarkdown, will come up with a plan. Meanwhile, I'll keep making PRs.

Browse the blog archive