Creating Custom Elements and Attributes in the Plot HTML Framework

Plot is a framework for creating HTML and XML documents in Swift. The Plot framework supports the most common HTML elements and attributes. But if you want to do something Plot doesn’t natively support, such as create an EPUB book, you must create custom elements and attributes. In this article I share what I’ve learned about creating custom elements and attributes.

Custom Elements

You can think of an element as a tag. Examples of HTML elements include <p> for paragraphs, <h1> for heading 1, and li for a list item. Create a custom element if there is an element you need that Plot does not have. You are more likely to create custom elements for XML than HTML.

Custom Attributes

An attribute is a value that appears inside an HTML or XML element. HTML links have a href attribute with the link destination.

<a href="https://github.com">

Like elements, you are more likely to create custom attributes for XML than HTML. Custom elements are likely to require custom attributes.

Creating a Custom Element

Call the .element function to create a custom element. There are multiple ways to call the .element function. A simple way to call it is to supply a text value for the element. The following call:

.element(named: "country", text: "Mexico")

Creates the following XML tag:

<country>Mexico</country>

You normally write a static function to create a custom element that makes a call to .element. The following function creates a custom element for a country:

static func country(_ location: String) -> Self {
    .element(named: "country", text: location)
}

Supplying Attributes

Another way to call .element is to supply a list of attributes. The following call:

.element(named: "meta", attributes: 
    [.attribute(named: "name", value: "cover"),
    .attribute(named: "content", value: "cover.png")
    ])

Creates the following tag:

<meta name="cover" content="cover.png" />

Supplying a List of Nodes

The final way to call .element is to supply a list of nodes. Supplying a list of nodes helps when creating nested tags. Normally when you have nested tags, the child nodes are nodes of a custom context.

extension XML {
    enum MyMetadataContext {}
}

static func metadata(_ nodes: Node<XML.MyMetadataContext>...) -> Self {
    .element(named: "metadata", nodes: nodes)
}

I explain custom contexts later in this article.

Another situation to supply a list of nodes is to create a custom element that includes both a text value and attributes. The following function creates a custom modified date element:

static func modifiedDate(_ dateString: String) -> Self {
    .element(named: "meta", nodes: 
        [.attribute(named: "property",  
            value:"dcterms:modified"), 
        .text(dateString)
        ])
}

Calling modifiedDate creates an element that looks like the following:

<meta property="dcterms:modified">2020-04-03</meta>

The exact value depends on the date string.

Creating a Custom Attribute

Call the .attribute function to create a custom attribute. Supply the name of the attribute and its value. The following call:

.attribute(named: "xml:lang", value: "en")

Creates the following attribute:

xml:lang="en"

Like with custom elements, you normally write a static function to create a custom attribute. The following function creates a custom attribute for the language of an XML document:

static func xmlLang(_ language: String) -> Self {
    .attribute(named: "xml:lang", value: language)
}

Example: Create a Spine for an EPUB Book

Now it’s time for a real example, creating the spine for an EPUB book. The spine contains a list of files in the order they appear in the book. The following code shows the XML of a sample spine:

<spine toc="ncx">
    <itemref idref="Chapter1"/>
    <itemref idref="Chapter2"/>
    <itemref idref="Chapter3"/>
    <itemref idref="Chapter4"/>
    <itemref idref="Chapter5"/>
</spine>

Building the spine in Plot requires two custom elements: one for the spine and one for the item references. The spine element requires a custom attribute for the table of contents (toc). The item reference element requires a custom attribute for the ID reference.

Create New Contexts

A context is a section of an HTML or XML document where elements and attributes reside. Plot has a BodyContext for HTML documents that corresponds to the <body> tag in the document. Most HTML elements reside in the body context.

Custom attributes usually require you to create a custom context. Sometimes custom elements also require a custom context.

To create the spine, create new XML contexts for the spine and spine item.

extension XML {
    enum SpineContext {}
    enum SpineItemContext {}
}

The SpineContext context contains anything inside a <spine> tag, which is going to be the spine items and the spine element’s custom attribute. The SpineItemContext context contains anything inside an <itemref> tag, which is each item’s ID reference.

Create the Spine Custom Element

The next step is to create the spine custom element, which you can see in the following code:

extension Node where Context == XML.DocumentContext {
    static func spine(_ nodes: Node<XML.SpineContext>...) -> Self {
        .element(named: "spine", nodes: nodes)
    }
}

The code creates an element named spine with a list of child nodes. The child nodes include the spine’s custom attributes and the item reference elements. The child nodes must be in the spine context.

Create the Spine Custom Attribute

Now let’s create the spine’s custom attribute, which you can see in the following code:

extension Node where Context == XML.SpineContext {
    static func spineTOC() -> Self {
        .attribute(named: "toc", value: "ncx")
    }
}

Notice the class extension for the spineTOC function applies to the spine context so the attribute appears inside the spine tag.

Create the Item Reference Element

The next task is to write the code to create the item reference element.

extension Node where Context == XML.SpineContext {
    static func item(_ nodes: Node<XML.SpineItemContext>...) -> Self {
        .element(named: "itemref", nodes: nodes)
    }
}

The item function must be inside the spine context for the item reference elements to appear inside the spine tag.

The nodes are going to be the ID references.

Create the ID Reference Attribute

There’s one more custom attribute to write, the item’s ID reference.

extension Node where Context == XML.SpineItemContext {
    static func itemID(_ name: String) -> Self {
        .attribute(named: "idref", value: name)
    }
}

The itemID function must be inside the spine item context for the attributes to appear inside the item reference tag.

Creating the Spine

The last step is to write a function to build the spine using the custom elements and attributes. Assume there is a Book struct that contains a list of chapters. Each chapter has a title.

func buildSpine() -> Node<XML.DocumentContext> {
    let spine = Node.spine(
        .spineTOC(),
        .forEach(chapters) {
            .item(Node.itemID($0.title))
        })

    return spine
}

The first line of code in the buildSpine function creates the spine custom element. The second line creates the spine element’s custom attribute.

For each chapter in the book, the code creates a spine item custom element and an item ID (idref) custom attribute for the spine item. The item ID’s value is the chapter’s title.

Once you do the work of writing functions for the custom elements and attributes, it doesn’t take much code to do something like build the spine for an EPUB book.