An Asciidoctor converter that generates the HTML component of a Bespoke.js presentation from AsciiDoc.
The goal of asciidoctor-bespoke is to enable you to craft HTML-based presentations from reusable content while avoiding the tedium of writing HTML. This library satisfies that goal by providing a converter that generates the HTML component of a Bespoke.js presentation from an AsciiDoc document. In other words, it allows you to use AsciiDoc in place of HTML (or an HTML template language like Jade) in your Bespoke.js project. (You still need to add an ample amount of CSS in order to achieve the presentation style you want).
The converter works in tandem with a typical JavaScript project structure based on npm and Gulp. npm is used to manage dependencies. A Gulp build is used to combine and “browserify” the JavaScript, compile the CSS, execute this converter (to convert AsciiDoc to HTML), launch the preview server and publish the presentation files.
The converter is implemented as a collection of Slim templates, which are packaged for your convenience as an Asciidoctor converter. The templates come into play when you want to customize the HTML the converter generates.
This guide explains how to integrate the asciidoctor-bespoke converter into an existing Bespoke.js presentation project and how to write slides in AsciiDoc.
In order to use asciidoctor-bespoke, you must satisfy the prerequisites of both Bespoke.js and Asciidoctor. You also need a Bespoke.js project.
Naturally, you’ll also need a Bespoke.js project, just as you would for any Bespoke.js presentation. If you don’t yet have a Bespoke.js project, you can clone the provided starter project:
$ git clone https://github.com/opendevise/presentation-bespoke-starter
Alternatively, you can use the Yeoman generator for Bespoke.js to initialize your project. As a word of warning, that generator has become substantially out of date. In the future, we plan to provide an updated Yeoman generator that incorporates asciidoctor-bespoke into a new Bespoke.js project for you.
Once you’ve initialized your Bespoke.js project, the next task is to replace Jade with AsciiDoc.
💡
|
If you’re creating a new project using the starter project previously mentioned, you can switch to the asciidoc branch in that repository to skip past the steps in this section and jump ahead to Creating Slides in AsciiDoc. If you’re curious, you can review a diff that contains the changes this section goes on to cover. |
The first step is to configure Bundler to fetch and install the required gems. Create a file named Gemfile at the root of the project and populate it with the following content:
source 'https://rubygems.org'
gem 'asciidoctor-bespoke', '1.0.0.alpha.1'
# To use the latest version from git, use the following line instead:
#gem 'asciidoctor-bespoke', github: 'asciidoctor/asciidoctor-bespoke'
Next, run bundle
from the root of the project to install the gems and any dependencies they have:
$ bundle
💡
|
If you want to install the gems inside the project, you can pass the $ bundle --path=.bundle/gems The |
The next step is to get the converter to generate the HTML from AsciiDoc when the presentation build runs. We’ll repurpose the task that currently generates HTML from Jade for this purpose.
Open package.json and add the following entries to the devDependencies
section:
"gulp-chmod": "^1.3.0",
"gulp-exec": "^2.1.2",
Save the file and run npm i
to install the new packages into your project:
$ npm i
Open gulpfile.js and add the following entries to the list of require
calls at the top of the file:
chmod = require('gulp-chmod'),
exec = require('gulp-exec'),
Also in gulpfile.js, replace the existing html
task with the one below:
gulp.task('html', ['clean:html'], function() {
return gulp.src('src/index.adoc')
.pipe(isDist ? through() : plumber())
.pipe(exec('bundle exec asciidoctor-bespoke -o - src/index.adoc', { pipeStdout: true }))
.pipe(exec.reporter({ stdout: false }))
.pipe(rename('index.html'))
.pipe(chmod(644))
.pipe(gulp.dest('public'))
.pipe(connect.reload());
});
Finally, to get the build to watch the AsciiDoc file(s) for changes, look for the following line in the watch task in gulpfile.js:
gulp.watch('src/**/*.pug', ['html']);
and replace it with:
gulp.watch('src/**/*.adoc', ['html']);
The build is now ready! Before we can use our new task, we need to create slide content in AsciiDoc.
Writing AsciiDoc to create slides is pretty much the same as writing AsciiDoc for any another purpose. There are two key differences. You’ll be writing a lot less content and you only need to use a single level of section headings (plus an optional document title).
Below is a basic presentation that is comprised of two slides, the title slide and one content slide. To add this presentation to your project, create the file src/index.adoc and populate it with the following content:
= My Awesome Presentation
:!sectids:
== First Topic
Believe it or not, that’s all it takes to make a presentation!
Here’s a close approximation of the HTML the converter generates from the example shown above (formatted for clarity).
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>My Awesome Presentation</title>
<meta name="mobile-web-app-capable" content="yes">
<link rel="stylesheet" href="build/build.css">
</head>
<body>
<article class="deck">
<section class="title">
<h1>My Awesome Presentation</h1>
</section>
<section>
<h2>First Topic</h2>
</section>
</article>
<script src="build/build.js"></script>
</body>
</html>
There are a few things you should notice:
-
Each slide is represented as a
<section>
, which is generated per section title.-
At runtime, Bespoke.js adds additional classes to each
<section>
, includingbespoke-slide
.
-
-
The title slide has the class
title
and uses an<h1>
heading. -
The section title for each content slide gets put in an
<h2>
heading. -
The presentation is wrapped in an
<article>
element with the classdeck
.-
At runtime, Bespoke.js adds additional classes to
<article>
, includingbespoke-parent
.
-
-
CSS is used to accomplish most of the styling and layout, so you’ll need to spend some time on it.
-
The JavaScript and CSS to power the Bespoke.js presentation are loaded from the build/ folder.
Of course, this is not a very interesting presentation, so let’s dig a bit deeper.
💡
|
To see a complete example of a corporate-style presentation, check out the AsciiDoc source of the Bespoke.js Emulating Shower demo. |
By default, the converter automatically creates a title slide from the document header and, if present, the preamble.
The document title (i.e., doctitle) becomes an <h1>
heading.
The slide then incorporates additional information from the following attributes and nodes (subject to change):
-
firstname (derived from the author attribute)
-
lastname (derived from the author attribute)
-
email (can be a URL)
-
position
-
organization
-
twitter
-
avatar (an image path relative to imagesdir)
-
preamble content
📎
|
The title slide is a built-in transform mapped to the slide_title.html.slim template, which you can override. See Custom Transforms for information about where to put this file and how to load it. You’ll need to incorporate CSS (optionally using the Stylus syntax) to arrange and style the title page. |
Here’s an example of an AsciiDoc document that generates a title slide that is fully populated:
= My Awesome Presentation
Author Name <http://example.com>
:organization: ACME Inc.
:position: Developer Advocate
:twitter: @asciidoctor
:avatar: author-avatar.png
:!sectids:
Additional content for the title slide.
== First Topic
If you don’t want the title slide to be created, add the noheader
attribute to the document header.
= My Awesome Presentation
:!sectids:
:noheader:
== First Topic
Another option is to simply leave out the document header altogether.
Each content slide is created from a level-1 section title.
The section title becomes an <h2>
heading.
The remainder of the content in the section is placed below this heading.
📎
|
Any section levels below level-1 will simply be used as content in the slide. |
Here’s an example of a typical content slide with a heading:
== Agenda
* Lesson
* Demo
* Discussion
While many of your slides may have a primary heading—perhaps as the only content on the slide—there are many slide types that don’t require a heading.
You can mark a slide without a heading by using !
as the section title.
Here’s an example:
== !
image::chart.svg[]
If you still want to assign a title to a slide, but not show it, you can add the option named conceal
.
[%conceal]
= An Amazing Chart
image::chart.svg[]
A shorthand for the conceal option is to prefix the section title with a !
.
= !An Amazing Chart
image::chart.svg[]
You can also add a named hash to a slide so you get a URL like /#intro
instead of /#3
.
[#intro]
= Intro
Notice how we’re keeping the concerns of content and presentation cleanly separated. Using very little AsciiDoc, you’re able to describe a lot of different functionality. There doesn’t even have to be a direct, literal mapping between the AsciiDoc and the HTML. Instead, you should think of the AsciiDoc as a DSL for content.
The converter includes an experimental speaker slide, which you can place anywhere in the presentation.
To activate the speaker slide, create a section with an optional title and add the transform=speaker
attribute.
[transform=speaker]
== Speaker
The speaker slide currently incorporates the following attributes:
-
author
-
position
-
avatar (resolved relative to
imagesdir
) -
twitter
-
email
-
section content (if any)
📎
|
The speaker slide is a built-in transform mapped to the slide_speaker.html.slim template, which you can override. See Custom Transforms for information about where to put this file and how to load it. |
Here’s a rough approximation of the HTML generated for the speaker slide:
<section class="speaker">
<header>
<h2>Speaker Name</h2>
<h3>Title</h3>
</header>
<figure class="image headshot">
<img src="images/speaker-name.jpg" alt="Speaker Name">
</figure>
<p class="contact">@speaker | [email protected]</p>
</section>
🔥
|
The speaker slide is labeled as “experimental” because the HTML (content and layout) is likely to change as we learn the best way to organize the information. |
One of the most common ways to control the rate at which content is shown in a presentation is to use builds. A build is a presentation technique in which fragments of content are revealed incrementally (usually triggered by an event such as a button press or time delay). The AsciiDoc converter supports a variety of ways to add builds to your presentation.
The build mechanism itself is handled by a Bespoke.js plugin (e.g., bespoke-bullets) with the help of some CSS. You’ll then use metadata in the AsciiDoc file to indicate which content should participate in a build.
The two ways to enlist content in a build are the build option and the build attribute. The first should handle most situations, while the latter enables you to fine-tune the behavior.
Before diving into that metadata, we first need to do a bit of configuration.
Here’s the JavaScript you’ll need to add to your Bespoke.js configuration to activate the bespoke-bullets plugin to implement the behavior described in this section.
var bespoke = require('bespoke'),
bullets = require('bespoke-bullets'), // // (1)
...
bespoke.from('article', [
...
bullets('.build,.build-items>*:not(.build-items)'), // // (2)
...
]);
-
Load the bespoke-bullets plugin, assigning it to the
bullets
variable. -
Activate the bespoke-bullets plugin, using a CSS selector to query for buildable content.
Here’s the CSS necessary to handle the visibility of build items and introduce several build effects. You can customize the styles to your liking.
.bespoke-bullet:not(.bespoke-bullet-active) {
visibility: hidden;
pointer-events: none;
}
.fade .bespoke-bullet-active:not(.bespoke-bullet-current) {
opacity: 0.1;
}
.vanish .bespoke-bullet-active:not(.bespoke-bullet-current) {
visibility: hidden;
}
Let’s assume you have an unordered list on one of your slides and you want to reveal the items one-by-one. Simply declare the build option on the list.
[%build]
* one
* two
* three
When the slide is first loaded, none of the items will be visible. (The list container itself is the active build item). Each time you press the button or key mapped to the “next” action, another item in the list will be revealed. Past items will remain visible.
For content that doesn’t have a container, such as a paragraph, you’ll need to also add the build option to the section.
[%build]
== Another Topic
[%build]
A point about this topic.
The first build is automatically activated on slide entry. Therefore, in order for the build on the paragraph to be deferred, the section title needs to be marked as the first build item.
At some point, you’re likely to encounter a build permutation that can’t be described using the option alone. That’s where the build attribute comes in.
The build attribute is used to describe more complex build scenarios. Right now, it supports the following values (though more may be added in the futrue):
- self
-
The block itself should be enlisted in the build, but not its children.
- items
-
The block’s children should be enlisted in the build, but not the block itself.
- self+items (equivalent to the build option)
-
The block and its children should be enlisted in the build.
Using the build attribute, we can tackle the following two cases:
-
Show the list all at once.
-
Show the first item in the list on slide entry.
Let’s first look at how to show the list all at once on the first “next” action.
[%build]
== Another Topic
[build=self]
* one
* two
* three
The section title is the first build step, which is automatically activated on slide entry. The next build step is the list as a whole.
Now, instead, let’s reveal the items in the list one-by-one, but show the first item on slide entry.
== Another Topic
[build=items]
* one
* two
* three
In this case, the first item in the list is the auto-activated build step. The next build step is the second item in the list.
As you can see, the build attribute gives you more fine-grained control over the build behavior.
You can use CSS to introduce additional build effects. The effects supported out of the box are as follows:
-
fade
-
vanish
-
spotlight (planned)
-
replace (planned)
The CSS in the Build Configuration section implements these effects.
The converter supports adding a background image to a slide while still preserving the semantics of the document.
If the first content in a slide is a block image, and that image has the role canvas
, the converter will pluck that image block out of the content and promote it to the background image of the slide.
== !
[.canvas]
image::background-image.png[]
This feature makes it really easy to create image-only slides that take up the full screen.
By default, the image is configured to cover the slide surface.
If you want to force the image to be contained within the dimensions of the slide (while preserving the aspect ratio), you can add the role contain
.
== !
[.contain.canvas]
image::background-image.png[]
Just like for other image types, you use the block and inline image macros to add SVGs to your presentation (via AsciiDoc). The difference comes in the fact that you can configure how the SVG is inserted into the HTML output.
The converter supports three ways of inserting an SVG into the HTML of a slide. Each method is labeled below by the HTML element that is used:
<img>
-
The SVG is linked as a rasterized image.
<object>
-
The SVG is embedded as a live, interactive object (aka “content document”).
<svg>
-
The SVG is embedded directly into the HTML itself.
There are pros and cons of using each method (which is why the converter supports all three). You can read more about the differences between these methods and their tradeoffs by studying the article Styling And Animating SVGs with CSS.
You declare an option on the image macro to control which method is used. The option values are documented in the table below alongside the HTML element they emit.
Option Name | HTML Element | AsciiDoc Example |
---|---|---|
none (default) |
|
image::sample.svg[] |
interactive |
|
[%interactive] image::sample.svg[] |
inline |
|
[%inline] image::sample.svg[] |
When using inline or interactive, the viewBox
attribute must be defined on the root <svg>
element in order for scaling to work properly.
When using the inline option, if you specify a width or height on the image macro in AsciiDoc, the width
, height
and style
attributes on the <svg>
element will be removed.
If you’re inserting an SVG using the inline method, we strongly recommend you optimize your SVG using a tool like svgo.
💡
|
The bespoke-multimedia plugin automatically adds the CSS class active to the root element of all “interactive” SVGs on the current slide, so long as the SVG is loaded from the same domain.
|
So which method should you choose? It depends on how you’re using the SVG. Here are some rules of thumb to follow.
-
Does the SVG have builds (aka bullets)?
⇒ Use inline. -
Do you want the SVG content to be reachable by JavaScript from the main DOM?
⇒ Use inline. -
Do you want the SVG content to inherit styles from the main DOM?
⇒ Use inline. -
Does the SVG have CSS animations?
⇒ Use inline or interactive.-
If using interactive, you must use the bespoke-multimedia plugin to control the animations on slide entry and exit.
-
-
Does the SVG reference custom fonts (i.e., webfonts)?
⇒ Use inline or interactive.-
If using interactive, you must link to the CSS that declares the fonts in the SVG file using an XML stylesheet declaration.
-
-
Are you simply using the SVG as a static image (and it doesn’t use custom fonts)?
⇒ Use the default.
As you work with SVGs in your presentations, you’ll become more comfortable making the decision about which method to employ given the circumstances. It’s only confusing the first couple of times.
The converter recognizes designated blocks containing speaker notes and incorporates them into the presentation as hidden elements. The speaker notes are then displayed adjacent to the current slide in a presentation console.
You add speaker notes to a slide by nesting them in a sidebar (or admonition) block and adding the role cue
to that block.
That block must then be placed at the end of the section for that slide.
== Topic
Slide content.
[.cue]
****
Topic is all around us.
Topic has the following benefits:
* Easy to use
* Easy to scale
* It's free!
****
To learn more about how to setup a presentation console, see the bespoke-onstage plugin.
It’s possible to inject supplemental content into the output document using docinfo files. This core feature of AsciiDoc has been adapted to work with the Bespoke converter.
Currently, there are three insertion locations for docinfo content in a Bespoke document:
- head
-
content is inserted after the last child of the
<head>
element - header
-
content is inserted before the first child of the
<article>
element (before the slides) - footer
-
content is inserted after the last child of the
<article>
element (after the slides)
The content you want to insert goes into a sibling file of the slide deck document with the following filename pattern:
docinfo-<location>-bespoke.html
For example, let’s say you want to embed a tweet into your slide deck. You might inject the shared embedding JavaScript using a footer docinfo file:
<script src="https://platform.twitter.com/widgets.js"></script>
You then need to set the following document attribute in the AsciiDoc header:
:docinfo: shared
When this attribute is defined, the converter will automatically read the docinfo file(s) and insert the contents into the specified location in the output document.
If you want to include content in every slide, we recommend using a tree processor extension. The tree processor would first query for all the level-1 sections in the document (which get transformed into slides), then append one or more blocks to each of the matched sections. The tree processor could even read this content from a shared file. In the future, the converter may support docinfo insertions per slide.
While conversion from AsciiDoc is meant to save you time producing common slide types, there are cases when you find yourself going against the grain or exceeding the limits of what CSS can handle. This situation is normal. The truth is, certain slides require an HTML layout that is tailored to the content. In these cases, you can use a custom transform.
You can delegate the conversion of a slide to a custom template by specifying the transform
attribute.
The converter will then look for a template file that follows the pattern slide_<transform>.html.slim
, where <transform>
is the value of this attribute, inside the directory (or directories) specified by the template_dir(s)
option.
Let’s assume you want to create a custom presenter slide. First, create a placeholder slide in the AsciiDoc and specify a custom transform.
[transform=presenter]
== Presenter
Next, create a file named slide_presenter.html.slim in the directory that holds your templates.
The template is responsible for creating the <section>
element for the slide.
(In fact, there’s nothing stopping you from creating multiple slides).
section.presenter id=id class=role
header
h2=document.attr :author
h3=document.attr :position
figure.image.headshot
img src=(image_uri document.attr :avatar) alt=(document.attr :author)
- unless (_content = content).empty?
=_content
Finally, when you invoke the converter, you must specify the location of the template file using the -T
option:
$ asciidoctor-bespoke -D public -T src/templates src/index.adoc
Since you can access the entire document model of the parsed AsciiDoc in the template, you are free to pick and choose the content you want to add to the slide and in what order.
Let’s look at an example that draws from the document model selectively. Assume you want to create one slide per item in a list.
[transform=step_by_slide]
== !
* one
* two
* three
Here’s a template that implements this behavior:
- blocks.first.items.each do |_item|
section
p=_item.text
This template applied to the previous slide content will generate the following HTML:
<section>
<p>one</p>
</section>
<section>
<p>two</p>
</section>
<section>
<p>three</p>
</section>
As you can see, there’s no reason you have to stick to a 1-to-1 mapping between what is in the AsciiDoc file and the slide(s) you’re generating. The custom transform gives you the flexibility to layout the content on the slide exactly how you want.
You can go deeper and customize the template used for any node (without having to add any hints in the AsciiDoc). This converter is based on a collection of Slim templates. You can copy any one of these templates into your custom templates directory and make modifications to it. Asciidoctor will use your copy instead of the matching template provided by the converter. To learn more about how to write Slim templates, refer to the Slim documentation.
You can build a static version of the slides using the following command:
$ gulp
The files are built into the public directory. You can then view the slides by navigating to public/index.html in your browser.
If you use the preview server, the build will monitor the project files for changes and automatically refresh the presentation in the browser when a change is detected. You can launch the preview server using:
$ gulp serve
Once the server is running, you can view the slides by navigating to http://localhost:8000 in your browser.
-
Service Workers, a presentation by Hubert Sablonnière (ported from DZSlides)
asciidoctor-bespoke was created by Dan Allen.
Bespoke.js was created by Mark Dalgleish and has received contributions, mostly in the form of plugins, from many other individuals in the Bespoke.js ecosystem.
Copyright © 2015-present Dan Allen and the Asciidoctor Project. Use of this software is granted under the terms of the MIT License.
See the LICENSE file for the full license text.