git: 4333918fab - main - documentation/copy-clipboard.js: Copy shell examples without the prompt

From: Danilo G. Baio <dbaio_at_FreeBSD.org>
Date: Wed, 26 Aug 2026 00:20:30 UTC
The branch main has been updated by dbaio:

URL: https://cgit.FreeBSD.org/doc/commit/?id=4333918fabd467009486ef01378283819b426c73

commit 4333918fabd467009486ef01378283819b426c73
Author:     Danilo G. Baio <dbaio@FreeBSD.org>
AuthorDate: 2026-08-16 17:54:52 +0000
Commit:     Danilo G. Baio <dbaio@FreeBSD.org>
CommitDate: 2026-08-26 00:18:26 +0000

    documentation/copy-clipboard.js: Copy shell examples without the prompt
    
    Shell blocks include the prompt ("% git remote -v") and the output of
    the command, and the copy button handed all of it to the clipboard, so
    the result could not be pasted into a shell.  Asciidoctor callouts went
    along with it, as "<1>" is rendered as markup inside the block.
    
    Rebuild the copied text from the commands alone, reading the block with
    the callout markup taken out, and keep the lines that carry a command
    on: a trailing "\", "|" or "&&", and heredoc bodies up to their
    delimiter.
    
    Take "%", "$" and "#" for prompts, together with the host name they
    sometimes carry, as in "freefall% gen-gitconfig.sh" in the Committer's
    Guide or "ellington% ypcat -k netgroup" in the Handbook.  A "#" is also
    a comment and a common first character of output, so it only counts as
    the root prompt where the block opens as root or gets there part way
    through, as the Handbook example that runs "% su -" and goes on with "#
    make install".  The "# extended LDIF" header that ldapsearch(1) prints
    is left behind with the rest of the output, and blocks that only look
    like a session, such as the Makefile quoted in the FDP Primer, are
    copied unchanged.
    
    PR:             272669
    Reviewed by:    carlavilla
    Pull Request:   https://ron-dev.freebsd.org/FreeBSD/doc/pulls/4
---
 .../themes/beastie/assets/js/copy-clipboard.js     | 89 +++++++++++++++++++++-
 1 file changed, 88 insertions(+), 1 deletion(-)

diff --git a/documentation/themes/beastie/assets/js/copy-clipboard.js b/documentation/themes/beastie/assets/js/copy-clipboard.js
index 5c1fb6a500..5b11403edf 100644
--- a/documentation/themes/beastie/assets/js/copy-clipboard.js
+++ b/documentation/themes/beastie/assets/js/copy-clipboard.js
@@ -30,8 +30,95 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 ;(function () {
   'use strict'
 
+  // "% git remote -v", "freefall% gen-gitconfig.sh": a prompt, optionally
+  // prefixed by a host name, and the command it introduces.
+  var PROMPT = /^([^\s#%$]{0,32})([#%$])[ \t]+(\S.*)$/
+
+  // "#" also starts a shell comment and turns up in command output, so it
+  // takes a stricter test than the unambiguous "%" and "$".
+  function isCommand(text, prompt) {
+    var word = /^\S+/.exec(text)
+    if (!word || /[:,]$/.test(word[0])) return false
+    return (prompt === '#' ? /^[a-z_.\/~\\$(]/ : /^[A-Za-z_.\/~\\$(]/).test(word[0])
+  }
+
+  // The prompts this block uses, or null when it is no session at all: a
+  // Makefile, a configuration file, plain output.  "#" is a prompt only in a
+  // block that opens as root or that runs su(1) part way through.
+  function promptsOf(lines) {
+    var found = {}, opens = null, root = false, i, match, prompt
+    for (i = 0; i < lines.length; i++) {
+      if (!lines[i].trim()) continue
+      match = PROMPT.exec(lines[i])
+      prompt = match && isCommand(match[3], match[2]) ? match[2] : null
+      if (prompt) found[prompt] = true
+      if (prompt === '%' || prompt === '$') root = root || /^su(do)?\b/.test(match[3])
+      if (opens === null) opens = prompt || ''
+    }
+    if (found['%'] || found['$']) {
+      return { '%': found['%'], '$': found['$'], '#': found['#'] && root }
+    }
+    if (found['#'] && opens === '#') return { '#': true }
+    return null
+  }
+
+  // What carries the command on to the next line, if anything.  A heredoc
+  // counts only once its delimiter really turns up, so that a stray "<<" in an
+  // example cannot swallow the rest of the block.
+  function continues(command, lines, from) {
+    if (/(\\|&&|\|)$/.test(command)) return {}
+    var heredoc = /<<-?[ \t]*(['"]?)([A-Za-z_][A-Za-z0-9_]*)\1/.exec(command)
+    if (!heredoc) return null
+    for (var i = from; i < lines.length; i++) {
+      if (lines[i].trim() === heredoc[2]) return { delimiter: heredoc[2] }
+    }
+    return null
+  }
+
+  // Collect the commands instead of stripping the prompts, so that an
+  // unrecognised line is left out rather than copied as if it were one.
+  function commandsIn(text) {
+    var lines = text.split('\n')
+    var prompts = promptsOf(lines)
+    if (!prompts) return text
+
+    var commands = [], pending = null, i, line, match, prompted
+
+    for (i = 0; i < lines.length; i++) {
+      line = lines[i].replace(/[ \t]+$/, '')
+      match = PROMPT.exec(line)
+      prompted = !!match && !!prompts[match[2]] && isCommand(match[3], match[2])
+
+      // A heredoc runs to its delimiter whatever the lines look like; any
+      // other continuation ends as soon as a line carries a prompt again.
+      if (pending && (pending.delimiter || !prompted)) {
+        commands.push(line)
+        pending = pending.delimiter
+          ? (line.trim() === pending.delimiter ? null : pending)
+          : continues(line, lines, i + 1)
+        continue
+      }
+
+      if (prompted) {
+        commands.push(match[3])
+        pending = continues(match[3], lines, i + 1)
+      }
+    }
+
+    return commands.join('\n')
+  }
+
+  // Asciidoctor renders a callout ("<1>") as markup inside the block, so the
+  // text has to be read with those nodes taken back out.
+  function textOf(block) {
+    var copy = block.cloneNode(true)
+    var conums = copy.querySelectorAll('.conum, .conum + b')
+    for (var i = 0; i < conums.length; i++) conums[i].remove()
+    return copy.textContent
+  }
+
   document.querySelectorAll(".rouge, .highlight").forEach(function(codeItem) {
-    var sourceCode = codeItem.textContent;
+    var sourceCode = commandsIn(textOf(codeItem));
 
     var icon = document.createElement("i");
     icon.className = "fa fa-clipboard";