Using JavaScriptCore in Swift

Creating a markdown parser in Swift using Javascript

30 Mar 2019

In my new application—Demo Remote—I needed a markdown parser to convert the user’s input markdown file into HTML. Unfortunately, most Swift markdown parsers suffer from a set of different issues, namely being large frameworks, having odd external dependencies, or too many features for what I needed. So I wondered if I could just use a simple markdown javascript parser that I know works, and the answer is yes I can. And I did.

JavaScriptCore is a framework available on macOS iOS, and tvOS to run javascript without a web browser. This is really great for when you want to reuse some javascript code, have dynamic business logic, or maybe even build a simple plugin system.

JavaScriptCore requires a JSContext to evaluate the script which returns a JSValue. This value can then be converted to a native object or primitive type, NSHipster has a great table on all the types.

My super simple Markdown parser is below, it imports a script from the bundle, but could very easily be enhanced by pulling one from a server so I can enhance the script quickly.

import Cocoa
import JavaScriptCore

class MarkdownRenderer: NSObject {
    
    // Fetch the script from file and parse it as a string
    private static var source : String? = {
        guard let url = Bundle.main.path(forResource: "marked.min", ofType: "js", inDirectory: nil, forLocalization: nil) else {
            return nil
        }
        guard let newsource = try? NSString(contentsOfFile: url, encoding: String.Encoding.utf8.rawValue) else {
            return nil
        }
        
        return newsource as String
    }()


    // The static function that is called from elsewhere in the app
    // returns HTML in a string 
    static func render(_ message: String) -> String? {
        if let jsSource = source {
            // Create a context
            let context = JSContext()

            // Evaluate the script
            context?.evaluateScript(jsSource)
            
            // Select the function in the script I want to call
            let testFunction = context?.objectForKeyedSubscript("marked")

            // Call the function with the arguments (in this case the markdown)
            let result = testFunction?.call(withArguments: [message])
            
            // Convert the result to a string
            return result?.toString()
        } else {
            return nil
        }
       
    }
    
}

JavaScriptCore is a really cool way to extend your native application and opens up your application to a greater range of options. Whatever your opinion of JS the langauge is, it’s hard to deny it’s a massive community with a package for everything.

I’ll be reaching for this again the future when native doesn’t make sense.