# -*- CPERL -*- # LaTeXML binding for apxproof.sty # # Copyright (C) 2026 by Pierre Senellart # # This file may be distributed and/or modified under the conditions of # the LaTeX Project Public License, either version 1.3 of this license # or (at your option) any later version. # # Strategy: the real apxproof.sty is processed by LaTeXML (its TeX code is # largely digestible), after providing LaTeXML-compatible implementations of # the two support packages that lack workable bindings: environ and catchfile. # The .axp deferral file round-trips through LaTeXML's in-memory \openout / # \write / \input cache, so the appendix is reproduced faithfully. package LaTeXML::Package::Pool; use strict; use warnings; use LaTeXML::Package; #====================================================================== # Shim for environ.sty: \NewEnviron / \RenewEnviron / \BODY #====================================================================== sub axp_end_env { my ($name) = @_; return Tokens(T_CS('\end'), T_BEGIN, ExplodeText($name), T_END); } # Read (balanced) tokens until \end{name}, consuming the \end{name}. # Modeled on readUntilMatch in beamer.cls.ltxml. sub axp_readBody { my ($gullet, $name) = @_; my ($head, @tail) = axp_end_env($name)->unlist; my $tail = Tokens(@tail); my @read = (); while (1) { my $chunk = $gullet->readUntil($head); return unless defined $chunk; push(@read, $chunk->unlist); return Tokens(@read) if defined $gullet->readMatch($tail->clone); push(@read, $head); } } sub axp_DefEnviron { my ($name, $nargs, $default, $code) = @_; $nargs = $nargs ? ToString($nargs) : 0; $nargs =~ s/\D//g; $nargs = int($nargs || 0); my $codecs = T_CS('\environ@code@' . $name); DefMacroI($codecs, convertLaTeXArgs($nargs, undef), $code); DefMacroI(T_CS('\begin{' . $name . '}'), convertLaTeXArgs($nargs, $default), sub { my ($gullet, @args) = @_; my $body = axp_readBody($gullet, $name); if (!defined $body) { Error('expected', "\\end{$name}", $gullet, "Runaway body of environment {$name}"); $body = Tokens(); } DefMacroI('\BODY', undef, $body); @args = map { defined $_ ? $_ : Tokens() } @args; return Tokens(Invocation($codecs, @args)->unlist, axp_end_env($name)->unlist)->unlist; }); DefMacroI(T_CS('\end{' . $name . '}'), undef, Tokens()); return; } DefPrimitive('\NewEnviron{}[][]{}', sub { my ($stomach, $name, $nargs, $default, $code) = @_; axp_DefEnviron(ToString(Expand($name)), $nargs, $default, $code); return; }); DefPrimitive('\RenewEnviron{}[][]{}', sub { my ($stomach, $name, $nargs, $default, $code) = @_; axp_DefEnviron(ToString(Expand($name)), $nargs, $default, $code); return; }); DefMacroI('\BODY', undef, Tokens()); # Mark environ as loaded so that \RequirePackage{environ} in apxproof.sty # does not try to read the raw environ.sty. AssignValue('environ.sty_loaded' => 1, 'global'); Let(T_CS('\ver@environ.sty'), T_CS('\fmtversion'), 'global'); #====================================================================== # Shim for catchfile.sty: \CatchFileDef / \CatchFileEdef #====================================================================== sub axp_file_contents { my ($file) = @_; my $name = ToString(Expand($file)); my $contents = LookupValue($name . '_contents'); if (!defined $contents) { my $path = FindFile($name); if ($path && !ref $path && open(my $fh, '<', $path)) { local $/ = undef; $contents = <$fh>; close($fh); } } return defined $contents ? $contents : ''; } DefPrimitive('\CatchFileDef DefToken {} {}', sub { my ($stomach, $cs, $file, $setup) = @_; DefMacroI($cs, undef, TokenizeInternal(axp_file_contents($file))); return; }); DefPrimitive('\CatchFileEdef DefToken {} {}', sub { my ($stomach, $cs, $file, $setup) = @_; DefMacroI($cs, undef, Tokens(Expand(TokenizeInternal(axp_file_contents($file))))); return; }); AssignValue('catchfile.sty_loaded' => 1, 'global'); Let(T_CS('\ver@catchfile.sty'), T_CS('\fmtversion'), 'global'); #====================================================================== # Load the real apxproof.sty, forwarding the options given by the document #====================================================================== my $axp_name = ToString(Expand(T_CS('\@currname'))); my $axp_opts = LookupValue('opt@' . $axp_name . '.sty') || []; # The synctex option makes apxproof re-read appendix material from the # source files with the primitive \input, which is pointless without # SyncTeX output and not something LaTeXML's emulation is meant to do: # drop it, so that the material is copied to the .axp file as usual. $axp_opts = [grep { !/^\s*synctex\s*(=|$)/ } @$axp_opts]; # Set up the package loading context by hand rather than through # InputDefinitions' handleoptions: the latter stores \@currext and the # option list with all-OTHER catcodes, which breaks the \ifx-based # comparisons in kvoptions (\ProcessLocalKeyvalOptions) and in apxproof's # own option validation (\equal of ifthen). my $axp_prevname = LookupDefinition(T_CS('\@currname')) && ToString(Expand(T_CS('\@currname'))); my $axp_prevext = LookupDefinition(T_CS('\@currext')) && ToString(Expand(T_CS('\@currext'))); DefMacroI('\@currname', undef, TokenizeInternal($axp_name)); DefMacroI('\@currext', undef, TokenizeInternal('sty')); DefMacroI(T_CS('\opt@' . $axp_name . '.sty'), undef, TokenizeInternal(join(',', @$axp_opts))); InputDefinitions($axp_name, type => 'sty', noltxml => 1); Let(T_CS('\ver@apxproof.sty'), T_CS('\fmtversion'), 'global'); DefMacroI('\@currname', undef, TokenizeInternal($axp_prevname)) if $axp_prevname; DefMacroI('\@currext', undef, TokenizeInternal($axp_prevext)) if $axp_prevext; #====================================================================== # LaTeXML-specific fixups #====================================================================== # apxproof replays the deferred appendix material from \@enddocumenthook # (patched via \pretocmd), but LaTeXML's \end{document} never executes that # kernel hook; run it first among the end-of-document actions, as in LaTeX. UnshiftValue('@at@end@document', T_CS('\@enddocumenthook')); # LaTeXML's \write serializes \noexpand-protected expandable macros as # \special_relax, losing their names. Rebinding \noexpand to \string during # the expansion reproduces TeX's behavior of writing the token name verbatim. DefPrimitive('\write Number {}', sub { my ($stomach, $port, $tokens) = @_; $port = ToString($port); $STATE->pushFrame; Let(T_CS('\noexpand'), T_CS('\string')); my $line = UnTeX(Expand($tokens), 1); $STATE->popFrame; if (my $filename = LookupValue('output_file:' . $port)) { my $handle = $filename . '_contents'; my $contents = LookupValue($handle); AssignValue($handle => ($contents // '') . $line . "\n", 'global'); } else { Note($line); } return; }); # LaTeXML locks \section against redefinition from raw TeX, so apxproof's # section-tracking wrapper (which records titles for \appendixsectionformat) # was silently dropped; reinstate it at the binding level. \axp@oldsection # already holds the original sectioning command. if (LookupDefinition(T_CS('\axp@oldsection'))) { DefMacroI('\section', undef, TokenizeInternal('\@ifstar\@section\@@section')); } # apxproof injects \noproofinappendix at the start of every rep-base theorem # environment via \pretocmd on \, but LaTeXML implements theorem # environments as \begin{} constructors, so that patch never fires. # Hook the constructor's beforeDigest daemons instead, and reformat the # appendix restatement head from "Theorem (1)." to "Theorem 1." (the effect # of the \thmhead patching that LaTeXML's amsthm does not go through). DefPrimitive('\axp@lxml@hookthm{}', sub { my ($stomach, $name) = @_; $name = ToString(Expand($name)); my $defn = LookupDefinition(T_CS('\begin{' . $name . '}')); if ($defn) { $$defn{beforeDigest} ||= []; unshift(@{ $$defn{beforeDigest} }, sub { Digest(T_CS('\noproofinappendix')); return; }); } my $rp = 'axp@' . $name . 'rp'; if (LookupDefinition(T_CS('\begin{' . $rp . '}'))) { DefMacroI('\format@title@' . $rp, convertLaTeXArgs(1, 0), TokenizeInternal('{\the\thm@headfont\lx@tag{\csname lx@name@' . $rp . '\endcsname\space#1}\the\thm@headpunct}')); } return; }); RawTeX('\makeatletter\ifdefined\axp@forward@setup' . '\let\axp@lxml@origfwsetup\axp@forward@setup' . '\def\axp@forward@setup#1#2#3#4{' . '\axp@lxml@origfwsetup{#1}{#2}{#3}{#4}\axp@lxml@hookthm{#1}}' . '\fi\makeatother'); # LaTeXML's hyperref binding emits \hyperlink's target name raw as idref, # while \hypertarget sanitizes its id through CleanID; the two never match # for names like axp@fw@ri, so forward links get dropped at cross-referencing. # Redefine \hyperlink with a CleanID'd idref; done at begin-document since # hyperref is commonly loaded after apxproof. DefPrimitive('\axp@lxml@fixhyperlink', sub { if (LookupDefinition(T_CS('\hyperlink'))) { DefConstructor('\hyperlink Semiverbatim {}', "#2", properties => sub { (target => CleanID($_[1])); }); } return; }); PushValue('@at@begin@document', T_CS('\axp@lxml@fixhyperlink')); 1;