# -*- mode: Perl -*-
# /=====================================================================\ #
# |  latexml.ltxml                                                      | #
# | Style file for latexml documents                                    | #
# |=====================================================================| #
# | 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;
no warnings 'redefine';    # ????

#======================================================================
# LaTeXML Implementation of latexml customization bindings
#  * controlling various conversion options
#  * presentation customization
#  * semantic enhancement macros
#  * exposing internal functionality
#======================================================================

DefConditional('\iflatexml', sub { 1; });
DefPrimitive('\latexmltrue', sub {
    Warning('unexpected', '\\latexmltrue', $_[0], "Cannot change \\iflatexml!"); });
DefPrimitive('\latexmlfalse', sub {
    Warning('unexpected', '\\latexmlfalse', $_[0], "Cannot change \\iflatexml!"); });

#======================================================================
# Package Options
DeclareOption('ids',   sub { AssignValue('GENERATE_IDS' => 1, 'global'); });
DeclareOption('noids', sub { AssignValue('GENERATE_IDS' => 0, 'global'); });

DeclareOption('comments',   sub { AssignValue('INCLUDE_COMMENTS' => 1, 'global'); });
DeclareOption('nocomments', sub { AssignValue('INCLUDE_COMMENTS' => 0, 'global'); });

DeclareOption('profiling',   sub { AssignValue('PROFILING' => 1, 'global'); });
DeclareOption('noprofiling', sub { AssignValue('PROFILING' => 0, 'global'); });

DeclareOption('mathparserspeculate',   sub { AssignValue('MATHPARSER_SPECULATE' => 1, 'global'); });
DeclareOption('nomathparserspeculate', sub { AssignValue('MATHPARSER_SPECULATE' => 0, 'global'); });

# 'nobibtex' intended to be used for arXiv-like build harnesses, where there
# is explicit instruction to only use ".bbl" and that bibtex will not be ran.
DeclareOption('bibtex',   sub { AssignValue('NO_BIBTEX' => 0, 'global'); });
DeclareOption('nobibtex', sub { AssignValue('NO_BIBTEX' => 1, 'global'); });

# Lexeme serialization for math formulas
DeclareOption('mathlexemes', sub { AssignValue('LEXEMATIZE_MATH' => 1, 'global'); });

# Finer control over which (if any) raw .sty/.cls files to include
DeclareOption('rawstyles',      sub { AssignValue('INCLUDE_STYLES'  => 1,             'global'); });
DeclareOption('localrawstyles', sub { AssignValue('INCLUDE_STYLES'  => 'searchpaths', 'global'); });
DeclareOption('norawstyles',    sub { AssignValue('INCLUDE_STYLES'  => 0,             'global'); });
DeclareOption('rawclasses',     sub { AssignValue('INCLUDE_CLASSES' => 1,             'global'); });
DeclareOption('localrawclasses', sub { AssignValue('INCLUDE_CLASSES' => 'searchpaths', 'global'); });
DeclareOption('norawclasses', sub { AssignValue('INCLUDE_CLASSES' => 0, 'global'); });

ProcessOptions();
#======================================================================
# From latexml.sty
# [does this really belong here? or should this be disableable?]
DefConstructor('\URL[] Verbatim', "<ltx:ref href='#href'>?#1(#1)(#href)</ltx:ref>",
  properties => sub { (href => CleanURL($_[2])); });
DefMacro('\XML',      '\URL[\texttt{XML}]{http://www.w3.org/XML/}');
DefMacro('\SGML',     '\URL[\texttt{HTML}]{http://www.w3.org/MarkUp/SGML/}');
DefMacro('\HTML',     '\URL[\texttt{HTML}]{http://www.w3.org/html/}');
DefMacro('\XHTML',    '\URL[\texttt{XHTML}]{http://www.w3.org/TR/xhtml11/}');
DefMacro('\XSLT',     '\URL[\texttt{XSLT}]{http://www.w3.org/Style/XSL/}');
DefMacro('\CSS',      '\URL[\texttt{CSS}]{http://www.w3.org/Style/CSS/}');
DefMacro('\MathML',   '\URL[\texttt{MathML}]{http://www.w3.org/Math/}');
DefMacro('\OpenMath', '\URL[\texttt{OpenMath}]{http://www.openmath.org/}');
DefMacro('\LaTeXML',  '\URL[\texttt{LaTeXML}]{http://dlmf.nist.gov/LaTeXML/}');
#DefMacro('\BibTeX','BibTeX');

#======================================================================
# id related features

# Set the id to used for the top-level document
DefMacro('\lxDocumentID{}', '\def\thedocument@ID{#1}');

# \LXMID{id}{math}  Associate the identifier id with the given math expression.
DefMacro('\LXMID{}{}', '\lx@xmarg{#1}{#2}');

# \LXRef{id} Refer to the math expression associated with id.
# In presentation, this is similar to using a shorthand macro.
# In content situations, an XMRef is generated.
DefMacro('\LXMRef{}', '\lx@xmref{#1}');

#======================================================================
# class related features
Let('\lxAddClass', '\@ADDCLASS');

DefConstructor('\lxAddClass Semiverbatim', sub {
    $_[0]->addClass($_[0]->getElement, ToString($_[1])); });

DefConstructor('\lxWithClass Semiverbatim {}', sub {
    my ($document, $class, $box) = @_;
    my $context = $document->getElement;    # Where we originally start inserting.
    my @nodes   = ();
    if (isTextNode($document->getNode)) {
      push(@nodes, $document->openElement('ltx:text')); }
    push(@nodes, $document->absorb($box));
    @nodes = $document->filterChildren($document->filterDeletions(@nodes));
    $document->closeToNode($context);
    $document->addClass($nodes[0], ToString($class)) if @nodes; });

#======================================================================
# links
# Similar to stuff from hyperref, but more straightforward
DefConstructor('\lxRef Semiverbatim {}',
  "<ltx:ref labelref='#label'>#2</ltx:ref>",
  properties => sub { (label => CleanLabel($_[1])); });

#======================================================================
# Resources
# \lxResource[options]{name}
#  RequireResource($name,%options);

#======================================================================
# Page customization
#  options to create or customize
#    navbar: full content, context TOC, ...
#    headers, footers

DefMacro('\lxKeywords{}',
  '\@add@frontmatter{ltx:keywords}[name={keywords}]{#1}');

DefConstructor('\lxContextTOC',
  "<ltx:TOC format='context'/>");

AssignValue('navigation' => [], 'global');

sub insertNavigation {
  my ($document) = @_;
  if (my @items = @{ LookupValue('navigation') }) {
    $document->appendTree($document->getDocument->documentElement,
      ['ltx:navigation', {}, @items]); }
  return; }

Tag('ltx:document', 'afterClose' => \&insertNavigation);

DefEnvironment('{lxNavbar}', sub { },
  beforeDigest    => sub { AssignValue(inPreamble => 0); },
  beforeConstruct => sub {
    my ($document, $whatsit) = @_;
    PushValue('navigation',
      ['ltx:inline-para', { class => 'ltx_page_navbar' }, $whatsit->getBody]);
    return; });

# Of course, it would be more interesting to supply a "template"
# for header & footer that would show where the next link goes,
# rather than predict what the next link will be! (after splitting!)
# Repeated header/footers should give multiple header/footer lines ?
# or do they just arrange the lines within it?
DefEnvironment('{lxHeader}', sub { },
  beforeDigest    => sub { AssignValue(inPreamble => 0); },
  beforeConstruct => sub {
    my ($document, $whatsit) = @_;
    PushValue('navigation',
      ['ltx:inline-para', { class => 'ltx_page_header' }, $whatsit->getBody]);
    return; });
DefEnvironment('{lxFooter}', sub { },
  beforeDigest    => sub { AssignValue(inPreamble => 0); },
  beforeConstruct => sub {
    my ($document, $whatsit) = @_;
    PushValue('navigation',
      ['ltx:inline-para', { class => 'ltx_page_footer' }, $whatsit->getBody]);
    return; });

#======================================================================

# Table beautification.
# Low-level support to mark column and row headers.
# This really calls for styling, but why should we get into that game?
# There are many other packages for that.

# To mark the table head/foot (table column headers)
# Put this before first table heading row
DefMacroI('\lxBeginTableHead', undef, '\@tabular@begin@heading');
# put this after the \\ ending the table heading.
DefMacroI('\lxEndTableHead', undef, '\@tabular@end@heading');

# Ditto for table foot (last rows in table)
DefMacroI('\lxBeginTableFoot', undef, '\@tabular@begin@heading');
# put this after the \\ ending the table heading.
DefMacroI('\lxEndTableFoot', undef, '\@tabular@end@heading');

# To mark an individual cell as a column header
DefMacroI('\lxTableColumnHead', undef, sub {
    if (my $alignment = LookupValue('Alignment')) {
      $alignment->currentColumn->{thead}{column} = 1; }
    return; });
# To mark an individual cell as a row header
DefMacroI('\lxTableRowHead', undef, sub {
    if (my $alignment = LookupValue('Alignment')) {
      $alignment->currentColumn->{thead}{row} = 1; }
    return; });
# Easy way to mark a whole column as row headers:
#  \usepackage{array}
# then put this in the column spec
#  >{\lxTableRowHead}

#%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
# Declarative information for Mathematics
# particularly those that assist parsing.
#%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%

#======================================================================
# Marking the type of particular instances of a symbol.

# \LxFcn{f} treat f as a function here (only).
DefConstructor('\lxFcn{}', "<ltx:XMWrap role='FUNCTION'>#1</ltx:XMWrap>",
  requireMath => 1, reversion => '#1', alias => '');
DefConstructor('\lxID{}', "<ltx:XMWrap role='ID'>#1</ltx:XMWrap>",
  requireMath => 1, reversion => '#1', alias => '');
DefConstructor('\lxPunct{}', "<ltx:XMWrap role='PUNCT'>#1</ltx:XMWrap>",
  requireMath => 1, reversion => '#1', alias => '');

# More general form: eg. \lxTweakMath{role=POSTFIX}{@}
# [same as \lx@math@tweak]
DefConstructor('\lxMathTweak RequiredKeyVals {}',
  "<ltx:XMWrap %&GetKeyVals(#1)>#2</ltx:XMWrap>",
  afterDigest => sub {
    my ($stomach, $whatsit) = @_;
    my ($kv,      $body)    = $whatsit->getArgs;
    $whatsit->setProperties($kv->getPairs);
    $whatsit->setFont($body->getFont);
    return; },
  reversion => '#2');

#======================================================================
# Define a mathematical object with both presentation & content information
#   \lxDefMath{\name}[nargs][optional]{presentation body}[content keywords]
#  The first part is essentially equivalent to \newcommand, it defines
# an expansion for \name used for the presentation.
# The content keywords are used to define the semantics of the object.
# See DefMath in LaTeXML::Package for more information.
DefPrimitive('\lxDefMath{}[Number][]{} OptionalKeyVals:XMath', sub {
    my ($stomach, $cs, $nargs, $opt, $presentation, $params) = @_;
    my ($name, $meaning, $cd, $role, $alias, $scope) =
      $params && map { $_ && ToString($_) } map { $params->getValue($_) }
      qw(name meaning cd role alias scope);
    my $needsid = $params && ($params->getValue('tag') || $params->getValue('description'));
    my $id = ($needsid ? next_declaration_id() : undef);
    DefMathI($cs, convertLaTeXArgs($nargs, $opt), $presentation,
      name  => $name,  meaning => $meaning, omcd      => $cd, role => $role, alias => $alias,
      scope => $scope, decl_id => $id,      revert_as => 'context');
    if ($needsid) {    # Also provide for decl_id hook for definition links.
      return Digest(Invocation('\@lxDefMathDeclare', $id, $params)); }
    else {
      return; }
});

DefConstructor('\@lxDefMathDeclare{} RequiredKeyVals:XMath', sub {
    my ($document, $id, $kv, %props) = @_;
    my $save = $document->floatToElement('ltx:declare');
    $document->openElement('ltx:declare', 'xml:id' => $id);
    if ($props{term} || $props{short}) {
      $document->openElement('ltx:tags');
      $document->insertElement('ltx:tag', $props{term},  role => 'term')  if $props{term};
      $document->insertElement('ltx:tag', $props{short}, role => 'short') if $props{short};
      $document->closeElement('ltx:tags'); }
    if (my $description = $props{description}) {
      $document->insertElement('ltx:text', $description); }
    $document->closeElement('ltx:declare');
    $document->setNode($save); },
  mode        => 'text',
  afterDigest => sub { my ($stomach, $whatsit) = @_;
    my ($id, $kv) = $whatsit->getArgs;
    normalizeDeclareKeys($kv, $whatsit);
    return; },
  properties => { alignmentSkippable => 1 },
  reversion  => '');

# We're interested in getting some useful phrases for several contexts:
#   Notations lists:  $term$ : description
#   [where layout may require substituting some markup for the :, so we'll want to split them]
#   tooltips: short (name), probably text preferred.
# And we want to synthesize these out of two keyvals: tag,description.
# If both are given tag is shortname, description is long form, possibly separating out the term
# If only 1 given, and matches the math term: record it, and use remainder for name & description
# Else use the given for name, description, with no term
sub normalizeDeclareKeys {
  my ($kv, $whatsit) = @_;
  my $tag         = $kv->getValue('tag');
  my $description = $kv->getValue('description');
  my ($term, $short, $desc);
  if (my $stuff = $description || $tag) {
    ($term, $desc) = splitDeclareTag($stuff); }
  $short = ($description ? $tag || $desc : undef);
  $desc = $desc || $description || $tag;
  $whatsit->setProperties(term => $term, short => $short, description => $desc);
  return; }

# Temporary(?) Hack for DLMF: Split an \lxDeclare tag of the form tag={math: description}
sub splitDeclareTag {
  my ($tag) = @_;
  my @boxes = $tag->unlist;
  my @tag   = ();
  # Or should we collect initial math box?
  while (@boxes && ($boxes[0]->getString ne ':')) {
    push(@tag, shift(@boxes)); }
  if (@boxes) {
    shift(@boxes);
    return (List(@tag), List(@boxes)); }
  else {
    return; } }

#======================================================================
# NOTE: I'm concerned about the order of applying these filters.
# even though it seems right so far.

# Keyword options:
#    scope=<scope> : Specifies the scope of the declaration, ie. to what portion of
#            the document the declarations apply
#            You can specify one of the counters associated with sections,
#            equations, etc.
#            If unspecified, the declaration is scoped to the current unit.
#            Note that this applies to equations, as well.
#    label=<label> : assigns a label to the declaration so that it can be reused
#            at another point in the document (with \lxRefDeclaration), particularly when
#            that point is not otherwise within the scope of the original declaration.
# To effect the declaration:
#    role=<role>  : Assigns a grammatical role to the matched item for parsing.
#    name=<name>  : Assigns a name to the matched item.
#    meaning=<meaning>  : Assigns a semantic name to the matched item.
#  Alternatively, use
#     replace : provides a replacement for the matched expression, rather than adding attributes.

# Potential keywords/operations needed(?)
#   nodef : inhibits the marking of the current point as the `definition' of the expression.
#          (a ref declaration would normally not be a def anyway)

DefKeyVal('Declare', 'nowrap', '{}', 1);
DefKeyVal('Declare', 'trace',  '{}', 1);
DefKeyVal('Declare', 'replace', 'UndigestedKey');

our $declare_keys = { scope => 1, role => 1, tag => 1, description => 1, name => 1, meaning => 1,
  trace => 1, nowrap => 1, replace => 1, label => 1 };
# Most is same as above; merge into one!!!!!
DefConstructor('\lxDeclare OptionalMatch:* OptionalKeyVals:Declare {}', sub {
    my ($document, $flag, $kv, $pattern, %props) = @_;
    if (my $id = $props{id}) {
      my $save = $document->floatToElement('ltx:declare');
      $document->openElement('ltx:declare', 'xml:id' => $id);
      if ($props{term} || $props{short}) {
        $document->openElement('ltx:tags');
        $document->insertElement('ltx:tag', $props{term},  role => 'term')  if $props{term};
        $document->insertElement('ltx:tag', $props{short}, role => 'short') if $props{short};
        $document->closeElement('ltx:tags'); }
      if (my $description = $props{description}) {
        $document->insertElement('ltx:text', $description); }
      $document->closeElement('ltx:declare');
      $document->setNode($save); } },
  mode => 'text',
  # Screwy bit to `neutralize' the font in the matches.
  beforeDigest => sub { AssignValue(font => LaTeXML::Common::Font->textDefault(), 'local');
    AssignValue(mathfont => LaTeXML::Common::Font->mathDefault(), 'local');
    return; },
  afterDigest => sub { my ($stomach, $whatsit) = @_;
    my ($star, $kv, $pattern) = $whatsit->getArgs;
    return unless $kv;
    CheckOptions("\\lxDeclare keys", $declare_keys, %{ $kv->getKeyVals });
    foreach my $key (qw(role tag name meaning replace)) {
      if (my $value = $kv->getValue($key)) {
        Warn('unexpected', $key, $stomach,
          "Repeated $key: " . join('; ', map { Stringify($_) } @$value))
          if ref $value eq 'ARRAY'; } }
    my $id = ($kv->getValue('tag') || $kv->getValue('description') ? next_declaration_id() : undef);
    if ($id && LookupValue('InPreamble')) {
      Warn('unexpected', 'tag', $stomach,
        "Declaration with tag cannot appear in preamble"
          . Stringify($whatsit)); }
    # Temporary(?) Hack: If no description, bui tag is of form <math>: text
    # make description = tag, and tag be only the shorter text part
    $whatsit->setProperties(scope => getDeclarationScope($kv),
      role    => ToString($kv->getValue('role')),
      name    => ToString($kv->getValue('name')),
      meaning => ToString($kv->getValue('meaning')),
      trace   => defined $kv->getValue('trace'),
      nowrap  => defined $kv->getValue('nowrap'),
      id      => $id,
      match   => $pattern,
      replace => $kv->getValue('replace'));
    normalizeDeclareKeys($kv, $whatsit);

    if (my $label = ToString($kv->getValue('label'))) {
      PushValue("Declaration_$label", $whatsit); }
    return; },
  afterConstruct => sub { my ($document, $whatsit) = @_;
    my $scope = $whatsit->getProperty('scope');
    createDeclarationRewrite($document, $scope, $whatsit); },
  properties => { alignmentSkippable => 1 },
  reversion  => '');

DefConstructor('\lxRefDeclaration OptionalKeyVals:Declare {}', '',
  afterDigest => sub { my ($stomach, $whatsit) = @_;
    my ($keys, $labels) = $whatsit->getArgs;
    $whatsit->setProperties(scope => getDeclarationScope($keys),
      labels => [split(',', ToString($labels))]); },
  afterConstruct => sub { my ($document, $whatsit) = @_;
    my $scope = $whatsit->getProperty('scope');
    foreach my $label (@{ $whatsit->getProperty('labels') }) {
      if (my $declaration = LookupValue("Declaration_$label")) {
        map { createDeclarationRewrite($document, $scope, $_) } @$declaration; }
      else {
        Warn('unexpected', $label, $document,
          "No Declaration with label=$label was found"); } } },
  properties => { alignmentSkippable => 1 },
  reversion  => '');

NewCounter('@XMDECL', 'section', idprefix => 'XMD');

sub next_declaration_id {
  StepCounter('@XMDECL');
  DefMacroI(T_CS('\@@XMDECL@ID'), undef,
    Tokens(Explode(LookupValue('\c@@XMDECL')->valueOf)),
    scope => 'global');
  return ToString(Expand(T_CS('\the@XMDECL@ID'))); }

sub getDeclarationScope {
  my ($keys) = @_;
  # Sort out the scope.
  my $scope = $keys && $keys->getValue('scope');
  $scope = ($scope ? ToString($scope) : LookupValue('current_counter'));
  if ($scope && LookupValue("\\c\@$scope")) {    # Scope is some counter.
    $scope = "id:" . ToString(Digest(Expand(T_CS("\\the$scope\@ID")))); }
  return $scope; }

sub createDeclarationRewrite {
  my ($document, $scope, $whatsit) = @_;
  my %props = $whatsit->getProperties;
  my ($id, $match, $nowrap, $role, $name, $meaning, $ref, $trace, $replace)
    = map { $props{$_} } qw(id match nowrap role name meaning ref trace replace);
  # Put this rule IN FRONT of other rules!
  UnshiftValue('DOCUMENT_REWRITE_RULES',
    LaTeXML::Core::Rewrite->new('math',
      ($trace ? (trace => $trace) : ()),
      ($scope ? (scope => $scope) : ()),
      ($match ? (match => $match) : ()),
      ($replace
        ? (replace => $replace)
        : attributes => { ($role ? (role => $role) : ()),
          ($name    ? (name    => $name)    : ()),
          ($meaning ? (meaning => $meaning) : ()),
          ($id      ? (decl_id => $id)      : ()),
          ($nowrap  ? (_nowrap => $nowrap)  : ()),
        }),
    ));
  return; }

#======================================================================
1;
