diff --git a/Gemfile b/Gemfile
index 3c8733f..f7ee8f7 100644
--- a/Gemfile
+++ b/Gemfile
@@ -2,6 +2,7 @@ source 'https://rubygems.org'
gem 'asciidoctor'
gem 'asciidoctor-html5s'
gem 'asciidoctor-rouge'
+gem 'asciidoctor-tabs', github: 'asciidoctor/asciidoctor-tabs'
gem 'rouge'
gem 'rubocop'
gem 'asciidoctor-foodogsquared-extensions', :path => './gems'
diff --git a/Gemfile.lock b/Gemfile.lock
index 90d74ad..9600fc4 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -1,3 +1,10 @@
+GIT
+ remote: https://github.com/asciidoctor/asciidoctor-tabs.git
+ revision: d5ee94b5253f4db75e6646f46511676af0be2cc3
+ specs:
+ asciidoctor-tabs (1.0.0.beta.3)
+ asciidoctor (>= 2.0.0, < 3.0.0)
+
PATH
remote: gems
specs:
@@ -56,6 +63,7 @@ DEPENDENCIES
asciidoctor-foodogsquared-extensions!
asciidoctor-html5s
asciidoctor-rouge
+ asciidoctor-tabs!
rouge
rubocop
ruby-lsp
diff --git a/assets/css/asciidoctor-tabs.css b/assets/css/asciidoctor-tabs.css
new file mode 100644
index 0000000..2f804f8
--- /dev/null
+++ b/assets/css/asciidoctor-tabs.css
@@ -0,0 +1,103 @@
+/*! Asciidoctor Tabs | Copyright (c) 2018-present Dan Allen | MIT License */
+.tabs {
+ margin-bottom: 1.25em;
+}
+
+.tablist > ul {
+ display: flex;
+ flex-wrap: wrap;
+ list-style: none;
+ margin: 0;
+ padding: 0;
+}
+
+.tablist > ul li {
+ align-items: center;
+ background-color: var(--base02);
+ cursor: pointer;
+ font-size: 0.8em;
+ display: flex;
+ padding: 0.5em;
+ position: relative;
+
+ &:hover {
+ background: var(--base01);
+ }
+}
+
+.tablist > ul li:focus-visible {
+ outline: none;
+}
+
+.tablist.ulist,
+.tablist.ulist > ul li {
+ margin: 0;
+}
+
+.tablist.ulist > ul li + li {
+ margin-left: 0.25em;
+}
+
+.tabs.is-loading .tablist li:not(:first-child),
+.tabs:not(.is-loading) .tablist li:not(.is-selected) {
+ background-color: var(--base00);
+}
+
+.tabs.is-loading .tablist li:first-child::after,
+.tabs:not(.is-loading) .tablist li.is-selected::after {
+ background-color: inherit;
+ content: "";
+ display: block;
+ height: 3px; /* Chrome doesn't always paint the line accurately, so add a little extra */
+ position: absolute;
+ bottom: -1.5px;
+ left: 0;
+ right: 0;
+}
+
+.tablist > ul p {
+ line-height: inherit;
+ margin: 0;
+}
+
+.tabpanel {
+ background-color: var(--base00);
+ padding: 1.25em;
+}
+
+.tablist > ul li,
+.tabpanel {
+ border: var(--border-style);
+}
+
+.tablist > ul li {
+ border-bottom: 0;
+}
+
+.tabs.is-loading .tabpanel + .tabpanel,
+.tabs:not(.is-loading) .tabpanel.is-hidden {
+ display: none;
+}
+
+.tabpanel > :first-child {
+ margin-top: 0;
+}
+
+/* #content is a signature of the Asciidoctor standalone HTML output */
+#content .tabpanel > :last-child,
+#content .tabpanel > :last-child > :last-child,
+#content .tabpanel > :last-child > :last-child > li:last-child > :last-child {
+ margin-bottom: 0;
+}
+
+.tablecontainer {
+ overflow-x: auto;
+}
+
+#content .tablecontainer {
+ margin-bottom: 1.25em;
+}
+
+#content .tablecontainer > table.tableblock {
+ margin-bottom: 0;
+}
diff --git a/assets/js/asciidoctor-tabs.js b/assets/js/asciidoctor-tabs.js
new file mode 100644
index 0000000..483d6bb
--- /dev/null
+++ b/assets/js/asciidoctor-tabs.js
@@ -0,0 +1,118 @@
+;(function () { /*! Asciidoctor Tabs | Copyright (c) 2018-present Dan Allen | MIT License */
+ 'use strict'
+
+ var config = (document.currentScript || {}).dataset || {}
+ var forEach = Array.prototype.forEach
+
+ init(document.querySelectorAll('.tabs'))
+
+ function init (tabsBlocks) {
+ if (!tabsBlocks.length) return
+ forEach.call(tabsBlocks, function (tabs) {
+ var syncIds = tabs.classList.contains('is-sync') ? {} : undefined
+ var tablist = tabs.querySelector('.tablist ul')
+ tablist.setAttribute('role', 'tablist')
+ var initial
+ forEach.call(tablist.querySelectorAll('li'), function (tab, idx) {
+ tab.setAttribute('role', (tab.className = 'tab')) // NOTE converter may not have set class on li
+ var id, anchor, syncId
+ if (!(id = tab.id)) {
+ if (!(anchor = tab.querySelector('a[id]'))) return // invalid state
+ tab.id = id = anchor.parentNode.removeChild(anchor).id
+ }
+ var panel = tabs.querySelector('.tabpanel[aria-labelledby~="' + id + '"]')
+ if (!panel) return // invalid state
+ tab.tabIndex = -1
+ syncIds && (((syncId = tab.textContent.trim()) in syncIds) ? (syncId = undefined) : true) &&
+ (syncIds[(tab.dataset.syncId = syncId)] = tab)
+ idx || (initial = { tab: tab, panel: panel }) && syncIds ? toggleHidden(panel, true) : toggleSelected(tab, true)
+ tab.setAttribute('aria-controls', panel.id)
+ panel.setAttribute('role', 'tabpanel')
+ forEach.call(panel.querySelectorAll('table.tableblock'), function (table) {
+ var container = Object.assign(document.createElement('div'), { className: 'tablecontainer' })
+ table.parentNode.insertBefore(container, table).appendChild(table)
+ })
+ var onClick = syncId === undefined ? activateTab : activateTabSync
+ tab.addEventListener('click', onClick.bind({ tabs: tabs, tab: tab, panel: panel }))
+ })
+ if (syncIds && initial) {
+ var syncGroupId
+ for (var i = 0, lst = tabs.classList, len = lst.length, className; i !== len; i++) {
+ if (!(className = lst.item(i)).startsWith('data-sync-group-id=')) continue
+ tabs.dataset.syncGroupId = syncGroupId = lst.remove(className) || className.slice(19).replace(/\u00a0/g, ' ')
+ break
+ }
+ if (syncGroupId === undefined) tabs.dataset.syncGroupId = syncGroupId = Object.keys(syncIds).sort().join('|')
+ var preferredSyncId = 'syncStorageKey' in config &&
+ window[(config.syncStorageScope || 'local') + 'Storage'].getItem(config.syncStorageKey + '-' + syncGroupId)
+ var tab = preferredSyncId && syncIds[preferredSyncId]
+ tab && Object.assign(initial, { tab: tab, panel: document.getElementById(tab.getAttribute('aria-controls')) })
+ toggleSelected(initial.tab, true) || toggleHidden(initial.panel, false)
+ }
+ })
+ onHashChange()
+ toggleClassOnEach(tabsBlocks, 'is-loading', 'remove')
+ window.setTimeout(toggleClassOnEach.bind(null, tabsBlocks, 'is-loaded', 'add'), 0)
+ window.addEventListener('hashchange', onHashChange)
+ }
+
+ function activateTab (e) {
+ var tab = this.tab
+ var tabs = this.tabs || (this.tabs = tab.closest('.tabs'))
+ var panel = this.panel || (this.panel = document.getElementById(tab.getAttribute('aria-controls')))
+ forEach.call(tabs.querySelectorAll('.tablist .tab'), function (el) {
+ toggleSelected(el, el === tab)
+ })
+ forEach.call(tabs.querySelectorAll('.tabpanel'), function (el) {
+ toggleHidden(el, el !== panel)
+ })
+ if (!this.isSync && 'syncStorageKey' in config && 'syncGroupId' in tabs.dataset) {
+ var storageKey = config.syncStorageKey + '-' + tabs.dataset.syncGroupId
+ window[(config.syncStorageScope || 'local') + 'Storage'].setItem(storageKey, tab.dataset.syncId)
+ }
+ if (!e) return
+ var loc = window.location
+ var hashIdx = loc.hash ? loc.href.indexOf('#') : -1
+ if (~hashIdx) window.history.replaceState(null, '', loc.href.slice(0, hashIdx))
+ e.preventDefault()
+ }
+
+ function activateTabSync (e) {
+ activateTab.call(this, e)
+ var thisTabs = this.tabs
+ var thisTab = this.tab
+ var initialY = thisTabs.getBoundingClientRect().y
+ forEach.call(document.querySelectorAll('.tabs'), function (tabs) {
+ if (tabs === thisTabs || tabs.dataset.syncGroupId !== thisTabs.dataset.syncGroupId) return
+ forEach.call(tabs.querySelectorAll('.tablist .tab'), function (tab) {
+ if (tab.dataset.syncId === thisTab.dataset.syncId) activateTab.call({ tabs: tabs, tab: tab, isSync: true })
+ })
+ })
+ var shiftedBy = thisTabs.getBoundingClientRect().y - initialY
+ if (shiftedBy && (shiftedBy = Math.round(shiftedBy))) window.scrollBy({ top: shiftedBy, behavior: 'instant' })
+ }
+
+ function toggleClassOnEach (elements, className, method) {
+ forEach.call(elements, function (el) {
+ el.classList[method](className)
+ })
+ }
+
+ function toggleHidden (el, state) {
+ el.classList[(el.hidden = state) ? 'add' : 'remove']('is-hidden')
+ }
+
+ function toggleSelected (el, state) {
+ el.setAttribute('aria-selected', '' + state)
+ el.classList[state ? 'add' : 'remove']('is-selected')
+ el.tabIndex = state ? 0 : -1
+ }
+
+ function onHashChange () {
+ var id = window.location.hash.slice(1)
+ if (!id) return
+ var tab = document.getElementById(~id.indexOf('%') ? decodeURIComponent(id) : id)
+ if (!(tab && tab.classList.contains('tab'))) return
+ 'syncId' in tab.dataset ? activateTabSync.call({ tab: tab }) : activateTab.call({ tab: tab })
+ }
+})()
diff --git a/config.toml b/config.toml
index 06fc6e4..1e358f3 100644
--- a/config.toml
+++ b/config.toml
@@ -15,7 +15,8 @@ path = "github.com/foo-dogsquared/hugo-mod-web-feeds"
[markup.asciidocExt]
extensions = [
- "asciidoctor-foodogsquared-extensions"
+ "asciidoctor-foodogsquared-extensions",
+ "asciidoctor-tabs",
]
workingFolderCurrent = true
diff --git a/gemset.nix b/gemset.nix
index e1f7c0d..016c380 100644
--- a/gemset.nix
+++ b/gemset.nix
@@ -49,6 +49,21 @@
targets = [ ];
version = "0.4.0";
};
+ asciidoctor-tabs = {
+ dependencies = ["asciidoctor"];
+ groups = ["default"];
+ platforms = [];
+ source = {
+ fetchSubmodules = false;
+ rev = "d5ee94b5253f4db75e6646f46511676af0be2cc3";
+ sha256 = "1s2ds0f3v308vw4ic5dm6zh8d7dbi6lxxw6y7ajb8hmm27d0nksi";
+ target = "ruby";
+ type = "git";
+ url = "https://github.com/asciidoctor/asciidoctor-tabs.git";
+ };
+ targets = [];
+ version = "1.0.0.beta.3";
+ };
ast = {
groups = [ "default" ];
platforms = [ ];
diff --git a/layouts/partials/head_extended.html b/layouts/partials/head_extended.html
index a27ec72..f6e09e2 100644
--- a/layouts/partials/head_extended.html
+++ b/layouts/partials/head_extended.html
@@ -5,6 +5,12 @@
{{ end }}
+{{ $asciidoctorTabs := resources.Get "css/asciidoctor-tabs.css" | resources.ToCSS }}
+
+
+{{ $asciidoctorTabsJS := resources.Get "js/asciidoctor-tabs.js" }}
+
+
{{- /* medium-zoom */ -}}