Yaml2Text now supports YAML with Liquid

Author’s picture Jeffrey Lau Author’s picture Mikhail Tretiakov on 20 Sep 2020

Introduction, redux!

Yaml2Text has been updated to support the popular Liquid syntax for accessing YAML data in metanorma-standoc version 1.5.3.

Note
This is an updated version of the original blog post located here. This new post represents the updated Yaml2Text block that uses Liquid syntax. Please see the original blog post to refer to the old syntax (metanorma-standoc ⇐ v1.5.3).

When authoring technical documents, there is often a need to represent structured data one way or the other. If the data happens to be stored in one of the many machine-readable formats, one may be compelled to write a helper script that turns them into document elements like tables and bullet points (if your document format allows you to do so 😉).

Metanorma now supports using YAML files as a data source. In English, that means you can now bust out YAML data as AsciiDoc elements with the newly implemented template mini-language.

In this blog post, we’ll see how this is done.

See the Yaml2Text page for more details!

Meet the updated yaml2text block

The yaml2text block defines a template, and the template gets its data from the specified YAML file.

[yaml2text,my_data.yaml,my_yaml_context] (1)
----
This template will be rendered as AsciiDoc content. (2)

{{... my_yaml_context ...}} (3)
----
  1. Define a yaml2text block. Data is sourced from my_data.yaml, which is represented by the context name my_yaml_context. Note that the YAML file path is relative to the directory of the AsciiDoc file defining the block.

  2. The template is whatever that goes inside the block.

  3. Content from the YAML file is interpolated via the context name, surrounded by double curly braces.

That’s the gist of it.

Next, we’ll see how various YAML data structures can be represented:

The yaml2text plugin also supports basic Liquid tags and expressions, see Liquid code snippets

Accessing object properties

EXAMPLE:

Given:

# strings.yaml
---
foo: bar
dead: beef

The following block…​

[yaml2text,strings.yaml,data]
----
I'm heading to the {{data.foo}} for {{data.dead}}.
----

... will render as:

I'm heading to the bar for beef.

Data from strings.yaml are being accessed via the context name data. The foo attribute is accessed via data.foo, which is interpolated in the template with curly braces.

Array length

EXAMPLE:

Given:

# array.yml
---
- lorem
- ipsum
- dolor

The following block…​

[yaml2text,array.yml,data]
----
The length of the YAML array is {{data.size}}.
----

... will render as:

The length of the YAML array is 3.

{{data.size}} evaluates to the length of the array.

Enumerating items in an array

EXAMPLE:

Given:

# strings.yaml
---
- lorem
- ipsum
- dolor

The following block…​

[yaml2text,strings.yaml,arr]
----
{% for item in arr %}
=== {{forloop.index0}} {{item}}

This section is about {{item}}.

{% endfor %}
----

... will render as:

=== 0 lorem

This section is about lorem.

=== 1 ipsum

This section is about ipsum.

=== 2 dolor

This section is about dolor.

Here, the expression {% for item in arr %} tells the template engine to define a new context, item, to represent each individual item from the array arr. The context item is accessible (=== is under scope) within the lines between this expression and the first occurrence of {% endfor %}.

This template is then concatenated for each array item, in the original order of the array, as one might reasonably expect.

{{forloop.index0}} gives the zero-based position of item item in the parent array arr.

Generally, given an array array_name, array_name[i] returns the value at index i (zero-based: starts with 0); negative indices count from the end: index -1 refers to the last item, -2 the second last, etc., etc.

Object size

EXAMPLE:

Given:

# object.yaml
---
name: Lorem ipsum
desc: dolor sit amet

The following block…​

[yaml2text,object.yaml,data]
----
=== {{data.name}}

{{data.desc}} {{data.size}}
----

... will render as:

=== Lorem ipsum

dolor sit amet 2

If data represents a YAML object, then {{data.size}} gives you the number of key-value pairs in that object.

Enumerating keys in an object

EXAMPLE:

Given:

# object.yaml
---
name: Lorem ipsum
desc: dolor sit amet

The following block…​

[yaml2text,object.yaml,my_item]
----
{% for item in my_item %}
=== {{item[0]}}

{{item[1]}}

{% endfor %}
----

... will render as:

=== name

Lorem ipsum

=== desc

dolor sit amet

item[0] gives the key of each key-value pair of the object my_item.

item[1] gives the value corresponding to the current iteration.

Enumerating using attributes .keys and .values

EXAMPLE:

Given:

# object.yaml
---
name: Lorem ipsum
desc: dolor sit amet

The following block…​

[yaml2text,object.yaml,item]
----
.{{item.values[1]}}
[%noheader,cols="h,1"]
|===
{% for elem in item %}
| {{elem[0]}} | {{elem[1]}}
{% endfor %}
|===
----

... will render as:

.dolor sit amet

[%noheader,cols="h,1"]
|===
| name | Lorem ipsum
| desc | dolor sit amet
|===

item.values gives an array of all values in the object item. It follows that item.values[1] gives you the second value.

An array with interpolated file names (for AsciiDoc consumption)

yaml2text blocks can be used for pre-processing document elements for AsciiDoc consumption.

EXAMPLE:

Given:

# strings.yaml
---
prefix: doc-
items:
- lorem
- ipsum
- dolor

The following block…​

[yaml2text,strings.yaml,yaml]
------
{% for item in yml.items %}
[source,ruby]
----
include::{{yaml.prefix}}{{forloop.index0}}.rb[]
----

{% enfor %}
------

... will render as:

[source,ruby]
----
include::doc-0.rb[]
----

[source,ruby]
----
include::doc-1.rb[]
----

[source,ruby]
----
include::doc-2.rb[]
----

Putting it altogether — Array of objects

EXAMPLE:

Given:

# array_of_objects.yaml
---
- name: Lorem
  desc: ipsum
  nums: [3, 5]
- name: dolor
  desc: sit
  nums: []
- name: amet
  desc: lorem
  nums: [2, 4, 6]

The following block…​

[yaml2text,array_of_objects.yaml,ar]
----
First array item of last array item is {{ar[-1].nums[0]}}.
Last array item of first array item is {{ar[0].nums[-1]}}.

{% for item in ar %}

{item.name}:: {item.desc}

{% for num in item.nums %}
- {{item.name}}: index = {{forloop.index0}}, index+1 = {{forloop.index0 | plus: 1 }},
  {{num}} === {{ar[forloop.index0]}}, prev = {% capture prev_index %}{{forloop.index0 | minus: 1}}{% endcapture %}{{ar[prev_index]}}
{% endfor %}
{% endfor %}
----

... will render as:

First array item of last array item is 2.
Last array item of first array item is 5.

Lorem:: ipsum

- Lorem: index = 0, index+1 = 1,
  3 === 3, prev = 5
- Lorem: index = 1, index+1 = 2,
  5 === 5, prev = 3

dolor:: sit


amet:: lorem

- amet: index = 0, index+1 = 1,
  2 === 2, prev = 6
- amet: index = 1, index+1 = 2,
  4 === 4, prev = 4
- amet: index = 2, index+1 = 3,
  6 === 6, prev = 2

You might also have noticed that one can use liquid math filters in order to do simple arithmetics in interpolations and array indexing, like {{forloop.index0 | plus: 1 }} and {{forloop.index0 | minus: 1}} in the example above.

Ending notes

In this blog post, we covered the most common use cases for including YAML data in a Metanorma document using the yaml2text block.

With the simple techniques shown in this article, you should be well equipped to handle any data structures YAML throws at you.

Happy authoring!