Skip to content

Instantly share code, notes, and snippets.

@zr-tex8r
Created January 4, 2021 10:45
Show Gist options
  • Save zr-tex8r/71c91cdbc565fbd669dd996c00fcf234 to your computer and use it in GitHub Desktop.
Save zr-tex8r/71c91cdbc565fbd669dd996c00fcf234 to your computer and use it in GitHub Desktop.
LaTeX:key-value型の引数指定をもつユーザ命令を(LaTeXレベルで)定義する
%%
%% This is file 'bxkvcmd.sty'.
%%
%% Copyright (c) 2021 Takayuki YATO (aka. "ZR")
%% GitHub: https://github.com/zr-tex8r
%% Twitter: @zr_tex8r
%%
%% This package is distributed under the MIT License.
%%
%% package declarations
\RequirePackage{expl3,xparse}
\ProvidesExplPackage {bxkvcmd} {2021/01/02} {0.2.0}
{User commands with key-value interface}
%--------------------------------------- general
%% internal parameters
\int_const:Nn \c_bxkvc_absize_int { 9 }
% A kv command is represented by a nullary function of the following form:
% ----
% {
% \bxkvc_invoke:nnnn
% { <number_of_keys> } { <number_of_arguments> }
% {
% \do { <key_1> } { <default_value_1> }
% \do { <key_2> } { <default_value_2> }
% ...
% }
% { <command_body> }
% ----
% In the command body, keys and arguments are represented by "uninterpreted
% parameter notation", which is a sequel of '#' token followed by a digit
% token. (NOte that the body contains no "real" parameter tokens, since
% the function is nullary.)
% - 1st--9th keys are represented by #1--#9 (1st level)
% - 10th--18th keys are represented by ##1--##9 (2nd level)
% - 19th--27th keys are represented by ####1--####9 (3rd level)
% - And so on. When all keys end up to use N levels, then (ordinary)
% arguments employ the (N+1)th level. For example, when a command
% has 25 keys and two arguments, then keys use #1 through ####7
% and arguments use ########1 and ########2 (4th level).
%% variables
\bool_new:N \l_bxkvc_ok_bool
\clist_new:N \l_bxkvc_tmpa_clist
\clist_new:N \l_bxkvc_tmpb_clist
\int_new:N \l_bxkvc_tmpa_int
\int_new:N \l_bxkvc_nkeys_int
\int_new:N \l_bxkvc_nargs_int
\prop_new:N \l_bxkvc_values_prop
\tl_new:N \l_bxkvc_tmpa_tl
\tl_new:N \l_bxkvc_tmpb_tl
\tl_new:N \l_bxkvc_tmpc_tl
\tl_new:N \l_bxkvc_definer_tl
\tl_new:N \l_bxkvc_keyinfo_tl
\int_new:N \l_bxkvc_cbs_int
\cs_new:Npn \bxkvc_tmpcs:w {}
%--------------------------------------- user interface
%%<*> \newkeyvalcommand \CS [<#arguments>] {<key-info>} {<body>}
\NewDocumentCommand \newkeyvalcommand { m O{0} m +m }
{
\bxkvc_check_new_cmd:nT {#1}
{
\tl_set:Nn \l_bxkvc_definer_tl { \NewDocumentCommand #1 {} }
\int_set:Nn \l_bxkvc_nargs_int {#2}
\tl_set:Nn \l_bxkvc_keyinfo_tl {#3}
\tl_set:Nn \l_bxkvc_body_tl {#4}
\bxkvc_declare:
}
}
%%<*> \renewkeyvalcommand \CS [<#arguments>] {<key-info>} {<body>}
\NewDocumentCommand \renewkeyvalcommand { m O{0} m +m }
{
\bxkvc_check_renew_cmd:nT {#1}
{
\tl_set:Nn \l_bxkvc_definer_tl { \RenewDocumentCommand #1 {} }
\int_set:Nn \l_bxkvc_nargs_int {#2}
\tl_set:Nn \l_bxkvc_keyinfo tl {#3}
\tl_set:Nn \l_bxkvc_body_tl {#4}
\bxkvc_declare:
}
}
%--------------------------------------- messages
\msg_new:nnnn { bxkvc } { bad-command }
{ Command~not~valid. }
{
You~have~used~#1~with~something~not~counted~as~a~valid~command.
}
\msg_new:nnnn { bxkvc } { already-defined }
{ Command~'#1'~already~defined. }
{
You~have~used~#2~with~a~command~that~already~has~a~definition.
}
\msg_new:nnnn { bxkvc } { not-kv-command }
{ Command~'#1'~is~not~an~existing~key-value~command. }
{
You~have~used~#2~with~a~command~that~is~not~defined~as~
a~key-value~command.
}
\msg_new:nnnn { bxkvc } { bad-key }
{ Bad~key~name~'#1'. }
{
A~key~name~must~not~contain~a~space~or~a~special~character.
}
%--------------------------------------- command declaration
%% variables
\regex_new:N \l_bxkvc_keys_regex
%% constants
\bool_if:nTF { \sys_if_engine_ptex_p: || \sys_if_engine_uptex_p: }
{
\regex_const:Nn \c_bxkvc_key_only_regex
{ ^ (\c[OL][^=,\|] | [^\x00-\xff])+ \z }
\regex_const:Nn \c_bxkvc_key_value_regex
{ ^ (\c[OL][^=,\|] | [^\x00-\xff])+ \ * = }
}
{
\regex_const:Nn \c_bxkvc_key_only_regex
{ ^ \c[OL][^=,\|]+ \z }
\regex_const:Nn \c_bxkvc_key_value_regex
{ ^ \c[OL][^=,\|]+ \ * = }
}
\regex_const:Nn \c_bxkvc_rxmeta_regex
{ [\!\"\'\(\)\*\+\-\.\/\:\;\<\=\>\?\@\[\]\`\|] }
\regex_const:Nn \c_bxkvc_param_regex
{ \cP. (\cO[1-9]) }
%% \bxkvc_check_[re]new_cd:nT {<command>} {T}
\prg_new_conditional:Nnn \bxkvc_check_new_cmd:n { T }
{ \bxkvc_check_cmd_gen:NnN \newkeyvalcommand {#1} \bxkvc_check_new_cmd_aux:NN }
\prg_new_conditional:Nnn \bxkvc_check_renew_cmd:n { T }
{ \bxkvc_check_cmd_gen:NnN \renewkeyvalcommand {#1} \bxkvc_check_renew_cmd_aux:NN }
\cs_new:Nn \bxkvc_check_cmd_gen:NnN
{
\bool_lazy_and:nnTF
{ \tl_if_single_token_p:n {#2} }
{ \token_if_cs_p:N #2 }
{#3#1#2}
{
\msg_error:nnx { bxkvc } { bad-command } { \token_to_str:N #1 }
\prg_return_false:
}
}
\cs_new:Nn \bxkvc_check_new_cmd_aux:NN
{
\cs_if_exist:NTF #2
{
\msg_error:nnxx { bxkvc } { already-defined } { \token_to_str:N #2 }
{ \token_to_str:N #1 }
\prg_return_false:
}
{ \prg_return_true: }
}
\cs_new:Nn \bxkvc_check_renew_cmd_aux:NN
{
\cs_if_exist:NTF #2
{ \prg_return_true: } % TODO
{
\msg_error:nnxx { bxkvc } { not-kv-command } { \token_to_str:N #2 }
{ \token_to_str:N #1 }
\prg_return_false:
}
}
\cs_new:Nn \bxkvc_declare:
{
\bool_set_true:N \l_bxkvc_ok_bool
\bxkvc_parse_keyinfo:
\bool_if:NT \l_bxkvc_ok_bool
{
\int_set:Nn \l_bxkvc_nkeys_int { \prop_count:N \l_bxkvc_values_prop }
\bxkvc_render_keyinfo:
\bxkvc_cook_cmd_body:
\bxkvc_define_command:
}
}
\cs_new:Nn \bxkvc_parse_keyinfo:
{
\clist_set:NV \l_bxkvc_tmpa_clist \l_bxkvc_keyinfo_tl
\clist_clear:N \l_bxkvc_tmpb_clist
\clist_map_function:NN \l_bxkvc_tmpa_clist \bxkvc_parse_keyinfo_aux:n
\exp_args:NNV \prop_set_from_keyval:Nn \l_bxkvc_values_prop \l_bxkvc_tmpb_clist
}
\cs_new:Nn \bxkvc_parse_keyinfo_aux:n
{
\regex_match:NnTF \c_bxkvc_key_only_regex {#1}
{ \clist_put_right:Nn \l_bxkvc_tmpb_clist { #1 = {} } }
{
\regex_match:NnTF \c_bxkvc_key_value_regex {#1}
{ \clist_put_right:Nn \l_bxkvc_tmpb_clist {#1} }
{
\msg_error:nnn { bxkvc } { bad-key } {#1}
}
}
}
\cs_new:Nn \bxkvc_render_keyinfo:
{
\tl_clear:N \l_bxkvc_tmpa_tl
\tl_set:Nn \l_bxkvc_tmpb_tl { #### } % double '#'
\int_zero:N \l_bxkvc_tmpa_int
\clist_clear:N \l_bxkvc_tmpa_clist
\prop_map_function:NN \l_bxkvc_values_prop \bxkvc_render_keyinfo_aux:nn
\tl_set_eq:NN \l_bxkvc_keyinfo_tl \l_bxkvc_tmpa_tl
% construct regex for key names
\exp_args:NNx \regex_set:Nn \l_bxkvc_keys_regex
{
\exp_not:n { \|( }
\clist_use:Nn \l_bxkvc_tmpa_clist { | }
\exp_not:n { )\| }
}
}
\cs_new:Nn \bxkvc_render_keyinfo_aux:nn
{
\tl_put_right:Nn \l_bxkvc_tmpa_tl { \do {#1} {#2} }
% create a parameter notation (\l_bxkvc_tmpc_tl)
\int_incr:N \l_bxkvc_tmpa_int
\int_compare:nNnT { \l_bxkvc_tmpa_int } > { \c_bxkvc_absize_int }
{
\int_set:Nn \l_bxkvc_tmpa_int { 1 }
\tl_put_right:NV \l_bxkvc_tmpb_tl \l_bxkvc_tmpb_tl % doubling '#'
}
\tl_set_eq:NN \l_bxkvc_tmpc_tl \l_bxkvc_tmpb_tl
\tl_put_right:Nx \l_bxkvc_tmpc_tl { \int_to_arabic:n { \l_bxkvc_tmpa_int } }
% set replacer variables
\tl_clear_new:c { l_bxkvc_R_ #1 _tl }
\tl_set_eq:cN { l_bxkvc_R_ #1 _tl } \l_bxkvc_tmpc_tl
% construct regex for key names
\tl_set:Nn \l_bxkvc_tmpc_tl {#1}
\regex_replace_all:NnN \c_bxkvc_rxmeta_regex { \\ \0 } \l_bxkvc_tmpc_tl
\clist_put_right:NV \l_bxkvc_tmpa_clist \l_bxkvc_tmpc_tl
}
\cs_new:Nn \bxkvc_cook_cmd_body:
{
\tl_set:Nn \l_bxkvc_tmpb_tl { #### }
\int_step_inline:nnnn { 1 } { \c_bxkvc_absize_int } { \l_bxkvc_nkeys_int }
{ \tl_put_right:NV \l_bxkvc_tmpb_tl \l_bxkvc_tmpb_tl }
\regex_replace_all:NnN \c_bxkvc_param_regex { \u{l_bxkvc_tmpb_tl} \1 }
\l_bxkvc_body_tl
\regex_replace_all:NnN \l_bxkvc_keys_regex { \u{l_bxkvc_R_ \1 _tl} }
\l_bxkvc_body_tl
}
\cs_new:Nn \bxkvc_define_command:
{
\tl_set:Nx \l_bxkvc_tmpa_tl
{
\exp_not:V \l_bxkvc_definer_tl
{
\exp_not:N \bxkvc_invoke:nnnn
{ \int_to_arabic:n { \l_bxkvc_nkeys_int } }
{ \int_to_arabic:n { \l_bxkvc_nargs_int } }
{ \exp_not:V \l_bxkvc_keyinfo_tl }
{ \exp_not:V \l_bxkvc_body_tl }
}
}
\tl_use:N \l_bxkvc_tmpa_tl
}
%--------------------------------------- command invocation
%% variables
\tl_new:N \l_bxkvc_body_tl
\tl_new:N \l_bxkvc_keys_tl
\tl_new:N \l_bxkvc_org_do_tl
%% constants
\tl_const:Nn \c_bxkvc_para_seq_tl
{
\if_case:w \l_bxkvc_cbs_int
\or: \exp_not:n {#1}
\or: \exp_not:n {#1#2}
\or: \exp_not:n {#1#2#3}
\or: \exp_not:n {#1#2#3#4}
\or: \exp_not:n {#1#2#3#4#5}
\or: \exp_not:n {#1#2#3#4#5#6}
\or: \exp_not:n {#1#2#3#4#5#6#7}
\or: \exp_not:n {#1#2#3#4#5#6#7#8}
\or: \exp_not:n {#1#2#3#4#5#6#7#8#9}
\fi:
}
%% \bxkvc_invoke:nnnn {<#keys>} {<#arguments>} {<key-info>} {<body>}
\cs_new:Nn \bxkvc_invoke:nnnn
{
\int_set:Nn \l_bxkvc_nkeys_int {#1}
\int_set:Nn \l_bxkvc_nargs_int {#2}
\tl_set:Nn \l_bxkvc_keyinfo_tl {#3}
\tl_set:Nn \l_bxkvc_body_tl {#4}
\bxkvc_read_values:n
}
\cs_new:Nn \bxkvc_read_values:n
{
\prop_set_from_keyval:Nn \l_bxkvc_values_prop {#1}
\tl_set_eq:NN \l_bxkvc_org_do_tl \do
\tl_set:Nn \do { \bxkvc_read_value_do:nn }
\tl_use:N \l_bxkvc_keyinfo_tl
\tl_set:Nn \do { \bxkvc_gather_key_do:nn }
\tl_set:Nx \l_bxkvc_keys_tl { \tl_use:N \l_bxkvc_keyinfo_tl }
\tl_set_eq:NN \do \l_bxkvc_org_do_tl
\bxkvc_invoke_main:
}
\cs_new:Nn \bxkvc_read_value_do:nn
{
\tl_if_empty:nF {#2}
{ \prop_put_if_new:Nnn \l_bxkvc_values_prop {#1} {#2} }
}
\cs_new:Nn \bxkvc_gather_key_do:nn
{ {#1} }
\cs_new:Nn \bxkvc_invoke_main:
{
\bxkvc_bind_keys:
\bxkvc_create_func:
\tl_clear:N \l_bxkvc_tmpa_tl
\tl_clear:N \l_bxkvc_tmpb_tl
\tl_clear:N \l_bxkvc_tmpc_tl
\prop_clear:N \l_bxkvc_values_prop
\bxkvc_tmpcs:w
}
\cs_new:Nn \bxkvc_bind_keys:
{
\int_step_function:nnnN { 1 } { \c_bxkvc_absize_int } { \l_bxkvc_nkeys_int }
\bxkvc_bind_key_block:n
}
\cs_new:Nn \bxkvc_bind_key_block:n
{
\int_set:Nn \l_bxkvc_cbs_int
{ \int_min:nn { \c_bxkvc_absize_int } { \l_bxkvc_nkeys_int - #1 + 1 } }
\tl_set:Nf \l_bxkvc_tmpa_tl
{ \tl_range:Nnn \l_bxkvc_keys_tl {#1} { #1 + \l_bxkvc_cbs_int - 1} }
\tl_clear:N \l_bxkvc_tmpb_tl
\tl_map_function:NN \l_bxkvc_tmpa_tl \bxkvc_bind_key_block_aux:n
\tl_set:Nx \l_bxkvc_tmpa_tl
{
\exp_not:n { \cs_set:Npn \bxkvc_tmpcs:w }
\c_bxkvc_para_seq_tl
{ \exp_not:V \l_bxkvc_body_tl }
}
\tl_use:N \l_bxkvc_tmpa_tl
\tl_set:Nx \l_bxkvc_tmpa_tl
{
\exp_not:n { \tl_set:No \l_bxkvc_body_tl }
{ \exp_not:N \bxkvc_tmpcs:w \exp_not:V \l_bxkvc_tmpb_tl }
}
\tl_use:N \l_bxkvc_tmpa_tl
}
\cs_new:Nn \bxkvc_bind_key_block_aux:n
{
\prop_get:NnNTF \l_bxkvc_values_prop {#1} \l_bxkvc_tmpc_tl
{ \tl_put_right:Nx \l_bxkvc_tmpb_tl { { \exp_not:V \l_bxkvc_tmpc_tl } } }
{ \tl_put_right:Nn \l_bxkvc_tmpb_tl { {} } }
}
\cs_new:Nn \bxkvc_create_func:
{
\int_set:Nn \l_bxkvc_cbs_int \l_bxkvc_nargs_int
\tl_set:Nx \l_bxkvc_tmpa_tl
{
\exp_not:n { \cs_set:Npn \bxkvc_tmpcs:w }
\c_bxkvc_para_seq_tl
{ \exp_not:V \l_bxkvc_body_tl }
}
\tl_use:N \l_bxkvc_tmpa_tl
}
%--------------------------------------- all done
% EOF
%#!uplatex
\documentclass[uplatex,dvipdfmx,a6paper]{jsarticle}
\usepackage{bxkvcmd}% key-valueしたい!!
\usepackage{scsnowman}
\newkeyvalcommand{\xTemplate}{
姓, 名, 好きなマフラーの色=red % '='以降は既定値
}{\newpage % '|...|'がそのキーに対する値に置き換わる
\noindent |姓| 様\par\medskip
先日、|姓|様の技術ブログを拝見し、大変興味深く感じました。\par
つきましては、弊社の提供するマフラー付きゆきだるまを
ご鑑賞いただければ幸いです。\par
\begin{center}
\scsnowman[muffler=|好きなマフラーの色|,
scale=12, hat, arms, snow, buttons]
\end{center}
}
\begin{document}
% key-valueのリストを引数にとる
\xTemplate{姓=黒☃, 名=大地, 好きなマフラーの色=blue}
\end{document}
@zr-tex8r
Copy link
Author

zr-tex8r commented Jan 4, 2021

前提環境

  • フォーマット: LaTeX
  • エンジン: expl3が動くやつ
    (最近のLaTeXが動くやつならOK)

パッケージオプション

なし。

使用法

  • \newkeyvalcommand{\命令名}[通常引数個数]{キー定義}{定義本体}:新しいkey-value命令\命令名を定義する。
  • \renewkeyvalcommand{\命令名}[通常引数個数]{キー定義}{定義本体}:既存のkey-value命令\命令名を再定義する。
    ※(仕様としては)\命令名はkey-value命令である必要がある。

引数の説明。

  • 通常引数個数は0~9の範囲の整数で、通常の命令と同様の「key-valueでない引数」の個数を表す。既定値は0(通常引数無し)。
    • オプション引数には非対応。
  • キー定義は以下の形式。([]は省略可能を示し、[]自体は書かない。)
    キー名1[={既定値1}],キー名2[={既定値2}],…
    • 既定値の既定値は空。
    • キー名{既定値}の周りの空白は無視される。
    • 既定値が,や両端の空白を含まないなら{}は省略可。
  • キー名に使えない文字は、,=|と半角空白とLaTeXの特殊文字。それ以外の文字は全て使える。
  • 定義本体は通常の命令定義と同じ。
    • ただし、|キー名|の形の文字列は当該のキーの使用を表す。
    • ||の間の文字列がキー定義で指定した有効なキー名でない場合はその文字列自体を表す。
    • #1#9は通常の引数の使用を表す。

key-value命令は以下の形式で呼び出す。

\命令名{キー設定}{通常引数1}…{通常引数N}
  • キー設定は以下の形式。(細則は「キー定義」と同じ。)
    キー名1={値1},キー名2={値2},…
    • を指定しなかった場合は当該キーの既定値が用いられる。
  • N通常引数個数の値。Nが0の場合は通常引数の部分は無しになる。

@doraTeX
Copy link

doraTeX commented Aug 29, 2023

upLaTeX で使用したときに,<body> 内に 和文文字 + 半角数字1~9 の並びが存在すると,

Illegal parameter number in definition of \bxkvc_tmpcs:w

というエラーが出るようです。(LuaLaTeXだとこの現象は起こりません。)

再現ソース

%#!uplatex
\documentclass[uplatex,dvipdfmx]{jsarticle}
\usepackage{bxkvcmd}
\newkeyvalcommand{\xTemplate}{}{あ1}
\begin{document}
\xTemplate{}
\end{document}

調査

原因を調べてみたところ,upLaTeX で l3regex を使用したときの \cO と和文文字のマッチングの問題に起因するようです。

とりあえず

\regex_const:Nn \c_bxkvc_param_regex
  { \cP. (\cO[1-9]) }

の部分を

\regex_const:Nn \c_bxkvc_param_regex
  { \cP. (\cO\relax[1-9]) }

にしてみたらとりあえずコンパイルは通り,テンプレートの動作もしているように見えますが,あまり理解できていないので,副作用の検証など十分にできておりませんが,とりあえずご報告まで。

@doraTeX
Copy link

doraTeX commented Aug 29, 2023

副作用の検証など十分にできておりませんが

当然の副作用として,「#1#9による通常の引数利用」が機能しなくなりますね……。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment