Skip to content

An extension for Asciidoctor that adds a tabs block to the AsciiDoc syntax.

License

Notifications You must be signed in to change notification settings

asciidoctor/asciidoctor-tabs

Repository files navigation

Asciidoctor Tabs

An Asciidoctor extension that adds a tabs block to the AsciiDoc syntax.

📎
This extension is intended to be used with HTML backends (e.g., html5). For all other backends (i.e., the filetype is not html), the custom block enclosure is discarded and its contents (a dlist) is converted normally.
💡
This extension is also published as an npm package named @asciidoctor/tabs for use with Asciidoctor.js, and hence, with Antora. See the README for the npm package and its Antora integration guide for details.

Overview

Each set of tabs (i.e., a “tabset” or tabs block) is constructed from a description list (dlist) enclosed in an example block annotated with the tabs style (i.e., [tab]). That nested combination of blocks gets translated by this extension into a single tabs block that is a specialization of an open block.

The tabbed interface produced from this block can help organize information by code language, operating system, or product variant. The benefit of organizing information in this way is that it condenses the use of vertical space by only showing what’s relevant to the reader (and thus hiding information that’s irrelevant or redundant). The result is that readers enjoy a better user experience when reading your documentation.

Install

Using gem command

$ gem install --prerelease asciidoctor-tabs

Using Bundler

Create a Gemfile in your project:

Gemfile
source 'https://rubygems.org'

gem 'asciidoctor-tabs'

# or use the code directlly from GitHub
# gem 'asciidoctor-tabs', github: 'asciidoctor/asciidoctor-tabs'

Then optionally configure Bundler to install gems locally:

$ bundle config --local path .bundle/gems

Then use Bundler to install the gem:

$ bundle

Syntax

A tabset is defined using a description list (dlist) enclosed in an example block annotated with the tabs style.

tabs.adoc
[tabs]
====
Tab A:: Contents of Tab A.

Tab B::
+
Contents of Tab B.

Tab C::
+
--
Contents of Tab C.

Contains more than one block.
--
====

The tabs themselves are modeled as a dlist. Each item in the dlist becomes a separate tab. The term is used as the tab’s label and the description is used as the tab’s contents. The contents can be defined as primary text, attached blocks, or both. If the tab has a single attached block, and that block is an open block with no attributes, the open block enclosure itself is discarded upon conversion.

You may choose to extend the block delimiter length from the typical 4 characters to 6 in order to avoid conflicts with any example blocks inside the tabs block (or just as a matter of style).

tab-with-example-block.adoc
[tabs]
======
Tab A::
+
====
Example block in Tab A.
====

Tab B:: Just text.
======

Using this technique, you can also create nested tabsets.

tab-with-nested-tabs.adoc
[tabs]
======
Tab A::
+
Selecting Tab A reveals a tabset with Tab Y and Tab Z.
+
[tabs]
====
Tab Y:: Contents of Tab Y, nested inside Tab A.
Tab Z:: Contents of Tab Z, nested inside Tab A.
====

Tab B:: Just text.
======

Tabs Sync

If you want to synchronize (i.e., sync) the tab selection across tabsets, set the tabs-sync-option on the document.

tabs-sync.adoc
:tabs-sync-option:

[tabs]
====
Tab A:: Triggers selection of Tab A in other congruent tabsets.
Tab B:: Triggers selection of Tab B in other congruent tabsets.
====

...

[tabs]
====
Tab A:: Triggers selection of Tab A in other congruent tabsets.
Tab B:: Triggers selection of Tab B in other congruent tabsets.
====

Only tabsets that have the same sync group ID are synchronized. By default, the sync group ID is computed by taking the text of each tab, sorting that list, and joining it on | (e.g., A|B). Each unique combination of tabs—​or congruent tablist—​implicitly creates a new sync group.

You can override the sync group ID of a tabset using the sync-group-id attribute on the block. This allows you to control the scope of the sync or to force a tabset to participate in a sync group even if its not congruent.

tabs-with-custom-sync-groups.adoc
:tabs-sync-option:

[tabs,sync-group-id=group-1]
====
Tab A:: Triggers selection of Tab A in second tabset.
Tab B:: Triggers selection of Tab B in second tabset.
====

[tabs,sync-group-id=group-1]
====
Tab A:: Triggers selection of Tab A in first tabset.
Tab B:: Triggers selection of Tab B in first tabset.
====

[tabs,sync-group-id=group-2]
====
Tab A:: Triggers selection of Tab A in fourth tabset.
Tab B:: Triggers selection of Tab B in fourth tabset.
====

[tabs,sync-group-id=group-2]
====
Tab A:: Triggers selection of Tab A in third tabset.
Tab B:: Triggers selection of Tab B in third tabset.
====

Instead of enabling tabs sync globally, you can set the sync option on individual tabs blocks.

tabs-with-sync-option.adoc
[tabs%sync]
====
Tab A:: Triggers selection of Tab A in third tabset.
Tab B:: Triggers selection of Tab B in third tabset.
====

[tabs]
====
Tab A:: Does not trigger selection of Tab A in other tabsets.
Tab B:: Does not trigger selection of Tab B in other tabsets.
====

[tabs%sync]
====
Tab A:: Triggers selection of Tab A in first tabset.
Tab B:: Triggers selection of Tab B in first tabset.
====

Conversely, if you want to delist a tabs block from the global sync, set the nosync option on that block.

tabs-with-nosync-option.adoc
:tabs-sync-option:

[tabs]
====
Tab A:: Triggers selection of Tab A in third tabset.
Tab B:: Triggers selection of Tab B in third tabset.
====

[tabs%nosync]
====
Tab A:: Does not trigger selection of Tab A in other tabsets.
Tab B:: Does not trigger selection of Tab B in other tabsets.
====

[tabs]
====
Tab A:: Triggers selection of Tab A in first tabset.
Tab B:: Triggers selection of Tab B in first tabset.
====

If you want to persist the sync selection, assign a value to the data-sync-storage-key attribute on the <script> tag.

<script data-sync-storage-key="preferred-tab">

By default, the sync selection (per group) will be persisted to local storage (i.e., data-sync-storage-scope="local") using the specified key. You can set the data-sync-storage-scope attribute on the <script> tag to session to use session storage instead of local storage.

<script data-sync-storage-key="preferred-tab" data-sync-storage-scope="session">

When using the extension on a standalone document (which will automatically embed the supporting script), you can configure these options using the tabs-sync-storage-key and tabs-sync-storage-scope document attributes, respectively.

:tabs-sync-storage-key: tabs
:tabs-sync-storage-scope: session

In this case, the converter will set the corresponding attributes on the <script> tag automatically.

Usage

CLI

$ asciidoctor -r asciidoctor-tabs tabs.adoc

You can specify an alternate stylesheet for tabs using the tabs-stylesheet document attribute.

$ asciidoctor -r asciidoctor-tabs -a tabs-stylesheet=my-tabs.css tabs.adoc

The value of the tabs-stylesheet attribute is handled in the same way as the built-in stylesheet document attribute. A relative path is resolved starting from the value of the stylesdir document attribute, which defaults to the directory of the document.

API

There are two ways to use the extension with the Asciidoctor API. In either case, you must require the Asciidoctor gem (asciidoctor) before requiring this one.

You can require asciidoctor/tabs to register the extension as a global extension, just like with the CLI.

require 'asciidoctor'
require 'asciidoctor/tabs'

Asciidoctor.convert_file 'tabs.adoc', safe: :safe

Or you can pass a registry instance to the Extensions.register method to register the extension with a scoped registry.

require 'asciidoctor'
require 'asciidoctor/tabs/extensions'

registry = Asciidoctor::Extensions.create
Asciidoctor::Tabs::Extensions.register registry

Asciidoctor.convert_file 'tabs.adoc', extension_registry: registry, safe: :safe

If you’re not using other scoped extensions, you can pass in the extensions group without first creating a registry instance:

Asciidoctor.convert_file 'tabs.adoc', extensions: Asciidoctor::Tabs::Extensions.group, safe: :safe

How it Works

This extension works by transforming the dlist inside the example block into a tabbed interface. The example block enclosure is discarded. The tabbed interface is supported by a stylesheet (style) and script (behavior) that are added to the HTML document by this extension. (These assets can be found in the data folder of the gem).

📎
The stylesheet and script are only added when producing a standalone document. The stylesheet is added to the end of the <head> tag and the script added to the end of the <body> tag. If the linkcss attribute is set by the API, the CLI, the document, or the safe mode, the HTML links to these assets. Otherwise, the contents of these assets are embedded into the HTML.

The tabbed interface consists of two output elements. The first element contains an unordered list of all the tab labels in document order. The second element contains all the tab panes. The labels and panes are correlated through the use of a unique ID. Each tab is assigned an id attribute and each pane is assigned an aria-labelledby attribute that references the corresponding ID. The added stylesheet sets up the appearance of the tabbed interface and the added script supports the interaction (i.e., tab selection).

A tab can be selected when the page loads using a URL fragment (e.g., #id-of-tab-here). Otherwise, the first tab is selected when the page loads.

Development

Follow the instructions below to learn how to get started developing on this project.

Retrieve the source code

Copy the GitHub repository URL and pass it to the git clone command:

$ git clone https://github.com/asciidoctor/asciidoctor-tabs

Next, switch to the project directory:

$ cd asciidoctor-tabs

Install the dependencies

The development dependencies are defined in the Gemfile at the root of the project. Use the bundle command from Bundler to install these dependencies under the project directory:

$ bundle --path=.bundle/gems

You must invoke bundle from the project’s root directory so it can locate the Gemfile.

Run the tests

The test suite is located in the spec directory. The tests are based on RSpec.

Run all tests

You can run all of the tests using Rake:

$ bundle exec rake spec

For more fine-grained control, you can also run the tests directly using RSpec:

$ bundle exec rspec

To run all tests in a single spec, pass the spec file to the rpec command:

$ bundle exec rspec spec/reducer_spec.rb

Run specific tests

If you only want to run a single test (or a group of tests), you can do so by first tagging the test cases, then filtering the test run using that tag.

Start by adding the only tag to one or more specifications:

it 'should do something new', only: true do
  expect(true).to be true
end

Next, run RSpec with the only flag enabled:

$ bundle exec rspec -t only

RSpec will only run the specifications that contain this flag.

You can also filter tests by keyword. Let’s assume we want to run all the tests that have role in their description. Run RSpec with the example filter:

$ bundle exec rspec -e role

RSpec will only run the specifications that have a description containing the text only.

Generate code coverage

To generate a code coverage report when running tests using simplecov, set the COVERAGE environment variable as follows when running the tests:

$ COVERAGE=deep bundle exec rake spec

You’ll see a total coverage score, a detailed coverage report, and a link to HTML report in the output. The HTML report helps you understand which lines and branches were missed, if any.

Authors

Asciidoctor Tabs was written by Dan Allen of OpenDevise Inc. and contributed to the Asciidoctor project.

Copyright © 2018-present Dan Allen (OpenDevise Inc.) and the individual contributors to this project. Use of this software is granted under the terms of the MIT License.

See the LICENSE for the full license text.

Trademarks

AsciiDoc® is a trademark of the Eclipse Foundation, Inc.