From 93f875a6e1374526288d814089cc2093cb114985 Mon Sep 17 00:00:00 2001 From: Deyan Ginev Date: Wed, 11 Mar 2026 19:51:13 -0400 Subject: [PATCH 1/2] fix tcolorbox support and related color/hooks issues - tcolorbox: pre-define \tcb@use@autoparskip before raw TeX loading, since pgfkeys initialization may not complete and the \AtBeginDocument hook at tcolorbox.sty:1142 would call it undefined. - color: define \current@color, \default@color, and \reset@color with safe defaults. These DVI color macros were previously left undefined, causing errors in pgf/tikz color processing. - latex_constructs: support optional [label] argument in \AtBeginDocument and \AtEndDocument, matching the modern LaTeX hooks system (since 2020/10/01). Without this, the [label] leaked as literal text into the document output. Co-Authored-By: Claude Opus 4.6 --- lib/LaTeXML/Engine/latex_constructs.pool.ltxml | 9 +++++---- lib/LaTeXML/Package/color.sty.ltxml | 10 +++++++--- lib/LaTeXML/Package/tcolorbox.sty.ltxml | 5 +++-- 3 files changed, 15 insertions(+), 9 deletions(-) diff --git a/lib/LaTeXML/Engine/latex_constructs.pool.ltxml b/lib/LaTeXML/Engine/latex_constructs.pool.ltxml index 97d5b676cd..c9997ea4f2 100644 --- a/lib/LaTeXML/Engine/latex_constructs.pool.ltxml +++ b/lib/LaTeXML/Engine/latex_constructs.pool.ltxml @@ -292,10 +292,11 @@ DefMacro('\@icentercr[]', '\vskip #1\ignorespaces'); # text # \end{document} -DefMacro('\AtBeginDocument{}', sub { - PushValue('@at@begin@document', $_[1]->unlist); }); -DefMacro('\AtEndDocument{}', sub { - PushValue('@at@end@document', $_[1]->unlist); }); +# Support optional [label] argument from modern LaTeX hooks system (e.g. \AtBeginDocument[tcolorbox]{...}) +DefMacro('\AtBeginDocument[]{}', sub { + PushValue('@at@begin@document', $_[2]->unlist); }); +DefMacro('\AtEndDocument[]{}', sub { + PushValue('@at@end@document', $_[2]->unlist); }); # Like "#body", # But more complicated due to id, at begin/end document and so forth. diff --git a/lib/LaTeXML/Package/color.sty.ltxml b/lib/LaTeXML/Package/color.sty.ltxml index a97269b7ce..1e7a1a2758 100644 --- a/lib/LaTeXML/Package/color.sty.ltxml +++ b/lib/LaTeXML/Package/color.sty.ltxml @@ -108,7 +108,7 @@ DefMacro('\colorbox[]{}{}', '\hbox{\ifx.#1.\pagecolor{#2}\else\pagecolor[#1]{#2} DefConstructor('\fcolorbox[]{}{} Undigested', "#text", - mode => 'internal_vertical', + mode => 'internal_vertical', afterDigest => sub { my ($stomach, $whatsit) = @_; my ($model, $fspec, $bspec, $text) = $whatsit->getArgs; @@ -119,8 +119,12 @@ DefConstructor('\fcolorbox[]{}{} Undigested', #******************************************************************************** # Low-level stuff; redefined from LaTeX stubs -# Not sure what \current@color should return... the string form? -# \current@color +# \current@color holds the current color specification for the DVI driver. +# For LaTeXML, provide a safe default so packages using it (e.g. pgf/tikz) don't error. +# The value must NOT contain braces since it may appear inside \csname...\endcsname. +DefMacroI('\current@color', undef, '0 0 0'); +DefMacroI('\default@color', undef, '0 0 0'); +DefMacroI('\reset@color', undef, ''); # Similarly, I'm not sure what set@color needs to do that isn't redundant DefMacroI('\set@color', undef, ''); diff --git a/lib/LaTeXML/Package/tcolorbox.sty.ltxml b/lib/LaTeXML/Package/tcolorbox.sty.ltxml index acca67778d..b9ca83ddaf 100644 --- a/lib/LaTeXML/Package/tcolorbox.sty.ltxml +++ b/lib/LaTeXML/Package/tcolorbox.sty.ltxml @@ -20,8 +20,9 @@ DefRegister('\doublecol@number' => Number(0)); # Ensure only unbreakable mode is possible DefMacro('\tcb@init@breakable', '\tcb@init@unbreakable', locked => 1); -RequirePackage('expl3'); -RequirePackage('xparse'); +# Pre-define \tcb@use@autoparskip before loading the raw tcolorbox.sty, +# as a safety net in case the pgfkeys initialization doesn't fully complete. +DefMacroI('\tcb@use@autoparskip', undef, '\relax'); InputDefinitions('tcolorbox', type => 'sty', noltxml => 1); From 09fb2e6f0d201c1cebe6383d49040b24b4f85cc7 Mon Sep 17 00:00:00 2001 From: Deyan Ginev Date: Wed, 11 Mar 2026 23:28:02 -0400 Subject: [PATCH 2/2] fix \hphantom display math leak in text/horizontal mode In text/horizontal mode, wrap \hphantom argument digestion in restricted_horizontal mode (matching TeX's \hbox behavior) to prevent display math from leaking through the argument. This fixes cascading errors with complex TikZ library expansions (e.g. quantikz2). In math mode, preserve the current mode for correct font metrics. Co-Authored-By: Claude Opus 4.6 --- lib/LaTeXML/Engine/math_common.pool.ltxml | 43 +++++++++++++++-------- 1 file changed, 29 insertions(+), 14 deletions(-) diff --git a/lib/LaTeXML/Engine/math_common.pool.ltxml b/lib/LaTeXML/Engine/math_common.pool.ltxml index d1508bc4da..276ee1bce1 100644 --- a/lib/LaTeXML/Engine/math_common.pool.ltxml +++ b/lib/LaTeXML/Engine/math_common.pool.ltxml @@ -356,7 +356,7 @@ DefRewrite(select => ["descendant-or-self::ltx:XMTok[text()='\x{FF0F}' and \@mea if (my $id = $not->getAttribute('xml:id')) { foreach my $n ($doc->findnodes("descendant-or-self::ltx:XMRef[\@idref='$id']")) { $doc->removeNode($n); } } # ? Hopefully this is safe. -} }); + } }); #---------------------------------------------------------------------- # \joinrel @@ -400,7 +400,7 @@ DefConstructor('\@@joinrel{}{}', sub { my $role = (scalar(keys %roles) == 1 ? [keys %roles]->[0] : ($roles{ARROW} ? 'ARROW' : 'RELOP')); map { $node->removeChild($_) } @rels; $document->insertElement('ltx:XMTok', [map { $_->textContent } @rels], role => $role); - } } }, + } } }, reversion => '#1\joinrel #2'); #---------------------------------------------------------------------- @@ -468,9 +468,9 @@ DefConstructorI('\lx@ldots', undef, # And so can \vdots DefConstructorI('\vdots', undef, "?#isMath(\x{22EE})(\x{22EE})", - sizer => "\x{22EE}", + sizer => "\x{22EE}", enterHorizontal => 1, - properties => sub { + properties => sub { (LookupValue('IN_MATH') ? (font => LookupValue('font')->merge(family => 'serif', series => 'medium', shape => 'upright')->specialize("\x{22EE}")) @@ -484,9 +484,9 @@ DefMathI('\colon', undef, ':', role => 'METARELOP'); # Seems like good # Aha, also can be in text... DefConstructorI('\dots', undef, "?#isMath(\x{2026})(\x{2026})", - sizer => "\x{2026}", + sizer => "\x{2026}", enterHorizontal => 1, - properties => sub { + properties => sub { (LookupValue('IN_MATH') ? (font => LookupValue('font')->merge(family => 'serif', series => 'medium', shape => 'upright')->specialize("\x{2026}")) @@ -574,9 +574,9 @@ DefMathI('\langle', undef, "\x{27E8}", role => 'OPEN', stretchy => 'false'); # L DefMathI('\rangle', undef, "\x{27E9}", role => 'CLOSE', stretchy => 'false'); # RIGHT-POINTING ANGLE BRACKET # Not sure these should be defined here, or latex, or even latex compat mode. -DefMathI('\lgroup', undef, "\x{27EE}", role => 'OPEN', stretchy => 'false'); -DefMathI('\rgroup', undef, "\x{27EF}", role => 'CLOSE', stretchy => 'false'); -DefMathI('\bracevert', undef, "|", font => { series => 'bold' }, role => 'VERTBAR'); +DefMathI('\lgroup', undef, "\x{27EE}", role => 'OPEN', stretchy => 'false'); +DefMathI('\rgroup', undef, "\x{27EF}", role => 'CLOSE', stretchy => 'false'); +DefMathI('\bracevert', undef, "|", font => { series => 'bold' }, role => 'VERTBAR'); ## DefMath('\lmoustache',"???", font=>{series=>'bold'}, role=>'OPEN'); ## DefMath('\rmoustache',"???", font=>{series=>'bold'}, role=>'OPEN'); @@ -587,9 +587,9 @@ DefMathI('\bracevert', undef, "|", font => { series => 'bold' }, role => 'VERTBA # mapping. Unfortunately, this doesn't (yet) support people declaring thier own delimiters! # These expand into \left#1, so the bracing disappears; only enlarge the 1st part of #1! -DefConstructor('\big TeXDelimiter', '#1', bounded => 1, font => { size => 'big' }, +DefConstructor('\big TeXDelimiter', '#1', bounded => 1, font => { size => 'big' }, afterConstruct => sub { augmentDelimiterProperties($_[0], $_[1], undef, 0); }); -DefConstructor('\Big TeXDelimiter', '#1', bounded => 1, font => { size => 'Big' }, +DefConstructor('\Big TeXDelimiter', '#1', bounded => 1, font => { size => 'Big' }, afterConstruct => sub { augmentDelimiterProperties($_[0], $_[1], undef, 0); }); DefConstructor('\bigg TeXDelimiter', '#1', bounded => 1, font => { size => 'bigg' }, afterConstruct => sub { augmentDelimiterProperties($_[0], $_[1], undef, 0); }); @@ -655,9 +655,24 @@ DefConstructor('\phantom{}', DefConstructor('\hphantom{}', "?#isMath()" . "(#1)", # !?!?!?! - properties => { isSpace => 1 }, + properties => { isSpace => 1 }, + # In TeX, \hphantom always processes its argument inside an \hbox. + # In text/horizontal mode, we enforce restricted_horizontal to prevent + # display math from leaking through the argument (as can happen with + # complex TikZ library expansions like quantikz2). + # In math mode, we preserve the current mode for correct font metrics. + beforeDigest => sub { + my ($stomach) = @_; + if (!$STATE->lookupValue('IN_MATH')) { + $stomach->beginMode('restricted_horizontal'); + AssignValue('_hphantom_mode_override' => 1); } + else { + AssignValue('_hphantom_mode_override' => 0); } + return; }, afterDigest => sub { - my $whatsit = $_[1]; + my ($stomach, $whatsit) = @_; + if (LookupValue('_hphantom_mode_override')) { + $stomach->endMode('restricted_horizontal'); } my ($w, $h, $d) = $whatsit->getArg(1)->getSize; $whatsit->setProperties(width => $w, height => $h, depth => $d); return; }); @@ -795,7 +810,7 @@ DefRewrite(xpath => 'descendant-or-self::ltx:XMWrap[count(child::*)=1]', $document->setNodeFont($wrap, $document->getNodeFont($node)); ## WHY THIS???? $document->getNode->appendChild($node); -} }); + } }); #%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% # Stop at