📝

LuaLaTeXの力で"クリップボードにコピー"ボタンをPDFに作る

2024/07/25に公開

前回までのあらすじ

こんなことしてました.
https://zenn.dev/cyber_hacnosuke/articles/41275710b8f594

今回やること

クリップボードにコピーするボタンを作ります.Zennをはじめをした技術系のブログや記事、サイトではコードブロック、コードスニペットにクリックするだけでクリップボードにソースコードがコピーされるボタンがありますよね?

こんなかんじで。(コードスニペットにマウスホバーで出現)↗️

それを作ります.ソースコードででない任意の文字も,クリップボードにコピーさせることができます.しかし,以下の制約が必要です.

  • Adobe Acrobatを使用していること
  • Adobe Acrobatで必要な設定を行っていること
  • 日本語を含まないこと(日本語があると文字化けします)

必要な設定とは以下の内容です.

  • Adobe Acrobatの環境設定を開く
  • JavaScriptの項目からAcrobat JavaScriptを使用にチェックをいれる
  • セキュリティ(拡張)の項目のセキュリティ特権の場所にこの PDF ファイルまたはそれが格納されたフォルダを追加する

発想

https://tex.stackexchange.com/questions/174637/copy-to-clipboard-feature-in-pdf-output/545107#545107

この質問を見つけたのでこれがベースになります.大まかな流れは以下です.

  1. VerbatimOutコマンドでソースコードをありのままファイル(今回はtemp.txt)に書き出す
  2. lstinputlistingでソースコードを表示
  3. ボタンが押されたら PDF 内に新しいfieldを追加してluaLaTeXでファイルを読み込んだ中身をセット
  4. ユーザのカーソルをfieldにフォーカス
  5. すべて選択をさせる
  6. コピーをさせる
  7. fieldを抹消

3 以降はAcrobat Javascript API(正式名称不明)を利用してJavascriptで書きます.5,6 はメニューにある機能を発火させるexecMenuItemを使います.また,発火のタイミングをずらすためにsetTimeOutを用います.このコマンドではTeX,lua,Javascriptの 3 言語を使うことになります.

なぜこんな回りくどいやり方をする(とくに,なぜluaを使用する)のかといいますと,TeXに特殊文字が多いのと,コマンドがいつ「展開」されるかがわからないからです.そのため,'Javascriptにおいて文字列リテラルを表す),#TeXにおいて引数を表す),%TeXにおいてコメントを表す), (スペース),改行などの特殊文字をluaでファイルを読む際に変な文字列(ABC<space>XYZなど)で置換して,Javascriptで元に戻しています.こうすることで,texのファイルにはこの様な特殊文字が一切現れない形になります.

完成品

ソースコードでない文字はこれでできます.

\usepackage{media9}

\mediabutton[%
  jsaction={
    var fld = this.addField("ToCopy", "text", \thepage-1, this.getPageBox({nPage: \thepage-1}));
    fld.textSize = 0.1;
    fld.fillColor = color.transparent;
    fld.multiline = true;
    fld.value = 'copy text'; % ここがコピーされるテキストです.
    fld.setFocus();
    app.setTimeOut("app.execMenuItem('SelectAll');", 100);
    app.setTimeOut("app.execMenuItem('Copy');", 200);
    app.setTimeOut("this.removeField('ToCopy');", 300);
  }]{
    \fbox{Copy to Clipboard}% この部分を変更すれば見た目を変更できます.
  }

数週間頑張った結果がこちらです.

\usepackage{luacode}
\usepackage{verbatim}
\usepackage{listings}
\usepackage{media9}
\usepackage{fancyvrb}
\usepackage{xparse}

\begin{luacode*}
function readtxt(filename, suffix)
  local body = ""
  local suffix = suffix or ""
  local firstline = true
  local sharp_char = string.char(35)
  local percent_char = string.char(37)
  local single_quote = string.char(39)
  local space_char = string.char(32)
  for line in io.lines(filename) do
    line = string.gsub(line, sharp_char, "ABC<sharp>XYZ")
    line = string.gsub(line, percent_char..percent_char, "ABC<percent>XYZ")
    line = string.gsub(line, single_quote, "ABC<quote>XYZ")
    line = string.gsub(line, space_char, "ABC<space>XYZ")
    if not firstline then
      body = body .. "ABC<br>XYZ"
    else
      firstline = false
    end
    body = body .. line
  end
  return body
end
\end{luacode*}

\NewDocumentEnvironment{zennlisting}{O{Python}}
  {\VerbatimOut{temp.txt}}
  {\endVerbatimOut\lstinputlisting[language=#1]{temp.txt}%
  \mediabutton[%
    jsaction={
      var fld = this.addField("ToCopy", "text", \thepage-1, this.getPageBox({nPage: \thepage-1}));
      fld.textSize = 0.1;
      fld.fillColor = color.transparent;
      fld.multiline = true;
      fld.value = '\directlua{tex.sprint(readtxt('temp.txt'))}'.replace(/ABC<br>XYZ/g, String.fromCharCode(10)).replace(/ABC<sharp>XYZ/g, String.fromCharCode(35)).replace(/ABC<quote>XYZ/g, String.fromCharCode(39)).replace(/ABC<space>XYZ/g, String.fromCharCode(32));
      fld.setFocus();
      app.setTimeOut("app.execMenuItem('SelectAll');", 100);
      app.setTimeOut("app.execMenuItem('Copy');", 200);
      app.setTimeOut("this.removeField('ToCopy');", 300);
    }]{
      \fbox{Copy to Clipboard}% この部分を変更すれば見た目を変更できます.
    }
  }

使い方

\begin{zennlisting}[Python]
# your code
\end{zennlisting}
GitHubで編集を提案

Discussion