# -*- mode: Perl -*-
# /=====================================================================\ #
# | xkeyval | #
# | Implementation of xkeyval for LaTeXML | #
# |=====================================================================| #
# | Thanks to Tom Wiesing <tom.wiesing@gmail.com> | #
# | Part of LaTeXML: | #
# | Public domain software, produced as part of work done by the | #
# | United States Government & not subject to copyright in the US. | #
# |---------------------------------------------------------------------| #
# | Bruce Miller <bruce.miller@nist.gov> #_# | #
# | http://dlmf.nist.gov/LaTeXML/ (o o) | #
# \=========================================================ooo==U==ooo=/ #
package LaTeXML::Package::Pool;
use strict;
use warnings;
use LaTeXML::Package;
use LaTeXML::Util::Pathname qw(pathname_split);
###
### Basic \setkeys
###
AssignValue('keyval.sty_loaded' => 1, 'global'); # pretend keyval loaded too.
# \setkeys[prefix]{keyset}[na]{keyvals}
DefMacro('\setkeys OptionalMatch:* OptionalMatch:+ []{}[]', sub {
my ($gullet, $star, $plus, $prefix, $keysets, $skip) = @_;
$skip = defined($skip) ? ToString($skip) : undef;
$star = defined($star) ? T_CS('\XKV@rm') : undef;
$prefix = ToString(Expand($prefix)) if defined $prefix;
$keysets = ToString(Expand($keysets)) if defined $keysets;
my $keyvals = LaTeXML::Core::KeyVals->new($prefix, $keysets,
setAll => defined($plus), setInternals => 1,
skip => $skip, skipMissing => $star);
$keyvals->readFrom($gullet, T_END);
$keyvals->setKeysExpansion; });
# \setrmkeys[prefix]{keyset}[na]
DefMacro('\setrmkeys OptionalMatch:* []{}[]', sub {
my ($gullet, $star, $prefix, $keysets, $na) = @_;
# expand and delete the list of token we need to work on
my @keyvals = Expand(T_CS('\XKV@rm'))->unlist;
DefMacro(T_CS('\XKV@rm'), '');
my @tokens = ();
push(@tokens, T_CS('\setkeys'));
push(@tokens, $star) if defined($star);
push(@tokens, T_OTHER('['), $prefix, T_OTHER(']')) if defined($prefix);
push(@tokens, T_BEGIN, $keysets, T_END);
push(@tokens, T_OTHER('['), $na, T_OTHER(']')) if defined($na);
push(@tokens, T_BEGIN, @keyvals, T_END);
@tokens; });
## ------------
## regular keys
## ------------
# \define@key[prefix]{keyset}{key}[default]{code}
DefPrimitive('\define@key[]{}{}[]{}', sub {
my ($stomach, $prefix, $keyset, $key, $default, $code) = @_;
$prefix = $prefix ? ToString(Expand($prefix)) : undef;
$default = defined($default) ? ToString($default) : undef;
$keyset = ToString(Expand($keyset));
$key = ToString(Expand($key));
DefKeyVal($keyset, $key, '', $default, prefix => $prefix, code => $code);
return; });
## ------------
## command keys
## ------------
# \define@cmdkey[prefix]{keyset}[macroprefix]{key}[default]{code}
DefPrimitive('\define@cmdkey[]{}[]{}[]{}', sub {
my ($stomach, $prefix, $keyset, $macroprefix, $key, $default, $code) = @_;
$prefix = defined($prefix) ? ToString(Expand($prefix)) : undef;
$macroprefix = defined($macroprefix) ? ToString(Expand($macroprefix)) : undef;
DefKeyVal(ToString(Expand($keyset)), ToString(Expand($key)), '', ToString($default),
prefix => $prefix,
kind => 'command', macroprefix => $macroprefix,
code => $code);
return; });
# \define@cmdkeys[prefix]{keyset}[macroprefix]{keys}[default]
DefPrimitive('\define@cmdkeys[]{}[]{}[]', sub {
my ($stomach, $prefix, $keyset, $macroprefix, $keys, $default) = @_;
$prefix = defined($prefix) ? ToString(Expand($prefix)) : undef;
$keyset = ToString(Expand($keyset));
$macroprefix = defined($macroprefix) ? ToString(Expand($macroprefix)) : undef;
$default = defined($default) ? ToString($default) : undef;
foreach my $key (split(',', ToString($keys))) {
DefKeyVal($keyset, $key, '', $default, prefix => $prefix,
kind => 'command', macroprefix => $macroprefix, code => Tokens()); }
return; });
## ------------
## choice keys
## ------------
# \define@choicekey*+[prefix]{keyset}{key}[bin]{choices}[default]{code}{mismatch}
DefMacro('\define@choicekey OptionalMatch:* OptionalMatch:+ []{}{}[]{}[]{}', sub {
my ($gullet, $star, $plus, $prefix, $keyset, $key, $bin, $choices, $default, $code) = @_;
my @tokens = ();
push(@tokens, T_CS('\\ltx@define@choicekey@int'));
push(@tokens, $star) if defined($star);
push(@tokens, $plus) if defined($plus);
push(@tokens, T_OTHER('['), $prefix, T_OTHER(']')) if defined($prefix);
push(@tokens, T_BEGIN, $keyset, T_END);
push(@tokens, T_BEGIN, $key, T_END);
push(@tokens, T_OTHER('['), $bin, T_OTHER(']')) if defined($bin);
push(@tokens, T_BEGIN, $choices, T_END);
push(@tokens, T_OTHER('['), $default, T_OTHER(']')) if defined($default);
push(@tokens, T_BEGIN, $code, T_END);
# handle the optional mismatch (for the not-plus case)
push(@tokens, T_BEGIN, T_END) unless defined($plus);
@tokens; });
DefPrimitive('\ltx@define@choicekey@int OptionalMatch:* OptionalMatch:+ []{}{}[]{}[]{}{}', sub {
my ($stomach, $star, $plus, $prefix, $keyset, $key, $bin, $choices, $default, $code, $mismatch) = @_;
$prefix = defined($prefix) ? ToString(Expand($prefix)) : undef;
$default = defined($default) ? ToString($default) : undef;
my @thechoices = split(',', ToString($choices));
DefKeyVal(ToString(Expand($keyset)), ToString(Expand($key)), '', $default, prefix => $prefix,
kind => 'choice', normalize => defined($star), choices => [@thechoices],
bin => $bin, code => $code, mismatch => $mismatch);
return; });
## ------------
## bool keys
## ------------
# \define@choicekey*+[prefix]{keyset}[macroprefix]{key}[default]{code}{mismatch}
DefMacro('\define@boolkey OptionalMatch:+ []{}[]{}[]{}', sub {
my ($gullet, $plus, $prefix, $keyset, $macroprefix, $key, $default, $code) = @_;
my @tokens = ();
push(@tokens, T_CS('\\define@boolkey@int'));
push(@tokens, $plus) if defined($plus);
push(@tokens, T_OTHER('['), $prefix, T_OTHER(']')) if defined($prefix);
push(@tokens, T_BEGIN, $keyset, T_END);
push(@tokens, T_OTHER('['), $macroprefix, T_OTHER(']')) if defined($macroprefix);
push(@tokens, T_BEGIN, $key, T_END);
push(@tokens, T_OTHER('['), $default, T_OTHER(']')) if defined($default);
push(@tokens, T_BEGIN, $code, T_END);
# handle the optional mismatch (for the not-plus case)
push(@tokens, T_BEGIN, T_END) unless defined($plus);
@tokens; });
DefPrimitive('\define@boolkey@int OptionalMatch:+ []{}[]{}[]{}{}', sub {
my ($stomach, $plus, $prefix, $keyset, $macroprefix, $key, $default, $code, $mismatch) = @_;
$prefix = defined($prefix) ? ToString(Expand($prefix)) : undef;
$macroprefix = defined($macroprefix) ? ToString(Expand($macroprefix)) : undef;
$default = defined($default) ? ToString($default) : undef;
DefKeyVal(ToString(Expand($keyset)), ToString(Expand($key)), '', $default, prefix => $prefix,
kind => 'boolean', macroprefix => $macroprefix,
code => $code, mismatch => $mismatch);
return; });
# \define@boolkeys[prefix]{keyset}[macroprefix]{keys}[default]
DefPrimitive('\define@boolkeys[]{}[]{}[]', sub {
my ($stomach, $prefix, $keyset, $macroprefix, $keys, $default) = @_;
$prefix = defined($prefix) ? ToString(Expand($prefix)) : undef;
$keyset = ToString(Expand($keyset));
$macroprefix = defined($macroprefix) ? ToString(Expand($macroprefix)) : undef;
$default = defined($default) ? ToString($default) : undef;
foreach my $key (split(',', ToString(Expand($keys)))) {
DefKeyVal($keyset, $key, '', $default, prefix => $prefix,
kind => 'boolean', macroprefix => $macroprefix, code => Tokens()); }
return; });
## ------------
## check for a defined key
## ------------
DefMacro('\key@ifundefined[]{}{}{}{}', sub {
my ($gullet, $prefix, $keysets, $key, $undefined, $defined) = @_;
my $sprefix = defined($prefix) ? ToString(Expand($prefix)) : undef;
my @skeysets = split(',', ToString(Expand($keysets)));
my $skey = ToString(Expand($key));
# if one of the keys exists, we return
foreach my $skeyset (@skeysets) {
if (HasKeyVal($sprefix, $skeyset, $skey)) {
DefMacro('\XKV@tfam', sub { Explode($skeyset); });
return $defined; } }
$undefined; });
## ------------
## disabling keys
## ------------
DefMacro('\disable@keys[]{}{}', sub {
my ($gullet, $prefix, $keyset, $keys) = @_;
my $sprefix = defined($prefix) ? ToString(Expand($prefix)) : undef;
my $skeyset = ToString(Expand($keyset));
my @skeys = split(',', ToString(Expand($keys)));
# if one of the keys exists, we return
foreach my $skey (@skeys) { DisableKeyVal($sprefix, $skeyset, $skey); }
return; });
##
## Option processing
##
# Base-bones parameter type to define an optional argument encased in angle
# brackets <>. Bare-bones implementation, WITHUOUT support for things like
# <Dimension>, ...
# This is used by \DeclareOptionX and friends.
DefParameterType('OptionalAngle', sub {
my ($gullet) = @_;
if ($gullet->ifNext(T_OTHER('<'))) {
$gullet->readToken; # skip <
return $gullet->readUntil(T_OTHER('>')); }
else { return (undef); } },
optional => 1, reversion => sub { ($_[0] ? (T_OTHER('<'), Revert($_[0]), T_OTHER('>')) : ()); });
# Utility function to get the current filename
sub xkeyval_getFileName {
my $name = ToString(Expand(T_CS('\@currname')));
my $ext = ToString(Expand(T_CS('\@currext')));
return $name . '.' . $ext; }
# Get and set the XKV@documentclass and XKV@classoptionslist macros
# This helper function should only be run once
sub xkeyval_setupDocumentClass {
my $fileslst = ToString(Expand(T_CS('\@filelist')));
my @files = split(',', $fileslst);
my $clsext = ToString(Expand(T_CS('\@clsextension')));
my $documentClass = undef;
foreach my $file (@files) {
my ($area, $base, $ext) = pathname_split($file);
if ($ext eq $clsext) {
if (defined $STATE->lookupMeaning(T_CS('\opt@' . $file))) {
DefMacro('\XKV@documentclass', Tokens(Explode($file)));
Let('\XKV@classoptionslist', '\@classoptionslist');
return; } } }
# oops, we did not have a documentclass
Error('undefined', 'xkeyval', 'Package xkeyval loaded before \documentclass');
DefMacro('\XKV@documentclass', Tokens());
DefMacro('\XKV@classoptionslist', Tokens());
return; }
xkeyval_setupDocumentClass();
# checks if we are inside a class file
# by checking if the documentclass is equal to the current file name
sub xkeyval_isInClassFile {
my $documentClass = ToString(Expand(T_CS('\XKV@documentclass')));
my $filename = xkeyval_getFileName();
return $documentClass eq $filename; }
## ------------
## DeclareOptionX
## ------------
# \DeclareOptionX[*]
DefMacro('\DeclareOptionX OptionalMatch:*', sub {
my ($gullet, $star) = @_;
if (defined($star)) { return Tokens(T_CS('\DeclareOptionX@int@star')); }
else { return Tokens(T_CS('\DeclareOptionX@int@normal')); } });
# \DeclareOptionX*{code}
DefMacro('\DeclareOptionX@int@star {}', sub {
my ($gullet, $code) = @_;
DefMacro('\XKV@doxs@int', $code);
DefMacro('\XKV@doxs{}', '\edef\CurrentOption{#1}\XKV@doxs@int');
return; });
# \DeclareOptionX [prefix]<keyset>{key}[default]{function}
DefMacro('\DeclareOptionX@int@normal [] OptionalAngle {}[]{}', sub {
my ($gullet, $prefix, $keyset, $key, $default, $code) = @_;
# defaults may be passed with an empty argument
my @defaults = defined($default) ? ($default) : ();
my @tokens = ();
push(@tokens, T_CS('\\define@key'));
push(@tokens, T_OTHER('['), $prefix, T_OTHER(']')) if defined($prefix);
push(@tokens, T_BEGIN, Tokens(Explode(xkeyval_getFileName)), T_END) unless defined($keyset);
push(@tokens, T_BEGIN, $keyset, T_END) if defined($keyset);
push(@tokens, T_BEGIN, $key, T_END);
push(@tokens, T_OTHER('['), @defaults, T_OTHER(']'));
push(@tokens, T_BEGIN, $code, T_END);
@tokens; });
## ------------
## ExecuteOptionsX
## ------------
# \ExecuteOptionsX [prefix]<keyset>[na]
DefMacro('\ExecuteOptionsX [] OptionalAngle []', sub {
my ($gullet, $prefix, $keyset, $na) = @_;
my @tokens = ();
push(@tokens, T_CS('\\setkeys'));
push(@tokens, T_OTHER('['), $prefix, T_OTHER(']')) if defined($prefix);
push(@tokens, T_BEGIN, Tokens(Explode(xkeyval_getFileName)), T_END) unless defined($keyset);
push(@tokens, T_BEGIN, $keyset, T_END) if defined($keyset);
push(@tokens, T_OTHER('['), $na, T_OTHER(']')) if defined($na);
return @tokens; });
## ------------
## ProcessOptions
## ------------
DefMacro('\ProcessOptionsX OptionalMatch:* [] OptionalAngle []', sub {
my ($gullet, $star, $prefix, $keysets, $skip) = @_;
# find the current file name, options and keysets
my $fileName = xkeyval_getFileName();
$keysets = defined($keysets) ? $keysets : Tokens(Explode($fileName));
my @options = Expand(T_CS('\opt@' . $fileName))->unlist;
# check if we are inside a class file
# and fall back to the basics (if applicable)
$star = undef if (defined($star) && xkeyval_isInClassFile());
# and build the call to the internal macro
my @tokens = ();
push(@tokens, T_CS('\\ProcessOptionsX@int'));
push(@tokens, $star) if defined($star);
push(@tokens, T_OTHER('['), $prefix, T_OTHER(']')) if defined($prefix);
push(@tokens, T_BEGIN, $keysets, T_END) if defined($keysets);
push(@tokens, T_OTHER('['), $skip, T_OTHER(']')) if defined($skip);
push(@tokens, T_BEGIN, Expand(T_CS('\XKV@classoptionslist'))->unlist, T_END) if defined($star);
push(@tokens, T_BEGIN, @options, T_END);
return @tokens; });
# \ExecuteOptionsX[*] [prefix]{}[na]{classoptions}{packageoptions}
DefMacro('\ProcessOptionsX@int OptionalMatch:* [] {} []', sub {
my ($gullet, $star, $prefix, $keysets, $skip) = @_;
# store the missing macros if defined
my $hookMissing = undef;
$hookMissing = T_CS('\XKV@doxs')
if ((defined $star) && (defined $STATE->lookupMeaning(T_CS('\XKV@doxs'))));
# skip processing class options if we are inside a class file.
$star = undef if (defined($star) && xkeyval_isInClassFile());
# we want to read keyvals options
my $keyvals = LaTeXML::Core::KeyVals->new(
$prefix, $keysets,
setAll => 0, setInternals => 1,
skip => $skip, skipMissing => defined($hookMissing),
hookMissing => $hookMissing
);
# read the class options
$keyvals->readFrom($gullet, T_END, silenceMissing => 1) if defined($star);
# read the package options and overwrite the existing ones
$keyvals->readFrom($gullet, T_END);
# read the keys and unset them at the end of the package
my @tokens = ();
push(@tokens, $keyvals->setKeysExpansion);
return @tokens; });
## ------------
## internals
## ------------
# internals that are empty by defaulf
DefMacro(T_CS('\XKV@rm'), '');
DefMacro(T_CS('\XKV@prefix'), '');
DefMacro(T_CS('\XKV@tfam'), '');
DefMacro(T_CS('\XKV@header'), '');
DefMacro(T_CS('\XKV@tkey'), '');
DefMacro(T_CS('\XKV@fams'), '');
DefMacro(T_CS('\XKV@na'), '');
## ------------
## Pointer System (Unsupported)
## ------------
# TODO: The XKeyVal pointer system is currently not supported.
# We throw a warning, when we can try to recover, else we throw an error
DefMacro('\savevalue {}', sub {
my ($gullet, $key) = @_;
Warn('unexpected', T_CS('\savevalue'), undef, "The xkeyval pointer system is currently not supported. ");
return; });
DefMacro('\gsavevalue {}', sub {
my ($gullet, $key) = @_;
Warn('unexpected', T_CS('\gsavevalue'), undef, "The xkeyval pointer system is currently not supported. ");
return $key; });
DefMacro('\savekeys []{}{}', sub {
my ($gullet, $prefix, $keyset, $keys) = @_;
Error('unexpected', T_CS('\savekeys'), undef, "The xkeyval pointer system is currently not supported. ");
return; });
DefMacro('\gsavekeys []{}{}', sub {
my ($gullet, $prefix, $keyset, $keys) = @_;
Error('unexpected', T_CS('\gsavekeys'), undef, "The xkeyval pointer system is currently not supported. ");
return; });
DefMacro('\delsavekeys []{}{}', sub {
my ($gullet, $prefix, $keyset, $keys) = @_;
Error('unexpected', T_CS('\savekeys'), undef, "The xkeyval pointer system is currently not supported. ");
return; });
DefMacro('\gdelsavekeys []{}{}', sub {
my ($gullet, $prefix, $keyset, $keys) = @_;
Error('unexpected', T_CS('\gdelsavekeys'), undef, "The xkeyval pointer system is currently not supported. ");
return; });
DefMacro('\unsavekeys []{}', sub {
my ($gullet, $prefix, $keyset) = @_;
Error('unexpected', T_CS('\unsavekeys'), undef, "The xkeyval pointer system is currently not supported. ");
return; });
DefMacro('\gunsavekeys []{}', sub {
my ($gullet, $prefix, $keyset) = @_;
Error('unexpected', T_CS('\gunsavekeys'), undef, "The xkeyval pointer system is currently not supported. ");
return; });
DefMacro('\usevalue {}', sub {
my ($gullet, $key) = @_;
Error('unexpected', T_CS('\usevalue'), undef, "The xkeyval pointer system is currently not supported. ");
return; });
## ------------
## Presetting keys (Unsupported)
## ------------
# TODO: Presetting keys is currently not supported.
# We throw a warning for all of these, as we can always try to recover
DefMacro('\presetkeys []{}{}{}', sub {
my ($gullet, $prefix, $keyset, $headKeys, $tailKeys) = @_;
Warn('unexpected', T_CS('\presetkeys'), undef, "Presetting keys is currently not supported. ");
return; });
DefMacro('\gpresetkeys []{}{}{}', sub {
my ($gullet, $prefix, $keyset, $headKeys, $tailKeys) = @_;
Warn('unexpected', T_CS('\gpresetkeys'), undef, "Presetting keys is currently not supported. ");
return; });
DefMacro('\delpresetkeys []{}{}{}', sub {
my ($gullet, $prefix, $keyset, $headKeys, $tailKeys) = @_;
Warn('unexpected', T_CS('\delpresetkeys'), undef, "Presetting keys is currently not supported. ");
return; });
DefMacro('\gdelpresetkeys []{}{}{}', sub {
my ($gullet, $prefix, $keyset, $headKeys, $tailKeys) = @_;
Warn('unexpected', T_CS('\gdelpresetkeys'), undef, "Presetting keys is currently not supported. ");
return; });
DefMacro('\unpresetkeys []{}', sub {
my ($gullet, $prefix, $keyset) = @_;
Warn('unexpected', T_CS('\unpresetkeys'), undef, "Presetting keys is currently not supported. ");
return; });
DefMacro('\gunpresetkeys []{}', sub {
my ($gullet, $prefix, $keyset) = @_;
Warn('unexpected', T_CS('\gunpresetkeys'), undef, "Presetting keys is currently not supported. ");
return; });
RawTeX(<<'EoTeX');
\newtoks\XKV@tempa@toks
\long\def\XKV@for@n#1#2#3{%
\XKV@tempa@toks{#1}\edef#2{\the\XKV@tempa@toks}%
\ifx#2\@empty
\XKV@for@break
\else
\expandafter\XKV@f@r
\fi
#2{#3}#1,\@nil,%
}
\long\def\XKV@f@r#1#2#3,{%
\XKV@tempa@toks{#3}\edef#1{\the\XKV@tempa@toks}%
\ifx#1\@nnil
\expandafter\@gobbletwo
\else
#2\expandafter\XKV@f@r
\fi
#1{#2}%
}
\long\def\XKV@for@break #1\@nil,{\fi}
EoTeX
#======================================================================
1;