by

Awesome Asciidoctor.js: Server Side Rendering with Font Awesome 5

With Font Awesome 5 icons can be rendered as SVG elements using JavaScript. This implementation includes an API that can be used to do Server Side Rendering. By rendering server side you save the browser the effort required to download additional files or perform the rendering calculations.

For reference, the built-in HTML5 converter in Asciidoctor.js 1.5.9 is using Font Awesome 4.7.0 and the icons are rendered as CSS pseudo-elements and styled with the Font Awesome font-family.

In a previous article, we explained how to create a custom HTML5 converter. In this article, we are going to see how we can use this knowledge to render icons as SVG using the Font Awesome JavaScript API.

Using the Font Awesome JavaScript API

Let’s take a quick look at the Font Awesome JavaScript API.
You can install the library using npm:

$ npm i @fortawesome/fontawesome-svg-core

In addition we also need to install the SVG icons:

$ npm i @fortawesome/free-solid-svg-icons \
        @fortawesome/free-regular-svg-icons \
        @fortawesome/free-brands-svg-icons

Font Awesome provides 3 sets of icons:

  • solid

  • regular

  • brands

Here we install them all but you can choose to install just one set of icons.

So now we can start using the Font Awesome JavaScript API.
The first thing to do is to add icons to the library:

const library = require('@fortawesome/fontawesome-svg-core').library;
const fas = require('@fortawesome/free-solid-svg-icons').fas
const far = require('@fortawesome/free-regular-svg-icons').far
const fab = require('@fortawesome/free-brands-svg-icons').fab
library.add(fas, far, fab)

Then we can use the icon function to find an icon by name and get the SVG:

const icon = require('@fortawesome/fontawesome-svg-core').icon
const flaskIcon = icon({ iconName: 'flask' })
console.log(flaskIcon) // { type: 'icon', prefix: 'fa',  iconName: 'flask',  icon: [...] }
console.log(flaskIcon.html) // [ '<svg...><path ...></path></svg>' ]

If the icon does not exist, the function will return undefined:

console.log(icon({ iconName: '404' })) // undefined

Also make sure to specify the prefix attribute when you search for a brand icon:

console.log(icon({ iconName: 'gitlab' })) // undefined
console.log(icon({ prefix: 'fab', iconName: 'gitlab' })) // { type: 'icon', prefix: 'fab',  iconName: 'gitlab',  icon: [...] }

Or when the icon is available in two styles (regular far and solid fas):

console.log(icon({ prefix: 'far', iconName: 'address-book' })) // { type: 'icon', prefix: 'far',  iconName: 'address-book',  icon: [...] }
console.log(icon({ prefix: 'fas', iconName: 'address-book' })) // { type: 'icon', prefix: 'fas',  iconName: 'address-book',  icon: [...] }

Server-side Rendering with a custom HTML5 converter

Now that we have learned how to use the Font Awesome JavaScript API, let’s write a custom converter to use it:

class TemplateConverter {
  constructor () {
    this.baseConverter = asciidoctor.Html5Converter.$new()
    const inlineImage = (node) => { /* */ } 
    this.templates = {
      inline_image: inlineImage
    }
  }

  convert (node, transform, opts) {
    const template = this.templates[transform || node.node_name]
    if (template) {
      return template(node)
    }
    return this.baseConverter.convert(node, transform, opts)
  }
}

asciidoctor.ConverterFactory.register(new TemplateConverter(), ['html5'])
1 This function controls how the inline_image element will be converted to HTML.

Here I will focus on the implementation of the inlineImage function.

Now that we know how the Font Awesome API is working, we can implement the inlineImage function.

    const inlineImage = (node) => {
      if (node.getType() === 'icon' 
        && node.getDocument().isAttribute('icons', 'svg')) { 
        const search = {}
        search.iconName = node.getTarget() 
        if (node.hasAttribute('prefix')) {
          search.prefix = node.getAttribute('prefix') 
        }
        const faIcon = icon(search)
        if (faIcon) {
          return faIcon.html 
        }
      } else {
        return this.baseConverter.$inline_image(node) 
      }
    }
1 Check that the node is an icon (ie. inline_image can also convert images)
2 Check that the document’s attribute named icons is equals to svg
3 Get the icon’s name using getTarget() function
4 If defined, add the prefix to the search
5 If the icon was found, return the SVG representation
6 If the node is not an icon or if the document’s attribute named icons is not svg, delegate to the default HTML5 converter

And a bit of CSS

To render an SVG icon effectively we need to add a few styles in the page. To do that, we are using a Docinfo processor:

const dom = require('@fortawesome/fontawesome-svg-core').dom;
const registry = asciidoctor.Extensions.create()
registry.docinfoProcessor(function () {
  const self = this
  self.atLocation('head')
  self.process(function () {
    return `<style>${dom.css()}</style>` 
  })
})
1 dom.css() returns a String representing all the styles required to display icons at the correct size

Usage

const input = `You can enable icon:flask[] experimental features on icon:gitlab[prefix=fab] GitLab.`
const options = {
  header_footer: true,
  safe: 'safe',
  extension_registry: registry,
  attributes: { icons: 'svg' }
}
console.log(asciidoctor.convert(input, options)) 
1 It will produce a full HTML5 page with SVG icons

And here’s the result:

You can enable experimental features on GitLab.

Bonus: Adding transformations

We can go one step further and add transformations:

    const inlineImage = (node) => {
      if (node.getType() === 'icon' && node.getDocument().isAttribute('icons', 'svg')) {
        const transform = {}
        if (node.hasAttribute('rotate')) {
          transform.rotate = node.getAttribute('rotate') 
        }
        if (node.hasAttribute('flip')) {
          const flip = node.getAttribute('flip') 
          if (flip === 'vertical' || flip === 'y' || flip === 'v') {
            transform.flipY = true
          } else {
            transform.flipX = true
          }
        }
        const options = {}
        options.transform = transform
        if (node.hasAttribute('title')) {
          options.title = node.getAttribute('title') 
        }
        options.classes = []
        if (node.hasAttribute('size')) {
          options.classes.push(`fa-${node.getAttribute('size')}`) 
        }
        if (node.getRoles()) {
          options.classes = node.getRoles().map(value => value.trim()) 
        }
        const meta = {}
        if (node.hasAttribute('prefix')) {
          meta.prefix = node.getAttribute('prefix')
        }
        meta.iconName = node.getTarget()
        const faIcon = icon(meta, options)
        if (faIcon) {
          return faIcon.html
        }
      } else {
        return this.baseConverter.$inline_image(node)
      }
    }
1 Use the rotate attribute to rotate the icon
2 Use the flip attribute to flip the icon vertically or horizontally
3 Set the title attribute if the title is defined
4 Add the size attribute as a class (prefixed by fa-)
5 Add the roles as HTML classes

Here’s an example of what we can do:

.Size & title
Do you want to drink a small icon:cocktail[sm] or a tall icon:beer[2x,title=pint] ?

.Fixed-width
icon:ruler-vertical[fw] vertical ruler +
icon:ruler-horizontal[fw] horizontal ruler

.Rotate
icon:flag[rotate=90] +
icon:flag[rotate=180] +
icon:flag[rotate=270] +
icon:flag[flip=horizontal] +
icon:flag[flip=vertical]

.Spin and pulse
We are working on it icon:cog[spin], please wait icon:spinner[role=fa-pulse]

.Roles
icon:heart[role=is-primary] icon:heart[role=is-success] icon:heart[role=is-warning] icon:heart[role=is-danger]

And here’s the result:

Size & title

Do you want to drink a small or a tall pint ?

Fixed-width

vertical ruler
horizontal ruler

Rotate





Spin and pulse

We are working on it , please wait

Roles