Generating text from YAML data

Purpose

When managing large amounts of data with identical data structures, such as registry data or requirements, Metanorma supports a templating mechanism for generating structured text using data structures as input.

The yaml2text block lets you generate document elements directly from a separate YAML file into the document. [added in https://github.com/metanorma/metanorma-standoc/releases/tag/v1.3.25]

Expressions

There are only two kinds of expressions:

  • rendering expressions: for string interpolation (single argument, e.g. {x})

  • control flow expressions: for directives to modify processing flow (more than one argument, e.g. {x,y})

And four types of values:

  • number

  • string

  • object / hash

  • array

Rendering expressions:

  • Only takes a single argument.

Control flow expressions:

  • More than one argument must be provided.

  • Allows control over enumerable items: arrays and objects

  • Provides locality context within enumerators

Syntax

Defining the block

A yaml2text block is created with the following syntax.

Block opening and closing is demarcated by an open block syntax (--) or the [source] block syntax (---- or more -).

[yaml2text,{YAML file path},{self-defined context name}]
----
this is content within the yaml2text block!
----

Where:

  • content within the block is called the “template”;

  • {YAML file path} is the location of the YAML file that contains data to be loaded. Location of the YAML file is computed relative to the source directory that [yaml2text] is used (e.g., if [yaml2text,data.yaml,data] is invoked in an .adoc file located at /foo/bar/doc.adoc, the data file is expected to be found at /foo/bar/data.yaml);

  • {self-defined context name} is the name where the YAML data read from the YAML file can be accessed with.

Interpolation

yaml2text takes an interpolation syntax that is similar to that of AsciiDoc with curly braces { }.

The value within the curly braces will be interpolated by yaml2text.

  • In {x}, x is the name of the variable or AsciiDoc attribute.

  • The location of {x} in text will be replaced with the value of x.

  • Evaluation order will be first from the defined context, then of the AsciiDoc document.

Accessing object values

Object values are accessed via the . (dot) separator.

EXAMPLE:

Given:

strings.yaml

---
foo: bar
dead: beef

And the block:

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

The file path is strings.yaml, and context name is data. {data.foo} evaluates to the value of the key foo in data.

Will render as:

I'm heading to the bar for beef.

Accessing arrays

Length

The length of an array can be obtained by {arrayname.*}.

EXAMPLE:

Given:

strings.yaml

---
- lorem
- ipsum
- dolor

And the block:

[yaml2text,strings.yaml,data]
----
The length of the YAML array is {data.*}.
----

The file path is strings.yaml, and context name is data. {data.*} evaluates to the length of the array.

Will render as:

The length of the YAML array is 3.

Enumeration and context

The following syntax is used to enumerate items within an array:

{array_name.*,item_name,block_delimiter}
  ...content...
{block_delimiter}

Where:

  • array_name is the name of the existing context that contains array data

  • item_name is used to refer to the current item within the array

  • block_delimiter indicates where the array enumeration block ends

Within an array enumerator, the following expressions can be used:

  • item_name.# gives the zero-based position of the item item_name within the parent array

  • array_name.* gives the length of the array array_name

  • array_name[i] provides the value at index i (zero-based: starts with 0) in the array array_name; -1 can be used to refer to the last item, -2 the second last item, and so on.

EXAMPLE:

Given:

strings.yaml

---
- lorem
- ipsum
- dolor

And the block:

[yaml2text,strings.yaml,arr]
----
{arr.*,item,EOS}
=== {item.#} {item}

This section is about {item}.

{EOS}
----

Where:

  • file path is strings.yaml

  • current context within the enumerator is called item

  • {item.#} gives the zero-based position of item item in the parent array arr.

Will render as:

=== 0 lorem

This section is about lorem.

=== 1 ipsum

This section is about ipsum.

=== 2 dolor

This section is about dolor.

Accessing objects

Size

Similar to arrays, the number of key-value pairs within an object can be obtained by {objectname.*}.

EXAMPLE:

Given:

object.yaml

---
name: Lorem ipsum
desc: dolor sit amet

And the block:

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

{data.desc}
----

The file path is object.yaml, and context name is data. {data.*} evaluates to the size of the object.

Will render as:

=== Lorem ipsum

dolor sit amet

Enumeration and context

The following syntax is used to enumerate key-value pairs within an object:

{object_name.*,key_name,block_delimiter}
  ...content...
{block_delimiter}

Where:

  • object_name is the name of the existing context that contains the object

  • key_name is used to refer to the current key under enumeration within the object

  • block_delimiter indicates where the object enumeration block ends

Within an object enumerator, the following expressions can be used:

  • item_name[key] gives the dereferenced value of the data path item_name.{key}. e.g. `{yaml.items[s.#]}, {my_object[key_name]}. Note that items should only be de-referenced with the item key, not with an integer index.

EXAMPLE:

Given:

object.yaml

---
name: Lorem ipsum
desc: dolor sit amet

And the block:

[yaml2text,object.yaml,my_item]
----
{my_item.*,key,EOI}
=== {key}

{my_item[key]}

{EOI}
----

Where:

  • file path is object.yaml

  • current key within the enumerator is called key

  • {my_item[key]} gives the value of key key in the parent array my_item.

Will render as:

=== name

Lorem ipsum

=== desc

dolor sit amet

Moreover, the keys and values attributes can also be used in object enumerators.

EXAMPLE:

Given:

object.yaml

---
name: Lorem ipsum
desc: dolor sit amet

And the block:

[yaml2text,object.yaml,item]
----
.{item.values[1]}
[%noheader,cols="h,1"]
|===
{item.*,key,EOK}
| {key} | {item[key]}

{EOK}
|===
----

Where:

  • file path is object.yaml

  • current key within the enumerator is called key

  • {item[key]} gives the value of key key in the parent array item

  • item.values[1] gives the value located at the second key within item

Will render as:

.dolor sit amet

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

Advanced examples

With the syntax of enumerating arrays and objects we can now try more powerful examples.

Array of objects

EXAMPLE:

Given:

array_of_objects.yaml

---
- name: Lorem
  desc: ipsum
  nums: [2]
- name: dolor
  desc: sit
  nums: []
- name: amet
  desc: lorem
  nums: [2, 4, 6]

And the block:

[yaml2text,array_of_objects.yaml,ar]
----
{ar.*,item,EOF}

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

{item.nums.*,num,EON}
- {item.name}: {num}
{EON}

{EOF}
----

Notice we are now defining multiple contexts:

  • using different context names: ar, item, and num

  • delimited by different block markers: EOF, EON (self-defined, heredoc-esque markers)

Will render as:

Lorem:: ipsum

- Lorem: 2

dolor:: sit

amet:: lorem

- amet: 2
- amet: 4
- amet: 6

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

And the block:

[yaml2text,strings.yaml,yaml]
------
First item is {yaml.items[0]}.
Last item is {yaml.items[-1]}.

{yaml.items.*,s,EOS}
=== {s.#} -> {s.# + 1} {s} == {yaml.items[s.#]}

[source,ruby]
----
include::{yaml.prefix}{s.#}.rb[]
----

{EOS}
------

Will render as:

First item is lorem.
Last item is dolor.

=== 0 -> 1 lorem == lorem

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

=== 1 -> 2 ipsum == ipsum

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

=== 2 -> 3 dolor == dolor

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