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.