Philosophy

CarboneJS is a powerfull templating engine which works in all XML-based document.

It can also convert your report to reliable PDFs and any other document formats.

It uses a simple declarative programming language, which is not intrusive. You don't need to insert control flow code in your document such as if or for-loop markers.

In computer science, declarative programming is a programming paradigm, a style of building the structure and elements of computer programs, that expresses the logic of a computation without describing its control flow.[1] Many languages applying this style attempt to minimize or eliminate side effects by describing what the program should accomplish in terms of the problem domain, rather than describing how to go about accomplishing it as a sequence of the programming language primitives[2] (the how being left up to the language's implementation). This is in contrast with imperative programming, in which algorithms are implemented in terms of explicit steps. source

This way of programming provides more flexibility for travelling data. You can transform your orginal JSON array directly in the template. See examples:

Moreover, the system is XML-agnostic. It means the template engine works on any valid XML-based documents, not only XML-documents created by Microsoft Office™, LibreOffice™ or OpenOffice™. CarboneJS finds automatically repetition patterns in your template.

How it works?

CarboneJS finds all markers {} in your document (xls, odt, docx, ...) and replaces these markers by datas. According to the syntax of your markers, you can make a lot of operations including

  • Replacing a field
  • Formatting data
  • Repeating a document portion (a table row or anything else)
  • Looping on unlimited nested arrays
  • Conditionally displaying a data based on a test expression

The syntax is easy to learn. It is like using a JSON Array or Object in Javascript.

Combined with features of LibreOffice™ or MS Office™, you can easily create documents with:

  • Graphics
  • Headers, footers
  • Automatically repeated table header across pages
  • Insert computed field
  • Page count
  • ...

How to read this documentation?

This documentation is made for report designers. There are five parts sorted by level of complexity.

Substitutions

Each part of this documentation shows exactly what happens if you send Data (JSON Object) and a Template (handmade docx, odt, ...) to CarboneJS.

Basic

This is the most basic example. Do I need to explain it?

Data

{
  "firstname" : "John",
  "lastname"  : "Doe"
}

Template

Hello {d.firstname} {d.lastname} !

Result

Hello John Doe !

Accessing sub-objects

Of course, if your data contains sub-objects, you can access them using the Dot Notation to go deeper in the object tree.

Data

{
  "firstname" : "John",
  "type"      : {
    "id"   : 1,
    "name" : "human"
  }
}

Template

{d.firstname} is a {d.type.name}

Result

John is a human

Accessing arrays

When data is an array of objects, you can access directly each object using the reserved word i, which represents the ith item of the array. CarboneJS uses zero-based arrays. The first item of an array is i=0.

An array is reachable using the Square Bracket Notation [].

Data

[
  { "movie" : "Inception"          },
  { "movie" : "Matrix"             },
  { "movie" : "Back to the future" }
]

Template

My preferred movie is {d[i=1].movie}

Result

My preferred movie is Matrix

Array filters

Instead of using the reserved word i, CarboneJS accepts filters using attributes of objects.

If the filter returns many rows, CarboneJS keeps only the first occurence.

Data

[
  { "name" : "Inception" , "year" : 2010 },
  { "name" : "Matrix"    , "year" : 1999 },
  { "name" : "BTTF"      , "year" : 1985 }
]

Template

The movie of 1999 was {d[year=1999].name}

Result

The movie of 1999 was Matrix

Filter and print parent

Access properties of the parent object with two points .. (or more) when you need to print a parent propertiy using filters in nested arrays:

Data

{
  "country" : "USA",
  "movies" : [
    { "name" : "Inception" , "year" : 2010 },
    { "name" : "Matrix"    , "year" : 1999 },
    { "name" : "BTTF"      , "year" : 1985 }
  ]
}

Template

{d.movies[year=1999]..country}

Result

USA

Multiple array filters

Multiple filters are accepted, even using a sub-object (depth-limited, a filter cannot go deeper in the object tree for the moment).

If the filter returns many rows, CarboneJS keeps only the first occurence.

Data

[
  { "name" : "Matrix"         , "year" : 1999, "meta" : { "type" : "SF"    } },
  { "name" : "The Green Mile" , "year" : 1999, "meta" : { "type" : "Drama" } }
]

Template

The movie of 1999 was {d[year=1999, meta.type="SF"].name}

Result

The movie of 1999 was Matrix

Alias

Aliases can be used to simplify the maintenance of your report avoiding code repetition or to insert markers somewhere in the document where special characters like square brackets are not allowed (worksheet names for exemple).

  • Write {#... = ...} to define an alias
  • Write {$...} to use this alias

Aliases can be defined anywhere in the document, at the end, or at the beginning. They are parsed and removed by CarboneJS before rendering.

Data

{
  "name" : "Cars",
  "wheels" : 4   
}

Template

{#myAlias = d.wheels}

{d.name} need {$myAlias} wheels!

Result

Cars need 4 wheels!

Parametrized Alias

Aliases accept unlimited number of parameters between parentheses, like a function in many languages. Each parameter must start by $.

Data

[
  { "name" : 'chicken', "weekday" : 1 },
  { "name" : 'fish'   , "weekday" : 2 }
]

Template

{#mealOf($weekday) = d[weekday = $weekday].name}

Tuesday, we eat {$mealOf(2)}.

Result

Tuesday, we eat fish.

Repetitions

CarboneJS can repeat a section (rows, title, pages...) of the document.

We don't need to describe where the repetition starts and ends, we just need to design a "repetition example" in the template using the reserved key word i and i+1. CarboneJS will find automatically the pattern to repeat using the first row (i) as an example. The second row (i+1) is removed before rendering the result.

Simple array

In this example, we want to travel an array of cars.

Data

{
  "cars" : [
    {"brand" : "Lumeneo"},
    {"brand" : "Tesla"  },
    {"brand" : "Toyota" }
  ]
}

Template

Cars
{d.cars[i].brand}
{d.cars[i+1].brand}

Result

Cars
Lumeneo
Tesla
Toyota

Nested arrays

Carbone manages nested arrays (unlimited depth). Here is an example where a whole portion of a document is repeated.

Data

[
  {
    "brand" : "Toyota",
    "models" : [ 
      {"size" : "Prius 2" },
      {"size" : "Prius 3" }
    ]
  },
  {
    "brand" : "Tesla",
    "models" : [ 
      {"size" : "S" }, 
      {"size" : "X" }
    ]
  }
]

Template

{d[i].brand}

Models
{d[i].models[i].size}
{d[i].models[i+1].size }

{d[i+1].brand}

Result

Toyota

Models
Prius 2
Prius 3

Tesla

Models
S
X

As you may notice, it is useless to repeat completely the first paragraph twice in the template, only the title of the second paragraph is necessary to help CarboneJS detect where the repetition pattern of the main array ends {d.cars[i+1].brand}.

Bi-directionnal loop

Data

[
  {
    "brand" : "Toyota",
    "models": [ 
      {"name" : "Prius 2" }, 
      {"name" : "Prius 3" }
    ]
  },
  {
    "brand" : "Tesla",
    "models" : [ 
      {"name" : "S" }, 
      {"name" : "X" }
    ]
  },
  {
    "brand" : "Lumeneo",
    "models" : [ 
      {"name" : "Smera" }, 
      {"name" : "Néoma" }
    ]
  }
]

Template

{d[i].models[i].brand} {d[i+1].models[i].brand}
{d[i].models[i].name} {d[i+1].models[i].name}
{d[i].models[i+1].name } {d[i+1].models[i+1].name }

Result

Toyota Tesla Lumeneo
Prius 2 S Smera
Prius 3 X Néoma

Sorting

CarboneJS allows to use attributes of objects, instead of the reserved iterator i to iterate through arrays. It can be used to sort data directly in the template.

In this example, all cars are sorted by "power" in ascendant order (descendant order is not possible for the moment).

Data

{
  "cars" : [
    { "brand" : "Lumeneo" , "power" : 3 },
    { "brand" : "Tesla"   , "power" : 1 },
    { "brand" : "Toyota"  , "power" : 2 }
  ]
}

Template

Cars
{d.cars[power].brand}
{d.cars[power+1].brand}

Result

Cars
Tesla
Toyota
Lumeneo

Distinct items

A custom iterator can be used to select distinct rows according to the attribute's value.

Data

[
  { "type" : "car"   , "brand" : "Tesla"   },
  { "type" : "plane" , "brand" : "Airbus"  },
  { "type" : "plane" , "brand" : "Boeing"  },
  { "type" : "car"   , "brand" : "Toyota"  }
]

Template

Vehicles
{d[type].brand}
{d[type+1].brand}

Result

Vehicles
car
plane

Iterate on multiple items

In the previous example, only distinct rows were kept. If we want to keep all rows, and sort them by type in ascendant order, we can add the reserved iterator i. CarboneJS accepts multiple iterators separated by a comma.

Data

[
  { "type" : "car"   , "brand" : "Tesla"   },
  { "type" : "plane" , "brand" : "Airbus"  },
  { "type" : "plane" , "brand" : "Boeing"  },
  { "type" : "car"   , "brand" : "Toyota"  }
]

Template

Cars
{d[type , i ].brand}
{d[type+1, i+1].brand}

Result

Vehicles
car
car
plane
plane

Filters

You can use conditional operators to filter arrays: > , < , >= , <= , =.

Filters should be the same on the ith+1 marker and all other markers if you want to keep only matching rows. Use Alias to simplify report maintenance.

Data

[
  {"name" : "John"   , "age" : 20 },
  {"name" : "Eva"    , "age" : 18 },
  {"name" : "Bob"    , "age" : 25 },
  {"name" : "Charly" , "age" : 30 }
]

Template

People
{d[i , age > 19, age < 30].name}
{d[i+1, age > 19, age < 30].name}

Result

People
John
Bob

Formatters

Formatters can be used to translate raw data into human readable text.

A formatter is applied on data using the separator :. Multiple formatters can be used one after another, each formatter input is the output of the previous one. Some formatters accept parameters.

Here is an example showing how to write "John" instead of "JOHN" using two chained formatters, and how to translate a raw ISO date into a human readable date.

Next chapters list all existing formatters by input data type.

Data

{
  "name"     : "JOHN",
  "birthday" : "2000-01-31"
}

Template

My name is {d.name:lowerCase:ucFirst}.

I was born on {d.birthday:convDate(YYYY-MM-DD, LL)}.

Result

My name is JOHN.

I was born on Monday 31th 2000.

String manipulation

lowerCase( )

Lower case all letters

Examples

"My Car":lowerCase() // "my car"
"my car":lowerCase() // "my car"
null:lowerCase() // null
1203:lowerCase() // 1203

upperCase( )

Upper case all letters

Examples

"My Car":upperCase() // "MY CAR"
"my car":upperCase() // "MY CAR"
null:upperCase() // null
1203:upperCase() // 1203

ucFirst( )

Upper case first letter

Examples

"My Car":ucFirst() // "My Car"
"my car":ucFirst() // "My car"
null:ucFirst() // null
undefined:ucFirst() // undefined
1203:ucFirst() // 1203

ucWords( )

Upper case the first letter of all words

Examples

"my car":ucWords() // "My Car"
"My cAR":ucWords() // "My CAR"
null:ucWords() // null
undefined:ucWords() // undefined
1203:ucWords() // 1203

Always return the same message if called (sort of "catch all" formatter)

params desciption type
message

text to print

String

Examples

"My Car":print("hello!") // "hello!"
"my car":print("hello!") // "hello!"
null:print("hello!") // "hello!"
1203:print("hello!") // "hello!"

convEnum( type )

Convert user-defined enums to human readable values

User-defined enums must be passed in options of carbone.render.

params desciption type
type

enum name passed in options of carbone.render(data, options)

String

Examples

// with options = {
//   "enum": {
//     "ORDER_STATUS": [
//       "pending",
//       "sent",
//       "delivered"
//     ]
//   }
// }
0:convEnum("ORDER_STATUS") // "pending"
1:convEnum("ORDER_STATUS") // "sent"
5:convEnum("ORDER_STATUS") // 5
// with options = {
//   "enum": {
//     "YES_NO": {
//       "true": "Yes",
//       "false": "No"
//     }
//   }
// }
false:convEnum("YES_NO") // "No"
true:convEnum("YES_NO") // "Yes"
null:convEnum("YES_NO") // null
3:convEnum("UNKNOWN_ENUM") // 3

Array manipulation

arrayJoin( separator )

Flatten an array of String or Number

params desciption type
separator

[optional] item separator (, by default)

String

Examples

["homer","bart","lisa"]:arrayJoin() // "homer, bart, lisa"
["homer","bart","lisa"]:arrayJoin(" | ") // "homer | bart | lisa"
["homer","bart","lisa"]:arrayJoin("") // "homerbartlisa"
[10,50]:arrayJoin() // "10, 50"
[]:arrayJoin() // ""
null:arrayJoin() // null
{}:arrayJoin() // {}
20:arrayJoin() // 20
undefined:arrayJoin() // undefined

arrayMap( objSeparator, attributeSeparator, attributes )

Flatten an array of objects?
It ignores nested objects and arrays

params desciption type
objSeparator

[optional] object separator (, by default)

String
attributeSeparator

[optional] attribute separator (: by default)

String
attributes

[optional] list of object's attributes to print

String

Examples

[{"id":2,"name":"homer"},{"id":3,"name":"bart"}]:arrayMap() // "2:homer, 3:bart"
[{"id":2,"name":"homer"},{"id":3,"name":"bart"}]:arrayMap(" - ") // "2:homer - 3:bart"
[{"id":2,"name":"homer"},{"id":3,"name":"bart"}]:arrayMap(" ; ", "|") // "2|homer ; 3|bart"
[{"id":2,"name":"homer"},{"id":3,"name":"bart"}]:arrayMap(" ; ", "|", "id") // "2 ; 3"
[{"id":2,"name":"homer","obj":{"id":20},"arr":[12,23]}]:arrayMap() // "2:homer"
["homer","bart","lisa"]:arrayMap() // "h:o:m:e:r, b:a:r:t, l:i:s:a"
[10,50]:arrayMap() // ", "
[]:arrayMap() // ""
null:arrayMap() // null
{}:arrayMap() // {}
20:arrayMap() // 20
undefined:arrayMap() // undefined

Conditioned output

By default, condition formatters have a special behavior, the formatter's result is not propagated to the next formatter if the result of the condition is true. This enables test chaining

You can change this behavior by setting the optional parameter continueOnSuccess. If continueOnSuccess=true, the result of the formatter will be passed to the next formatter like in the last line of the example below.

Data

{
  "status1" : 1,
  "status2" : 2,
  "status3" : 3
}

Template

one = { d.status1:ifEqual(2, "two"):ifEqual(1, "one"):print("unknown") }

two = { d.status2:ifEqual(2, "two"):ifEqual(1, "one"):print("unknown") }

three = { d.status3:ifEqual(2, "two"):ifEqual(1, "one"):print("unknown") }

two = { d.status2:ifEqual(2, "two", true):ifEqual("two", "second") }

Result

one = "one"

two = "two"

three = "unknown"

two = "second"

ifEmpty( message, continueOnSuccess )

Test if data is empty (null, undefined, [], {}, ...)

params desciption type
message

message to print if JSON data is empty

String
continueOnSuccess

[optional], if true, next formatter will be called even if the condition is true

Boolean

Examples

null:ifEmpty("D'oh!") // "D'oh!"
[]:ifEmpty("D'oh!") // "D'oh!"
{}:ifEmpty("D'oh!") // "D'oh!"
"":ifEmpty("D'oh!") // "D'oh!"
0:ifEmpty("D'oh!") // 0
"homer":ifEmpty("D'oh!") // "homer"
[23]:ifEmpty("D'oh!") // [23]
{"id":3}:ifEmpty("D'oh!") // {"id":3}

ifEqual( value, messageIfTrue, continueOnSuccess )

Test if a value equals a variable

params desciption type
value

value to test

String, Integer, Boolean
messageIfTrue

message to print if the value equals JSON data

String
continueOnSuccess

[optional], if true, next formatter will be called even if the condition is true

Boolean

Examples

100:ifEqual(100, "bingo") // "bingo"
100:ifEqual(101, "bingo") // 100
"homer":ifEqual("homer", "bingo") // "bingo"
"homer":ifEqual("bart", "bingo") // "homer"
"":ifEqual("", "bingo") // "bingo"
null:ifEqual(100, "bingo") // null
null:ifEqual(null, "bingo") // "bingo"
0:ifEqual(100, "bingo") // 0

ifContain( value, messageIfTrue, continueOnSuccess )

Test if a string or an array contains a value

params desciption type
value

value to search

String, Integer, Boolean
messageIfTrue

message to print if JSON data contains the value

String
continueOnSuccess

[optional], if true, next formatter will be called even if the condition is true

Boolean

Examples

"your beautiful eyes":ifContain("beauti", "bingo") // "bingo"
"your beautiful eyes":ifContain("leg", "bingo") // "your beautiful eyes"
"your beautiful eyes":ifContain("eyes", "bingo") // "bingo"
"":ifContain("eyes", "bingo") // ""
"your beautiful eyes":ifContain("", "bingo") // "bingo"
[100,120,20]:ifContain(120, "bingo") // "bingo"
[100,120,20]:ifContain(99, "bingo") // [100,120,20]
["your","eyes"]:ifContain("eyes", "bingo") // "bingo"
[]:ifContain("eyes", "bingo") // []

Date manipulation

Carbone uses MomentJS to convert date. List of all tokens are described here.

convDate( patternIn, patternOut )

Format dates

params desciption type
patternIn

input format

String
patternOut

output format

String

Examples

// with options = {
//   "lang": "en"
// }
"20160131":convDate("YYYYMMDD", "L") // "01/31/2016"
"20160131":convDate("YYYYMMDD", "LL") // "January 31, 2016"
"20160131":convDate("YYYYMMDD", "LLLL") // "Sunday, January 31, 2016 12:00 AM"
"20160131":convDate("YYYYMMDD", "dddd") // "Sunday"
1410715640:convDate("X", "LLLL") // "Sunday, September 14, 2014 7:27 PM"
// with options = {
//   "lang": "fr"
// }
"20160131":convDate("YYYYMMDD", "LLLL") // "dimanche 31 janvier 2016 00:00"
"20160131":convDate("YYYYMMDD", "dddd") // "dimanche"

Token list of date formatter

Token Output
Month M 1 2 ... 11 12
Mo 1st 2nd ... 11th 12th
MM 01 02 ... 11 12
MMM Jan Feb ... Nov Dec
MMMM January February ... November December
Quarter Q 1 2 3 4
Qo 1st 2nd 3rd 4th
Day of Month D 1 2 ... 30 31
Do 1st 2nd ... 30th 31st
DD 01 02 ... 30 31
Day of Year DDD 1 2 ... 364 365
DDDo 1st 2nd ... 364th 365th
DDDD 001 002 ... 364 365
Day of Week d 0 1 ... 5 6
do 0th 1st ... 5th 6th
dd Su Mo ... Fr Sa
ddd Sun Mon ... Fri Sat
dddd Sunday Monday ... Friday Saturday
Day of Week (Locale) e 0 1 ... 5 6
Day of Week (ISO) E 1 2 ... 6 7
Week of Year w 1 2 ... 52 53
wo 1st 2nd ... 52nd 53rd
ww 01 02 ... 52 53
Week of Year (ISO) W 1 2 ... 52 53
Wo 1st 2nd ... 52nd 53rd
WW 01 02 ... 52 53
Year YY 70 71 ... 29 30
YYYY 1970 1971 ... 2029 2030
Y 1970 1971 ... 9999 +10000 +10001
Note: This complies with the ISO 8601 standard for dates past the year 9999
Week Year gg 70 71 ... 29 30
gggg 1970 1971 ... 2029 2030
Week Year (ISO) GG 70 71 ... 29 30
GGGG 1970 1971 ... 2029 2030
AM/PM A AM PM
a am pm
Hour H 0 1 ... 22 23
HH 00 01 ... 22 23
h 1 2 ... 11 12
hh 01 02 ... 11 12
k 1 2 ... 23 24
kk 01 02 ... 23 24
Minute m 0 1 ... 58 59
mm 00 01 ... 58 59
Second s 0 1 ... 58 59
ss 00 01 ... 58 59
Fractional Second S 0 1 ... 8 9
SS 00 01 ... 98 99
SSS 000 001 ... 998 999
SSSS ... SSSSSSSSS 000[0..] 001[0..] ... 998[0..] 999[0..]
Z -07:00 -06:00 ... +06:00 +07:00
ZZ -0700 -0600 ... +0600 +0700
Unix Timestamp X 1360013296
Unix Millisecond Timestamp x 1360013296123

Localized formats

Because preferred formatting differs based on language, there are a few tokens that can be used to format a date based on report language.

There are upper and lower case variations on the same formats. The lowercase version is intended to be the shortened version of its uppercase counterpart.

Time LT 8:30 PM
Time with seconds LTS 8:30:25 PM
Month numeral, day of month, year L 09/04/1986
l 9/4/1986
Month name, day of month, year LL September 4 1986
ll Sep 4 1986
Month name, day of month, year, time LLL September 4 1986 8:30 PM
lll Sep 4 1986 8:30 PM
Month name, day of month, day of week, year, time LLLL Thursday, September 4 1986 8:30 PM
llll Thu, Sep 4 1986 8:30 PM

Source: http://momentjs.com/docs/#/displaying/format/

Translations

Special markers {t( )} can be used to create multi-language reports.

Carbone parses all your template and generate a JSON lang file. Then, change the lang of your report when generating it with option variable.

Data

options = {
  lang : 'fr'
};

Template

{t('meeting')}

{t('apples')}

Result

rendez-vous

pommes

Real Examples