An open API service indexing awesome lists of open source software.

https://github.com/asciidoctor/asciidoctor-tabs

An extension for Asciidoctor that adds a tabs block to the AsciiDoc syntax.
https://github.com/asciidoctor/asciidoctor-tabs

Last synced: 9 days ago
JSON representation

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

Awesome Lists containing this project

README

        

= Asciidoctor Tabs
Dan Allen
v1.0.0-beta.6, 2023-08-01
:idprefix:
:idseparator: -
ifndef::env-github[:icons: font]
ifdef::env-github[]
:note-caption: :paperclip:
:tip-caption: :bulb:
endif::[]
:url-repo: https://github.com/asciidoctor/asciidoctor-tabs

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

NOTE: 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.

TIP: This extension is also published as an npm package named `@asciidoctor/tabs` for use with Asciidoctor.js, and hence, with Antora.
See the xref:js/README.adoc[README] for the npm package and its xref:docs/use-with-antora.adoc[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 [.path]_Gemfile_ in your project:

.Gemfile
[,ruby]
----
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
[,asciidoc]
----
[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
[,asciidoc]
----
[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
[,asciidoc]
----
[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
[,asciidoc]
----
: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
[,asciidoc]
----
: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
[,asciidoc]
----
[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
[,asciidoc]
----
: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 `` tag.

[,js]
----
<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.

[,js]
----
<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.

[,asciidoc]
----
: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.

[,js]
----
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.

[,js]
----
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:

[,js]
----
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 [.path]_data_ folder of the gem).

NOTE: 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.
By default, the contents of these assets are embedded into the HTML.
If the `linkcss` attribute is set by the API, the CLI, the document, or the safe mode, the HTML links to these assets.
*In this case, you must ensure the assets (found in the [.path]_data_ folder of the gem or project repository) get copied to the linked location.*

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 {url-repo}[GitHub repository URL] and pass it to the `git clone` command:

[subs=attributes+]
$ git clone {url-repo}

Next, switch to the project directory:

[subs=attributes+]
$ cd asciidoctor-tabs

=== Install the dependencies

The development dependencies are defined in the [.path]_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 [.path]_Gemfile_.

=== Run the tests

The test suite is located in the [.path]_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:

[source,ruby]
----
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 and License

Copyright (C) 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 link:LICENSE[LICENSE] for the full license text.

== Trademarks

AsciiDoc(R) is a trademark of the Eclipse Foundation, Inc.