add copy to clipboard button to source blocks
authorDan Allen <dan@opendevise.com>
Fri, 11 Dec 2020 00:57:57 +0000 (17:57 -0700)
committerDan Allen <dan@opendevise.com>
Sat, 12 Dec 2020 02:30:58 +0000 (19:30 -0700)
* add subsetted SVG icon sprite for octicons
* add copy to clipboard button to source blocks (visible on hover)
* replace pseudo-element for source language with a toolbox (managed by JavaScript)
* autodetect literal block with command and promote to a console source block
* intelligently extract commands from console source block, flatten them, and join them in a chain
* prevent command prompt(s) in console source block from being selected
* darken color for annotation on source block
* configure svgo to preserve desc element
* configure svgo to preserve ID prefixes for icons

gulp.d/tasks/build.js
preview-src/index.adoc
src/css/doc.css
src/css/vars.css
src/img/octicons.svg [new file with mode: 0644]
src/js/06-copy-to-clipboard.js [new file with mode: 0644]
src/partials/footer-scripts.hbs

index c4c2795..64642ad 100644 (file)
@@ -100,20 +100,24 @@ module.exports = (src, dest, preview) => () => {
       .src(['css/site.css', 'css/vendor/*.css'], { ...opts, sourcemaps })
       .pipe(postcss((file) => ({ plugins: postcssPlugins, options: { file } }))),
     vfs.src('font/*.{ttf,woff*(2)}', opts),
-    vfs
-      .src('img/**/*.{gif,ico,jpg,png,svg}', opts)
-      .pipe(
-        preview
-          ? through()
-          : imagemin(
-            [
-              imagemin.gifsicle(),
-              imagemin.jpegtran(),
-              imagemin.optipng(),
-              imagemin.svgo({ plugins: [{ removeViewBox: false }] }),
-            ].reduce((accum, it) => (it ? accum.concat(it) : accum), [])
-          )
-      ),
+    vfs.src('img/**/*.{gif,ico,jpg,png,svg}', opts).pipe(
+      preview
+        ? through()
+        : imagemin(
+          [
+            imagemin.gifsicle(),
+            imagemin.jpegtran(),
+            imagemin.optipng(),
+            imagemin.svgo({
+              plugins: [
+                { cleanupIDs: { preservePrefixes: ['symbol-', 'view-'] } },
+                { removeViewBox: false },
+                { removeDesc: false },
+              ],
+            }),
+          ].reduce((accum, it) => (it ? accum.concat(it) : accum), [])
+        )
+    ),
     vfs.src('helpers/*.js', opts),
     vfs.src('layouts/*.hbs', opts),
     vfs.src('partials/*.hbs', opts)
index 3759146..bc52e5f 100644 (file)
@@ -69,6 +69,15 @@ vfs
 <2> Wrap each streaming file in a buffer so the files can be processed by uglify.
 Uglify can only work with buffers, not streams.
 
+Execute these commands to validate and build your site:
+
+ $ podman run -v $PWD:/antora:Z --rm -t antora/antora \
+   version
+ 3.0.0
+ $ podman run -v $PWD:/antora:Z --rm -it antora/antora \
+   --clean \
+   antora-playbook.yml
+
 Cum dicat #putant# ne.
 Est in <<inline,reque>> homero principes, meis deleniti mediocrem ad has.
 Altera atomorum his ex, has cu elitr melius propriae.
index caa6fd1..3c27ae5 100644 (file)
   padding: 0.75rem;
 }
 
-/* NOTE assume pre.highlight contains code[data-lang] */
 .doc pre.highlight {
   position: relative;
 }
 
-.doc .listingblock code[data-lang]::before {
-  content: attr(data-lang);
-  display: none;
+.doc .language-console .hljs-meta {
+  user-select: none;
+}
+
+.doc .source-toolbox {
+  display: flex;
+  visibility: hidden;
+  position: absolute;
+  top: 0.25rem;
+  right: 0.5rem;
   color: var(--pre-annotation-font-color);
+  font-family: var(--body-font-family);
   font-size: calc(13.5 / var(--rem-base) * 1rem);
-  letter-spacing: 0.05em;
   line-height: 1;
+}
+
+.doc .listingblock:hover .source-toolbox {
+  visibility: visible;
+}
+
+.doc .source-toolbox .source-lang {
   text-transform: uppercase;
+  letter-spacing: 0.075em;
+  font-size: 0.96em;
+  line-height: 1.0425;
+}
+
+.doc .source-toolbox > :not(:last-child)::after {
+  content: "|";
+  letter-spacing: 0;
+  padding: 0 1ch;
+}
+
+.doc .source-toolbox .copy-button {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  background: transparent;
+  border: none;
+  color: inherit;
+  outline: none;
+  padding: 0;
+  font-size: inherit;
+  line-height: inherit;
+  width: 1em;
+  height: 1em;
+}
+
+.source-toolbox .copy-button * {
+  flex: none;
+}
+
+.source-toolbox .copy-button svg {
+  fill: currentColor;
+  width: inherit;
+  height: inherit;
+}
+
+.source-toolbox .copy-toast {
+  position: relative;
+  display: inline-flex;
+  justify-content: center;
+  margin-top: 1em;
+  background-color: var(--doc-font-color);
+  border-radius: 0.25em;
+  padding: 0.5em;
+  color: var(--color-white);
+  cursor: auto;
+  opacity: 0;
+  transition: opacity 0.5s ease 0.75s;
+}
+
+.source-toolbox .copy-toast::after {
+  content: "";
   position: absolute;
-  top: 0.25rem;
-  right: 0.25rem;
+  top: 0;
+  width: 1em;
+  height: 1em;
+  border: 0.55em solid transparent;
+  border-left-color: var(--doc-font-color);
+  transform: rotate(-90deg) translateX(50%) translateY(50%);
+  transform-origin: left;
 }
 
-.doc .listingblock:hover code[data-lang]::before {
-  display: block;
+.source-toolbox .copy-button.clicked .copy-toast {
+  opacity: 1;
+  transition: none;
 }
 
 .doc .hdlist1,
   font-family: var(--body-font-family);
   font-size: calc(13.5 / var(--rem-base) * 1rem);
   font-style: normal;
-  height: 1.25em;
   line-height: 1.2;
   text-align: center;
   width: 1.25em;
+  height: 1.25em;
   letter-spacing: -0.25ex;
   text-indent: -0.25ex;
 }
index 888cea8..56d5454 100644 (file)
   --kbd-border-color: var(--color-gray-10);
   --pre-background: var(--panel-background);
   --pre-border-color: var(--panel-border-color);
-  --pre-annotation-font-color: var(--color-gray-10);
+  --pre-annotation-font-color: var(--color-gray-50);
   --quote-background: var(--panel-background);
   --quote-border-color: var(--color-gray-70);
   --quote-font-color: var(--color-gray-70);
diff --git a/src/img/octicons.svg b/src/img/octicons.svg
new file mode 100644 (file)
index 0000000..3503b58
--- /dev/null
@@ -0,0 +1,36 @@
+<svg xmlns="http://www.w3.org/2000/svg">
+  <title>Octicons (subset)</title>
+  <desc>Octicons v11.2.0 by GitHub - https://primer.style/octicons/ - License: MIT</desc>
+  <metadata
+    xmlns:dc="http://purl.org/dc/elements/1.1/"
+    xmlns:cc="http://creativecommons.org/ns#"
+    xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
+    <rdf:RDF>
+      <cc:Work rdf:about="">
+        <dc:title>@primer/octicons</dc:title>
+        <dc:identifier>11.2.0</dc:identifier>
+        <dc:description>A scalable set of icons handcrafted with &lt;3 by GitHub</dc:description>
+        <dc:format>image/svg+xml</dc:format>
+        <dc:creator>
+          <cc:Agent>
+            <dc:title>GitHub</dc:title>
+          </cc:Agent>
+        </dc:creator>
+        <dc:rights>
+          <cc:Agent>
+            <dc:title>Copyright (c) 2020 GitHub Inc.</dc:title>
+          </cc:Agent>
+        </dc:rights>
+        <cc:license rdf:resource="https://opensource.org/licenses/MIT" />
+        <dc:relation>https://primer.style/octicons/</dc:relation>
+      </cc:Work>
+    </rdf:RDF>
+  </metadata>
+  <symbol id="symbol-clippy-16" viewBox="0 0 16 16">
+    <path
+       fill-rule="evenodd"
+       d="M5.75 1a.75.75 0 00-.75.75v3c0 .414.336.75.75.75h4.5a.75.75 0 00.75-.75v-3a.75.75 0 00-.75-.75h-4.5zm.75 3V2.5h3V4h-3zm-2.874-.467a.75.75 0 00-.752-1.298A1.75 1.75 0 002 3.75v9.5c0 .966.784 1.75 1.75 1.75h8.5A1.75 1.75 0 0014 13.25v-9.5a1.75 1.75 0 00-.874-1.515.75.75 0 10-.752 1.298.25.25 0 01.126.217v9.5a.25.25 0 01-.25.25h-8.5a.25.25 0 01-.25-.25v-9.5a.25.25 0 01.126-.217z" />
+  </symbol>
+  <use href="#symbol-clippy-16" width="16" height="16" x="0" y="0" />
+  <view id="view-clippy-16" viewBox="0 0 16 16" />
+</svg>
diff --git a/src/js/06-copy-to-clipboard.js b/src/js/06-copy-to-clipboard.js
new file mode 100644 (file)
index 0000000..4f4b802
--- /dev/null
@@ -0,0 +1,66 @@
+;(function () {
+  'use strict'
+  ;[].slice.call(document.querySelectorAll('.doc pre.highlight, .doc .literalblock pre')).forEach(function (pre) {
+    var code, language, lang, copy, toast, toolbox
+    if (pre.classList.contains('highlight')) {
+      code = pre.querySelector('code')
+      if ((language = code.dataset.lang) && language !== 'console') {
+        ;(lang = document.createElement('span')).className = 'source-lang'
+        lang.appendChild(document.createTextNode(language))
+      }
+    } else if (pre.innerText.startsWith('$ ')) {
+      var block = pre.parentNode.parentNode
+      block.classList.remove('literalblock')
+      block.classList.add('listingblock')
+      pre.classList.add('highlightjs')
+      pre.classList.add('highlight')
+      ;(code = document.createElement('code')).className = 'language-console hljs'
+      code.dataset.lang = 'console'
+      code.appendChild(pre.firstChild)
+      pre.appendChild(code)
+    } else {
+      return
+    }
+    ;(toolbox = document.createElement('div')).className = 'source-toolbox'
+    if (lang) toolbox.appendChild(lang)
+    if (window.navigator.clipboard) {
+      ;(copy = document.createElement('button')).className = 'copy-button'
+      copy.setAttribute('title', 'Copy to clipboard')
+      var svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg')
+      svg.setAttribute('aria-hidden', 'true')
+      svg.setAttribute('class', 'copy-icon')
+      var use = document.createElementNS('http://www.w3.org/2000/svg', 'use')
+      use.setAttribute('href', window.uiRootPath + '/img/octicons.svg#symbol-clippy-16')
+      svg.appendChild(use)
+      copy.appendChild(svg)
+      ;(toast = document.createElement('span')).className = 'copy-toast'
+      toast.appendChild(document.createTextNode('Copied!'))
+      copy.appendChild(toast)
+      toolbox.appendChild(copy)
+    }
+    pre.appendChild(toolbox)
+    if (copy) copy.addEventListener('click', writeToClipboard.bind(copy, code))
+  })
+
+  function extractCommands (text) {
+    var cmdRx = /^\$ (\S[^\\\n]*(\\\n(?!\$ )[^\\\n]*)*)(?=\n|$)/gm
+    var cleanupRx = /( )? *\\\n */g
+    var cmds = []
+    var m
+    while ((m = cmdRx.exec(text))) cmds.push(m[1].replace(cleanupRx, '$1'))
+    return cmds.join(' && ')
+  }
+
+  function writeToClipboard (code) {
+    var text = code.innerText
+    if (code.dataset.lang === 'console' && text.startsWith('$ ')) text = extractCommands(text)
+    window.navigator.clipboard.writeText(text).then(
+      function () {
+        this.classList.add('clicked')
+        this.offsetHeight // eslint-disable-line no-unused-expressions
+        this.classList.remove('clicked')
+      }.bind(this),
+      function () {}
+    )
+  }
+})()
index d106cd3..b4f5fff 100644 (file)
@@ -1,2 +1,3 @@
+<script>window.uiRootPath = '{{{uiRootPath}}}'</script>
 <script src="{{{uiRootPath}}}/js/site.js"></script>
 <script async src="{{{uiRootPath}}}/js/vendor/highlight.js"></script>