Webinar Slide Example Webinar Slide Example

Today we're going to play with the templating module. It lets you render a document using a template. You can easily install it through our Marketplace. It comes with a bunch of examples to give you an idea of what's doable. You will also find information in our user guide.

A live example is the last Webinar slide deck. It was written in a Markdown Note document and rendered using a DeckJS based template.

If you want to try it now it's available in the nuxeo-template-rendering-deckjs bundle. Just copy the jar in the plugins folder and the template and its example should be automatically loaded. Be aware that the pdf conversion will only work if you have PhantomJS (version >= 1.8) installed on your server.

The template document is a regular WebTemplate. You need to import JavaScripts, StyleSheets, images, etc. as document attachments. Then where you usually have a path like:

<link rel="stylesheet" href="./core/deck.core.css">

You need to replace it by:

  <link rel="stylesheet" href="${jaxrs.getResourceUrl("deck.core.css")}">

This is how you load document attachments during rendering. We've also added a custom JavaScript file to do preprocessing of the Markdown content. It creates the different sections and transforms divs like:

<div class="picture" title="app-lifecycle.png">Application LifeCycle schema
</div>

Into this:

<div class="split-box split-37">
<img src="/nuxeo/site/templates/doc/e9b1cbeb-d0dc-450b-816f-de931f557ce9/resource/NuxeoWorld2K12HtmlSlides/long-road.jpg">
</div>

Here's our JavaScript preprocessor:

function buildSections(baseUrl) {
var titles = $("h1[id!='slideDeckTitle']");

for (var i = titles.length-1; i >= 0; i--) {
var t = $(titles[i]).html();
var tid = t.replace(new RegExp(" "), "-").toLowerCase();
var slide = $("<section></section>");
slide.attr("id", tid);
slide.addClass("slide");
slide.append("<h3>" + t + "</h3>");
var slideContent =$("<div></div>");
var subsections = $(titles[i]).nextUntil("h1");
for ( var j = 0; j < subsections.length; j++) {
if (subsections[j].id=='thank-you') {
break;
}
slideContent.append(subsections[j]);
}
slide.append(slideContent);
processSlideLayout(slideContent, slide, baseUrl);

$("#nuxeo-slides").after(slide);
}
$("h1[id!='slideDeckTitle']").remove();
}

function processSlideLayout(slideContent, parent, baseUrl) {

var pictures = slideContent.find("div.pictureInline");
if (pictures.length>=0) {
for ( var i = 0; i < pictures.length; i++) {
var img = $("<img/>");
img.attr("src",baseUrl + $(pictures[i]).attr("title"));
$(pictures[i]).append(img);
}
}

pictures = slideContent.find("div.picture");
var pSize = 37;
if (pictures.length==0) {
pictures = slideContent.find("div.pictureBig");
if (pictures.length==0) {
pictures = slideContent.find("div.pictureLarge");
if (pictures.length==0) {
return;
} else {
pSize=75;
}
} else {
pSize=50;
}
}

slideContent.addClass("split-box").addClass("split-63");
var picHolder =$('<div class="split-box"></div>');
if (pSize==50) {
slideContent.addClass("split-50");
picHolder.addClass("split-50");
}
else if (pSize==75) {
slideContent.addClass("split-25");
picHolder.addClass("split-75");
}
else {
slideContent.addClass("split-63");
picHolder.addClass("split-37");
}

for ( var i = 0; i < pictures.length; i++) {
var img = $("<img/>");
img.attr("src",baseUrl + $(pictures[i]).attr("title"));
picHolder.append(img);
$(pictures[i]).remove();
}
parent.append(picHolder);
}

function preProcessHtml(url) {
buildSections(url);
}


As you can see this script makes it mandatory to have a h1 tag or an element with id 'slideDeckTitle'. This is where the script will start its processing. It stops the processing once it finds a section with the 'thank-you' id.

Now that the template is setup, you can create a Note in Markdown, associate it to your template and see the results using the webview rendition or the render button from the template tab.

A small word about rendition vs. template rendering. This is not the same thing, even if the name is close. A rendition is :


  • a name

  • a label

  • a class implementing the RenditionProvider interface


So you can have rendition based on a simple converter, on an operation or even a template rendering. More details about rendition are available on the rendition module ReadMe.

Anyway here's a sample for our DeckJS template:

# 2013 Roadmap: Strategy

Extend the platform approach

  • Continue to improve the infrastructure
  • Manage the complete application life-cycle

Prepare Nuxeo Platform 6.0

  • Prepare an infrastructure update

<div class="picture" title="app-lifecycle.png">Application LifeCycle schema
</div>


But what you usually want to do when creating slides is to get a PDF version of it. So we need to convert that HTML file to a PDF. To do that you can select the output format available in the template configuration. Problem is the default PDF converter won't work on out DeckJS file. It won't use the JavaScript during the conversion. So you will end up with a weird one page PDF.

This means we have to use a custom output format. There's an extension point for this. You can specify a mime type and an operation chain id. If there is no operation chain specified, a simple convert operation is called using the mime type as parameter. If there is a chain, we simply call that operation chain.

<extension target="org.nuxeo.template.service.TemplateProcessorComponent" point="outputFormat">
<outputFormat id="pdf" label="PDF" mimetype="application/pdf"/>
</extension>

So we can add a custom operation chain that calls a PhantomJS based converter. PhantomJS is a headless WebKit with JavaScript API. We made a little script that removes some CSS class that we don't need in our PDF, then take a PNG screencap of each slide and put them together as a single PDF file.

<extension target="org.nuxeo.template.service.TemplateProcessorComponent"
point="outputFormat">
<outputFormat id="deckJsToPDF" label="PDF (from DeckJS)"
chainId="deckJs2PDF" mimetype="application/pdf" />
</extension>

The chain simply call the Blob.DeckJSToPDF operation. It's not a simple conversion, there is a little trick. When you get the rendered HTML file, all the resources are relative to the server, like this:

<link rel="stylesheet" href="/nuxeo/site/templates/doc/9b58adaf-68bc-4cda-ae6b-2503c05b610b/resource/NuxeoWorld2K12HtmlSlides/deck.core.css">

So PhantomJS has to go through usual Nuxeo authentication. To avoid that, we can start by removing the parent path, leaving only the filename. This way PhantomJS will be looking for the resources as local files instead of trying to fetch them on the server. Once this is done, we need to make those resources available locally. It means we need to write all doc attachments from the template document and the rendered document to the filesystem. Here's the operation that does this work:

/*

  • (C) Copyright 2006-2012 Nuxeo SA (http://nuxeo.com/) and contributors.
    *
  • All rights reserved. This program and the accompanying materials
  • are made available under the terms of the GNU Lesser General Public License
  • (LGPL) version 2.1 which accompanies this distribution, and is available at
  • http://www.gnu.org/licenses/lgpl.html
    *
  • This library is distributed in the hope that it will be useful,
  • but WITHOUT ANY WARRANTY; without even the implied warranty of
  • MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
  • Lesser General Public License for more details.
    *
  • Contributors:
  • ldoguin
    */
    package org.nuxeo.template;

import java.io.File;
import java.io.FileWriter;
import java.io.IOException;

import org.apache.commons.io.FileUtils;
import org.apache.commons.io.IOUtils;
import org.nuxeo.ecm.automation.OperationContext;
import org.nuxeo.ecm.automation.core.Constants;
import org.nuxeo.ecm.automation.core.annotations.Context;
import org.nuxeo.ecm.automation.core.annotations.Operation;
import org.nuxeo.ecm.automation.core.annotations.OperationMethod;
import org.nuxeo.ecm.core.api.Blob;
import org.nuxeo.ecm.core.api.DocumentModel;
import org.nuxeo.ecm.core.api.blobholder.BlobHolder;
import org.nuxeo.ecm.core.api.impl.blob.FileBlob;
import org.nuxeo.ecm.core.convert.api.ConversionService;
import org.nuxeo.ecm.core.convert.cache.SimpleCachableBlobHolder;
import org.nuxeo.template.jaxrs.context.JAXRSExtensions;

@Operation(id = DeckJSPDFOperation.ID, category = Constants.CAT_CONVERSION, label = "Convert a deckJS slide to a pdf", description = "Convert a deckJS slide to a pdf.")
public class DeckJSPDFOperation {

public static final String ID = "Blob.DeckJSToPDF";

@Context
OperationContext ctx;

@Context
ConversionService conversionService;

@OperationMethod
public Blob run(Blob blob) throws Exception {
DocumentModel templateSourceDocument = (DocumentModel) ctx.get("templateSourceDocument");
DocumentModel templateBasedDocument = (DocumentModel) ctx.get("templateBasedDocument");
String templateName = (String) ctx.get("templateName");

String workingDirPath = System.getProperty("java.io.tmpdir")

  • "/nuxeo-deckJS-cache/" + templateBasedDocument.getId();
    File workingDir = new File(workingDirPath);
    if (!workingDir.exists()) {
    workingDir.mkdirs();
    }
    JAXRSExtensions jaxRsExtensions = new JAXRSExtensions(
    templateBasedDocument, null, templateName);
    BlobHolder sourceBh = templateSourceDocument.getAdapter(BlobHolder.class);
    for (Blob b : sourceBh.getBlobs()) {
    writeToTempDirectory(workingDir, b);
    }
    BlobHolder templatebasedBh = templateBasedDocument.getAdapter(BlobHolder.class);
    for (Blob b : templatebasedBh.getBlobs()) {
    writeToTempDirectory(workingDir, b);
    }

String content = blob.getString();
String resourcePath = jaxRsExtensions.getResourceUrl("");
content = content.replaceAll(resourcePath, "./");
File index = new File(workingDir, blob.getFilename());
FileWriter fw = new FileWriter(index);
IOUtils.write(content, fw);
fw.flush();
fw.close();

FileBlob indexBlob = new FileBlob(index);
indexBlob.setFilename(blob.getFilename());
BlobHolder bh = conversionService.convert("deckJSToPDF",
new SimpleCachableBlobHolder(indexBlob), null);
FileUtils.deleteDirectory(workingDir);
return bh.getBlob();
}

private void writeToTempDirectory(File workingDir, Blob b) throws IOException {
File f = new File(workingDir, b.getFilename());
File parentFile = f.getParentFile();
parentFile.mkdirs();
b.transferTo(f);
}
}


Then, once the resources are written to the filesystem, we can call the converter based on PhantomJS. The code is available on GitHub if you want to check it out.