Supporting RenderNode diffing in Swift-DocC

Hello! I’m here to announce a new API we recently implemented in Swift-DocC. When writing, building, and exporting documentation, it’s useful to know which changes have been introduced between different versions of the same content, after the content has been compiled by DocC and before it gets published somewhere. To achieve this, we can now compare the documentation generated as RenderNode JSON files and packaged within the corresponding documentation archive. Let’s take a look on how we can do this with the new difference API in RenderNode.

Diffing RenderNode in action

Let’s imagine we’re writing documentation for a new Swift framework called SlothCreator. After crafting the content and building it with DocC, we can export it as a documentation archive. Within the documentation folder in the archive, we can find all the documentation pages as RenderNode JSON files. Let’s say we’ve been working on an article called Getting Started with Sloths, that has been compiled as a gettingstarted.json file. The following is a snippet of how that file might look like:

{
    ...
    "identifier": {
        "url": "doc:\/\/com.apple.SlothCreator\/documentation\/SlothCreator\/GettingStarted",
        "interfaceLanguage": "swift"
    },
    "abstract": [
        {
            "type": "text",
            "text": "Create a sloth and assign personality traits and abilities."
        }
    ],
    "kind": "article",
    "primaryContentSections": [
        {
            "kind": "content",
            "content": [
                {
                    "type": "paragraph",
                    "inlineContent": [
                        {
                            "type": "text",
                            "text": "Sloths are complex creatures that require careful creation and a suitable habitat."
                        }
                    ]
                },
    ... 
}

After working on the content some more, we have now another version of the documentation archive. But we’re not sure of the differences compared with the previous version. The following is a snippet of how gettingstarted.json file could be looking like in the most recent version of our content:

{
    ...
    "identifier": {
        "url": "doc:\/\/com.apple.SlothCreator\/documentation\/SlothCreator\/GettingStarted",
        "interfaceLanguage": "swift"
    },
    "abstract": [
        {
            "type": "text",
            "text": "Create a sloth and assign personality traits and abilities  to them.."
        }
    ],
    "kind": "article",
    "primaryContentSections": [
        {
            "kind": "content",
            "content": [
                {
                    "type": "paragraph",
                    "inlineContent": [
                        {
                            "type": "text",
                            "text": "Sloths are complex creatures that require careful creation and a suitable habitat."
                        },
                        {
                            "type": "text",
                            "text": "After creating a sloth, you’re responsible for feeding them, providing fulfilling activities, and giving them opportunities to exercise and rest."
                        }
                    ]
                },
    ... 
}

Now we could write a small command-line app that reads both JSON files, decodes them to RenderNode objects, and calculates the differences!

import ArgumentParser
import Foundation
import SwiftDocC

@main
struct RenderNodeDiff: ParsableCommand {

    @Option(
        name: [.customShort("f"), .customLong("from")],
        help: "Path to RenderNode JSON to compare from."
    )
    var fromPath: String

    @Option(
        name: [.customShort("t"), .customLong("to")],
        help: "Path to RenderNode JSON to compare to."
    )
    var toPath: String
    
    func run() throws {
        
        let originalJsonData = try Data(NSData(contentsOfFile: fromPath))
        let originalRenderNode = try RenderNode.decode(fromJSON: originalJsonData)
        
        let modifiedJsonData = try Data(NSData(contentsOfFile: toPath))
        let modifiedRenderNode = try RenderNode.decode(fromJSON: modifiedJsonData)
        
        let differences = modifiedRenderNode._difference(from: originalRenderNode)
        
        for diff in differences {
            print(diff)
        }
    }
}

The differences collection will contain a list of JSON Patches, representing the operations to convert originalRenderNode to modifiedRenderNode .

If we run our app with the SlothCreator example above, we should get something like this:

[
    replace(pointer: /abstract/0, value: text("Create a sloth and assign personality traits and abilities to them."))
    add(pointer: /primaryContentSections/0/content/1, value: paragraph(SwiftDocC.RenderBlockContent.Paragraph(inlineContent: [SwiftDocC.RenderInlineContent.text("Sloths are complex creatures that require careful creation and a suitable habitat."), SwiftDocC.RenderInlineContent.text("After creating a sloth, you're responsible for feeding them, providing fulfilling activities, and giving them opportunities to exercise and rest.")])))
]

Future directions

Diffing RenderNode in Swift-DocC is still an experimental API that will evolve to support additional content in the future. Currently, there are some unsupported types of content that we expect to add support for soon, such as:

  • Language-specific overrides in articles and symbols
  • Tutorials

The API enables functionality that could diff different versions an entire DocC documentation archive, and also include versioning and diff information as part of the archive itself. Because we’re using the JSON Patch format to track differences, this would also allow to implement patching on RenderNode JSON and switch between different versions. In the future, we’d like to build all this support directly into DocC, but we’re excited for the community to be able to take advantage of it today.

4 Likes