From Code to Community: Sponsoring The Perl and Raku Conference 2025 Learn more

# /=====================================================================\ #
# | LaTeXML::Core::KeyVal | #
# | Key-Value Defintions in 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> #_# | #
# \=========================================================ooo==U==ooo=/ #
use strict;
our @EXPORT = (
qw(&DefKeyVal &DisableKeyVal &HasKeyVal)
);
#======================================================================
# Exposed Methods
#======================================================================
# Defines a new key-value pair and returns it's header
sub DefKeyVal {
my ($keyset, $key, $type, $default, %options) = @_;
# extract the prefix
my $prefix = $options{prefix};
delete $options{prefix};
# create a new keyval object
my $keyval = LaTeXML::Core::KeyVal->new($prefix, $keyset, $key);
$keyval->define($type, $default, %options);
# return the keyval object (should we need it)
return $keyval; }
# check if a key-value pair is defined
sub HasKeyVal {
my ($prefix, $keyset, $key) = @_;
my $keyval = LaTeXML::Core::KeyVal->new($prefix, $keyset, $key);
return $keyval->isDefined(1); }
# disable a given key-val
sub DisableKeyVal {
my ($prefix, $keyset, $key) = @_;
my $keyval = LaTeXML::Core::KeyVal->new($prefix, $keyset, $key);
return $keyval->disable; }
#======================================================================
# Creating KeyVal objects
#======================================================================
# create new class instance
sub new {
my ($class, $prefix, $keyset, $key) = @_;
return bless {
prefix => defined($prefix) ? ToString($prefix) : 'KV',
keyset => ToString($keyset), key => ToString($key)
}, $class; }
#======================================================================
# Accessors
#======================================================================
sub getPrefix {
my ($self) = @_;
return $$self{prefix}; }
sub getKeySet {
my ($self) = @_;
return $$self{keyset}; }
sub getKey {
my ($self) = @_;
return $$self{key}; }
sub getFullPrefix {
my ($self) = @_;
# if we already have this cached, we can return
return $$self{fullprefix} if defined($$self{fullprefix});
# else, define the full prefix
$$self{fullprefix} = $self->getPrefix . '@' . $self->getKeySet . '@';
# and return it
return $$self{fullprefix}; }
sub getHeader {
my ($self) = @_;
# if we already have this cached, we can return
return $$self{header} if defined($$self{header});
# else, define the header
$$self{header} = $self->getPrefix . '@' . $self->getKeySet . '@' . $self->getKey;
# and return it
return $$self{header}; }
#======================================================================
# Property access
#======================================================================
# set a key-value property for this key
sub setProp {
my ($self, $prop, $value, $scope) = @_;
$STATE->assignValue('KEYVAL@' . $prop . '@' . $self->getHeader, $value, $scope);
return; }
# get a key-value property for this key
sub getProp {
my ($self, $prop) = @_;
return $STATE->lookupValue('KEYVAL@' . $prop . '@' . $self->getHeader); }
sub getType {
my ($self) = @_;
return $$self{type} = $self->getProp('type'); }
sub setType {
my ($self, $type) = @_;
my $prefix = $self->getPrefix;
my $keyset = $self->getKeySet;
my $key = $self->getKey;
# parse the parameters
my $paramlist = LaTeXML::Package::parseParameters($type || "{}", "KeyVal $key in set $keyset with prefix $prefix");
if (scalar(@$paramlist) != 1) {
Warn('unexpected', 'keyval', $key,
"Too many parameters in keyval $key (in set $keyset with prefix $prefix); taking only first", $paramlist); }
my $parameter = $$paramlist[0];
# store the property
$self->setProp('type', $parameter);
return; }
sub getDefault {
my ($self) = @_;
return $self->getProp('default'); }
sub setDefault {
my ($self, $default, $setMacros) = @_;
my @tdefault = LaTeXML::Package::Tokenize($default);
$self->setProp('default', Tokens(@tdefault));
if ($setMacros) {
my $header = $self->getHeader;
LaTeXML::Package::DefMacroI('\\' . $header . '@default', undef,
Tokens(T_CS('\\' . $header), T_BEGIN, @tdefault, T_END));
return; } }
#======================================================================
# Key Definition
#======================================================================
sub isDefined {
my ($self, $checkMacro) = @_;
# start by checking if the defined key exists
return 1 if (defined $self->getProp('exists'));
# check if the macro is given, if it is defined
return (defined $STATE->lookupMeaning(T_CS('\\' . $self->getHeader))) if $checkMacro;
return 0; }
# (re-)define this key
sub define {
my ($self, $type, $default, %options) = @_;
# define that the key exists and is not disabled
$self->setProp('exists', 1);
$self->setProp('disabled', 0);
# set the type
$self->setType($type);
# set the default
$self->setDefault($default, 1) if defined($default);
# figure out the kind of key-val parameter we are defining
my $kind = $options{kind} || 'ordinary';
$self->setProp('kind', $kind);
if ($kind eq 'ordinary') {
$self->defineOrdinary($options{code}); }
elsif ($kind eq 'command') {
$self->defineCommand($options{code}, $options{macroprefix}); }
elsif ($kind eq 'choice') {
$self->defineChoice($options{code}, $options{mismatch},
$options{choices}, ($options{normalize} || 0), $options{bin}); }
elsif ($kind eq 'boolean') {
$self->defineBoolean($options{code}, $options{mismatch}, $options{macroprefix}); }
else { Warn('unknown', undef, "Unknown KeyVals kind $kind, should be one of 'ordinary', 'command', 'choice', 'boolean'. "); }
return; }
sub defineOrdinary {
my ($self, $code) = @_;
LaTeXML::Package::DefMacroI('\\' . $self->getHeader,
LaTeXML::Core::Parameters->new(LaTeXML::Core::Parameter->new('Plain', '{}')),
defined($code) ? $code : '');
return; }
sub defineCommand {
my ($self, $code, $macroprefix) = @_;
# store the header we have to define
my $macroHeader = $macroprefix || ("cmd" . $self->getFullPrefix);
$self->setProp('macroHeader', $macroHeader);
# prefix the original macro with the code
$self->defineOrdinary(sub {
my ($gullet, $value) = @_;
my $orig = '\\ltxml@orig@' . $self->getHeader;
LaTeXML::Package::DefMacroI($orig,
LaTeXML::Core::Parameters->new(LaTeXML::Core::Parameter->new('Plain', '{}')),
$code);
Tokens(
T_CS("\\def"), T_CS('\\' . $macroHeader . $self->getKey), T_BEGIN, $value, T_END,
T_CS($orig), T_BEGIN, T_PARAM, $value, T_END
);
});
return; }
sub defineChoice {
my ($self, $code, $mismatch, $choices, $normalize, $bin) = @_;
# store the choices
$self->setProp('choices', join(',', $choices));
# we might need to 'normalize', i.e. turn labels into lowercase
my $norm;
if ($normalize) { $norm = sub { lc $_[0]; }; }
else { $norm = sub { $_[0]; }; }
$self->setProp('normalize', $normalize ? 0 : 1);
# Unpack macros (if available)
my ($varmacro, $idxmacro) = defined($bin) ? $bin->unlist : (undef, undef);
$self->setProp('varmacro', $varmacro);
$self->setProp('idxmacro', $idxmacro);
# define advanced macro code
$self->defineOrdinary(sub {
my ($gullet, $value) = @_;
# Store the normalized value (if applicable)
my $nvalue = &$norm(ToString($value));
LaTeXML::Package::DefMacro($varmacro, sub { Explode($nvalue); }) if defined($varmacro);
# iterate over the possible choices and store them
my $ochoice;
my $index = 0;
my $valid = 0;
foreach my $choice (@{$choices}) {
if (&$norm(ToString($choice)) eq $nvalue) {
$ochoice = $choice;
$valid = 1;
LaTeXML::Package::DefMacro($idxmacro, Explode(ToString($index))) if defined($idxmacro); }
$index += 1; }
# find a name for the original macro to store in
my @tokens = ();
my $orig = '\\ltxml@orig@' . $self->getHeader;
# if we have chosen a valid index, run $code
if ($valid) {
if (defined($code)) {
LaTeXML::Package::DefMacroI($orig,
LaTeXML::Core::Parameters->new(LaTeXML::Core::Parameter->new('Plain', '{}')),
$code);
push(@tokens, T_CS($orig), T_BEGIN, $value, T_END); } }
# else run $mismatch
elsif (defined($mismatch)) {
LaTeXML::Package::DefMacroI($orig,
LaTeXML::Core::Parameters->new(LaTeXML::Core::Parameter->new('Plain', '{}')),
$mismatch);
push(@tokens, T_CS($orig), T_BEGIN, $value, T_END); }
@tokens; });
return; }
sub defineBoolean {
my ($self, $code, $mismatch, $macroprefix) = @_;
# find the header and define a new conditional
my $macroHeader = ($macroprefix || $self->getFullPrefix) . $self->getKey;
LaTeXML::Package::DefConditional(T_CS("\\if$macroHeader")); # We might need to $scope here
$self->setProp('macroHeader', $macroHeader);
# and define a choice key
$self->defineChoice(sub {
my ($gullet, $value) = @_;
# set the value to true (if needed)
my @tokens = ();
push(@tokens, T_CS('\\' . $macroHeader . (((lc ToString($value)) eq 'true') ? 'true' : 'false')));
# Store and invoke the original macro if needed
if ($code) {
my $orig = '\\ltxml@@rig@' . $self->getHeader;
LaTeXML::Package::DefMacroI($orig,
LaTeXML::Core::Parameters->new(LaTeXML::Core::Parameter->new('Plain', '{}')),
$code);
push(@tokens, T_CS($orig), T_BEGIN, $value, T_END); }
@tokens; },
$mismatch, [("true", "false")], 1);
return; }
sub isDisabled {
my ($self) = @_;
return $self->getProp('disabled', 1); }
sub disable {
my ($self) = @_;
# disable the key
$self->defineOrdinary(sub {
LaTeXML::Package::Tokenize("\\PackageWarning{keyval}{`" . $self->getKey . "' has been disabled. }");
});
$self->setProp('disabled', 1);
return; }
#======================================================================
# Value Related Reversion
#======================================================================
sub setKeysExpansion {
my ($self, $value, $useDefault, $checkExistence, $checkDisabled, $setInternals) = @_;
# if we do not exist, return undef
if ($checkExistence && !($self->isDefined(1))) {
Error(
'undefined', 'Encountered unknown KeyVals key',
"'" . $self->getKey . "' with prefix '" . $self->getPrefix . "' not defined in '" . join(",", $self->getKeySet) . "'");
return; }
# if we are disabled, return an empty tokens
if ($checkDisabled && $self->isDisabled) {
Warn(
'undefined', "`" . $self->getKey . "' has been disabled. ");
return Tokens(); }
# definition of 'xkeyval' internals (if applicable)
my @tokens = $setInternals ? (
T_CS('\def'), T_CS('\XKV@prefix'), T_BEGIN, Explode($self->getPrefix . '@'), T_END,
T_CS('\def'), T_CS('\XKV@tfam'), T_BEGIN, Explode($self->getKeySet), T_END,
T_CS('\def'), T_CS('\XKV@header'), T_BEGIN, Explode($self->getFullPrefix), T_END,
T_CS('\def'), T_CS('\XKV@tkey'), T_BEGIN, Explode($self->getKey), T_END
) : ();
# we have a value given, call the appropriate macro with it
unless ($useDefault) { push(@tokens, T_CS('\\' . $self->getHeader), T_BEGIN, Revert($value), T_END); }
# if it was not given explicitly, call the default macro
else { push(@tokens, T_CS('\\' . $self->getHeader . '@default')); }
# and reset the internals (if applicable)
push(@tokens,
T_CS('\def'), T_CS('\XKV@prefix'), T_BEGIN, T_END,
T_CS('\def'), T_CS('\XKV@tfam'), T_BEGIN, T_END,
T_CS('\def'), T_CS('\XKV@header'), T_BEGIN, T_END,
T_CS('\def'), T_CS('\XKV@tkey'), T_BEGIN, T_END) if $setInternals;
return Tokens(@tokens); }
sub toString {
my ($self) = @_;
return $self->getHeader; }
#======================================================================
1;
__END__
=pod
=head1 NAME
C<LaTeXML::Core::KeyVal> - Key-Value Defintions in LaTeXML
=head1 DESCRIPTION
Provides an interface to define and access KeyVal definition.
Used in conjunction with C<LaTeXML::Core::KeyVals> to fully implement KeyVal
pairs. It extends L<LaTeXML::Common::Object>.
=head2 Exposed Methods
=over 4
=item C<DefKeyVal(I<keyset>, I<key>, I<type>, I<default>, I<%options>); >
Defines a new KeyVal Parameter in the given I<keyset>, I<key> and with optional
prefix I<option{prefix}>. For descriptions of further parameters, see I<LaTeXML::Core::KeyVal::define>.
=item C<HasKeyVal(I<prefix>, I<keyset>, I<key>); >
Checks if the given KeyVal pair exists.
=item C<DisableKeyVal(I<prefix>, I<keyset>, I<key>); >
Disables the given KeyVal so that it can not be used.
=back
=head2 Constructors
=over 4
=item C<<LaTeXML::Core::KeyVal->new(I<preset>, I<keyset>, I<key>); >>
Creates a new I<KeyVal> object. This serves as a simple reference to the given
KeyVal object, regardless of its existence or not.
=back
=head2 KeyVal Accessors
=over 4
=item C<< $prefix = $keyval->getPrefix; >>
Gets the prefix of this KeyVal object.
=item C<< $keyset = $keyval->getKeySet; >>
Gets the keyset of this KeyVal object.
=item C<< $key = $keyval->getKey; >>
Gets the key of this KeyVal object.
=item C<< $prefix = $keyval->getFullPrefix; >>
Gets the full prefix of this keyval object, to be used when composing macros.
=item C<< $prefix = $keyval->getHeader; >>
Gets the header of this keyval object, to be used when composing macros.
=back
=head2 Keyval Property Getters
=over 4
=item C<< $value = $keyval->getProp($prop); >>
Gets the value of a given property of this KeyVal. Intended for internal use
only.
=item C<< $value = $keyval->setProp($prop, $value, $scope); >>
Sets the value of the given property with the given value and scope. Intended
for internal use only.
=item C<< $type = $keyval->getType(); >>
Gets the type of this KeyVal object, as found in $STATE.
=item C<< $keyval->setType($type); >>
Sets the type of this KeyVal object, as found in $STATE.
=item C<< $default = $keyval->getDefault(); >>
Gets the default of this KeyVal object, as found in $STATE.
=item C<< $keyval->setDefault($default, $setMacros); >>
Sets the default of this KeyVal object, and optionally sets the macros as well.
=back
=head2 KeyVal Key Definition
=over 4
=item C<< $defined = $keyval->isDefined($checkMacros); >>
Checks if this KeyVal item is actually defined. If checkMacros is set to true,
also check if macros are defined.
=item C<< $keyval->define($type, $default, %options); >>
(Re-)defines this Key of kind 'kind'.
Defines a keyword I<key> used in keyval arguments for the set I<keyset> and,
and if the option I<code> is given, defines appropriate macros
when used with the I<keyval> package (or extensions thereof).
If I<type> is given, it defines the type of value that must be supplied,
such as C<'Dimension'>. If I<default> is given, that value will be used
when I<key> is used without an equals and explicit value in a keyvals argument.
A I<scope> option can be given, which can be used to defined the key-value pair
globally instead of in the current scope.
Several more I<option>s can be given. These implement the behaviour of the
xkeyval package.
The I<prefix> parameter can be used to configure a custom prefix for
the macros to be defined. The I<kind> parameter can be used to configure special types of xkeyval
pairs.
The 'ordinary' kind behaves like a normal keyval parameter.
The 'command' kind defines a command key, that when run stores the value of the
key in a special macro, which can be further specefied by the I<macroprefix>
option.
The 'choice' kind defines a choice key, which takes additional options
I<choices> (to specify which choices are valid values), I<mismatch> (to be run
if an invalid choice is made) and I<bin> (see xkeyval documentation for
details).
The 'boolean' kind defines a special choice key that takes possible values true and
false, and defines a new Conditional according to the assumed value. The name of
this conditional can be specified with the I<macroprefix> option.
The kind parameter only takes effect when I<code> is given, otherwise only
meta-data is stored.
=item C<< $keyval->defineOrdinary($code); >>
Helper function to define $STATE neccesary for an ordinary key.
=item C<< $keyval->defineCommand($code, $macroprefix); >>
Helper function to define $STATE neccesary for a command key.
=item C<< $keyval->defineChoice($code, $mismatch, $choices, $normalize, $bin); >>
Helper function to define $STATE neccesary for an choice key.
=item C<< $keyval->defineBoolean($code, $mismatch, $macroprefix); >>
Helper function to define $STATE neccesary for a boolean key.
=item C<< disabled = $keyval->isDisabled(); >>
Checks if this keyval property is disabled.
=item C<< $keyval->disable(); >>
Disables this KeyVal object in $STATE.
=back
=head2 Value Related
=over 4
=item C<< $expansion = $keyval->setKeys($value, $useDefault, $checkExistence, $checkDisabled, $setInternals); >>
Expands this KeyVal into a Tokens() to be used with \setkeys.
I<value> contains the value to be used in the expansion, I<useDefault> indicates
if the value argument should be ignored and the default should be used instead.
If I<checkExistence> is set to 1 and the macro does not exist, undef is returned.
If I<checkDisabled> is set to 1 and the macro is disabled, an empty Tokens() is
returns.
If I<$setInternals> is set, sets XKeyVal internal macros.
=item C<< $reversion = $keyval->digest($stomach, $value); >>
Digests this KeyVal with the given stomach and value.
=item C<< $reversion = $keyval->revert($value, $useDefault, $compact, $isFirst, $punct, $assign); >>
Reverts this KeyVal with a given I<value> (or the default if I<IsDefault> is set)
and punctuation and assignment tokens.
If I<compact> is given, spaces will be omitted when possible.
The I<isDefault> parameter indicates if appropriate seperation tokens should be
inserted.
=item C<< $str = $keyval->toString(); >>
Turns this KeyVal object into a string.
=back
=head1 AUTHOR
Tom Wiesing <tom.wiesing@gmail.com>
=head1 COPYRIGHT
Public domain software, produced as part of work done by the
United States Government & not subject to copyright in the US.
=cut