Functionality
Metanorma plugin that allows you to access static data structures like JSON, YAML, XML from a Metanorma document
Installation
$ gem install metanorma-plugin-datastruct
Usage
In order to use the macros in Metanorma, add the gem gem 'metanorma-plugin-datastruct' in your Gemfile. Or have the gem installed, and Metanorma can use them automatically via the extension :plugin: datastruct.
Expressions
Currently, there are 2 plugins available: yaml2text
and json2text
. As states from the name, yaml2text
allows to load yaml file and use it in expressions, json2text
works with json files.
Macroses supports all Liquid syntax expressions, including:
-
variables, variable assignment
-
flow control (if/case)
-
filters
-
loops
See here for the full description of Liquid tags and expressions.
Defining the block
A yaml2text
(json2text
) 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(JSON) file path},{self-defined context name}]
----
this is content within the block!
----
Where:
-
content within the block is called the “template”;
-
{YAML(JSON) file path}
is the location of the YAML(JSON) file that contains data to be loaded. Location of the file is computed relative to the source directory that[yaml2text]
([json2text]
) 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 data read from the data file can be accessed with.
Interpolation
yaml2text
(json2text
) accepts string interpolation of the following forms:
-
{variable}
: as in AsciiDoc syntax; -
{{ variable }}
,{% if/else/for/case %}
: basic Liquid tags and expressions are supported.
The value within the curly braces will be interpolated by yaml2text
(json2text
).
Where:
-
In
{variable}
({{variable}}
),variable
is the name of the variable or AsciiDoc attribute. -
The location of
{variable}
({{variable}}
) in text will be replaced with the value ofvariable
. -
Evaluation order will be first from the defined context, then of the Metanorma 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.size}}
.
EXAMPLE:
Given:
strings.yaml
---
- lorem
- ipsum
- dolor
And the block:
[yaml2text,strings.yaml,data]
----
The length of the YAML array is {{data.size}}.
----
The file path is strings.yaml
, and context name is data
.
{{data.size}}
evaluates to the length of the array using liquid size
filter.
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:
{% for item in array_name %}
...content...
{% endfor %}
Where:
-
array_name
is the name of the existing context that contains array data -
item
is the current item within the array
Within an array enumerator, the following expressions can be used:
-
{{forloop.index0}}
gives the zero-based position of the itemitem_name
within the parent array -
{{forloop.length}}
returns the number of iterations of the loop. -
{{forloop.first}}
returnstrue
if it’s the first iteration of the for loop. Returnsfalse
if it is not the first iteration. -
{{forloop.last}}
returnstrue
if it’s the last iteration of the for loop. Returnsfalse
if it is not the last iteration. -
{{array_name.size}}
gives the length of the arrayarray_name
-
{{array_name[i]}}
provides the value at indexi
(zero-based: starts with0
) in the arrayarray_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]
----
{% for item in arr %}
=== {{forloop.index0}} {item}
This section is about {item}.
{endfor}
----
Where:
-
file path is
strings.yaml
-
current context within the enumerator is called
item
-
{{forloop.index0}}
gives the zero-based position of itemitem
in the parent arrayarr
.
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.size}}
.
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.size}}
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:
{% for item in object_name %}
{{item[0]}}, {{item[1]}}
{% endfor %}
Where:
-
object_name
is the name of the existing context that contains the object -
{{item[0]}}
contains the key of the current enumrated object -
{{item[1]}}
contains the value -
{% endfor %}
indicates where the object enumeration block ends
EXAMPLE:
Given:
object.yaml
---
name: Lorem ipsum
desc: dolor sit amet
And the block:
[yaml2text,object.yaml,my_item]
----
{% for item in my_item %}
=== {{item[0]}}
{{item[1]}}
{% endfor %}
----
Where:
-
file path is
object.yaml
-
current key within the enumerator is called
item[0]
-
{{item[0]}}
gives the key name in the current iteration -
{{item[1]}}
gives the value in the current iteration
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"]
|===
{% for elem in item %}
| {{elem[0]}} | {{elem[1]}}
{% endfor %}
|===
----
Where:
-
file path is
object.yaml
-
current key within the enumerator is called
key
-
{{item[1]}}
gives the value of key in the current iteration the parent arraymy_item
. -
{{item.values[1]}}
gives the value located at the second key withinitem
Will render as:
.dolor sit amet
[%noheader,cols="h,1"]
|===
| name | Lorem ipsum
| desc | dolor sit amet
|===
There are several optional arguments to the for tag that can influence which items you receive in your loop and what order they appear in:
-
limit:<INTEGER> lets you restrict how many items you get.
-
offset:<INTEGER> lets you start the collection with the nth item.
-
reversed iterates over the collection from last to first.
EXAMPLE:
Given:
strings.yaml
---
- lorem
- ipsum
- dolor
- sit
- amet
And the block:
[yaml2text,strings.yaml,items]
----
{% for elem in items limit:2 offset:2 %}
{{item}}
{% endfor %}
----
Where:
-
file path is
strings.yaml
-
limit
- how many items we should take from the array -
offset
- zero-based offset of item from which start the loop -
{{item}}
gives the value of item in the array
Will render as:
dolor sit
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]
----
{% for item in ar %}
{{item.name}}:: {{item.desc}}
{% for num in item.nums %}
- {{item.name}}: {{num}}
{% endfor %}
{% endfor %}
----
Notice we are now defining multiple contexts:
-
using different context names:
ar
,item
, andnum
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
(json2text
) 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.first}}.
Last item is {{yaml.items.last}}.
{% for s in yaml.items %}
=== {{forloop.index0}} -> {{forloop.index0 | plus: 1}} {{s}} == {{yaml.items[forloop.index0]}}
[source,ruby]
----
include::{{yaml.prefix}}{{forloop.index0}}.rb[]
----
{% endfor %}
------
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[]
----
Multiple contexts
Multiple contexts can be defined in a single block.
EXAMPLE:
Given:
strings1.yaml
---
foo: bar
dead: beef
strings2.yaml
---
hello: world
color: red
shape: square
And the block:
[yaml2text,data1=strings1.yaml,data2=strings2.yaml]
----
I'm heading to the {{data1.foo}} for {{data1.dead}}.
This is hello {{data2.hello}}.
The color is {{data2.color}} and the shape is {{data2.shape}}.
----
The file path is strings1.yaml
, and context name is data1
.
{{data1.foo}}
evaluates to the value of the key foo
in data1
.
The file path is strings2.yaml
, and context name is data2
.
{{data2.hello}}
evaluates to the value of the key hello
in data2
.
Will render as:
I'm heading to the bar for beef.
This is hello world.
The color is red and the shape is square.
Multiple contexts with mixed json files and yaml files (data2text)
Multiple contexts can be mixed with json files and yaml files by using
data2text
.
EXAMPLE:
Given:
strings1.json
{
"foo": "bar",
"dead": "beef"
}
strings2.yaml
---
hello: world
color: red
shape: square
And the block:
[data2text,my_json=strings1.json,my_yaml=strings2.yaml]
----
I'm heading to the {{my_json.foo}} for {{my_json.dead}}.
This is hello {{my_yaml.hello}}.
The color is {{my_yaml.color}} and the shape is {{my_yaml.shape}}.
----
The file path is strings1.json
, and context name is my_json
.
{{my_json.foo}}
evaluates to the value of the key foo
in my_json
.
The file path is strings2.yaml
, and context name is my_yaml
.
{{my_yaml.hello}}
evaluates to the value of the key hello
in my_yaml
.
Will render as:
I'm heading to the bar for beef.
This is hello world.
The color is red and the shape is square.
Content contains json or yaml filepaths
When the content of the loaded files contains filepaths of json files or
yaml files, you can load extra data by these filepaths by load_file
liquid
filter.
EXAMPLE:
Given:
strings1.json
{
"foo": "bar",
"paths": ["a.yaml", "b.yaml"]
}
Where:
-
paths
is an array of filepaths relative to the Metanorma document
a.yaml
---
shape: circle
color: red
b.yaml
---
shape: square
color: blue
corners: 4
And the block:
[data2text,my_context=strings1.json]
----
I'm heading to the {{my_context.foo}}.
{% for path in my_context.paths %}
{% assign foo = path | load_file %}
This is {{ foo[shape] }} with color {{ foo[color] }}.
{% endfor %}
----
Where:
-
load_file is a liquid filter that loads the file content
Will render as:
I'm heading to the bar.
This is circle with color red.
This is square with color blue.